/* Motion for the shared layout (tools and admin pages, not the games). Loaded by includes/layout.php after the other
   layout styles; the behaviour is in assets/layout/motion.js (window.LayoutMotion).

   Rules: only transform and opacity move (plus width for bars), everything uses the tokens at the end of layout.css,
   and nothing waits on an animation before it can be used. Reduced motion and html.lay-motion-off cut the tokens to
   zero; the rules below also switch the looping animations (shimmer, spinners, the top bar) to a still state.

   Admin's loading motion, and what the rest of the tools app lacked (2026-10 inventory), now all from this file:
     admin had                                     tools / home / knowledge had
     - shimmer skeleton rows, lists, tiles while   - checkers: a centred card with pulse dots, a wave or a bar
       data loads (layout_skeleton*, .adm-skel     (assets/loading.js) -> now the admin look: top-edge bar, ring
       pulsed in admin.css as well)                  spinner, skeleton lines (tool-ui.css + loader rules below)
                                                   - data browser: its own spinner; workflows: its own wf-skel
                                                     shimmer; history panel: nothing -> .lay-spin / .lay-skeleton
     - ring spinner .st-spin (admin.css only)      - four other spinners -> one .lay-spin (.st-spin is an alias)
     - top bar for fetch/XHR, busy buttons         - same (global already), but nothing for navigation; the checkers
                                                     had loading.js's own line that stuck after a JS-handled submit
                                                     -> one top bar for link clicks and real submits (motion.js)
     - KPI tiles, cards, rows rise in staggered    - only .tw-card / .tw-stat / check rows -> the page's blocks
                                                     (.tool-page / .kb children, .tw-section, record cards) too
     - sliding underline on .adm-tabs              - .td-tabs and [data-tab-ink] strips get it too
     - drawers, menus, flashes, dialogs, toasts,   - shared already (dialogs, toasts, details); the history panel
       details, tab panels animate                   slides on the motion tokens
   Duplicates removed from admin.css: .adm-skel's pulse, .st-spin and the chevron transitions.

   Sections: skeletons · spinner · top progress bar · busy buttons · first-load entrance · tool loader · bars ·
   count-up · dialogs, drawers, menus, toasts · details and tabs · reduced motion / off switch. */

