/* ==========================================================================
   physics.css — the visual half of js/shared/physics.js (added 2026-10-25).
   Loads LAST on every page that links it: the driven-transform rules below
   must outrank each app's own `:active`/`.is-pressing` transform rules by
   order, without reaching for !important on `transform` itself.

   Contents:
     1. Line colours — the root home page's per-app accent pair
        (css/home.css §1 is the source of both sets; keep the values equal).
     2. The card light treatment — hairline, pointer sheen, hover shadow —
        the same treatment the root page's .hm-card/.hm-tile carry, lifted
        into a shared sheet so every sub-app page reads as one system.
     3. Driven-state rules — what the JS springs are allowed to write.
     4. Momentum rails.
     5. Ambient light — the skylight every `body[data-app]` page stands under.

   The selector group in §2 mirrors PHYS_CARD_SELECTOR in
   js/shared/physics.js; physics.test.mjs fails the build if they drift.
   Anything can join the system with `data-phys` without touching either.
   ========================================================================== */

/* ==========================================================================
   1. Line colours
   Light-ground values; every one clears 3:1 on the suite's paper tones
   (WCAG 1.4.11). The bright set swaps in for the dark scheme.
   ========================================================================== */

:root {
    --ln-studio: #d6302a;
    --ln-transcribe: #00877a;
    --ln-notebook: #2d5fd3;
    --ln-mindmap: #d96c00;
    --ln-slides: #8a3fc7;
    --ln-storyboard: #23913f;
    --ln-mt: #9c6234;
    --ln-expense: #9e7800;
    --ln-score: #cc3d8a;
    --ln-billing: #5f6f82;
    --ln-reminders: #0f7b8a;
    /* Pages that belong to no one app (auth, pricing, status, legal, admin):
       the root page's single interactive blue, css/home.css --hm-accent. */
    --ln-suite: #0b57c9;
}

/* A page that is dark by design (thanks, share) never sets html.dark, so it
   declares its ground on the body instead: <body data-ground="dark">. */
html.dark,
body[data-ground="dark"] {
    --ln-studio: #ff6a5f;
    --ln-transcribe: #2fc7b5;
    --ln-notebook: #6f9bff;
    --ln-mindmap: #ffa040;
    --ln-slides: #c18cff;
    --ln-storyboard: #5fd17d;
    --ln-mt: #d39a6a;
    --ln-expense: #f0c540;
    --ln-score: #ff7fbe;
    --ln-billing: #9aabbf;
    --ln-reminders: #4fd6e8;
    --ln-suite: #6ea8ff;
}

/* One line per app. A page declares itself with <body data-app="…">; the
   root home page instead sets --line inline per card, exactly as before. */
body[data-app="studio"]      { --line: var(--ln-studio); }
body[data-app="transcribe"]  { --line: var(--ln-transcribe); }
body[data-app="notebook"]    { --line: var(--ln-notebook); }
body[data-app="present"]     { --line: var(--ln-mindmap); }
body[data-app="slides"]      { --line: var(--ln-slides); }
body[data-app="storyboard"]  { --line: var(--ln-storyboard); }
body[data-app="mt"]          { --line: var(--ln-mt); }
body[data-app="expense"]     { --line: var(--ln-expense); }
body[data-app="score"]       { --line: var(--ln-score); }
body[data-app="billing"]     { --line: var(--ln-billing); }
body[data-app="reminders"]   { --line: var(--ln-reminders); }
body[data-app="suite"]       { --line: var(--ln-suite); }

/* ==========================================================================
   2. The card light treatment
   `:where()` keeps every declaration below at zero specificity, so an app's
   own rules for the same property always win — this sheet only fills gaps.
   The JS half (js/shared/physics.js) engages these surfaces on hover/press
   and writes the --phys-* custom properties; without JS (or under reduced
   motion) what remains is the static hairline + hover shadow.
   ========================================================================== */

:where([data-phys], .hm-card, .hm-tile, .tx-capture-drop, .tx-engine-card,
    .pres-card, .nb-recent-card, .nb-studio-tile, .project-card, .credit-card) {
    /* the light host below is absolutely positioned against the card */
    position: relative;
    --phys-glow: 0;
}