/* ---------- skeletons: <span class="lay-skeleton"></span>, LayoutMotion.skeletonRows(), .skeleton() ---------- */
.lay-skeleton, .adm-skel {
    position: relative; display: block; height: 10px; border-radius: 4px; overflow: hidden;
    background: var(--sel); animation: none;
}
.lay-skeleton::after, .adm-skel::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bg) 55%, transparent), transparent);
    transform: translateX(-100%); animation: lay-shimmer 1.3s var(--mo-ease) infinite;
}
@keyframes lay-shimmer { to { transform: translateX(100%); } }
.lay-skeleton.is-text { height: 9px; margin: 3px 0; }
.lay-skeleton.is-title { height: 14px; width: 40%; }
.lay-skeleton.is-num { height: 22px; width: 56px; border-radius: 5px; }
.lay-skeleton.is-block { height: 64px; border-radius: 6px; }
.lay-skeleton.is-circle { width: 28px; height: 28px; border-radius: 50%; flex: none; }
tr.lay-skel-row td { padding-top: 11px; padding-bottom: 11px; }
tr.lay-skel-row:hover td { background: none; }
.lay-skel-list { list-style: none; margin: 0; padding: 4px 0; display: grid; }
.lay-skel-list > li { display: flex; align-items: center; gap: 10px; padding: 9px 14px; }
.lay-skel-list > li > div { flex: 1; display: grid; gap: 6px; min-width: 0; }
.lay-skel-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.lay-skel-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; display: grid; gap: 9px; background: var(--bg); }
.lay-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---------- ring spinner: <span class="lay-spin" aria-hidden="true"></span> (admin's .st-spin is the same) ---------- */
.lay-spin, .st-spin {
    display: inline-block; flex: none; width: 13px; height: 13px; border-radius: 50%; vertical-align: -2px;
    border: 2px solid color-mix(in srgb, var(--ink) 18%, transparent); border-top-color: var(--accent);
    animation: lay-busy-spin .7s linear infinite;
}
.lay-spin.is-lg { width: 22px; height: 22px; }

/* ---------- top progress bar for fetches in flight (motion.js puts <div class="lay-progress"> in the body) ---------- */
.lay-progress {
    position: fixed; left: 0; right: 0; top: 0; height: 2px; z-index: 600; pointer-events: none; overflow: hidden;
    opacity: 0; transition: opacity var(--mo-base) var(--mo-ease);
}
.lay-progress.is-on { opacity: 1; }
.lay-progress > i {
    position: absolute; top: 0; bottom: 0; left: 0; width: 40%; background: var(--accent); border-radius: 0 2px 2px 0;
    transform: translateX(-100%); animation: lay-progress 1.15s var(--mo-ease) infinite;
}
.lay-progress.is-done > i { width: 100%; animation: none; transform: none; transition: width var(--mo-fast) var(--mo-ease-out); }
@keyframes lay-progress { 0% { transform: translateX(-100%); } 100% { transform: translateX(260%); } }

/* ---------- busy buttons: LayoutMotion.busy(button, promise), or automatic for the button that started a fetch ---------- */
.lay-busy { position: relative; }
:is(.ui-btn, .lay-btn, .tw-btn, button).lay-busy { color: transparent !important; }
:is(.ui-btn, .lay-btn, .tw-btn, button).lay-busy > * { visibility: hidden; }
:is(.ui-btn, .lay-btn, .tw-btn, button).lay-busy::after {
    content: ""; position: absolute; left: 50%; top: 50%; width: 13px; height: 13px; margin: -7.5px 0 0 -7.5px;
    border-radius: 50%; border: 2px solid var(--lay-busy-ring, color-mix(in srgb, var(--ink) 22%, transparent));
    border-top-color: var(--lay-busy-ink, var(--ink)); animation: lay-busy-spin .7s linear infinite;
}
.ui-btn-pri.lay-busy { --lay-busy-ink: var(--pri-ink); --lay-busy-ring: color-mix(in srgb, var(--pri-ink) 30%, transparent); }
@keyframes lay-busy-spin { to { transform: rotate(360deg); } }

/* ---------- first-load entrance: html.lay-mo-load is set in <head> by motion.js and removed once the page settles ----------
   Rows, list items, cards and stat tiles that are in the page (or arrive while it loads) fade and rise in with a
   small stagger capped at the tenth item, and only the first 30 of a list move at all. */
@keyframes lay-rise { from { opacity: 0; transform: translateY(4px); } }
@keyframes lay-fade { from { opacity: 0; } }
html.lay-mo-load .lay-main :is(tbody > tr, .adm-list > li, .adm-feed > li, .home-list > li, [data-motion-list] > li, .lay-mo-item):nth-child(-n+30):not(.lay-skel-row, [data-skeleton]),
html.lay-mo-load .lay-main :is(.adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row),
html.lay-mo-load .lay-main [data-motion-item] {
    animation: lay-rise var(--mo-base) var(--mo-ease-out) both;
    animation-delay: calc(var(--mo-stagger) * var(--mo-i, 0));
}
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(2) { --mo-i: 1; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(3) { --mo-i: 2; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(4) { --mo-i: 3; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(5) { --mo-i: 4; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(6) { --mo-i: 5; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(7) { --mo-i: 6; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(8) { --mo-i: 7; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(9) { --mo-i: 8; }
html.lay-mo-load .lay-main :is(tr, li, .adm-kpi, .adm-card, .tw-card, .tw-stat, .ipm-pool, .home-tool, .gs-bar-row, .lay-mo-item):nth-child(n+10) { --mo-i: 9; }
html.lay-mo-load .lay-main [data-motion="off"] *,
html.lay-mo-load .lay-main [data-motion="off"] { animation: none; }

/* ---------- tool results: a result block rises in each time it is shown (display none to shown restarts it) ---------- */
.lay-main :is(.check-section, .verdict-banner, .result-box, .friendly-result-wrapper, .quick-summary, .technical-details-accordion, [data-motion-reveal]) {
    animation: lay-rise var(--mo-base) var(--mo-ease-out) both;
}
.lay-main .check-section:nth-of-type(2) { animation-delay: calc(var(--mo-stagger) * 1); }
.lay-main .check-section:nth-of-type(3) { animation-delay: calc(var(--mo-stagger) * 2); }
.lay-main .check-section:nth-of-type(n+4) { animation-delay: calc(var(--mo-stagger) * 3); }

/* ---------- page entrance: a tool page's blocks (head, cards, sections) rise in like admin's cards ----------
   Four steps of stagger at most, so the last block is in place by ~270 ms; the blocks are usable at once (opacity and
   transform only, no layout change). The home page's tool grid and knowledge's nav drawer move on their own. */
html.lay-mo-load .lay-main .tool-page > :not(.home-tools, script, style, template, [hidden], [data-motion="off"]),
html.lay-mo-load .lay-main .wf-app > :not(script, style, template, [hidden]),
html.lay-mo-load .lay-main .kb-article {
    animation: lay-rise var(--mo-base) var(--mo-ease-out) both;
    animation-delay: calc(var(--mo-stagger) * var(--mo-p, 0));
}
html.lay-mo-load .lay-main :is(.tool-page, .wf-app) > :nth-child(2) { --mo-p: 1; }
html.lay-mo-load .lay-main :is(.tool-page, .wf-app) > :nth-child(3) { --mo-p: 2; }
html.lay-mo-load .lay-main :is(.tool-page, .wf-app) > :nth-child(n+4) { --mo-p: 3; }
html.lay-mo-load .lay-main :is(.tw-section, .record-selector-card) { animation: lay-rise var(--mo-base) var(--mo-ease-out) both; }

/* ---------- tool loader (assets/loading.js UnifiedLoader, laid out in tool-ui.css) ----------
   The card fades in; its top edge runs the same sweep as the page's top bar; the status icon is the ring spinner;
   skeleton lines shimmer below. Done and error states stop the spinner and show their mark. */
.unified-loader.is-visible { animation: lay-fade var(--mo-base) var(--mo-ease-out); }
.unified-loader .loader-bar-fill { transition: width var(--mo-slow) var(--mo-ease); }
.unified-loader .loader-bar-fill.indeterminate { transform: translateX(-100%); animation: lay-progress 1.15s var(--mo-ease) infinite; }
.unified-loader .loader-text-icon {
    border-radius: 50%; border: 2px solid color-mix(in srgb, var(--ink) 18%, transparent); border-top-color: var(--accent);
    animation: lay-busy-spin .7s linear infinite;
}
.unified-loader:is(.is-error, .is-done) .loader-text-icon { border: 0; animation: none; }
.unified-loader:is(.is-error, .is-done) .loader-skel { display: none; }

/* ---------- bars: grow from the left when they appear, ease to a new width when the value changes ----------
   Any bar can join with [data-motion-bar] on the filled element. */
@keyframes lay-bar-grow { from { transform: scaleX(0); } }
.adm-meter > i, .gs-bar-fill, .lookup-meter .fill, .usage-bar > span, [data-motion-bar] {
    transform-origin: left center;
    transition: width var(--mo-slow) var(--mo-ease);
    animation: lay-bar-grow var(--mo-slow) var(--mo-ease-out) both;
}
.loader-bar-fill, .progress-bar-fill { transition: width var(--mo-slow) var(--mo-ease); }
:where(.lay-app) :where(progress)::-webkit-progress-value { transition: width var(--mo-slow) var(--mo-ease); }
html.lay-motion-off :is(.adm-meter > i, .gs-bar-fill, .lookup-meter .fill, .usage-bar > span, [data-motion-bar]) { animation: none; transition: none; }

/* sparklines (admin overview, games stats) rise from their baseline when drawn */
@keyframes lay-spark { from { opacity: 0; transform: scaleY(.15); } }
.adm-spark:not(.is-empty) path, .gs-spark path {
    transform-box: view-box; transform-origin: 50% 100%;
    animation: lay-spark var(--mo-slow) var(--mo-ease-out) both;
}
.gs-heat-cell { animation: lay-fade var(--mo-slow) var(--mo-ease-out) both; }

/* ---------- count-up numbers: motion.js counts .adm-kpi-v, .ipm-pool-n, .tw-stat b and [data-count-up] ---------- */
.lay-counting { font-variant-numeric: tabular-nums; }

/* ---------- dialogs, drawers, menus, palette and toasts ---------- */
@keyframes lay-pop-in { from { opacity: 0; transform: translateY(6px) scale(.98); } }
@keyframes lay-slide-in { from { opacity: 0; transform: translateX(10px); } }
@keyframes lay-drop-in { from { opacity: 0; transform: translateY(-4px); } }
@keyframes lay-panel-in { from { opacity: 0; transform: translateY(3px); } }

/* modal <dialog>: scale in; with Chromium's allow-discrete the close fades out too (other browsers close at once) */
:where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog]) {
    transition: opacity var(--mo-fast) var(--mo-ease-in), transform var(--mo-fast) var(--mo-ease-in),
        overlay var(--mo-fast) allow-discrete, display var(--mo-fast) allow-discrete;
}
:where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog]):not([open]) { opacity: 0; transform: scale(.98); }
:where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog])[open] { animation: lay-pop-in var(--mo-base) var(--mo-ease-out); }
:where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog])::backdrop { animation: lay-fade var(--mo-base) var(--mo-ease-out); }
.confirm-modal-overlay:not([hidden]) .confirm-modal { animation: lay-pop-in var(--mo-base) var(--mo-ease-out); }

.lay-pal-scrim:not([hidden]) { animation: lay-fade var(--mo-fast) var(--mo-ease-out); }
.lay-pal-scrim:not([hidden]) .lay-pal { animation: lay-pop-in var(--mo-base) var(--mo-ease-out); }
.lay-menu:not([hidden]) { animation: lay-drop-in var(--mo-fast) var(--mo-ease-out); }
.adm-menu-list:not([hidden]) { animation: lay-drop-in var(--mo-fast) var(--mo-ease-out); }

/* side drawer (users) and expanded rows */
.adm-split:not(.is-closed) > .adm-drawer:not([hidden]) { animation: lay-slide-in var(--mo-base) var(--mo-ease-out); }
tr.adm-row-detail:not([hidden]) > td > * { animation: lay-panel-in var(--mo-base) var(--mo-ease-out); }

/* toasts (assets/toast.js) slide in from the right and lift out */
.toast-notification {
    transform: translateX(12px);
    transition: opacity var(--mo-base) var(--mo-ease-out), transform var(--mo-base) var(--mo-ease-out);
}
.toast-notification.show { transform: none; }
.toast-notification.hide { transform: translateY(-6px); transition-timing-function: var(--mo-ease-in); transition-duration: var(--mo-fast); }
.adm-flash { animation: lay-drop-in var(--mo-base) var(--mo-ease-out); }