/* A `data-phys="flat"` row lies in its list, not on it: it keeps the light and
   the press, but no floating-card shadow. The trailing :where(:not()) keeps the
   rule at (0,1,0), exactly where it was before the variant existed. */
@media (hover: hover) {
    :where([data-phys], .hm-card, .hm-tile, .tx-capture-drop, .tx-engine-card,
        .pres-card, .nb-recent-card, .nb-studio-tile, .project-card, .credit-card):hover:where(:not([data-phys="flat"])) {
        /* the root page's hover shadow, verbatim: a tinted wash of the app's
           own line colour, not a generic grey */
        box-shadow: 0 18px 44px -30px color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) 70%, transparent);
    }
}

/* The injected light host. physics.js appends/removes this span while a card
   is engaged; ::before is the hairline of light along the top edge, ::after
   is the pointer-tracked sheen. */
.phys-light {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    z-index: 1;
}

.phys-light::before {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent,
        color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) 65%, transparent),
        transparent);
    opacity: 0.55;
}

.phys-light::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(260px circle at var(--phys-x, 50%) var(--phys-y, 50%),
        color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) 20%, transparent),
        transparent 70%);
    opacity: var(--phys-glow, 0);
}

/* The root page's own ::before IS this hairline; a second one painted on top
   would just read as brighter. Root tiles have no hairline of their own. */
.hm-card .phys-light::before { display: none; }

/* The light painted by the surface itself. physics.js marks a `data-phys`
   element whose ::before/::after are free with `data-phys-light="self"` and
   drives it entirely through custom properties. This is the preferred path and
   the reason it exists: the browser Tailwind runtime
   (js/shared/tailwind-browser.js) rebuilds and REPLACES the whole utility sheet
   when a class attribute changes or a node is inserted — a full-document restyle
   that reads as a flicker plus a stutter on every row a pointer crosses
   (measured on /studio/connections/, 2026-11-02). Inline style writes and
   non-class attributes sit outside that mutation filter, so a hover here costs
   zero DOM writes. The injected `.phys-light` span above remains the fallback
   for surfaces whose pseudo-elements are already spent. */
/* `:where()` keeps these two at (0,0,1): the marker is only set when the slots
   are free at engage time, but a state class applied later (a loading spinner's
   ::after, an :active mark's ::before) must still outrank the light. */
:where([data-phys][data-phys-light="self"])::before {
    content: "";
    position: absolute;
    left: 14px;
    right: 14px;
    top: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent,
        color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) 65%, transparent),
        transparent);
    /* Fades with the sheen instead of popping in, and paints nothing at rest:
       a released card drops the inline --phys-glow, so the fallback 0 applies. */
    opacity: calc(var(--phys-glow, 0) * 0.55);
    pointer-events: none;
    z-index: 1;
}

:where([data-phys][data-phys-light="self"])::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(260px circle at var(--phys-x, 50%) var(--phys-y, 50%),
        color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) 20%, transparent),
        transparent 70%);
    opacity: var(--phys-glow, 0);
    pointer-events: none;
    z-index: 1;
}

/* ==========================================================================
   3. Driven state — what the springs write
   While physics.js drives a card it sets [data-phys-driven] (an attribute, not
   a class — see the self-painted light block above); the one transform
   declaration composes everything from the custom properties the spring
   loop updates per frame. Declared HERE (not inline) so the app stylesheet's
   own transform rules keep working the moment the attribute comes off.
   ========================================================================== */

/* `transform` is !important on purpose, and only while driven. An entrance
   animation with a held fill (`.stagger-item { animation: md-fade-in … both }`,
   `nb-rise-in … both`) keeps applying its final `transform: translateY(0)` for
   good, and an animation outranks every normal author declaration — measured:
   a driven deck card computed matrix(1,0,0,1,0,0), so lift, tilt and press
   were dead on every stagger-entered card while the sheen still worked. An
   important author declaration outranks animations; it touches transform
   alone, so a fill that holds opacity is left exactly as it was. The same
   importance covers the apps' own hover and press transforms
   (`.pres-card:hover`, `.hm-card:active`, `.x.is-pressing`), which used to
   need a matching press rule here. The doubled class keeps (0,2,0), so an
   app's equal-specificity !important rule still loses on sheet order. */