/* ---------- details, chevrons, tab panels ---------- */
details[open] > :not(summary) { animation: lay-panel-in var(--mo-base) var(--mo-ease-out); }
.adm-card-chev, .adm-x-chev, .check-toggle-icon, .lay-me-chev, .adm-chev-r { transition: transform var(--mo-base) var(--mo-ease); }
.adm-tabpanel:not([hidden]), [role="tabpanel"]:not([hidden]) { animation: lay-fade var(--mo-base) var(--mo-ease-out); }
.adm-tabs button { transition: color var(--mo-fast) var(--mo-ease), border-color var(--mo-fast) var(--mo-ease); }
.tw-seg span { transition: background-color var(--mo-fast) var(--mo-ease), color var(--mo-fast) var(--mo-ease), box-shadow var(--mo-fast) var(--mo-ease); }
/* sliding underline for .adm-tabs, .td-tabs and [data-tab-ink] (motion.js adds <i class="lay-tab-ink"> and .lay-has-ink) */
.lay-has-ink { position: relative; }
.lay-has-ink button[aria-selected="true"] { border-bottom-color: transparent; }
.td-tabs .lay-tab-ink { background: var(--accent); }
.lay-tab-ink {
    position: absolute; left: 0; bottom: 0; height: 2px; width: 1px; background: var(--ink); pointer-events: none;
    transform-origin: left center; transition: transform var(--mo-base) var(--mo-ease);
}

/* ---------- reveal pill: LayoutMotion.reveal() points at off-screen results; clicks never scroll on their own ---------- */
.lay-reveal {
    position: fixed; left: 0; right: 0; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); margin: 0 auto; width: max-content;
    z-index: 590; padding: 7px 14px; border: 0; border-radius: 999px; background: var(--pri); color: var(--pri-ink);
    font: 600 12.5px/1.2 var(--f-sans, inherit); box-shadow: 0 6px 20px rgba(0, 0, 0, .18); cursor: pointer;
    animation: lay-pop-in var(--mo-base, 200ms) var(--mo-ease-out, ease-out) both;
}
.lay-reveal:hover { filter: brightness(1.12); }
.lay-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
html.lay-motion-off .lay-reveal { animation: none; }

/* ---------- reduced motion and the off switch: no movement, looping indicators hold still ---------- */
@media (prefers-reduced-motion: reduce) {
    .lay-skeleton::after, .adm-skel::after { animation: none; opacity: 0; }
    .lay-progress > i { animation: none; transform: none; width: 100%; opacity: .6; }
    :is(.ui-btn, .lay-btn, .tw-btn, button).lay-busy::after { animation: none; border-color: var(--lay-busy-ink, var(--ink)); opacity: .55; }
    .lay-spin, .st-spin, .unified-loader .loader-text-icon { animation: none; border-color: var(--accent); opacity: .55; }
    .unified-loader .loader-bar-fill.indeterminate { animation: none; transform: none; width: 100% !important; opacity: .6; }
    .unified-loader.is-visible, .lay-reveal { animation: none; }
    html.lay-mo-load .lay-main * { animation: none !important; }
    :is(.adm-meter > i, .gs-bar-fill, .lookup-meter .fill, .usage-bar > span, [data-motion-bar]) { animation: none; }
    .toast-notification, .toast-notification.show, .toast-notification.hide { transform: none; }
    :where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog]):not([open]) { transform: none; }
}
html.lay-motion-off .lay-skeleton::after, html.lay-motion-off .adm-skel::after { animation: none; opacity: 0; }
html.lay-motion-off .lay-progress > i { animation: none; transform: none; width: 100%; }
html.lay-motion-off :is(.ui-btn, .lay-btn, .tw-btn, button).lay-busy::after { animation: none; }
html.lay-motion-off .toast-notification { transform: none; }
html.lay-motion-off :is(.lay-spin, .st-spin, .unified-loader .loader-text-icon, .unified-loader.is-visible) { animation: none; }
html.lay-motion-off .unified-loader .loader-bar-fill.indeterminate { animation: none; transform: none; width: 100% !important; }
html.lay-motion-off :is(dialog, .lay-pal-scrim, .lay-pal, .lay-menu, .adm-menu-list, .adm-drawer, .adm-flash, .confirm-modal, .adm-tabpanel, [role="tabpanel"], details > *, tr.adm-row-detail td > *),
html.lay-motion-off dialog::backdrop { animation: none !important; }
html.lay-motion-off :where(dialog.adm-modal, dialog.lay-dialog, .lay-app dialog[data-motion-dialog]):not([open]) { transform: none; }