[data-phys-driven][data-phys-driven] {
    transform: perspective(700px)
        rotateX(var(--phys-rx, 0deg)) rotateY(var(--phys-ry, 0deg))
        translateY(var(--phys-ty, 0px)) scale(var(--phys-s, 1)) !important;
    will-change: transform;
    /* A CSS transition on `transform` would interpolate between the spring's
       per-frame values and turn the physics to mush, so transform must leave
       the transition list. This !important redeclaration is the price of
       keeping hover border/shadow fades while driven; durations match the
       suite's standard motion (css/shared.css tokens). */
    transition: border-color 220ms var(--md-easing-standard, cubic-bezier(0.2, 0, 0, 1)) !important,
        box-shadow 220ms var(--md-easing-standard, cubic-bezier(0.2, 0, 0, 1)) !important,
        background-color 220ms var(--md-easing-standard, cubic-bezier(0.2, 0, 0, 1)) !important;
}

/* Reduced motion: the JS never binds, so none of this should exist — these
   guards cover a mid-session switch away from motion, in the frames before
   physics.js releases every card. Only the light and the motion go: the card
   itself must stay visible (a `display: none` here once hid the whole card). */
@media (prefers-reduced-motion: reduce) {
    .phys-light {
        display: none;
    }
    :where([data-phys][data-phys-light="self"])::before,
    :where([data-phys][data-phys-light="self"])::after {
        display: none;
    }
    [data-phys-driven][data-phys-driven] {
        transform: none !important;
    }
}

/* ==========================================================================
   4. Momentum rails
   `data-phys-scroll` marks a horizontal rail that a mouse can drag
   (js/shared/physics.js adds inertia and rubber-band edges). Touch and pen
   keep the platform's native momentum.
   ========================================================================== */

[data-phys-scroll] {
    cursor: grab;
    /* The rail no longer preventDefaults pointerdown (that captured the pointer
       and ate every click on its buttons), so a mouse drag would otherwise
       select the button labels and drag the thumbnails as images. A rail is a
       row of controls: neither is something a user wants from it. */
    -webkit-user-select: none;
    user-select: none;
}
[data-phys-scroll] img {
    -webkit-user-drag: none;
}
[data-phys-scroll]:active {
    cursor: grabbing;
}

/* ==========================================================================
   5. Ambient light
   The root hero stands under a blueprint grid and a soft glow; every app page
   gets the quiet version of the same room: one skylight above the viewport,
   tinted with the app's own line colour, falling off before it reaches the
   content. Static — a light source does not move with the scroll, so this is
   fixed to the viewport, and nothing here animates (no vestibular motion).

   Painted on body::before at z-index -1 inside an isolated body, so it sits
   ABOVE the body's background and BELOW every element. An app whose main
   wrapper paints its own full-bleed opaque background hides it there, which
   is the correct outcome: the wrapper is a surface the light falls on, and
   the cards on it carry their own light (§2).

   Pages on css/signal.css (status, errors) already stand under their own dot
   grid on the same pseudo-element, so they are left alone.
   ========================================================================== */

:root {
    --phys-sky-mix: 10%;
    --phys-sky-grid: rgba(14, 27, 44, 0.045);
}
html.dark,
body[data-ground="dark"] {
    --phys-sky-mix: 13%;
    --phys-sky-grid: rgba(232, 238, 245, 0.035);
}

body[data-app]:not(.signal-body) {
    isolation: isolate;
}

body[data-app]:not(.signal-body)::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: min(560px, 85vh);
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(70% 100% at 50% 0%,
            color-mix(in srgb, var(--line, var(--md-primary, var(--brand))) var(--phys-sky-mix), transparent),
            transparent 72%),
        linear-gradient(var(--phys-sky-grid) 1px, transparent 1px) 0 0 / 44px 44px,
        linear-gradient(90deg, var(--phys-sky-grid) 1px, transparent 1px) 0 0 / 44px 44px;
    -webkit-mask-image: radial-gradient(85% 100% at 50% 0%, black 0%, transparent 78%);
    mask-image: radial-gradient(85% 100% at 50% 0%, black 0%, transparent 78%);
}

/* Decoration only: it goes wherever the user has asked for less of it. */
@media (prefers-contrast: more), (prefers-reduced-transparency: reduce), (forced-colors: active), print {
    body[data-app]:not(.signal-body)::before {
        display: none;
    }
}
