/* ══════════════════════════════════════════════════════════════════════════
   KNOX VISION · V7
   Extracted from knox-vision-v7.html. Do not edit this file.

   Change the master, then re-run dist-v7/_extract.py. Anything typed directly
   in here is lost at the next extract and, worse, is invisible until it is.

   Apps LINK this file. They do not copy it.
   ══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   KNOX VISION V6 · THE UNIVERSAL MASTER
   V5's visual language, locked 23-08-2026 and carried verbatim. V6 adds what
   three real apps needed: composition, the console instrument, one spotlight,
   media, the phone, and three proof screens. Built 27-08-2026, through Adam.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── LAYER 0 · THE SEEDS ─────────────────────────────────────────────────────
   V4 had five. V5 has SIX: accent is split out from status, because a
   red-branded client currently cannot have a visible error state and a
   gold-branded one cannot have a warning. Hard rule: accent and status sit at
   least 45 degrees apart in hue.                                             */
:root {
  --seed-ink:      #0D0D0D;   /* LOCKED 23-08-2026 */   /* dark ground */
  --seed-paper:    #E6E6E6;   /* LOCKED */   /* light ground */
  --seed-accent-hi:#A82938;   /* LOCKED */   /* brand gradient, high */
  --seed-accent-lo:#1F0007;   /* LOCKED */   /* brand gradient, low  */
  --seed-status:   #FF7033;   /* LOCKED, Knox's own value, restored 23-08-2026 after he saw the
                                amber alternative and rejected it. KNOWN TRADEOFF: this sits only
                                22 degrees from the brand red in OKLCH, so danger and accent are
                                not separable by hue. Destructive actions must therefore be told
                                apart by POSITION, LABEL and ICON, never by colour alone, and
                                anything financial in Knox Artiste Accounts needs an explicit
                                confirm step. Do not "fix" this back to amber.
                                RULE MOVED 29-08-2026, by Knox. The old "an accent must sit 45
                                degrees from status" is retired: the HOUSE palette fails it at 22
                                and no Knox app ever passed, so it was a rule nothing met. The
                                compensation on the line above is now the HOUSE DEFAULT in every
                                app, whatever its accent. The threshold is 20 and it asks a
                                different question: do these two read as ONE colour. Under 20 the
                                seed moves. The panel computes it live. */   /* danger anchor. warn / ok / info rotate off it */

  /* ── LAYER 0b · SHAPE, SPACE, MOTION, TYPE SCALE ──
     All four are dials in the console. V4 hard-coded every one of them. */
  --u: 5px;                                    /* spacing unit, density-driven */
  --r: 20px;                                   /* THE radius. Everything derives. */
  --r-xs: calc(var(--r) * .4);                 /* marks, inputs, menu rows */
  --r-lg: calc(var(--r) * 1.4);                /* cards, modals, the app frame */
  --r-pill: 999px;
  --dur: 1;                                    /* motion multiplier */
  /* --ctrl-h moved to LAYER 0d, where the density dial that drives it lives. */
  /* THE CONTROL BAR LAW. Everything on one horizontal bar shares this height AND
     this corner. A pill next to a rounded rectangle reads as two systems, which
     is what the composer looked like. The only exception is a glyph key: a plus
     or a minus is meant to read as a small square button. */
  --bar-r: var(--btn-r);
  --ts: 1;                                     /* type scale multiplier */
  --elev: 1.1;                                   /* elevation strength multiplier */

  /* ── WHAT THE 5px UNIT ACTUALLY GOVERNS ──────────────────────────────────
     WRITTEN DOWN 27-08-2026, because it was being enforced as a rule nobody had
     stated and the system does not follow.

     Measured, in this file: 365 spacing declarations, 126 of them raw px that
     are NOT on the 5px grid, across seventeen distinct values (1, 2, 2.5, 3, 4,
     6, 7, 8, 9, 11, 12, 14, 16, 22, 23, 26, 32). That is not drift. `.menu`
     ships padding 6px, `.field` ships gap 7px, `.optrow` ships
     `padding: 9px var(--sp-3)` with one of each in a single declaration, and
     every one of those was chosen by eye against a real control.

     THE UNIT GOVERNS LAYOUT. Space BETWEEN components, section padding, rail
     and panel widths, the rhythm of a page. Those take --sp-1 through --sp-8
     and there is no good reason to ever write a raw number for them.

     CONTROL INTERNALS ARE OPTICAL. The padding inside a chip, the gap between a
     glyph and its label, a menu's 6px inset, the 9px that makes a 32px row read
     as centred. A 5px grid applied there makes controls that are a pixel wrong
     in a way you can see and cannot name.

     Why this had to be said out loud: Darkroom was audited against the grid and
     came back with fifteen off-grid values, all but two of them control
     internals, while reaching for a token MORE often than this file does (71%
     against 58%). The app was more disciplined than the system judging it. The
     next step proposed was adding a ninth token at 3px, which this file's own
     126 exceptions would have carried on ignoring.

     So: an app off the grid on LAYOUT is a finding. An app off the grid inside a
     control is not, and neither is this file. */
  --sp-1: calc(var(--u) * 1);   --sp-2: calc(var(--u) * 2);
  --sp-3: calc(var(--u) * 3);   --sp-4: calc(var(--u) * 4);
  --sp-5: calc(var(--u) * 6);   --sp-6: calc(var(--u) * 8);
  --sp-7: calc(var(--u) * 12);  --sp-8: calc(var(--u) * 16);

  /* Clearance for the floating dock. Derived, never typed: a hand-derived
     number goes stale the moment the dock changes, and it did. Full reasoning
     sits with the .dock rule itself. */
  --dock-h: calc(44px + var(--sp-2) * 2 + 2px);   /* item + own padding + border */
  --dock-clear: calc(var(--dock-h) + var(--sp-4) + var(--sp-6));

  /* ── LAYER 0c · SHELL GEOMETRY ────────────────────────────────────────────
     ADDED 28-08-2026. This is the layer the system was missing, and its absence
     is the whole of "the gaps and spaces are all out of place".

     V6 already fixed composition INSIDE a screen: .screen sets the rhythm and
     .band holds one concern. What no version has ever described is the SHELL
     the screen sits in. So each app typed its own numbers, and they disagree.
     Measured across the four live builds, 28-08-2026:

       rail width      Accounts 218 · Grid 218 · Pulse 218 · Darkroom 216
                       (and Grid's own design pack says 232, which the build
                        never matched)
       canvas padding  20/30/60 · 20/26/60 · 18/20/22 · 60/32/120
       canvas cap      none · none · 1560 · 1040-1160
       shell breakpoint 760 and 860 in Accounts · 1000 in Grid ·
                       760 AND 860 both live in the same Pulse file ·
                       860, 900 and 820 across three Darkroom artefacts

     Four apps, four shells, no disagreement anyone chose. A producer moving
     from Pulse to Accounts should not feel the furniture move under them.

     These are the numbers. An app SETS them, it does not retype them. */
  --rail-w:        218px;   /* the nav rail. Three of four apps already ship this. */
  --rail-w-icons:   62px;   /* [data-rail="icons"] */
  --rail-w-wide:   248px;   /* only where a rail carries two-line labels */
  --panel-w:       320px;   /* inspector / side panel, docked beside the canvas */
  --panel-w-lg:    400px;   /* drawer over the canvas */
  --canvas-max:   1560px;   /* the reading cap. Per-app, see the table below. */
  --canvas-pad-x: var(--sp-5);
  --canvas-pad-t: var(--sp-4);
  --canvas-pad-b: var(--sp-7);
  --gutter:       var(--sp-4);   /* the 12-column page gutter */
  --row-h:          44px;   /* one table row at default density */

  /* ── THE THREE BREAKPOINTS ────────────────────────────────────────────────
     Custom properties cannot be read inside @media, so these are written down
     rather than tokenised. That makes the discipline social, so it is stated
     once, here, and nowhere else. The four apps between them currently ship
     SIXTEEN distinct breakpoints. Three is the whole system:

       600px   PHONE.  One column. The dock replaces the rail. Touch density.
       860px   SHELL.  The rail lies down. Canvas gutters drop a step.
      1180px   PANEL.  A docked side panel goes under the canvas, not beside it.

     Anything else is a component asking for a per-component rule, which is
     fine and is not a breakpoint. Do not add a fourth. */

  /* ── LAYER 0d · DENSITY ───────────────────────────────────────────────────
     One dial, three settings, set once per app on <html data-density>.
     Accounts runs compact because a P&L is a wall of numbers. Darkroom runs
     comfortable because the frame is the content and chrome should recede.
     Grid and Pulse sit at default. Nothing else in the system changes. */
  --ctrl-h: 36px;

  /* Z-scale. V4 hard-coded 0,1,2,50,60,70,1000 with no system. */
  --z-base: 1; --z-chrome: 20; --z-sticky: 30; --z-drop: 40; --z-lift: 50;
  --z-overlay: 60; --z-modal: 70; --z-drop-over: 75; --z-toast: 80; --z-tip: 85; --z-console: 90;
  /* --z-drop-over ADDED 27-08-2026, from Darkroom. A MENU PORTALLED OUT OF ITS
     CONTAINER NEEDS A HIGHER FLOOR THAN ONE THAT STAYS IN IT.

     `--z-drop` at 40 is correct for this system's own `.drop-menu`, which is
     position:absolute inside `.drop` and therefore inherits whatever stacking
     context opened it. Inside a modal it rides the modal up and 40 is relative.

     An app that portals its menus to document.body loses that inheritance, and
     40 becomes an absolute number sitting BELOW --z-modal at 70: every dropdown
     inside every dialog lands underneath it. Darkroom portals every menu on
     purpose, because a canvas with overflow:hidden, a transform on the dock
     that breaks position:fixed, and isolation on the side panels are four
     separate traps for a menu rendered in place, and chasing them one at a time
     is what once put a resolution picker behind a surface nobody could see.

     So this is the paired token, not a replacement. In-place menus stay on
     --z-drop. A portalled menu takes --z-drop-over, which clears the modal and
     still sits under the toast, because a toast has to be readable over both.

     Pulse and Grid both have modals and will each rediscover this the first
     time they portal a menu.

     THEY BOTH DID. Verified 28-08-2026, against the four live builds:
       Grid   found it as menus positioned off-screen, because a modal that
              animates `transform` becomes the containing block for its own
              `position:fixed` children. Fixed by portalling to body.
       Pulse  ships `.drop-menu` at z-index 50 and `.sdrawer` at 76, so every
              menu opened inside a drawer lands underneath it. Knox found it on
              the New Shoot crew picker. STILL OPEN at the time of writing.
       KAA    portals at a hand-typed z-index of 1100, outside the scale
              entirely, which works and tells the next reader nothing.
     Three apps, three different answers, one bug. That is a system failure,
     not three app failures.

     --z-lift ADDED 28-08-2026, and it is the one that fixes the common case
     without any JavaScript at all. See THE LIFT LAW, with the surfaces. */

  /* Type scale. Seven steps, and only TWO of them live below body. V5's first
     pass ran six sizes between 9px and 13.5px, which is not a hierarchy.
     Below body, difference comes from weight, case and colour. */
  --fs-caps:    calc(11px * var(--ts));   /* every uppercase label, eyebrow, table head, status, count */
  --fs-sm:      calc(13px * var(--ts));   /* meta, help, dense control text, card titles */
  --fs-md:      calc(15px * var(--ts));   /* body */
  --fs-lg:      calc(19px * var(--ts));   /* lede, card title, secondary metric */
  --fs-xl:      calc(26px * var(--ts));   /* section heading */
  --fs-2xl:     calc(34px * var(--ts));   /* metric */
  --fs-display: clamp(calc(30px * var(--ts)), 5.2vw, calc(64px * var(--ts)));

  /* Line heights and tracking, tokenised. V4 set both inline, everywhere. */
  --lh-display: .96; --lh-head: 1.14; --lh-body: 1.6; --lh-ui: 1.35;
  --ls-display: -.02em; --ls-head: .01em; --ls-body: 0; --ls-caps: .12em;

  /* Motion. Two house curves plus a press and a spring. Adam caps this at three
     in new work; V4's four are grandfathered and kept because the press curve
     is load-bearing on every key. */
  --ease-entrance: cubic-bezier(.23,1,.32,1);
  --ease-hover:    cubic-bezier(.33,1,.68,1);
  --ease-press:    cubic-bezier(.22,.61,.36,1);
  --ease-spring:   cubic-bezier(.34,1.56,.64,1);
  --d-press: calc(70ms  * var(--dur));
  --d-fast:  calc(140ms * var(--dur));
  --d-base:  calc(280ms * var(--dur));
  --d-slow:  calc(400ms * var(--dur));

  /* Type roles. Hero is LOCKED to Special Gothic Expanded One (Knox, 23-08).
     The other four are console dials. */
  --f-hero:  'Special Gothic Expanded One', 'Arial Black', sans-serif;
  --f-head:  'Special Gothic Expanded One', 'Arial Black', sans-serif;
  --f-ctrl:  'Archivo', system-ui, sans-serif;
  --f-body:  'Figtree', system-ui, sans-serif;
  --f-num:   'Special Gothic Expanded One', 'Arial Black', sans-serif;   /* Knox, 27-08-2026 */
  /* ── NUMERALS, AND WHY THERE ARE TWO OF THEM ──────────────────────────────
     CHANGED 27-08-2026. Urbanist is gone. It was never chosen for this job: in
     V4 the BODY face was Urbanist and `--f-num` was that same face at 700, so
     numerals were simply the body, bolded. V5 moved the body to Figtree and
     left the numerals behind, and Urbanist became an orphan that existed only
     for numbers. It is also, measured against 28 faces on 27-08, one of only
     three with NO digit alignment at all: 000000 runs 91.92px wider than
     111111 at 60px, and `tabular-nums` changes nothing. Forty-four
     `tabular-nums` declarations in this file were doing nothing.

     Special Gothic Expanded One is the numeral face now, which ties the number
     to the display voice.

     IT CANNOT HOLD A COLUMN, and that is measured, not assumed. Per-digit
     advance at 60px runs 28.74px for the 1 against 54.72px for the 0.
     `tabular-nums` is a PARTIAL feature in this face: it equalises the 1 and
     the 7 and leaves a 7.50px spread across the rest. Five money strings of
     identical length land 13px apart at a 13px font size, which is a whole
     character of disagreement down the right edge of a P&L. Geist Mono lands
     them at 0.00px. It is also 24% wider, so it eats a dense table.

     So: `--f-num` is the DISPLAY numeral, for a value that stands alone. A KPI,
     a count on a chip, a price on a key, a duration on a thumbnail. Nothing
     sits under it to line up with, and the wide face reads as brand.

     `--f-num-col` is the COLUMN numeral, for anything that stacks and has to
     agree down the right edge. Twelve rules in this file, and every one of them
     already declared `tabular-nums`, which is the previous author saying this
     same thing without a token to say it with.

     The test is not "is this a number". It is "is there another number directly
     above or below this one". */
  --f-num-col: 'Geist Mono', ui-monospace, monospace;
  --f-brand: 'Special Gothic Expanded One', 'Arial Black', sans-serif;  /* eyebrows, stamps, brand moments */
  --f-ui:    var(--f-ctrl);
  --f:       var(--f-body);
  --fm:      'Geist Mono', ui-monospace, monospace;
  --num-wt:  400;    /* Special Gothic Expanded One ships ONE weight. 400, 600 and 800
                        all measure 461.59px on the same string, so the 700 that sat here
                        was inert on every one of the 54 rules that read it. */
  --num-wt-col: 500; /* the mono does need 500 */

  /* ── NOISE, ADDED V7 ─────────────────────────────────────────────────────
     V6 hardcoded grain opacity at eight sites, at seven different values
     between .03 and .06, each chosen by eye against its own surface. Those
     ratios are kept EXACTLY. --noise-o is a multiplier over all of them, so
     at the default of 1 every site renders the identical value it did in V6
     and the diff against dist-v6 is empty. Turn it to 0 for no grain, or up
     for more, and the relationship between surfaces holds. */
  /* ── SEED SIX · MEDIA, ADDED 30-08-2026 ──────────────────────────────────
     Knox: the colour on the reference boxes is the wrong accent for this
     palette. He is right, and it was worse than wrong: those three values were
     lifted from example markup in a wireframe and existed nowhere in the token
     system, so three colours were shipping in an instrument that no dial could
     reach and no export could carry.

     A media seed is not an accent. It is the ground an image-shaped surface
     shows when there is no image yet: a reference slot, a building proxy, an
     empty tile. It must read as a held space rather than as a state, so it sits
     off the accent entirely and carries no meaning.

     References need to be told apart, so the seed generates a FAMILY: each slot
     rotates a fixed step off the seed hue, deterministic by index, so reference
     two is the same tint every session. One seed, one dial, N slots. */
  --seed-media: #2A2E33;
  /* Hue rotation per slot. The family REPEATS after five: fourteen references
     at a fixed step walks 338 degrees, which is a rainbow, and a rainbow of
     held spaces is the multicoloured-status tell in another costume. Five
     tints is enough to tell neighbours apart while the tray still reads as one
     tonal set. The slot sets --i to its index modulo five. */
  --media-step: 26deg;
  --media-family: 5;

  --noise-o: 1;

  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  --grad-accent: linear-gradient(160deg in oklab, var(--seed-accent-hi) 0%,
    color-mix(in oklab, var(--seed-accent-hi) 45%, var(--seed-accent-lo)) 48%,
    var(--seed-accent-lo) 100%);
}

/* ── LAYER 1 · DARK DERIVATION ───────────────────────────────────────────── */
:root, [data-theme="dark"] {
  color-scheme: dark;

  --bg:            var(--seed-ink);
  --surface:       color-mix(in oklch, var(--seed-ink) 96%, white);
  /* THE DEEPEST SURFACE IS THE GROUND, AND IT HAS NO HUE. CHANGED 31-08-2026.

     This was `color-mix(in oklch, var(--seed-ink) 85%, var(--seed-accent-lo))`,
     which reads as "tint the recess toward the brand" and is a reasonable idea
     that turned out to be a trap. It hands the recess to whatever an app names
     as the LOW end of its accent gradient, and that end is chosen to be a dark
     partner for the bright end, not to be a surface. Darkroom's is `#010029`,
     a near-black navy, so every sunken thing in a red-accented app came out
     cold blue: `.pwell`, `.well`, slider tracks, and the viewer's own wells.
     Measured at oklch(0.1545 0.0130 267) inside a panel at oklch(0.2347 0 0).

     Knox saw it on the viewer's prompt box and said it was not following the
     design system. He was right, and the tell was already in this file: the
     LIGHT theme's `--surface-deep` is `color-mix(var(--seed-paper) 93%, black)`
     - no accent, no hue, just further from the light. The two themes disagreed
     about what sunken means and only one of them had a colour in it.

     So the recess is the ground. A raised surface is the ink lifted toward
     white; the deepest one is the ink, unlifted. Nothing in the palette needs a
     hue to say "this is below the thing around it", and a tinted box behind
     text competes with the one accent in the panel that is doing work. */
  --surface-deep:  var(--seed-ink);
  --surface-raise: color-mix(in oklch, var(--seed-ink) 91%, white);
  --ctrl:          color-mix(in oklch, var(--seed-ink) 80%, white);
  --ctrl-hi:       color-mix(in oklch, var(--seed-ink) 78%, white);
  --hover:         color-mix(in oklch, var(--seed-ink) 91%, white);
  --grad-ground: linear-gradient(172deg, color-mix(in oklch, var(--seed-ink) 93%, white) 0%, var(--surface) 40%, var(--surface-deep) 100%);

  --text:    color-mix(in oklch, var(--seed-ink) 12%, white);
  --muted:   color-mix(in oklch, var(--seed-ink) 42%, white);
  --subtle:  color-mix(in oklch, var(--seed-ink) 58%, white);
  /* --subtle IS NOT READABLE, AND THAT IS NOT A BUG IN IT. Measured in OKLCH
     from the seeds: 3.23:1 on --surface, 3.42:1 on --surface-deep and 2.93:1 on
     --surface-raise. AA wants 4.5:1 for anything under 18.66px bold, and every
     job --subtle holds is 9px to 13px.

     So it gets the treatment the status colours already have, and for the same
     reason: --danger is a fill and --danger-text is the readable version, because
     a red that works as a 3px bar measures 2.97:1 as 13px type. One token cannot
     be both the quiet tier and a legible one.

     --subtle KEEPS the jobs it is right for and nothing changes for them:
     placeholders, calendar days outside the month, decorative separators, the
     disabled rail. Those are meant to recede and brightening them would be the
     actual regression.

     --subtle-text carries anything set as WORDS: table headers, field help, the
     breadcrumb, rail group labels, empty-state copy.

     ON DARK IT LANDS ALMOST ON --muted, AND THAT IS THE HONEST ANSWER rather
     than a mistake. A dark ground compresses the bottom of the range: --muted is
     42% and the AA floor is 44%, so there is no readable tier below muted worth
     naming. On light there IS room, so the light value stays a visible step
     below its --muted. Do not "correct" the asymmetry.

     Added 27-08-2026, from the Knox Artiste Accounts V5 pass, where the app had
     been quietly patching this in eight places on its own since V4. */
  --subtle-text: color-mix(in oklch, var(--seed-ink) 44%, white);
  --disabled:color-mix(in oklch, var(--seed-ink) 72%, white);

  --line: rgba(255,255,255,.08);
  --line-strong: rgba(255,255,255,.15);
  --rim: rgba(255,255,255,.15);

  --accent-text: color-mix(in oklch, var(--seed-accent-hi) 45%, white);
  /* OUTLINES AND LINK TEXT ONLY. Half white, so it is bright enough to read as a
     ring on the dark ground. Never use it as a fill: it washes the brand out. */
  --accent-live: color-mix(in oklch, var(--seed-accent-hi) 50%, white);
  /* THE fill for anything that means "this is on". */
  --accent-lit:  color-mix(in oklch, var(--seed-accent-hi) 96%, white);
  /* A SECOND fill tone, for a chart segment or a track that has to sit beside the
     first. Derived from the accent's own dark end, so it stays in the family. */
  --accent-deep: color-mix(in oklab, var(--seed-accent-hi) 55%, var(--seed-accent-lo));
  /* ── THE CHART PALETTE ──
     The ONLY place in the system that carries more than one hue. A legend has to
     be matchable to a wedge, and three tints of one red are not.

     THESE ARE ANCHORED, NOT ROTATED, and the reason is measured. The first cut
     derived both secondaries by rotating the accent's own hue, which is elegant
     and wrong: rotating an arbitrary accent by a fixed angle eventually lands in
     the purple band, and across the eleven lab systems it did, twice. It also
     multiplied the accent's CHROMA, so a deliberately near-neutral system got
     three greys that no legend could be matched to.

     So the two secondaries sit at fixed teal and blue, with their own chroma,
     and only chart-1 follows the brand. --chart-2-h and --chart-3-h are the
     escape hatch: a system whose accent is ITSELF teal or blue re-points them
     rather than forking the component.

     --chart-1-h is the third hatch and only a near-neutral system needs it: when
     the accent has no meaningful hue of its own the browser hands back whatever
     residue is left in it, which drifted into purple on one system and swung 100
     degrees between the two modes on another. Pinning the HUE keeps lightness
     and chroma still adapting per mode, which a flat colour override throws
     away. */
  --chart-2-h: 174;
  --chart-3-h: 254;
  /* max() on the chroma is the floor that stops a neutral system going grey. */
  --chart-1: oklch(from var(--accent-lit) clamp(.44, l, .68) max(c, .085) var(--chart-1-h, h));
  --chart-2: oklch(.56 .115 var(--chart-2-h));
  --chart-3: oklch(.55 .125 var(--chart-3-h));
  --accent-tint: color-mix(in srgb, var(--seed-accent-hi) 14%, transparent);
  /* Text on an accent fill. Knox asked whether this white was the paper colour
     or one picked by eye: it was neither, five different times. It now comes
     from --seed-paper, so change the paper and every label on every accent fill
     follows. Nothing on an accent is a literal white again. */
  --on-accent:   color-mix(in srgb, var(--seed-paper) 58%, white);

  /* Status family. V4 had ONE status colour. All four derive from one seed by
     hue rotation, so it stays a single dial. */
  --danger:  var(--seed-status);
  --warning: oklch(from var(--seed-status) calc(l + .11) calc(c * .82) 78);
  --success: oklch(from var(--seed-status) calc(l + .08) calc(c * .60) 152);
  --info:    oklch(from var(--seed-status) calc(l + .06) calc(c * .55) 236);
  /* Readable on the dark ground: lift lightness, drop chroma so it does not glare. */
  --danger-text:  oklch(from var(--danger)  clamp(.72, calc(l + .16), .93) calc(c * .82) h);
  --warning-text: oklch(from var(--warning) clamp(.72, calc(l + .10), .93) calc(c * .82) h);
  --success-text: oklch(from var(--success) clamp(.72, calc(l + .10), .93) calc(c * .82) h);
  --info-text:    oklch(from var(--info)    clamp(.72, calc(l + .10), .93) calc(c * .82) h);
  --alert: var(--danger); --err: var(--danger);

  /* Elevation, six tiers. V4 had two shadows and no scale.
     Dark mode: the SURFACE lifts, the shadow only occludes. */
  --e0: none;
  --e1: inset 0 1px 0 0 rgba(255,255,255,calc(.05 * var(--elev))),
        0 0 0 1px rgba(255,255,255,calc(.05 * var(--elev)));
  --e2: inset 0 1px 0 0 rgba(255,255,255,calc(.06 * var(--elev))),
        0 0 0 1px rgba(255,255,255,calc(.06 * var(--elev))),
        0 2px 8px -4px rgba(0,0,0,calc(.50 * var(--elev)));
  --e3: inset 0 1px 0 0 rgba(255,255,255,calc(.07 * var(--elev))),
        0 0 0 1px rgba(255,255,255,calc(.08 * var(--elev))),
        0 8px 24px -12px rgba(0,0,0,calc(.60 * var(--elev))),
        0 16px 40px -20px rgba(0,0,0,calc(.50 * var(--elev)));
  --e4: inset 0 1px 0 0 rgba(255,255,255,calc(.08 * var(--elev))),
        0 0 0 1px rgba(255,255,255,calc(.09 * var(--elev))),
        0 16px 40px -16px rgba(0,0,0,calc(.70 * var(--elev))),
        0 40px 80px -32px rgba(0,0,0,calc(.60 * var(--elev)));
  --e5: var(--e4), 0 80px 140px -48px rgba(0,0,0,calc(.55 * var(--elev)));
  --e-sunk: inset 0 2px 6px rgba(0,0,0,calc(.55 * var(--elev))), inset 0 -1px 0 rgba(255,255,255,.05);
  --sh: var(--e3); --sh-sm: var(--e2); --well: var(--e-sunk);
  --extrude: 0 7px 16px -5px rgba(0,0,0,calc(.55 * var(--elev))), 0 1px 0 rgba(255,255,255,.08) inset;
  /* The instrument's rest shadow: it sits above the canvas, it does not float off it. */
  --e-rest: 0 18px 44px -16px rgba(0,0,0,calc(.62 * var(--elev))), inset 0 1px 0 rgba(255,255,255,.06);

  --edge-ring: linear-gradient(155deg, rgba(255,255,255,.20) 0%, rgba(255,255,255,.06) 30%, rgba(255,255,255,0) 52%, rgba(0,0,0,.16) 100%);
  --glass-bg:     color-mix(in srgb, var(--surface) 42%, transparent);
  --glass-border: rgba(255,255,255,.13);
  --glass-hi:     rgba(255,255,255,.17);
  --scrim: color-mix(in srgb, var(--seed-ink) 62%, transparent);
}

/* ── LAYER 1b · LIGHT DERIVATION ─────────────────────────────────────────── */
[data-theme="light"] {
  color-scheme: light;
  /* V4 hard-coded a second status hex here (#B64D14). That broke the moment the
     console wrote --seed-status inline, because an inline value on <html> beats
     a [data-theme] rule and the light correction silently stopped applying.
     Light mode now DERIVES its status from the same seed, so re-theming reaches
     both modes and there is only ever one status dial. */

  --bg:            var(--seed-paper);
  --surface:       color-mix(in oklch, var(--seed-paper) 42%, white);
  --surface-deep:  color-mix(in oklch, var(--seed-paper) 93%, black);
  --surface-raise: color-mix(in oklch, var(--seed-paper) 26%, white);
  --ctrl:          color-mix(in oklch, var(--seed-paper) 89%, black);
  --ctrl-hi:       color-mix(in oklch, var(--seed-paper) 85%, black);
  --hover:         color-mix(in oklch, var(--seed-paper) 88%, black);
  --grad-ground: linear-gradient(172deg, color-mix(in oklch, var(--seed-paper) 30%, white) 0%, var(--surface) 40%, color-mix(in oklch, var(--seed-paper) 97%, black) 100%);

  --text:    color-mix(in oklch, var(--seed-paper) 24%, black);
  --muted:   color-mix(in oklch, var(--seed-paper) 46%, black);
  --subtle:  color-mix(in oklch, var(--seed-paper) 56%, black);
  --subtle-text: color-mix(in oklch, var(--seed-paper) 50%, black);   /* 4.61:1 worst case */
  --disabled:color-mix(in oklch, var(--seed-paper) 72%, black);

  --line: rgba(25,40,48,.10);
  --line-strong: rgba(25,40,48,.18);
  --rim: rgba(255,255,255,.95);

  --accent-text: var(--seed-accent-hi);
  --accent-live: var(--seed-accent-hi);
  --accent-lit:  var(--seed-accent-hi);
  --accent-deep: color-mix(in oklab, var(--seed-accent-hi) 55%, var(--seed-accent-lo));
  --chart-1: oklch(from var(--accent-lit) clamp(.36, l, .58) max(c, .095) var(--chart-1-h, h));
  --chart-2: oklch(.46 .120 var(--chart-2-h));
  --chart-3: oklch(.43 .145 var(--chart-3-h));
  --accent-tint: color-mix(in srgb, var(--seed-accent-hi) 11%, transparent);
  --on-accent:   color-mix(in srgb, var(--seed-paper) 70%, white);

  --danger:  color-mix(in oklch, var(--seed-status) 84%, black);
  --warning: oklch(from var(--seed-status) calc(l - .16) calc(c * .92) 72);
  --success: oklch(from var(--seed-status) calc(l - .18) calc(c * .62) 150);
  --info:    oklch(from var(--seed-status) calc(l - .16) calc(c * .58) 240);
  /* Readable on paper: drive lightness DOWN hard. A gold that reads as a bar at
     l .62 is unreadable as 13px text; it has to come to roughly l .45. */
  --danger-text:  oklch(from var(--danger)  clamp(.30, calc(l - .16), .50) calc(c * .96) h);
  --warning-text: oklch(from var(--warning) clamp(.30, calc(l - .16), .50) calc(c * .96) h);
  --success-text: oklch(from var(--success) clamp(.30, calc(l - .14), .50) calc(c * .96) h);
  --info-text:    oklch(from var(--info)    clamp(.30, calc(l - .14), .50) calc(c * .96) h);
  --alert: var(--danger); --err: var(--danger);

  /* Light mode: the surface holds still, the shadow carries the whole signal. */
  --e0: none;
  --e1: 0 0 0 1px rgba(15,32,39,calc(.045 * var(--elev))),
        0 1px 1px -.5px rgba(15,32,39,calc(.06 * var(--elev))),
        0 2px 4px -1.5px rgba(15,32,39,calc(.06 * var(--elev)));
  --e2: 0 0 0 1px rgba(15,32,39,calc(.045 * var(--elev))),
        0 1px 1px -.5px rgba(15,32,39,calc(.06 * var(--elev))),
        0 3px 6px -2px rgba(15,32,39,calc(.07 * var(--elev))),
        0 8px 16px -6px rgba(15,32,39,calc(.06 * var(--elev)));
  --e3: 0 0 0 1px rgba(15,32,39,calc(.05 * var(--elev))),
        0 4px 8px -3px rgba(15,32,39,calc(.06 * var(--elev))),
        0 12px 24px -8px rgba(15,32,39,calc(.08 * var(--elev))),
        0 24px 40px -16px rgba(15,32,39,calc(.06 * var(--elev)));
  --e4: 0 0 0 1px rgba(15,32,39,calc(.05 * var(--elev))),
        0 8px 16px -6px rgba(15,32,39,calc(.08 * var(--elev))),
        0 24px 48px -16px rgba(15,32,39,calc(.12 * var(--elev))),
        0 48px 96px -32px rgba(15,32,39,calc(.10 * var(--elev)));
  --e5: var(--e4), 0 80px 160px -48px rgba(15,32,39,calc(.14 * var(--elev)));
  --e-sunk: inset 0 2px 5px rgba(15,32,39,calc(.16 * var(--elev))), inset 0 -1px 0 rgba(255,255,255,.75);
  --sh: var(--e3); --sh-sm: var(--e2); --well: var(--e-sunk);
  --extrude: 0 9px 20px -7px rgba(15,32,39,calc(.20 * var(--elev))), 0 1px 0 rgba(255,255,255,.95) inset;
  --e-rest: 0 18px 44px -18px rgba(15,32,39,calc(.30 * var(--elev))), inset 0 1px 0 rgba(255,255,255,.85);

  --edge-ring: linear-gradient(155deg, rgba(255,255,255,.95) 0%, rgba(255,255,255,.40) 32%, rgba(15,32,39,.05) 68%, rgba(15,32,39,.13) 100%);
  --glass-bg:     color-mix(in srgb, var(--surface) 62%, transparent);
  --glass-border: rgba(255,255,255,.70);
  --glass-hi:     rgba(255,255,255,.95);
  --scrim: color-mix(in srgb, var(--seed-ink) 44%, transparent);
}

/* ── BROWSER RESET · nothing default ships ──────────────────────────────── */
* { margin:0; padding:0; box-sizing:border-box; }
html { transition: background-color var(--d-base) var(--ease-hover); -webkit-text-size-adjust:100%; }
body { background:var(--bg); color:var(--text); font-family:var(--f); font-size:var(--fs-md);
  font-weight:450; line-height:var(--lh-body); min-height:100dvh; -webkit-font-smoothing:antialiased;
  transition: background-color var(--d-base) ease, color var(--d-base) ease; }
[data-theme="light"] body { font-weight:400; }
body::after { content:""; position:fixed; inset:0; z-index:2; pointer-events:none;
  background-image:var(--noise); background-size:140px; opacity:calc(.03 * var(--noise-o)); mix-blend-mode:overlay; }
[data-theme="light"] body::after { opacity:.02; mix-blend-mode:multiply; }
img, svg, video { max-width:100%; display:block; }
button, input, select, textarea { font:inherit; color:inherit; background:none; border:none; }
button { cursor:pointer; }
a { color:var(--accent-text); }
a:hover { color:var(--accent-live); }
/* NOTHING is selectable by default. Only real text fields are, because only a
   field holds content the user typed and may legitimately want to copy. */
* { -webkit-tap-highlight-color:transparent; -webkit-user-select:none; user-select:none; }
/* `.vs-read` JOINS THE LIST, 30-08-2026, and it belongs by the rule's own
   words. The rule above says only real text fields are selectable "because only
   a field holds content the user typed and may legitimately want to copy" - and
   a prompt readout is exactly that content, being read back. Knox: "I should be
   able to select part of the prompt from its text to copy it, not just the
   entire prompt." The Copy button takes all of it; selection is how you take a
   clause. Without this the global rule silently made that impossible and the
   readout looked broken rather than protected. */
input, textarea, [contenteditable="true"], .well-input, .t6-edit, .stepper input,
.vs-read {
  -webkit-user-select:text; user-select:text; }
/* An outline follows the element's OWN corner, so anything that can take focus
   needs a radius even when it has no visible border. `border-radius:inherit`
   here was wrong twice over: it changes the ELEMENT, not the ring, and it
   inherits from the parent rather than matching the shape you can see. */
:focus-visible { outline:2px solid var(--accent-live); outline-offset:2px; }
/* A field inside a well does NOT draw its own ring. The well carries focus, and
   two indicators for one event is one too many. This is also why the ring looked
   square: the input has no corner, the well has all of it. */
/* A CONTAINER THAT LOOKS LIKE A FIELD SHOWS THE FOCUS, NOT THE INPUT INSIDE IT.
   The ring belongs on the thing the eye reads as the control. Drawing it on the
   bare <input> puts a hard rectangle inside a pill and the caret becomes the
   least visible thing in it.

   .tsearch and .srch were MISSING from this list, so every top bar in the
   estate drew a browser-style box on click instead of just showing a cursor.
   Fixed 27-08-2026, in the system rather than per app: Knox should not have to
   say this again on the next build. Any new field-shaped container goes in this
   list at the same time it is written. */
.well :is(input, textarea, button, [contenteditable]):focus-visible,
.well:focus-within :is(input, textarea),
.tsearch :is(input, button):focus-visible,
.tsearch:focus-within input,
.srch :is(input, button):focus-visible,
.srch:focus-within input { outline:none; }

/* And the container takes it instead, the same way a well deepens. */
.tsearch:focus-within { box-shadow:var(--e-sunk), 0 0 0 2px color-mix(in srgb, var(--seed-accent-hi) 45%, transparent); }
/* The well's own focus is unmistakable without a ring: the rim takes the accent
   and the trough deepens. */
.well:focus-within:has(:is(input, textarea, [contenteditable]):focus) {
  border-color:var(--accent-live);
  box-shadow:var(--e-sunk), inset 0 0 0 1px color-mix(in srgb, var(--seed-accent-hi) 45%, transparent); }
/* Anything focusable that renders square gets a corner for the ring to follow. */
.rowitem, .optrow, .frow, .check, .ckrow, .tree-row, .cmdk .crow,
.menu button, .drop-menu button, .combo-list button, .sp-menu button {
  border-radius:calc(var(--r-xs) * .8); }
.rowitem:focus-visible, .frow:focus-visible { outline-offset:-2px; }
::selection { background:color-mix(in srgb, var(--seed-accent-hi) 30%, transparent); }
* { scrollbar-width:thin; scrollbar-color:var(--line-strong) transparent; }
*::-webkit-scrollbar { width:9px; height:9px; }
*::-webkit-scrollbar-thumb { background:var(--line-strong); border-radius:99px; border:2px solid transparent; background-clip:content-box; }
*::-webkit-scrollbar-track { background:transparent; }
/* ══ THE FOUR APPS ═════════════════════════════════════════════════════════
   ADDED 28-08-2026, and this is the point of LAYER 0c.

   Every app runs the SAME components, the SAME type, the SAME depth and the
   SAME three breakpoints. What differs between an accounting ledger and a
   generation canvas is density and reading width, and that is all that is
   allowed to differ. Set two attributes on <html> and stop.

     <html data-app="accounts" data-density="compact">

   ┌──────────────┬──────────┬─────────────┬───────────┬──────────────────────┐
   │ App          │ density  │ canvas cap  │ panel     │ why                  │
   ├──────────────┼──────────┼─────────────┼───────────┼──────────────────────┤
   │ Accounts     │ compact  │ 1560px      │ 320px     │ a P&L is a wall of   │
   │              │          │             │           │ numbers; rows should │
   │              │          │             │           │ be thin and the tabl │
   │              │          │             │           │ e should breathe wid │
   │              │          │             │           │ e. Knox: "the line   │
   │              │          │             │           │ item rows are far to │
   │              │          │             │           │ o thick."            │
   │ Grid         │ default  │ none, full  │ 400px     │ a content calendar   │
   │              │          │ bleed       │ (drawer)  │ is a wall; capping   │
   │              │          │             │           │ it wastes the width. │
   │              │          │             │           │ Its own design pack  │
   │              │          │             │           │ said full bleed and  │
   │              │          │             │           │ the build capped at  │
   │              │          │             │           │ 1440 anyway.         │
   │ Darkroom     │ comfort- │ none, full  │ 320px     │ the frame IS the     │
   │              │ able     │ bleed       │           │ content. Chrome      │
   │              │          │             │           │ recedes, the canvas  │
   │              │          │             │           │ takes the room.      │
   │ Pulse        │ default  │ 1560px      │ 400px     │ mixed: lists, kanban │
   │              │          │             │           │ call sheets, gear.   │
   │              │          │             │           │ Nothing extreme in   │
   │              │          │             │           │ either direction.    │
   └──────────────┴──────────┴─────────────┴───────────┴──────────────────────┘

   The rail is 218px in all four, because it already is in three and the fourth
   is 216 by accident. Canvas padding is the same in all four, because there was
   never a reason for 20/30/60 and 20/26/60 and 18/20/22 to differ. Nobody chose
   any of that; four sessions typed four numbers. */
[data-app="accounts"] { --canvas-max: 1560px; --panel-w: 320px; }
[data-app="grid"]     { --canvas-max: none;   --panel-w-lg: 400px; }
[data-app="darkroom"] { --canvas-max: none;   --panel-w: 320px; }
[data-app="pulse"]    { --canvas-max: 1560px; --panel-w-lg: 400px; }

/* ══ DENSITY ═══════════════════════════════════════════════════════════════
   ADDED 28-08-2026. Set once, on <html data-density>, and never referenced
   again. Everything downstream is already derived from --u and --ctrl-h, so
   this is a two-line change per app rather than a re-skin.

   WHY THIS BELONGS TO THE SYSTEM AND NOT TO THE APPS. Grid, on its own build:
   "it's too chunky, everything is way too boxy, the layout is not well
   planned." Accounts, on its line items: "the line item rows are far too
   thick." Both are the same request, made a month apart, about the same
   missing dial. Without it an app can only reach the density it wants by
   retyping padding on every component, which is precisely how Grid ended up
   with 888 lines of raw pixels and no token scale at all. */
[data-density="compact"]     { --u: 4px; --ctrl-h: 32px; --row-h: 36px; }
[data-density="comfortable"] { --u: 6px; --ctrl-h: 40px; --row-h: 52px; }

/* ══ THE TOUCH FLOOR ═══════════════════════════════════════════════════════
   A 36px control is right under a mouse and wrong under a thumb, and a 29px
   filter chip is wrong under both. Measured at 375px on this file: 133 distinct
   controls under the 44px floor, including every .b-sm, every .fchip and the
   theme keys.

   This is not a mobile stylesheet. It keys off the POINTER, not the width, so
   a touch laptop and a tablet in landscape get it too and a narrow desktop
   window does not. Density still applies on top: a compact app on a phone
   lands at 44, not at 32. */
@media (pointer: coarse) {
  :root { --ctrl-h: 44px; --row-h: 48px; }
  .b-sm  { height: 40px; }
  .b-xs  { height: 36px; }
  .fchip { padding: 12px 16px; }
  .b-icon.b-sm { width: 40px; height: 40px; }
  /* Anything that reads as a row gets the floor whether or not it is a button. */
  .rail a, .mrow, .optrow, .crow, .drop-menu button, .menu button, .combo-list button
    { min-height: 44px; }}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration:.01ms !important; animation-iteration-count:1 !important;
    transition-duration:.01ms !important; scroll-behavior:auto !important; }}
h1,h2,h3,h4 { max-width:100%; text-wrap:balance; }
.eyebrow { font-family:var(--f-brand); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--accent-text); }
h1 { font-family:var(--f-hero); font-size:var(--fs-display); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-display); line-height:var(--lh-display); margin-top:var(--sp-3); }
.note { font-size:var(--fs-sm); color:var(--subtle); }
.mono { font-family:var(--fm); font-size:var(--fs-sm); color:var(--muted); }
/* Section label inside a card. Lived in the console block in the lab, so it came
   out with it; the reference sections still use it. */
.c-lab { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle); margin-bottom:var(--sp-2); }
.stack { display:flex; flex-direction:column; gap:var(--sp-3); }
.row { display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap; }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-4); }
.grid3 { display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-4); }
@media (max-width:760px){ .grid2,.grid3 { grid-template-columns:1fr; }}
/* A grid item defaults to min-width:auto, so its min-content can push the whole
   column past the viewport. This is what overflowed the page at 375px. */
.grid2 > *, .grid3 > *, .kpi > *, .kb > *, .media-grid > * { min-width:0; }
/* The genuinely wide pieces then scroll inside themselves rather than the page. */
.filing, .steps, .timep { max-width:100%; }
.steps { overflow-x:auto; }
.cal, .iconrow { max-width:100%; }
.frow { min-width:0; }
.fname, .fmeta { min-width:0; overflow-wrap:anywhere; }

/* ── SURFACES ───────────────────────────────────────────────────────────── */
.card { background:var(--grad-ground); border-radius:var(--r-lg);
  padding:var(--sp-5); box-shadow:var(--e2); }
.card, .t6-wrap, .kb-card, .modal, .wayfind, .pillar, .ramp > div { position:relative; isolation:isolate; }

/* ══ THE LIFT LAW ══════════════════════════════════════════════════════════
   ADDED 28-08-2026. Knox: "there are some dropdowns that are falling behind
   certain elements."

   Reproduced, in a browser, on this file's own CSS: a `.drop` near the bottom
   of a `.card`, with a second `.card` below it. The menu opens at
   `--z-drop: 40` and the next card cuts straight through it. The screenshot is
   unambiguous and the cause is one word on the line above.

   `isolation:isolate` is CORRECT there and must stay: `.card::after` carries
   the grain on `mix-blend-mode:overlay`, and without isolation that grain
   blends through to the page instead of to the card. The isolation is what
   contains it.

   But isolation creates a STACKING CONTEXT, and a stacking context is a
   sealed box. Inside `.card`, `z-index: 40` means "fortieth of the things in
   this card". It does not mean "above the next card". The card itself sits at
   `z-index: auto`, so the card that follows it in the DOM paints on top of the
   whole box, menu included. Every value from 40 to 2147483647 loses this,
   which is exactly why three apps reached for 1100, 120 and 50 and why none
   of them fixed it.

   You cannot win this from inside the box. You have to raise the box.

   So: a stacking-context host holding an open popover lifts above its siblings
   for exactly as long as it is open, and drops back the moment it closes.
   `:has()` does it with no JavaScript, no portal, and nothing for an app to
   remember, which is the only kind of fix that survives four apps. */
:is(.card, .band, .t6-wrap, .kb-card, .modal, .wayfind, .pillar, .instrument,
    .media-card, .proof, .seg, .split, .ground, .phoneframe, .screen > *,
    .frow, .panel, .well, .ramp > div, .topb, .rail, .cset):has(
      :is(.drop, .combo, .split, .engwrap, .who).open,
      :is(.drop-menu, .combo-list, .sp-menu, .menu, .pop, .engmenu).open
    ) { position: relative; z-index: var(--z-lift); }
/* AND THE HOST THAT IS ITSELF THE OPEN THING. `:has()` only looks DOWN. A
   `.split` carries `.open` on itself and holds the menu inside, so
   `.split:has(.split.open)` is false and the split-button menu kept landing
   under the button row beside it. Second selector, same law. */
:is(.split, .drop, .combo, .engwrap, .who, .cset).open
  { position: relative; z-index: var(--z-lift); }
/* NO WHITESPACE BEFORE :has(). Written with a space on the first pass, which
   is a descendant combinator: it lifted the host's CHILDREN and left the host
   exactly where it was, so the fix tested identical to the bug. A selector that
   looks right and matches the wrong element is the same class of failure as the
   `.drop.open .drop-menu` ancestor gate two sections up. Both were found by
   rendering it, not by reading it. */

/* ══ THE CLIP LAW ══════════════════════════════════════════════════════════
   The lift answers a menu COVERED by a sibling. It cannot answer a menu CUT
   OFF by an ancestor, because `overflow:hidden` clips regardless of z-index.
   Both failures look identical to Knox and they have different fixes, which is
   why they kept getting solved one at a time and coming back.

   A container clips for one of two reasons and the fix is different for each.
   Getting this wrong is not theoretical: applying `.popsafe` to the wrong one
   during this very pass put 115px of table off the right edge at 375px, on a
   file that had zero horizontal overflow at every width a minute earlier.

     CLIPS FOR THE CORNER. `overflow:hidden` is masking square children inside a
     rounded box, and nothing in there was ever going to scroll. Use `.popsafe`.
     The corner gets drawn on the cells instead. Free, no JavaScript.

     CLIPS FOR THE SCROLL. The content is genuinely wider or taller than the
     box: a table on a phone, a review strip, a side panel. `.popsafe` CANNOT
     help here and will make it worse, because CSS has no way to keep one axis
     clipped and let the other escape. Setting `overflow-y:visible` next to
     `overflow-x:hidden` silently computes the visible axis to `auto`. Use
     knoxPop() and let the menu leave the container entirely.

   If you are unsure which one you have: does anything in the box need to
   scroll, at any width, ever? Yes is the portal. No is `.popsafe`.

   Where the container MUST keep clipping, because it genuinely scrolls, the
   menu leaves the container. That is what --z-drop-over and the
   `.drop-menu.open` element-level twin exist for, and knoxPop() below now
   ships the portal so the fifth app does not write a fourth version of it. */
/* Doubled class, deliberately. `.popsafe` and `.t6-wrap` are both one class,
   so source order decides, and every clipping container in this file is defined
   BELOW this line. `.popsafe.popsafe` is 0,2,0 and wins on specificity without
   reaching for !important, which would then have to be beaten by the next
   person who needs a genuine scroll container. */
.popsafe.popsafe { overflow: visible; }

/* ══ THE CLOSED MENU ═══════════════════════════════════════════════════════
   ADDED 28-08-2026, and it is a THIRD failure wearing the same coat.

   Every popover in this system hides with `opacity:0` and
   `pointer-events:none`. It is still laid out. An absolutely positioned box
   that is 330px wide and invisible still extends the scrollable area of the
   document, and a 250px tall one still counts toward a dialog's scrollHeight.

   Both have already shipped. Accounts: Knox reported the expense modal three
   times, "text spacing is out of place", "goes past the window and cuts off",
   "both horizontally and vertically scrollable". The cause written into that
   app's own notes is "A CLOSED DROPDOWN MENU IS 250PX OF INVISIBLE HEIGHT".
   And releasing one clip during this pass put 85px of invisible engine menu off
   the right edge at 375px on a file that had none a minute before.

   `visibility:hidden` does not fix it: a hidden box is still in the overflow
   region. Only `display:none` removes it, and `display` used to be
   untransitionable, which is why nobody used it and why every app grew this bug
   independently.

   It is transitionable now. `transition-behavior:allow-discrete` plus
   `@starting-style` gives the fade AND the removal. A browser too old for it
   snaps instead of fading, which is a smaller defect than a menu that
   silently widens the page. */
:is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop) {
  display: none;
  transition-behavior: allow-discrete;
}
:is(.drop, .combo, .split, .engwrap, .who).open > :is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop),
:is(.drop, .combo, .split, .engwrap, .who).open :is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop),
:is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop).open {
  display: block;
}
@starting-style {
  :is(.drop, .combo, .split, .engwrap, .who).open :is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop),
  :is(.drop-menu, .combo-list, .sp-menu, .engmenu, .who-menu, .pop).open {
    opacity: 0;
  }}
/* The corner without the clip. `overflow:hidden` was doing two jobs on
   .t6-wrap: masking the table's square corners, and clipping. Releasing it
   loses the corner, so the corner is drawn where it belongs instead, on the
   cells that actually sit in it. Re-clipping on the table was the first
   attempt and it just moved the clipper one level down. */
.popsafe > table { border-collapse: separate; border-spacing: 0; }
.popsafe > table > thead > tr:first-child > *:first-child { border-top-left-radius: inherit; }
.popsafe > table > thead > tr:first-child > *:last-child  { border-top-right-radius: inherit; }
.popsafe > table > tbody > tr:last-child  > *:first-child { border-bottom-left-radius: inherit; }
.popsafe > table > tbody > tr:last-child  > *:last-child  { border-bottom-right-radius: inherit; }
.card::before, .t6-wrap::before, .kb-card::before, .modal::before, .wayfind::before,
.pillar::before, .ramp > div::before, .seedcard .scw::before {
  content:""; position:absolute; inset:0; border-radius:inherit; padding:1px; pointer-events:none; z-index:3;
  background:var(--edge-ring); -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite:exclude; }
.card::after { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background-image:var(--noise); background-size:120px; opacity:calc(.035 * var(--noise-o)); mix-blend-mode:overlay; }
[data-theme="light"] .card::after { opacity:.028; mix-blend-mode:multiply; }
.glass { position:relative; overflow:hidden; background:var(--glass-bg);
  -webkit-backdrop-filter:blur(30px) saturate(150%); backdrop-filter:blur(30px) saturate(150%);
  border:1px solid var(--glass-border); box-shadow:var(--e3), inset 0 1px 0 var(--glass-hi);
  border-radius:var(--r); }
.glass::before { content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(155deg, var(--glass-hi), transparent 38%); opacity:.45; mix-blend-mode:screen; }
.glass::after { content:""; position:absolute; inset:0; pointer-events:none;
  background-image:var(--noise); background-size:130px; opacity:calc(.05 * var(--noise-o)); mix-blend-mode:overlay; }
/* .well is defined ONCE, with the controls. A second padded copy here was double-padding every input. */

/* ═══ SURFACE FINISH · v3 ═══ machined (V4 default) · liquid glass · soft ═══
   One attribute, three physics. Machined is the carved-hardware read the lab
   already has, so it adds no rules. The other two only re-light SURFACES:
   colour, type and spacing tokens stay untouched, so finishes compose with
   every palette. */
[data-finish="soft"] { --soft-hi:rgba(255,255,255,.045); --soft-lo:rgba(0,0,0,.44); }
[data-theme="light"][data-finish="soft"] { --soft-hi:rgba(255,255,255,.92); --soft-lo:rgba(140,152,160,.40); }
[data-finish="soft"] .card, [data-finish="soft"] .kb-card, [data-finish="soft"] .toast, [data-finish="soft"] .modal {
  border-color:transparent; box-shadow:-5px -5px 12px var(--soft-hi), 7px 9px 20px var(--soft-lo); }
[data-finish="soft"] .well, [data-finish="soft"] .stepper {
  border-color:transparent; box-shadow:inset 3px 3px 7px var(--soft-lo), inset -3px -3px 7px var(--soft-hi); }
[data-finish="soft"] .btn-key { border-color:transparent;
  box-shadow:-3px -3px 8px var(--soft-hi), 4px 5px 12px var(--soft-lo); }
[data-finish="soft"] .btn-key:active {
  box-shadow:inset 2px 2px 6px var(--soft-lo), inset -2px -2px 6px var(--soft-hi); }

[data-finish="glass"] .card, [data-finish="glass"] .kb-card, [data-finish="glass"] .toast,
[data-finish="glass"] .modal, [data-finish="glass"] .drop-menu, [data-finish="glass"] .combo-list {
  background:color-mix(in srgb, var(--surface-raise) 58%, transparent);
  -webkit-backdrop-filter:blur(20px) saturate(160%); backdrop-filter:blur(20px) saturate(160%);
  border-color:var(--glass-border);
  box-shadow:0 12px 30px rgba(0,0,0,.16), inset 0 1px 0 var(--glass-hi); }
[data-finish="glass"] .btn-key {
  background:color-mix(in srgb, var(--ctrl) 55%, transparent);
  -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
  border-color:var(--glass-border);
  box-shadow:inset 0 1px 0 var(--glass-hi), 0 4px 12px rgba(0,0,0,.14); }

/* ═══ CONTROLS ═════════════════════════════════════════════════════════════
   Rebuilt 23-08-2026 on V4's physics after Knox called these boring. The first
   V5 pass turned every control into a soft pill, which is exactly the generic
   look V4 was built to avoid. What comes back: keys with a real seated edge,
   chunky padding, uppercase labels at the tracked micro size, and a press that
   SCALES into a carved well on a calc(70ms * var(--dur)) override rather than nudging down 1px.  */

/* Button character is a dial. Key is V4's machined rounded-rect. */
:root { --btn-r: 11px; --btn-pad: 13px 24px; --btn-fs: var(--fs-caps); --btn-ls: .07em; }

.btn-accent, .btn-key, .btn-ghost, .btn-danger {
  position:relative; display:inline-flex; align-items:center; justify-content:center; gap:9px;
  /* A label that wraps changes the height of the row it sits in. It never wraps. */
  white-space:nowrap;
  /* Every variant carries a 1px border, transparent where it is not drawn, so a
     ghost and a key measure the same. Without this the outlined one is 2px taller
     and a button row never quite lines up. */
  border:1px solid transparent;
  font-family:var(--f-ui); font-size:var(--btn-fs); font-weight:600; letter-spacing:var(--btn-ls);
  text-transform:uppercase; line-height:1; border-radius:var(--btn-r); padding:var(--btn-pad);
  transition:transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease,
             background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover),
             border-color var(--d-fast) var(--ease-hover); }

/* Primary. The Fill Law: oklab stops, DIAGONAL light, veil hover, seated edge. */
.btn-accent { overflow:hidden; isolation:isolate; color:var(--on-accent);
  background:linear-gradient(115deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%,
    var(--seed-accent-hi) 55%, color-mix(in oklab, var(--seed-accent-hi) 82%, black) 100%);
  border:1px solid color-mix(in oklab, var(--seed-accent-lo) 70%, black);
  box-shadow:0 2px 5px rgba(0,0,0,.30), inset 0 1px 0 rgba(255,255,255,.18); }
[data-theme="light"] .btn-accent { box-shadow:0 2px 6px rgba(15,32,39,.22), inset 0 1px 0 rgba(255,255,255,.30); }
.btn-accent::before { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:rgba(255,255,255,.10); opacity:0; transition:opacity calc(160ms * var(--dur)) ease; z-index:-1; }
.btn-accent:hover::before { opacity:1; }
.btn-accent:active { transform:scale(.98); transition-duration:calc(70ms * var(--dur)); box-shadow:var(--e-sunk); }

/* Secondary. The key: sits low, no border, presses into the well. */
.btn-key { color:var(--text); background:var(--surface);
  box-shadow:0 2px 4px rgba(0,0,0,.24); }
[data-theme="light"] .btn-key { box-shadow:0 2px 5px rgba(15,32,39,.12); }
.btn-key::before { content:""; position:absolute; inset:0; border-radius:inherit; padding:1px; pointer-events:none;
  background:var(--edge-ring); -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite:exclude; }
.btn-key:hover { background:var(--hover); }
.btn-key:active { transform:scale(.98); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }

/* Tertiary. A real outlined key, not a naked word. */
.btn-ghost { color:var(--muted); background:transparent; border-color:var(--line-strong); }
.btn-ghost:hover { color:var(--text); border-color:var(--ctrl-hi); background:rgba(255,255,255,.03); }
[data-theme="light"] .btn-ghost:hover { background:rgba(15,32,39,.04); }
.btn-ghost:active { transform:scale(.98); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }

.btn-accent[disabled], .btn-key[disabled], .btn-ghost[disabled], .btn-danger[disabled] { color:var(--disabled); pointer-events:none;
  background:var(--ctrl); box-shadow:none; border-color:var(--line); filter:saturate(.15); }
.b-sm { height:32px; padding:0 16px; border-radius:calc(var(--btn-r) * .8); }
/* ── .b-xs ── the density a data table needs ───────────────────────────────
   ADDED 27-08-2026. `.b-sm` is 32px. A finance table puts a row of action keys
   on every row, and 500 rows at 32px is 6px of extra height per row spent on a
   control nobody is reading. Knox Artiste Accounts had hand-rolled 26px keys
   with 10px type for exactly this, which is how it ended up overriding the
   system in eleven places.

   THE TYPE STAYS AT --fs-caps AND DOES NOT SHRINK, which is the deliberate part.
   This system's scale stops at 11px on purpose: "Below body, difference comes
   from weight, case and colour." A dense TABLE does not license a smaller
   alphabet, it licenses a shorter control. So this is a height and a padding
   and nothing else. */
.b-xs { height:26px; padding:0 10px; border-radius:calc(var(--btn-r) * .7); gap:6px; }
/* THE DEFAULT BUTTON SIZE NEEDS THIS TOO. Every other context that can hold
   an icon sizes it, but the four faces at default size did not, so an icon
   collapsed to 0x0 while the button's gap:9px still applied. The symptom is
   a label pushed 9px right with nothing beside it. */
:where(.btn-accent, .btn-key, .btn-ghost, .btn-danger) > svg {
  width:14px; height:14px; flex-shrink:0; }
.b-xs svg { width:14px; height:14px; }
.b-lg { height:52px; padding:0 32px; font-size:var(--fs-sm); border-radius:calc(var(--btn-r) * 1.25); }
.b-icon { width:42px; height:42px; padding:0; }
.b-icon svg { width:16px; height:16px; }
/* THE ICON BUTTON HAD ONE SIZE AND THE SYSTEM HAS THREE. 42px is right beside a
   heading and far too big for a month arrow inside a date picker or a key on a
   table row, which is why Knox Artiste Accounts had grown a private `.icon-key`
   at 30 and 26. Added 27-08-2026: the size modifiers now mean the same thing on
   an icon button as they do on a labelled one. */
.b-icon.b-sm { width:32px; height:32px; padding:0; }
.b-icon.b-sm svg { width:15px; height:15px; }
.b-icon.b-xs { width:26px; height:26px; padding:0; }
.b-icon.b-xs svg { width:13px; height:13px; }

/* The X. One icon button, one size logic, everywhere a surface needs closing. */
.ib { display:grid; place-items:center; width:28px; height:28px; border-radius:calc(var(--r-xs) * .9);
  color:var(--subtle); flex-shrink:0; cursor:pointer;
  transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover); }
.ib:hover { color:var(--text); background:var(--hover); }
.ib:active { box-shadow:var(--e-sunk); }
.ib svg { width:12px; height:12px; }

/* ── THE SPLIT BUTTON ──
   Two halves, two jobs. The FACE runs the currently chosen action. The CHEVRON
   opens the menu and only changes which action that is. The first build pressed
   as ONE object, which makes a split button a plain button wearing a chevron.
   Each half now presses on its own, and the shared face keeps them reading as
   a single key rather than two buttons shoved together. */
.split { position:relative; display:inline-flex; isolation:isolate; border-radius:var(--btn-r);
  border:1px solid color-mix(in oklab, var(--seed-accent-lo) 70%, black);
  box-shadow:0 2px 5px rgba(0,0,0,.30), inset 0 1px 0 rgba(255,255,255,.18);
  background:linear-gradient(115deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%,
    var(--seed-accent-hi) 55%, color-mix(in oklab, var(--seed-accent-hi) 82%, black) 100%); }
[data-theme="light"] .split { box-shadow:0 2px 6px rgba(15,32,39,.22), inset 0 1px 0 rgba(255,255,255,.30); }
.split > .btn-accent, .split > .sp-more { position:relative; background:none; border:none;
  box-shadow:none; transform:none; overflow:hidden;
  transition:transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
.split > .btn-accent { border-radius:calc(var(--btn-r) - 1px) 0 0 calc(var(--btn-r) - 1px); }
.split > .sp-more { border-radius:0 calc(var(--btn-r) - 1px) calc(var(--btn-r) - 1px) 0;
  display:grid; place-items:center; padding:0 14px; color:var(--on-accent);
  border-left:1px solid rgba(0,0,0,.30); box-shadow:inset 1px 0 0 rgba(255,255,255,.10); }
/* Each half takes its own hover veil and its own press. */
.split > .btn-accent::before, .split > .sp-more::before { content:""; position:absolute; inset:0;
  background:rgba(255,255,255,.10); opacity:0; pointer-events:none;
  transition:opacity var(--d-fast) var(--ease-hover); }
.split > .btn-accent:hover::before, .split > .sp-more:hover::before { opacity:1; }
.split > .btn-accent:active { transform:scale(.975); transition-duration:calc(70ms * var(--dur));
  box-shadow:inset 0 2px 5px rgba(0,0,0,.4); }
.split > .sp-more:active { transform:scale(.94); transition-duration:calc(70ms * var(--dur));
  box-shadow:inset 0 2px 5px rgba(0,0,0,.4), inset 1px 0 0 rgba(255,255,255,.10); }
.split > .sp-more svg { width:12px; height:12px;
  transition:transform var(--d-base) var(--ease-spring); }
.split.open > .sp-more svg { transform:rotate(180deg); }
.split .sp-menu { position:absolute; top:calc(100% + 8px); right:0; z-index:var(--z-drop);
  /* A menu is as wide as its trigger, and grows only if its OWN content needs
     more. The 224px floor here was arbitrary: it had nothing to do with the
     button it hangs off or the items inside it, so a 169px Export key opened a
     221px menu for no reason. min-width:100% is the floor that actually means
     something; max-content is the ceiling. */
  min-width:100%; width:max-content; max-width:min(340px, 90vw);
  opacity:0; transform:translateY(-6px) scale(.985);
  pointer-events:none; transform-origin:top right;
  transition:opacity var(--d-fast) var(--ease-hover), transform var(--d-base) var(--ease-spring); }
/* Twin of the ancestor gate. See the note on .drop-menu. 27-08-2026. */
.split.open .sp-menu, .sp-menu.open { opacity:1; transform:none; pointer-events:auto; }
/* THE TINT CARRIES THE STATE, THE TYPE STAYS FULL STRENGTH. Accent type on an
   accent-tinted ground is the same washed-out pairing that made the model keys
   read light red, and it also leans on colour alone to say "current". A tinted
   ground plus --text says it without either problem, which is what the tabs
   already do. */
.split .sp-menu button.on { color:var(--text); background:var(--accent-tint); }

/* ── THE WELL ──
   V4's well is a CONTAINER, not an input. Anything can sit in it, and the
   trough visibly DEEPENS on focus rather than gaining an outline ring. */
.well { position:relative; display:flex; align-items:stretch; background:var(--surface-deep); border:1px solid var(--line);
  border-radius:var(--r-xs); box-shadow:var(--e-sunk);
  transition:border-color var(--d-fast) var(--ease-hover), box-shadow var(--d-fast) var(--ease-hover); }
/* A FOCUS TREATMENT IS FOR THE THING YOU ARE TYPING INTO, NOT FOR EVERY THING
   THAT CAN HOLD FOCUS. Knox, 21-08-2026: "selection of date is outlining the
   field in the accent colour. This has to be fixed across the entire site."

   `:focus-within` fires for anything inside, and a `.drop-trig` is a div with a
   tabindex and nothing to type into. Picking a date returns focus to the
   trigger, so the field kept an accent rim long after the job was done, on
   every date and every dropdown in every app. Knox Artiste Accounts patched it
   locally; that is why it is fixed here instead.

   `:has()` is the whole fix: the well lights up when what holds focus is an
   input or a textarea, and a trigger takes the ordinary `:focus-visible` ring
   for the keyboard and nothing at all for the mouse. Same shape as the
   `.tsearch` / `.srch` fix above. Added 27-08-2026. */
.well:focus-within:has(:is(input, textarea, [contenteditable]):focus) {
  border-color:color-mix(in srgb, var(--seed-accent-hi) 50%, transparent);
  box-shadow:var(--e-sunk), inset 0 0 12px rgba(0,0,0,.10); }
.well-input { flex:1; min-width:0; width:100%; font-family:var(--f-body); font-size:var(--fs-md);
  line-height:1.2; color:var(--text); background:transparent; border:none; outline:none; padding:12px 15px; }
.well-input::placeholder { color:var(--subtle); }
textarea.well-input { min-height:84px; resize:none; line-height:1.5; display:block; }
.well.err { border-color:var(--danger); }
.well.err:focus-within { border-color:var(--danger); }

.field { display:flex; flex-direction:column; gap:7px; }
.f-label { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); }
.f-label em { font-style:normal; color:var(--accent-text); }
.f-help { font-size:var(--fs-sm); color:var(--subtle-text); }
.f-err { font-family:var(--f-ui); font-size:var(--fs-sm); font-weight:600; color:var(--danger-text); }
/* The textarea's native grip is browser chrome and never ships. */
.grip { position:absolute; right:7px; bottom:7px; width:14px; height:14px; cursor:ns-resize;
  touch-action:none; color:var(--subtle); display:grid; place-items:center; border-radius:4px;
  transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover); }
.grip:hover { color:var(--accent-text); background:var(--accent-tint); }

/* Number stepper, seated in a well. */
.stepper { display:inline-flex; align-items:stretch; overflow:hidden; border-radius:inherit; }
/* Sized for three digits and no more. The cap is 100, and anything above that
   is a quantity nobody types by accident, so it goes through approval rather
   than through a wider box. A field built for four digits invites a fourth. */
.stepper input { width:46px; text-align:center; font-family:var(--f-num); font-weight:var(--num-wt);
  font-size:var(--fs-md); line-height:1.2; font-variant-numeric:tabular-nums; padding:11px 0; -moz-appearance:textfield;
  background:transparent; outline:none; color:var(--text); }
.stepper input::-webkit-outer-spin-button, .stepper input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
.stepper:has(input:focus-visible) { outline:2px solid var(--accent-live); outline-offset:2px; }
.stepper input:focus-visible { outline:none; }
/* The keys inside a stepper are glyph keys, not buttons in the row's sense, so
   they carry their own square press and never a ring on click. */
.stepper button:focus-visible { outline:none; }
.stepper button:focus-visible::after { content:""; position:absolute; inset:2px;
  border-radius:calc(var(--r-xs) * .6); outline:2px solid var(--accent-live); }
.stepper button { position:relative; }
.stepper button { width:38px; color:var(--muted); display:grid; place-items:center;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
/* The accent means "this is live under your hand". A grey hover on a plus and
   minus reads as disabled, which is the opposite of what a hover is for. */
.stepper button:hover { background:var(--accent-tint); color:var(--accent-text); }
.stepper button:active { box-shadow:var(--e-sunk); }
.stepper button svg { width:12px; height:12px; }
.stepper button:first-of-type { border-right:1px solid var(--line); }
.stepper button:last-of-type { border-left:1px solid var(--line); }

/* ── DROPDOWN ──
   The trigger has NO chrome of its own; it sits inside a well like any other
   input. The menu is glass over the page, opening from the top on the spring. */
.drop { position:relative; width:100%; }
.drop-trig { display:flex; width:100%; align-items:center; justify-content:space-between; gap:10px;
  padding:12px 15px; font-family:var(--f-body); font-size:var(--fs-md); line-height:1.2;
  color:var(--text); background:none; }
.drop-trig svg { width:11px; height:7px; color:var(--subtle); flex-shrink:0;
  transition:transform calc(240ms * var(--dur)) var(--ease-spring); }
.drop.open .drop-trig svg { transform:rotate(180deg); }
.drop-menu { position:absolute; top:calc(100% + 8px); left:0; right:0; z-index:var(--z-drop); padding:6px;
  border-radius:var(--r-xs); background:color-mix(in srgb, var(--surface-raise) 94%, transparent);
  -webkit-backdrop-filter:blur(22px) saturate(160%); backdrop-filter:blur(22px) saturate(160%);
  border:1px solid var(--glass-border); box-shadow:var(--e3), inset 0 1px 0 var(--glass-hi);
  opacity:0; transform:translateY(-6px) scale(.985); pointer-events:none; transform-origin:top center;
  transition:opacity calc(160ms * var(--dur)) ease, transform calc(240ms * var(--dur)) var(--ease-spring); }
/* ══ THE ANCESTOR GATE, AND WHY EVERY ONE OF THEM NOW HAS A TWIN ═══════════
   ADDED 27-08-2026, from Darkroom, where it had left a picker invisible in
   production on a page in daily use.

   `.drop.open .drop-menu` is a DESCENDANT selector. It is correct and it is
   also an assumption: that the menu is still inside the trigger's element when
   it opens. An app that portals its menus to document.body breaks that
   assumption, and the rule then matches nothing at all. The menu keeps
   `opacity:0` and `pointer-events:none` for ever, which does not look like a
   bug from the outside. It looks like a click that did nothing.

   Darkroom portals every menu on purpose. A canvas with overflow:hidden, a
   review strip with overflow-x:auto, a side panel with overflow-y:auto, a
   transform on the dock that breaks position:fixed, and isolation on the panels
   are five separate traps for a menu rendered in place. Chasing them one at a
   time is what once put a resolution picker behind a surface nobody could see.

   So each gate gets a twin that reads the class on the ELEMENT. Nothing changes
   for an in-place menu, which keeps satisfying the ancestor form. A portalled
   menu carries `open` itself and works. Both forms are live, deliberately.

   Same shape as the `.scrim` gap closed earlier today: this system kept
   describing a component and not shipping the part an app actually needs. Three
   of the four below have never been portalled by anyone yet. They are fixed now
   so the next app does not spend an afternoon finding out. */
.drop.open .drop-menu, .drop-menu.open { opacity:1; transform:none; pointer-events:auto; }
/* `:not(.optrow)` IS LOAD-BEARING AND IT WAS MISSING HERE.
   `.menu > button` already carried this guard; `.drop-menu button` did not.
   An element-plus-class selector outranks a bare class, so `.drop-menu button`
   beat `.optrow` on specificity no matter what order they sat in, forced
   `display:block` over `display:flex`, and took the gap and the right-aligned
   price with it. Knox saw it as "there is no space between Full Day and 2,500,
   and 2,500 is not showing towards the right side". Fixed 27-08-2026. */
.drop-menu button:not(.optrow) { display:block; width:100%; text-align:left; font-family:var(--f-body);
  font-size:var(--fs-sm); color:var(--muted); padding:10px 12px; border-radius:calc(var(--r-xs) * .8);
  transition:background calc(130ms * var(--dur)) ease, color calc(130ms * var(--dur)) ease; }
.drop-menu button:not(.optrow):hover { background:color-mix(in srgb, var(--seed-accent-hi) 22%, transparent); color:var(--text); }
.drop-menu button.on:not(.optrow) { color:var(--text); background:color-mix(in srgb, var(--seed-accent-hi) 16%, transparent); }

/* Combobox, same glass list. */
.combo { position:relative; width:270px; }
.combo-list { position:absolute; top:calc(100% + 8px); left:0; right:0; z-index:var(--z-drop); padding:6px;
  border-radius:var(--r-xs); background:color-mix(in srgb, var(--surface-raise) 94%, transparent);
  -webkit-backdrop-filter:blur(22px) saturate(160%); backdrop-filter:blur(22px) saturate(160%);
  border:1px solid var(--glass-border); box-shadow:var(--e3), inset 0 1px 0 var(--glass-hi);
  max-height:180px; overflow-y:auto; display:none; }
/* Twin of the ancestor gate. See the note on .drop-menu. 27-08-2026. */
.combo.open .combo-list, .combo-list.open { display:block; }
.combo-list button { display:block; width:100%; text-align:left; font-family:var(--f-body);
  font-size:var(--fs-sm); color:var(--muted); padding:10px 12px; border-radius:calc(var(--r-xs) * .8); }
.combo-list button:hover, .combo-list button.hi { background:color-mix(in srgb, var(--seed-accent-hi) 22%, transparent); color:var(--text); }
.combo-list mark { background:none; color:var(--accent-text); font-weight:600; }
.combo-list .none { font-size:var(--fs-sm); color:var(--subtle-text); padding:12px; }

/* ── SEGMENTED ──
   The pill is a gradient, the slide is sprung, and the label padding is
   asymmetric on purpose: letter-spacing adds its space after the LAST letter
   too, so symmetric padding leaves the word half a space left of its own pill. */
.seg { position:relative; display:inline-flex; flex-shrink:0; padding:4px; border-radius:var(--r-pill);
  background:var(--surface-deep); border:1px solid var(--line); box-shadow:var(--e-sunk); isolation:isolate; }
.seg-b { position:relative; z-index:1; font-family:var(--f-ctrl); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.11em; text-transform:uppercase; line-height:1; color:var(--muted);
  padding:11px calc(22px - .055em) 11px calc(22px + .055em); border-radius:var(--r-pill);
  display:inline-flex; align-items:center; justify-content:center;
  transition:color calc(240ms * var(--dur)) var(--ease-hover); }
.seg-b.on { color:var(--on-accent); }
.seg-b:not(.on):hover { color:var(--text); }
/* THE PILL SLIDES ON THE COMPOSITOR, NOT ON LAYOUT.
   Knox, 22-08-2026: "when I switch from Films to Studio it's a very snappy
   change. It's not smooth." Measured on a 400-row list: 7 dropped frames out of
   24, worst frame 70ms.

   `left` and `width` both force LAYOUT on every frame, on the main thread,
   which at that exact moment is rebuilding the whole table for the company that
   was just picked. The animation and the re-render fought over one thread and
   the animation lost. `transform` is composited and keeps 60fps whatever the
   main thread is doing.

   Knox Artiste Accounts carried this correction alone from 22-08. It is in the
   system as of 27-08 because a segmented control that stutters under load
   stutters in Pulse and Grid too. `knox-v5.js` writes the transform. */
.seg-pill { position:absolute; z-index:0; top:4px; bottom:4px; left:0; border-radius:var(--r-pill);
  background:linear-gradient(180deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%, var(--seed-accent-hi) 100%);
  box-shadow:var(--e2);
  transition:transform calc(320ms * var(--dur)) var(--ease-spring),
             width calc(320ms * var(--dur)) var(--ease-spring); }

/* `.seg.even` — EQUAL BUTTONS, so the pill never animates its width at all.
   Width is the one property left in that transition that still forces layout,
   and it only has to move because the buttons differ. It also stops the control
   sliding when a button is hidden, which was previously solved by padding the
   whole thing out with dead space to hold a position. */
.seg.even .seg-b { flex:1 1 0; min-width:0; }
[data-theme="light"] .seg-b.on { color:var(--on-accent); }

/* ── TOGGLE ── recessed track, domed thumb, groove kept when lit */
.tgl { width:46px; height:26px; border-radius:var(--r-pill); background:var(--surface-deep); position:relative;
  border:1px solid var(--line); box-shadow:var(--e-sunk); flex-shrink:0; cursor:pointer;
  transition:background var(--d-base) var(--ease-hover), border-color var(--d-base) var(--ease-hover); }
.tgl.on { background:var(--accent-lit); border-color:color-mix(in srgb, var(--seed-accent-hi) 60%, transparent);
  box-shadow:var(--e-sunk); }
.tgl::after { content:""; position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%;
  background:radial-gradient(120% 100% at 50% -10%, var(--ctrl-hi) 0%, var(--ctrl) 100%);
  box-shadow:0 2px 5px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.14) inset;
  transition:transform var(--d-base) var(--ease-spring), background var(--d-base) var(--ease-hover); }
[data-theme="light"] .tgl::after { background:radial-gradient(120% 100% at 50% -10%, #fff 0%, var(--surface-raise) 100%);
  box-shadow:0 2px 6px rgba(15,32,39,.28), 0 1px 0 rgba(255,255,255,1) inset; }
.tgl.on::after { transform:translateX(20px); }

/* ── THE SWITCH'S LOOK DERIVES FROM ITS ARIA STATE · 30-08-2026 ──────────────
   Knox, on Darkroom: "when I am clicking on the dark mode / light mode toggle,
   it is going into light mode, but the toggle is not working."

   The theme was changing. The knob was not moving, because the ONLY thing that
   moves it is `.tgl.on`, and the markup a real app renders carries the state on
   the WRAPPER as `aria-checked`, which is where a switch's state belongs and is
   what a screen reader reads. The component shipped with two sources of truth
   for one state and no link between them: an app that sets the accessible state
   correctly gets a switch that never moves, and an app that sets the class gets
   a switch that lies to a screen reader.

   Measured before the fix, both engines: `aria-checked="true"` moved the knob
   by nothing at all, `.tgl.on` moved it 20px.

   Same shape as `.sdrawer` yesterday - a visual state whose only path in was one
   nobody would take. The accessible state is already correct in every app that
   bothered, so the visual derives FROM it. `.tgl.on` stays for anything already
   using it: both live, deliberately, like the portalled-menu twins. */
[role="switch"][aria-checked="true"] .tgl { background:var(--accent-lit);
  border-color:color-mix(in srgb, var(--seed-accent-hi) 60%, transparent); }
[role="switch"][aria-checked="true"] .tgl::after { transform:translateX(20px); }

/* ── CHECK AND RADIO ── carved when off, POPS OUT of the well when on */
.ck, .rd { width:20px; height:20px; flex-shrink:0; display:grid; place-items:center; cursor:pointer;
  background:var(--surface-deep); border:1px solid var(--line); box-shadow:var(--e-sunk); color:transparent;
  transition:background calc(200ms * var(--dur)) var(--ease-hover), border-color calc(200ms * var(--dur)) var(--ease-hover), box-shadow calc(200ms * var(--dur)) var(--ease-hover); }
.ck { border-radius:calc(var(--r-xs) * .85); } .rd { border-radius:50%; }
.ck.on { background:var(--accent-lit); border-color:rgba(0,0,0,.18); box-shadow:var(--e2); color:var(--on-accent); }
.ck svg { width:11px; height:11px; opacity:0; transform:scale(.4);
  transition:opacity calc(160ms * var(--dur)) ease, transform calc(240ms * var(--dur)) var(--ease-spring); }
.ck.on svg { opacity:1; transform:scale(1); }
.rd::after { content:""; width:9px; height:9px; border-radius:50%; background:var(--accent-lit);
  opacity:0; transform:scale(.4); transition:opacity calc(160ms * var(--dur)) ease, transform calc(240ms * var(--dur)) var(--ease-spring); }
.rd.on { border-color:color-mix(in srgb, var(--seed-accent-hi) 60%, transparent); }
.rd.on::after { opacity:1; transform:scale(1); }
.ckrow { display:flex; align-items:center; gap:11px; padding:7px 0; cursor:pointer;
  font-size:var(--fs-md); color:var(--muted); transition:color calc(130ms * var(--dur)) var(--ease-hover); }
.ckrow:hover { color:var(--text); }
.check.done .lbl { text-decoration:line-through; text-decoration-color:var(--accent-text);
  text-decoration-thickness:1.5px; color:var(--subtle); }

/* ── SLIDER ── carved track, SOLID LIT FILL, domed thumb */
.sld { position:relative; height:28px; display:flex; align-items:center; }
.sld .track { position:absolute; left:0; right:0; height:8px; border-radius:99px;
  background:var(--surface-deep); border:1px solid var(--line); box-shadow:var(--e-sunk); }
.sld .fill { position:absolute; left:1px; height:8px; border-radius:99px; background:var(--accent-lit); }
.sld input { position:relative; z-index:2; width:100%; -webkit-appearance:none; appearance:none;
  background:transparent; height:28px; cursor:pointer; }
.sld input::-webkit-slider-thumb { -webkit-appearance:none; width:20px; height:20px; border-radius:50%;
  background:radial-gradient(120% 100% at 50% -10%, var(--ctrl-hi) 0%, var(--ctrl) 100%); border:none;
  box-shadow:0 2px 5px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.14) inset; }
.sld input::-moz-range-thumb { width:20px; height:20px; border:none; border-radius:50%;
  background:var(--ctrl-hi); box-shadow:0 2px 5px rgba(0,0,0,.45); }
.sld input::-moz-range-track { background:transparent; }
[data-theme="light"] .sld input::-webkit-slider-thumb {
  background:radial-gradient(120% 100% at 50% -10%, #fff 0%, var(--surface-raise) 100%);
  box-shadow:0 2px 6px rgba(15,32,39,.28), 0 1px 0 rgba(255,255,255,1) inset; }

/* ── CHIP AND TAG ── */
.chip { display:inline-flex; align-items:center; gap:8px; font-family:var(--f-ctrl); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.07em; text-transform:uppercase; line-height:1;
  color:var(--muted); background:var(--ctrl); border:1px solid var(--line-strong);
  border-radius:var(--r-pill); padding:8px 15px; cursor:pointer;
  box-shadow:0 1px 3px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.04);
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover),
             transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
[data-theme="light"] .chip { box-shadow:0 1px 3px rgba(15,32,39,.08), inset 0 1px 0 rgba(255,255,255,.85); }
.chip:hover { background:var(--ctrl-hi); color:var(--text); }
.chip:active { transform:scale(.97); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }
.chip.on { background:var(--accent-lit); color:var(--on-accent); border-color:rgba(0,0,0,.2); }
[data-theme="light"] .chip.on { color:var(--on-accent); }
.taginput { display:flex; flex-wrap:wrap; align-items:center; gap:7px; padding:9px 12px; }
.taginput input { flex:1; min-width:90px; background:none; border:none; outline:none;
  font-family:var(--f-body); font-size:var(--fs-sm); color:var(--text); }
/* Built like a key rather than a tint. A dark tint carrying pale accent text is
   two weak reds arguing; the fill plus the paper label is one clear object. */
.tag { display:inline-flex; align-items:center; gap:6px; font-family:var(--f-ctrl); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.07em; text-transform:uppercase; line-height:1;
  color:var(--on-accent);
  background:linear-gradient(115deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%,
    var(--seed-accent-hi) 60%, color-mix(in oklab, var(--seed-accent-lo) 55%, var(--seed-accent-hi)) 100%);
  border:1px solid color-mix(in oklab, var(--seed-accent-lo) 70%, black);
  box-shadow:0 1px 3px rgba(0,0,0,.26), inset 0 1px 0 rgba(255,255,255,.16);
  border-radius:var(--r-pill); padding:5px 8px 5px 12px; }
.tag button { color:color-mix(in srgb, var(--on-accent) 70%, transparent); width:16px; height:16px; display:grid; place-items:center; border-radius:50%;
  transition:color calc(130ms * var(--dur)) ease, background calc(130ms * var(--dur)) ease; }
.tag button:hover { color:var(--on-accent); background:rgba(0,0,0,.24); }
.tag button svg { width:8px; height:8px; }

/* The keycap. The thicker bottom edge is what makes it read as a key. */
.kbd { display:inline-flex; align-items:center; font-family:var(--f-ctrl); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.07em; text-transform:uppercase; color:var(--subtle); background:var(--surface-deep);
  border:1px solid var(--line-strong); border-bottom-width:2px; border-radius:6px; padding:3px 7px; line-height:1; }
/* Inside a search well it is plain text, never a boxed key. */
.tsearch .kbd { background:none; border:none; padding:0; }

/* Status. Typography only: a standalone dot and a caps label. No container. */
.status { display:inline-flex; align-items:center; gap:7px; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:var(--ls-caps); text-transform:uppercase; white-space:nowrap; }
.status i { width:6px; height:6px; border-radius:50%; background:currentColor; flex-shrink:0; }
.status.done { color:var(--subtle-text); }
.status.live { color:var(--text); }
.status.wait { color:var(--muted); } .status.wait i { background:none; box-shadow:inset 0 0 0 1.5px currentColor; }
.status.attn { color:var(--danger-text); }
/* A PASS. ADDED 30-08-2026. The palette has carried `--success-text` since the
   status ramp was written and `.status` had no way to say it: `.done` is
   `--subtle-text`, which is the colour of a thing that is FINISHED, not a thing
   that is GOOD. So a check that passed and a check that merely ran looked
   identical, and Knox asked for the obvious thing - "it should be in a proper
   red or green text". Red was already there. This is green. */
.status.ok { color:var(--success-text); }

/* Level bars */
.levels { display:inline-flex; gap:3px; align-items:flex-end; }
.levels i { width:13px; height:38px; border-radius:3px; background:var(--surface-deep);
  border:1px solid var(--line); box-shadow:var(--e-sunk); }
.levels i.lit { background:var(--accent-lit); border-color:transparent; box-shadow:none; }

/* The pillow: a big extruded squircle key for primary tile actions. */
.pillow { width:86px; height:86px; border-radius:calc(var(--r) * 1.3); display:grid; place-items:center;
  color:var(--text); cursor:pointer; position:relative; isolation:isolate;
  background:linear-gradient(150deg in oklab, var(--ctrl-hi) 0%, var(--ctrl) 55%, var(--surface) 100%);
  box-shadow:var(--extrude);
  transition:transform calc(320ms * var(--dur)) var(--ease-hover), box-shadow calc(320ms * var(--dur)) var(--ease-hover); }
.pillow::before { content:""; position:absolute; inset:0; border-radius:inherit; padding:1px; pointer-events:none;
  background:var(--edge-ring); -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite:exclude; }
.pillow:hover { transform:translateY(-2px); }
.pillow:active { transform:scale(.98); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }
.pillow svg { width:26px; height:26px; }

/* ═══ WAYFINDING ═══════════════════════════════════════════════════════════
   Four surfaces, each with variants driven off a data-attribute on <html>.
   Everything still rides the seeds: change the accent and all of it re-dresses. */
.wayfind { display:grid; grid-template-columns:var(--rail-w) 1fr;
  border-radius:var(--r-lg); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e3);
  transition:grid-template-columns var(--d-slow) var(--ease-entrance); }
.wayfind .grain { position:absolute; inset:0; pointer-events:none; z-index:0;
  background-image:var(--noise); background-size:120px; opacity:calc(.04 * var(--noise-o)); mix-blend-mode:overlay; }
[data-theme="light"] .wayfind .grain { opacity:.025; mix-blend-mode:multiply; }

/* ── RAIL ── */
.rail { position:relative; z-index:1; padding:var(--sp-4) var(--sp-2);
  display:flex; flex-direction:column; gap:var(--sp-1);
  background:rgba(0,0,0,.16); border-right:1px solid var(--line);
  transition:background var(--d-base) var(--ease-hover), margin var(--d-slow) var(--ease-entrance),
             border-radius var(--d-slow) var(--ease-entrance), box-shadow var(--d-slow) var(--ease-entrance); }
.rail-logo { font-family:var(--f-hero); font-size:var(--fs-sm); text-transform:uppercase;
  letter-spacing:var(--ls-head); color:var(--text); padding:6px var(--sp-3) var(--sp-4); white-space:nowrap; overflow:hidden; }
/* The lit accent, not the text tint. --accent-text is 45% white, which at
   display size is a pale pink; that is what made "Vision" look wrong. */
.rail-logo span { color:var(--accent-lit); }
.rail-group { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--subtle-text); padding:var(--sp-4) var(--sp-3) var(--sp-2); white-space:nowrap; }
.rail a { position:relative; z-index:1; display:flex; align-items:center; gap:var(--sp-3);
  font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted); padding:10px var(--sp-3);
  border-radius:var(--r-xs); text-decoration:none; white-space:nowrap; overflow:hidden;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.rail a:hover { background:var(--hover); color:var(--text); }
.rail a.on { color:var(--on-accent); }
[data-theme="light"] .rail a.on { color:var(--on-accent); }
.rail a.on:hover { background:transparent; }
.rail a svg { width:15px; height:15px; flex-shrink:0; }
.rail a .lbl { transition:opacity var(--d-base) var(--ease-hover); }
.rail a .ct { margin-left:auto; font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm);
  color:var(--subtle-text); font-variant-numeric:tabular-nums; }
.rail a.on .ct { color:color-mix(in srgb, var(--on-accent) 85%, transparent); }
.rail-pill { position:absolute; left:var(--sp-2); right:var(--sp-2); top:0; height:0; opacity:0; z-index:0;
  border-radius:var(--r-xs); background:var(--accent-lit); box-shadow:var(--e2);
  transition:top var(--d-slow) var(--ease-entrance), height var(--d-slow) var(--ease-entrance),
             opacity var(--d-fast) ease, left var(--d-slow) var(--ease-entrance), right var(--d-slow) var(--ease-entrance); }

/* Variant A · CARVED (V4's current) */
[data-rail="carved"] .rail { background:rgba(0,0,0,.16); border-right:1px solid var(--line); }
[data-theme="light"] [data-rail="carved"] .rail { background:rgba(15,32,39,.045); }

/* Variant B · FLOATING. The rail detaches and becomes an object on the ground. */
[data-rail="floating"] .wayfind { grid-template-columns:var(--rail-w) 1fr; }
[data-rail="floating"] .rail { margin:var(--sp-3) 0 var(--sp-3) var(--sp-3); border-radius:var(--r);
  background:var(--glass-bg); -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%); border:1px solid var(--glass-border);
  box-shadow:var(--e3), inset 0 1px 0 var(--glass-hi); }

/* Variant C · ICONS. Collapses to 62px; labels fade, pill goes square. */
[data-rail="icons"] .wayfind { grid-template-columns:var(--rail-w-icons) 1fr; }
[data-rail="icons"] .rail { background:rgba(0,0,0,.16); border-right:1px solid var(--line); align-items:center; }
[data-theme="light"] [data-rail="icons"] .rail { background:rgba(15,32,39,.045); }
[data-rail="icons"] .rail a { justify-content:center; padding:var(--sp-3) 0; width:42px; }
[data-rail="icons"] .rail a .lbl, [data-rail="icons"] .rail a .ct, [data-rail="icons"] .rail-group { opacity:0;
  width:0; overflow:hidden; padding:0; height:0; }
[data-rail="icons"] .rail-group { margin:var(--sp-2) 0; height:1px; width:26px; background:var(--line-strong); }
/* KNOX in an expanded face does not fit 62px. The collapsed rail shows the K. */
[data-rail="icons"] .rail-logo { padding:var(--sp-2) 0 var(--sp-4);
  text-align:center; width:100%; font-size:0; }
[data-rail="icons"] .rail-logo .full { display:none; }
[data-rail="icons"] .rail-logo::before { content:"K"; font-size:var(--fs-xl); color:var(--accent-text); }
[data-rail="icons"] .rail-pill { left:10px; right:10px; }

/* Variant D · GROUPED. Sections become carved wells with their own headers. */
[data-rail="grouped"] .rail { background:rgba(0,0,0,.16); border-right:1px solid var(--line); gap:var(--sp-1); }
[data-theme="light"] [data-rail="grouped"] .rail { background:rgba(15,32,39,.045); }
[data-rail="grouped"] .rail-group { color:var(--accent-text); border-top:1px solid var(--line);
  margin-top:var(--sp-2); padding-top:var(--sp-3); }
[data-rail="grouped"] .rail-group:first-of-type { border-top:none; margin-top:0; }
[data-rail="grouped"] .rail a { font-size:var(--fs-sm); padding:7px var(--sp-3); }

/* ── TOP BAR ── */
.topb { position:relative; z-index:var(--z-chrome); display:flex; align-items:center; gap:var(--sp-3);
  margin:var(--sp-3) var(--sp-4) 0; padding:var(--sp-2) var(--sp-4); border-radius:var(--r-pill);
  background:var(--glass-bg); -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%); border:1px solid var(--glass-border);
  box-shadow:var(--e2), inset 0 1px 0 var(--glass-hi);
  transition:margin var(--d-slow) var(--ease-entrance), border-radius var(--d-slow) var(--ease-entrance),
             background var(--d-base) var(--ease-hover), box-shadow var(--d-base) var(--ease-hover); }
/* The breadcrumb was 11px uppercase with .1em tracking applied to a PROJECT
   NAME. Caps mangle a proper noun and tracked caps at 11px are unreadable at a
   glance, which is what Knox saw in "BEAUTYWORLD HALL FILM". The trail stays a
   caps label because it is a category. The current page is a name, so it is set
   in sentence case at the small step and carries the weight instead. */
/* One voice for the whole trail (Knox, 23-08). The crumb takes ALL the slack,
   so a project called "ABC" and one called "Beautyworld hall film" leave the
   search well and the avatar in exactly the same place. */
.crumb { display:flex; align-items:center; gap:var(--sp-2); white-space:nowrap;
  overflow:hidden; min-width:0; flex:1 1 0;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; }
.crumb .trail { color:var(--subtle-text); flex-shrink:0; }
.crumb b { font:inherit; letter-spacing:inherit; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; min-width:0; }
.crumb i { font-style:normal; color:var(--subtle); opacity:.55; flex-shrink:0; }
.topb .sp { display:none; }
.tsearch { display:flex; align-items:center; gap:var(--sp-2); background:var(--surface-deep);
  border-radius:var(--r-pill); box-shadow:var(--e-sunk); padding:0 var(--sp-3);
  flex:0 0 210px; width:210px; min-width:0; }
.tsearch input { font-size:var(--fs-sm); font-family:var(--f-body); color:var(--text); padding:var(--sp-2) 0; min-width:0; width:100%; }
.tsearch input::placeholder { color:var(--disabled); }
.tsearch svg { width:13px; height:13px; color:var(--subtle); flex-shrink:0; }
.tsearch .kbd { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:500; letter-spacing:.04em;
  color:var(--subtle-text); white-space:nowrap; }
.ava { width:30px; height:30px; border-radius:50%; flex-shrink:0; display:grid; place-items:center;
  font-family:var(--f-ui); font-size:calc(var(--fs-caps) * .95); font-weight:600;
  /* TRACKING AND INDENT REMOVED 30-08-2026. Measured: every monogram sat 0.3px
     right of centre, and "MW" was 23.8px of text in a 24px box with a right gap
     of MINUS 0.1px, so it touched both walls.
     The cause was a pair, not one bug. Tracking puts its space AFTER the last
     letter too, so centred text drifts left by the full letter-space; the
     text-indent was added to compensate and shifted it right by the whole
     amount rather than half, swinging it the other way. A two-letter monogram
     in a 24px circle does not need tracking, so both go, and the centring is
     then exact with no compensation to keep in sync. */ color:var(--on-accent); cursor:pointer;
  background:linear-gradient(150deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%,
    var(--seed-accent-hi) 60%, color-mix(in oklab, var(--seed-accent-hi) 82%, black) 100%); box-shadow:var(--e2); }

/* Variant A · PILL (V4's current). Floating glass, detached from every edge. */
[data-topbar="pill"] .topb { margin:var(--sp-3) var(--sp-4) 0; padding:var(--sp-2) var(--sp-4);
  border-radius:var(--r-pill); background:var(--glass-bg);
  -webkit-backdrop-filter:blur(22px) saturate(150%); backdrop-filter:blur(22px) saturate(150%);
  border:1px solid var(--glass-border); box-shadow:var(--e2), inset 0 1px 0 var(--glass-hi); }

/* Variant B · FULL. Edge to edge glass, hairline under it, crumb gets room. */
[data-topbar="full"] .topb { margin:0; padding:var(--sp-3) var(--sp-5); border-radius:0;
  background:var(--glass-bg); -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%); border-bottom:1px solid var(--line); box-shadow:none; }
[data-topbar="full"] .tsearch { flex:0 0 240px; width:240px; }

/* Variant C · SPLIT. Crumb sits on the ground, the command cluster floats alone. */
[data-topbar="split"] .topb { margin:var(--sp-4) var(--sp-4) 0; padding:0; background:none;
  border:none; box-shadow:none; backdrop-filter:none; -webkit-backdrop-filter:none; }
[data-topbar="split"] .topb .cluster { display:flex; align-items:center; gap:var(--sp-2);
  padding:var(--sp-1) var(--sp-1) var(--sp-1) var(--sp-3); border-radius:var(--r-pill);
  background:var(--glass-bg); -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%); border:1px solid var(--glass-border);
  box-shadow:var(--e2), inset 0 1px 0 var(--glass-hi); min-width:0; }
[data-topbar="pill"] .topb .cluster, [data-topbar="full"] .topb .cluster {
  display:flex; align-items:center; gap:var(--sp-3); flex:0 0 auto; }

/* ── TABS ── */
/* THE CANVAS. Padding and cap both come from LAYER 0c, so an app changes its
   reading width by setting one token rather than by editing this rule. The cap
   is a max-width on the CONTENT, not on the canvas: the ground still runs to
   the edge, which is what stops a wide monitor showing a letterboxed app. */
.canvas { position:relative; z-index:1;
  padding:var(--canvas-pad-t) var(--canvas-pad-x) var(--canvas-pad-b); }
.canvas > .screen { max-width:var(--canvas-max); margin-inline:auto; width:100%; }
/* THE SHELL BREAKPOINT. One number, 860px, and it is the same number in every
   app. Below it the rail lies down and the gutters drop a step together, so the
   header and the rail can never disagree about where the shell starts, which is
   a bug Darkroom shipped for a fortnight. */
@media (max-width:860px){
  :root { --canvas-pad-x: var(--sp-3); --canvas-pad-t: var(--sp-3); }}
@media (max-width:600px){
  :root { --canvas-pad-x: var(--sp-2); }
  .canvas { padding-bottom: var(--dock-clear); }}

/* ── THE SCREEN ────────────────────────────────────────────────────────────
   ADDED 27-08-2026, for the same reason as the overlay scale: this system
   described what a component looks like and never once described what a SCREEN
   looks like. So each app invented a header, invented a rhythm, and the two
   drifted. Knox, on Accounts: "the design flow of things is not correct...
   the gaps and spaces are all out of place."

   A screen is a head, then bands. Nothing else. The rhythm lives HERE so that
   no screen has to decide it, which is the whole point: a screen that decides
   its own spacing is a screen that disagrees with the next one. */
.screen { display:flex; flex-direction:column; gap:var(--sp-5); min-width:0; }

/* The head is the title, what it is, and what you can do to it. One line on a
   desktop, wrapping to two on a phone, and the actions stay together. */
.screen-head { display:flex; align-items:flex-end; justify-content:space-between;
  gap:var(--sp-4); flex-wrap:wrap; }
.screen-head .st { display:flex; flex-direction:column; gap:var(--sp-1); min-width:0; }
/* A screen title is a screen title whichever tag carries it. As an h2 it
   was picking up the gallery's section-heading styling and losing the head
   face entirely, so two screens in a set could disagree about their own
   titles depending on how the markup was nested. */
/* BOTH TAGS, which is what the note above actually says and the first pass did
   not do. Scoping the treatment to h2 alone left h1 falling through to the hero
   size, so a screen whose title happened to be an h1 rendered at 64px against
   26px on the screen beside it. Found by putting the system on a real app on
   27-08-2026: Knox Artiste Accounts writes every screen title as an h1. */
.screen-head h1, .screen-head h2 { margin:0; font-family:var(--f-head);
  font-size:var(--fs-xl); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-head); line-height:var(--lh-head); }
.screen-head .sub { font-size:var(--fs-sm); color:var(--muted); max-width:60ch; }
.screen-head .sa { display:flex; align-items:center; gap:var(--sp-2); flex-shrink:0; }

/* A BAND IS ONE CONCERN. Knox asked for "section boxes, like a dashboard view,
   or a section divider somewhere" and this is that: a band carries its own
   label and its own ground, so two concerns can never read as one list that
   happens to have a gap in it. `.band.plain` drops the ground for a band that
   is already a card or a table and should not be double-boxed. */
.band { display:flex; flex-direction:column; gap:var(--sp-3); min-width:0;
  padding:var(--sp-4); border-radius:var(--r); background:var(--grad-ground);
  box-shadow:var(--e1); }
.band.plain { padding:0; background:none; box-shadow:none; }
.band > .c-lab { margin:0; }
/* The rule between two concerns inside ONE band, where a second ground would
   be too much. Never used between bands: that is what the gap is for. */
.band-rule { height:1px; background:var(--line); border:none; margin:var(--sp-2) 0; }
/* A strip wider than its container had no way to reach the tabs past the
   edge. It scrolls, without a visible bar, which would sit under the
   underline indicator. */
.vtabs { position:relative; display:flex; gap:2px;
  overflow-x:auto; scrollbar-width:none;
  transition:background var(--d-base) var(--ease-hover), padding var(--d-base) var(--ease-hover),
             border-radius var(--d-base) var(--ease-hover), box-shadow var(--d-base) var(--ease-hover); }
.vtabs::-webkit-scrollbar { display:none; }
.vtabs button { position:relative; z-index:1; font-family:var(--f-ctrl); font-size:var(--fs-sm); font-weight:500;
  color:var(--subtle-text); padding:var(--sp-3) var(--sp-4); line-height:1;
  transition:color var(--d-base) var(--ease-hover); }
.vtabs button:hover { color:var(--muted); }
.vtabs button.on { color:var(--text); }
.vtabs .ind { position:absolute; z-index:0; bottom:-1px; height:2px; border-radius:2px; background:var(--accent-lit);
  transition:left var(--d-base) var(--ease-spring), width var(--d-base) var(--ease-spring),
             top var(--d-base) var(--ease-spring), height var(--d-base) var(--ease-spring); }

/* Variant A · UNDERLINE (V4's current) */
[data-tabs="underline"] .vtabs { border-bottom:1px solid var(--line); }
[data-tabs="underline"] .vtabs .ind { bottom:-1px; top:auto; height:2px; border-radius:2px; background:var(--accent-lit); }

/* Variant B · PILL. Indicator becomes a filled pill behind the label. */
[data-tabs="pill"] .vtabs { gap:var(--sp-1); }
[data-tabs="pill"] .vtabs button { padding:var(--sp-2) var(--sp-4); border-radius:var(--r-pill); }
[data-tabs="pill"] .vtabs button.on { color:var(--on-accent); }
[data-theme="light"] [data-tabs="pill"] .vtabs button.on { color:var(--on-accent); }
[data-tabs="pill"] .vtabs .ind { top:0; bottom:0; height:auto; border-radius:var(--r-pill);
  background:var(--accent-lit); box-shadow:var(--e2); }

/* Variant C · CARVED. The whole strip is a well; the indicator is the raised key. */
[data-tabs="carved"] .vtabs { background:var(--surface-deep); border-radius:var(--r-pill);
  padding:3px; box-shadow:var(--e-sunk); align-self:flex-start; }
[data-tabs="carved"] .vtabs button { padding:var(--sp-2) var(--sp-4); border-radius:var(--r-pill); }
[data-tabs="carved"] .vtabs button.on { color:var(--text); }
[data-tabs="carved"] .vtabs .ind { top:3px; bottom:3px; height:auto; border-radius:var(--r-pill);
  background:var(--ctrl-hi); box-shadow:var(--extrude); }

/* minmax(0,1fr) IS LOAD-BEARING. Every pane sits in the same grid cell, so the
   WIDEST pane sizes the cell, including the ones that are not showing. A plain
   1fr track refuses to shrink below its content, so one hidden pane holding a
   horizontal scroller silently widens the whole page. Found in Darkroom on
   23-08-2026: a review strip on an inactive tab pushed the document to 1907px
   inside a 1512px window. */
.panes { display:grid; grid-template-columns:minmax(0,1fr); margin-top:var(--sp-4); }
.pane { min-width:0; }
.pane { grid-area:1/1; opacity:0; transform:translateY(6px); pointer-events:none;
  transition:opacity var(--d-base) var(--ease-hover), transform var(--d-base) var(--ease-hover); }
.pane.on { opacity:1; transform:none; pointer-events:auto; }

/* ── DOCK ── */
/* THE DOCK FLOATS OVER CONTENT, AND CONTENT IS NOT ALWAYS A DOCUMENT.
   Amended 27-08-2026, from Darkroom. --glass-bg is tuned for glass over a page:
   a surface, a table, a form, all of them low contrast and close to --bg. It
   was measured over a full-bleed wall of photographs and the inactive glyphs
   disappeared into skin tones, which is a legible ground problem and not an
   icon problem.

   Knox raised it on 27-08 as "you can't really see it" and Darkroom patched its
   own copy with a raised ground, a stronger border and a heavier shadow. That
   patch is deleted and this is where it belongs, because Pulse floats the same
   dock over thumbnails and Grid floats it over video posters, and both would
   have found this the same way.

   The change is the GROUND, not the glyph. --muted stays --muted, so the
   inactive tier still recedes exactly as much as it does everywhere else in the
   system; it now recedes against a surface instead of against a photograph.
   Raising the glyph instead would have made the dock louder than the rail on
   every ordinary page, to fix a case only two apps have. */
/* THE DOCK FLOATS, SO SOMETHING UNDERNEATH IT OWES CLEARANCE.
   Added 27-08-2026. A floating dock covers the bottom of whatever scrolls behind
   it, and the last row of content sits under it and cannot be clicked. That does
   not read as "something is covering this". It reads as the list having lost a
   row, which is why it survives review.

   Every app that floats this dock owes the same clearance, and Darkroom had it
   as a hardcoded 76px in one place and 84px in another, both derived by hand
   from a dock that was 46px tall at the time. It is 66px now, so both numbers
   went silently wrong the moment the dock was fixed. A hand-derived number is a
   number that goes stale without telling anyone.

   --dock-h derives from the parts: item height, the dock's own padding on both
   sides, and its border. --dock-clear is what a scroller reserves: the dock,
   its offset from the bottom edge, and a real gap so content stops above it
   rather than behind its shadow. Add env(safe-area-inset-bottom) at the point
   of use, because only the app knows whether its scroller reaches the edge.
   The two tokens themselves live with the rest of the scale up in LAYER 0b. */
.dock { position:relative; display:inline-flex; align-items:center; gap:var(--sp-2); padding:var(--sp-2);
  border-radius:var(--r-pill);
  background:color-mix(in srgb, var(--surface-raise) 88%, transparent);
  -webkit-backdrop-filter:blur(24px) saturate(150%); backdrop-filter:blur(24px) saturate(150%);
  border:1px solid var(--line-strong); box-shadow:var(--e4), inset 0 1px 0 var(--glass-hi);
  transition:border-radius var(--d-slow) var(--ease-entrance), width var(--d-slow) var(--ease-entrance); }
.dock > span { position:relative; z-index:1; height:44px; border-radius:var(--r-pill);
  display:inline-flex; align-items:center; justify-content:center; gap:var(--sp-2); color:var(--muted);
  cursor:pointer; padding:0; width:44px; overflow:hidden;
  transition:background var(--d-base) var(--ease-hover), color var(--d-base) var(--ease-hover),
             width var(--d-slow) var(--ease-entrance), padding var(--d-slow) var(--ease-entrance); }
.dock > span:hover { color:var(--text); background:rgba(255,255,255,.05); }
[data-theme="light"] .dock > span:hover { background:rgba(15,32,39,.05); }
.dock > span.on { color:var(--on-accent); }
[data-theme="light"] .dock > span.on { color:var(--on-accent); }
.dock > span.on:hover { background:transparent; }
.dock > span svg { width:18px; height:18px; flex-shrink:0; }
.dock > span .dlbl { font-family:var(--f-ui); font-size:var(--fs-sm); font-weight:600; letter-spacing:.06em;
  text-transform:uppercase; white-space:nowrap; display:none; }
.dock .dsep { width:1px; height:24px; background:var(--line-strong); cursor:default; }
.dock .dsep:hover { background:var(--line-strong); }
.dock > span.dock-pill { position:absolute; top:var(--sp-2); height:44px; width:44px; padding:0;
  border-radius:var(--r-pill); background:var(--accent-lit); box-shadow:var(--e2); opacity:0; z-index:0;
  overflow:visible; cursor:default;
  transition:left var(--d-slow) var(--ease-entrance), width var(--d-slow) var(--ease-entrance), opacity var(--d-fast) ease; }
.dock > span.dock-pill:hover { background:var(--accent-lit); }

/* Variant B · LABELS. Icons keep company; the pill grows to fit. */
[data-dock="labels"] .dock > span { width:auto; padding:0 var(--sp-4); }
[data-dock="labels"] .dock > span .dlbl { display:inline; }
/* The separator is not an item and must not take the item padding. */
[data-dock="labels"] .dock .dsep { width:1px; padding:0; }
/* A labelled dock is 496px wide. Below 560 the labels drop and it becomes the
   icon dock, which is what a real dock does rather than overflowing the page. */
@media (max-width:560px){
  [data-dock="labels"] .dock > span { width:44px; padding:0; }
  [data-dock="labels"] .dock > span .dlbl { display:none; }}
.dock { max-width:100%; }

/* Variant C · BAR. Bottom tab bar for the Crew PWA. Square corners, full width. */
[data-dock="bar"] .dock { border-radius:var(--r) var(--r) 0 0; width:100%; justify-content:space-around;
  padding:var(--sp-2) var(--sp-3) calc(var(--sp-2) + env(safe-area-inset-bottom, 0px)); }
[data-dock="bar"] .dock > span { flex-direction:column; gap:3px; height:auto; width:auto;
  padding:var(--sp-2) var(--sp-3); border-radius:var(--r-xs); }
[data-dock="bar"] .dock > span .dlbl { display:inline; font-size:var(--fs-caps); }
[data-dock="bar"] .dock .dsep { display:none; }
[data-dock="bar"] .dock > span.dock-pill { display:none; }
[data-dock="bar"] .dock > span.on { color:var(--accent-text); background:var(--accent-tint); }

/* Was 760px. Moved to the shell breakpoint 28-08-2026 so the rail, the gutters
   and the dock all change on the same line. Three of the four apps had the rail
   and the canvas changing at different widths. */
/* FIXED 29-08-2026: this was scoped to .wayfind alone. An app whose shell is
   .proof kept its rail track while .rail below went display:none, so the main
   column landed IN the rail track and the whole shell rendered 218px wide on
   every phone. Same breakpoint, same declaration, reaching both shells the
   system ships. minmax(0,1fr) because a bare 1fr floors at min-content and lets
   a wide table push a horizontal page scroll onto a 375px screen. */
@media (max-width:860px){ :is(.wayfind, .proof) { grid-template-columns:minmax(0,1fr) !important; } .rail { display:none; }}
/* The demo canvas is a small viewport of its own, so its contents answer to its
   width, not the page's. Without this the KPI row and the top bar clip. */
@supports (container-type: inline-size) {
  .wayfind > div:last-child { container-type:inline-size; }
  @container (max-width: 560px) { .canvas .kpi { grid-template-columns:1fr 1fr; } .topb .tsearch { flex-basis:130px; }}
  @container (max-width: 400px) { .canvas .kpi { grid-template-columns:1fr; } .topb .tsearch { display:none; }}}


/* ═══ CARRIED FROM V4 ══════════════════════════════════════════════════════ */
.t6-wrap { position:relative; isolation:isolate; border-radius:var(--r); overflow:hidden;
  background:var(--grad-ground); box-shadow:var(--e2); }
.t6 { width:100%; border-collapse:collapse; font-size:var(--fs-sm); }
.t6 th { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--subtle-text); text-align:left; padding:var(--sp-3) var(--sp-4);
  border-bottom:1px solid var(--line-strong); position:sticky; top:0; z-index:var(--z-sticky);
  background:var(--surface); }
.t6 th.num, .t6 td.num { text-align:right; }
.t6 td { padding:var(--sp-3) var(--sp-4); border-bottom:1px solid var(--line); color:var(--muted); font-family:var(--f-body); }
.t6 tbody tr:last-child td { border-bottom:none; }
.t6 tbody tr { transition:background var(--d-fast) var(--ease-hover); }
.t6 tbody tr:hover { background:var(--hover); }
.t6 td.num { font-family:var(--f-num-col); font-weight:var(--num-wt-col); color:var(--text); font-variant-numeric:tabular-nums lining-nums; }
.t6 td.t-name { color:var(--text); font-weight:500; }
/* .who is ALSO a full-width flex menu trigger, and a <td class="who">
   matches it: display:flex, width:100%, 7px 12px, a radius and a pointer
   cursor, all landing on a table cell. The cell has to say what it is. */
.t6 td.who { display:table-cell; width:auto; padding:var(--sp-3) var(--sp-4);
  border-radius:0; cursor:auto; font-weight:600; color:var(--accent-text); }
.t6 tr.sel { background:var(--accent-tint); }
.t6 tr.sel:hover { background:color-mix(in srgb, var(--seed-accent-hi) 18%, transparent); }
.t6-edit { width:74px; text-align:right; font-family:var(--f-num-col); font-weight:var(--num-wt-col);
  font-size:var(--fs-sm); color:var(--text); background:var(--surface-deep);
  border:1px solid color-mix(in srgb, var(--seed-accent-hi) 55%, transparent); border-radius:6px;
  padding:5px 9px; outline:none; box-shadow:var(--e-sunk); margin-right:-9px; }

.kpi { display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-4); }
@media (max-width:760px){ .kpi { grid-template-columns:1fr; }}
.klabel { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); }
.kval { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-2xl); color:var(--text);
  margin-top:var(--sp-2); line-height:1.05; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
.kval small { font-size:var(--fs-md); font-weight:600; color:var(--subtle-text); }
.kdelta { display:inline-flex; gap:5px; font-family:var(--f-num); font-weight:var(--num-wt);
  font-size:var(--fs-sm); margin-top:var(--sp-2); font-variant-numeric:tabular-nums; }
.kdelta.up { color:var(--success-text); } .kdelta.attn { color:var(--danger-text); }
.spark { display:block; margin-top:var(--sp-3); width:100%; height:34px; cursor:crosshair; }
.spark-tip { position:fixed; z-index:var(--z-overlay); pointer-events:none; white-space:nowrap;
  font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps); color:var(--text);
  background:var(--glass-bg); -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px);
  border:1px solid var(--glass-border); border-radius:8px; padding:5px 10px;
  opacity:0; transition:opacity calc(130ms * var(--dur)) ease; }
.spark .sline { stroke-dasharray:200; stroke-dashoffset:200; animation:draw calc(900ms * var(--dur)) var(--ease-entrance) calc(200ms * var(--dur)) forwards; }
.spark .sfill { opacity:0; animation:fadein calc(600ms * var(--dur)) ease calc(600ms * var(--dur)) forwards; }
@keyframes draw { to { stroke-dashoffset:0; }}
@keyframes fadein { to { opacity:1; }}

.kb { display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-4); }
@media (max-width:760px){ .kb { grid-template-columns:1fr; }}
.kb-col { background:var(--surface-deep); border-radius:var(--r); padding:var(--sp-2); box-shadow:var(--e-sunk); }
.kb-head { display:flex; align-items:baseline; gap:var(--sp-2); padding:6px var(--sp-2) var(--sp-2); }
.kb-head b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); }
.kb-head .ct { margin-left:auto; font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm); color:var(--subtle-text); }
.kb-card { position:relative; background:var(--grad-ground); border-radius:var(--r-xs); padding:var(--sp-3);
  box-shadow:var(--e1); cursor:grab; margin-bottom:var(--sp-2); transition:background var(--d-fast) var(--ease-hover), box-shadow var(--d-fast) var(--ease-hover); }
.kb-card:hover { background:var(--hover); box-shadow:var(--e2); }
.kb-card:last-child { margin-bottom:0; }
.kb-card b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); display:block; }
.kb-meta { display:flex; align-items:center; gap:var(--sp-2); margin-top:var(--sp-3); }
.kb-meta small { font-family:var(--f-num); font-weight:600; font-size:var(--fs-sm); color:var(--subtle-text); }
.ava-s { width:24px; height:24px; border-radius:calc(var(--r-xs) * .7); display:grid; place-items:center; flex-shrink:0;
  font-family:var(--f-ui); font-size:calc(var(--fs-caps) * .88); font-weight:600; line-height:1; color:var(--text); background:var(--ctrl); box-shadow:var(--e1); }
.ava-stack { display:flex; }
.ava-stack .ava-s { margin-left:-8px;
  background:linear-gradient(150deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 92%, white) 0%,
    var(--seed-accent-hi) 55%, color-mix(in oklab, var(--seed-accent-lo) 55%, var(--seed-accent-hi)) 100%);
  color:var(--on-accent); box-shadow:0 0 0 2px var(--surface-deep); }
[data-theme="light"] .ava-stack .ava-s { color:var(--on-accent); }
.ava-stack .ava-s:first-child { margin-left:0; }

.prog { height:7px; border-radius:99px; background:var(--surface-deep); box-shadow:var(--e-sunk); overflow:hidden; }
.prog > i { display:block; height:100%; border-radius:99px; background:var(--accent-lit);
  transition:width var(--d-slow) var(--ease-entrance); }
.skel { position:relative; overflow:hidden; background:var(--surface-deep); border-radius:var(--r-xs); box-shadow:var(--e-sunk); }
.skel::after { content:""; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.06), transparent);
  animation:shim 1.4s infinite; }
[data-theme="light"] .skel::after { background:linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent); }
@keyframes shim { to { transform:translateX(100%); }}

.tip { position:relative; }
/* A tooltip at opacity 0 is invisible but STILL LAID OUT. With white-space:nowrap
   a long tip becomes a 330px box that pushes the page wider than the viewport,
   which is exactly what overflowed this page at 375px. It wraps and is capped. */
.tip::after { content:attr(data-tip); position:absolute; bottom:calc(100% + 8px); left:50%;
  transform:translate(-50%,4px) scale(.97); white-space:normal; width:max-content;
  max-width:min(230px, 60vw); text-align:center; line-height:1.4;
  font-family:var(--f-ui); font-size:var(--fs-sm);
  color:var(--text); background:var(--glass-bg); -webkit-backdrop-filter:blur(16px); backdrop-filter:blur(16px);
  border:1px solid var(--glass-border); border-radius:var(--r-xs); padding:5px var(--sp-3);
  box-shadow:var(--e3); opacity:0; pointer-events:none; z-index:var(--z-overlay);
  transition:opacity var(--d-fast) ease, transform var(--d-fast) var(--ease-entrance); }
.tip:hover::after { opacity:1; transform:translate(-50%,0) scale(1); }

/* A TIP ABOVE AN ELEMENT AT THE TOP OF THE WINDOW IS A TIP NOBODY CAN READ.
   The default opens upward, which is right almost everywhere and wrong in the
   one place the system puts tips by default: the top bar. The bell sits ~27px
   from the top, so its tip rendered off-screen behind the browser chrome.

   CSS cannot detect clipping, but it does not need to here: nothing in a top
   bar ever has room above it. So anything in .topb, and anything asking for it
   by name, opens downward. Added 27-08-2026. */
.topb .tip::after, .tip.tip-below::after {
  bottom:auto; top:calc(100% + 8px); transform:translate(-50%,-4px) scale(.97); }
.topb .tip:hover::after, .tip.tip-below:hover::after {
  transform:translate(-50%,0) scale(1); }

/* The same for a tip on the right edge, which otherwise runs off the side. */
.tip.tip-end::after { left:auto; right:0; transform:translate(0,4px) scale(.97); }
.tip.tip-end:hover::after { transform:translate(0,0) scale(1); }
.topb .tip.tip-end::after { transform:translate(0,-4px) scale(.97); }
.topb .tip.tip-end:hover::after { transform:translate(0,0) scale(1); }

/* A menu sits OVER content, so it is mostly opaque. The blur stays for the edge
   and the sense of a pane, but you should never be able to read the table
   underneath it. */
.menu { border-radius:var(--r-xs); padding:6px; min-width:200px;
  background:color-mix(in srgb, var(--surface-raise) 94%, transparent);
  -webkit-backdrop-filter:blur(26px) saturate(150%); backdrop-filter:blur(26px) saturate(150%);
  border:1px solid var(--glass-border); box-shadow:var(--e3), inset 0 1px 0 var(--glass-hi); }
/* ══ THE THREE REGISTERS ══════════════════════════════════════════════════════
   Every string in the system sits in exactly one of three, and almost every
   casing fault is a string in the wrong one.

   1. THE LABEL LAYER, small tracked uppercase. Anything that names or
      classifies content instead of BEING content: table heads, eyebrows, status
      labels, accordion headers, and every command in a menu. The tell is that it
      never appears inside a sentence.
   2. THE NAME LAYER, Title Case with small words down. Discrete selectable
      things: service lines, options, chips, tags, workflow states, and buttons
      that name an object. "Hero Cut, up to 90 Seconds" keeps "up" and "to" low,
      which is what makes it title case rather than Capitalising Every Word.
   3. THE PROSE LAYER, sentence case. Body copy, help text, accordion bodies,
      errors, tooltips. Anything read as language.

   CAPS IS NOT LOUDER HERE, IT IS QUIETER, and that is the whole reason it
   works. The command register drops from 13px body to 11px tracked UI type, so
   a menu reads as a row of machined labels rather than a row of sentences. Caps
   set at body size WOULD shout, which is why a full size button naming an
   object stays in the name layer and never takes this treatment.

   The source strings are stored in correct Title Case regardless, so a surface
   that renders them without the transform is still right. */
.menu > button:not(.optrow) { font-family:var(--f-ui); font-size:var(--fs-caps);
  letter-spacing:var(--ls-caps); text-transform:uppercase; }
/* .optrow is the exception that proves it: that menu holds service NAMES with
   prices, not commands, so it stays in the name layer and at a readable size. */
.menu button { display:flex; width:100%; align-items:center; gap:var(--sp-3); font-family:var(--f-body);
  font-size:var(--fs-sm); color:var(--muted); padding:var(--sp-2) var(--sp-3); border-radius:calc(var(--r-xs)*.7);
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.menu button:hover { background:color-mix(in srgb, var(--seed-accent-hi) 18%, transparent); color:var(--text); }
.menu button.attn:hover { background:color-mix(in srgb, var(--danger) 16%, transparent); }
.menu button.attn { color:var(--danger-text); }
.menu button .k { margin-left:auto; font-family:var(--f-ui); font-size:var(--fs-caps); color:var(--subtle-text); }
.menu hr { height:1px; background:var(--line); border:none; margin:var(--sp-1) var(--sp-2); }

.empty { text-align:center; padding:var(--sp-7) var(--sp-4);
  border:1px dashed var(--line-strong); border-radius:var(--r-lg); }
.empty .ic { width:46px; height:46px; margin:0 auto var(--sp-3); border-radius:var(--r-xs);
  display:grid; place-items:center; background:var(--surface-deep); box-shadow:var(--e-sunk); color:var(--subtle); }
.empty b { font-family:var(--f-head); font-size:var(--fs-sm); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-head); display:block; margin-bottom:6px; }
.empty p { font-size:var(--fs-sm); color:var(--subtle-text); max-width:38ch; margin:0 auto var(--sp-4); }

/* ═══ NEW IN V5 · the atoms V4 never had ═══════════════════════════════════ */

/* Inline alert / banner. V4 had a toast but nothing that lives IN the page. */
.banner { display:flex; gap:var(--sp-3); align-items:flex-start; padding:var(--sp-3) var(--sp-4);
  border-radius:var(--r-xs); background:var(--surface-deep); box-shadow:inset 3px 0 0 var(--tone, var(--info)); }
.banner .bi { color:var(--tone-text, var(--info-text)); flex-shrink:0; margin-top:1px; }
.banner .bi svg { width:15px; height:15px; }
.banner b { display:block; font-family:var(--f-ui); font-size:var(--fs-sm); font-weight:600;
  letter-spacing:.06em; text-transform:uppercase; color:var(--tone-text, var(--info-text)); margin-bottom:3px; }
.banner p { font-size:var(--fs-sm); color:var(--muted); }
.banner.danger  { --tone:var(--danger);  --tone-text:var(--danger-text); }
.banner.warning { --tone:var(--warning); --tone-text:var(--warning-text); }
.banner.success { --tone:var(--success); --tone-text:var(--success-text); }
.banner.info    { --tone:var(--info);    --tone-text:var(--info-text); }

/* Spinner. Only for indeterminate waits where a skeleton cannot know the shape. */
/* `display:block` ADDED 31-08-2026, and it is a real trap closed. Width and
   height do not apply to a non-replaced INLINE element, and every existing use
   of `.spin` puts it inside a flex container - a button, `.busy`, `.connbar` -
   which blockifies it, so the 18px worked by accident of its neighbours. Drop
   one into an ordinary span and it collapses to the line box: the conic
   gradient renders as a thin crimson crescent, which is exactly what shipped to
   Knox for one build. A component whose size depends on what it happens to be
   inside is a component that will do this again. Blockifying changes nothing
   for a flex item, which is blockified either way. */
.spin { display:block; width:18px; height:18px; border-radius:50%; flex-shrink:0; padding:2.5px;
  background:conic-gradient(from 0deg, transparent 0deg, var(--accent-lit) 300deg);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite:exclude;
  animation:spin calc(800ms * var(--dur)) linear infinite; }
@keyframes spin { to { transform:rotate(360deg); }}

/* Popover. Distinct from tooltip (hover, text) and menu (list of actions). */
.pop { position:absolute; z-index:var(--z-drop); width:260px; background:var(--surface-raise);
  border-radius:var(--r-xs); box-shadow:var(--e3); padding:var(--sp-4); }
.pop h4 { font-family:var(--f-ui); font-size:var(--fs-sm); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); margin-bottom:var(--sp-2); }
.drawer { position:absolute; top:0; right:0; bottom:0; width:min(280px, 78%); background:var(--surface-raise);
  box-shadow:var(--e4); padding:var(--sp-4); transform:translateX(100%);
  transition:transform var(--d-slow) var(--ease-entrance); display:flex; flex-direction:column; gap:var(--sp-3); }
/* ── THE OPEN STATE HAS TO LIVE ON THE ELEMENT · 30-08-2026 ──────────────────
   Found building Darkroom's Library against the real dist, which is the only
   way any of these have ever been found.

   `.drawer` ships `transform:translateX(100%)`, and the ONLY rule that brought it
   back was `.drawer-demo.open .drawer` - scoped to the GALLERY'S DEMO WRAPPER. `.drawer-demo`
   is in _extract.py's DEMO drop set, so the open state is dropped from every
   app's dist and the panel sits permanently off screen. It looks like a
   component that shipped. It is a component that can never open.

   This is the `.scrim-demo` fault of 29-08-2026 in a different part: a
   documentation wrapper carrying something load-bearing away with it when it is
   dropped. `.modal` escaped it only because someone had already written the
   real `.scrim.open > .modal` beside the demo's.

   So the element carries its own state, and the demo keeps its wrapper form.
   Both live, deliberately, exactly like the portalled-menu twins above. */
.drawer.open { transform:none; }
.drawer .dh { display:flex; align-items:center; justify-content:space-between; }
.drawer .dh b { font-family:var(--f-head); font-size:var(--fs-sm); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-head); }

/* Combobox: defined once, with the pickers. Glass list, same skin as the dropdown. */

/* Dropzone. */
.dz { border-radius:var(--r-xs); background:var(--surface-deep); box-shadow:var(--e-sunk);
  padding:var(--sp-6) var(--sp-4); text-align:center; cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover), box-shadow var(--d-fast) var(--ease-hover); }
.dz:hover, .dz.over { background:var(--accent-tint); box-shadow:var(--e-sunk); outline:2px dashed var(--accent-live); outline-offset:-4px; }
.dz svg { width:22px; height:22px; color:var(--subtle); margin:0 auto var(--sp-2); }
.dz b { display:block; font-family:var(--f-ui); font-size:var(--fs-sm); font-weight:600; color:var(--text); }
.dz small { font-size:var(--fs-sm); color:var(--subtle-text); }

/* Timeline / activity feed. */
.tl { position:relative; padding-left:var(--sp-5); }
.tl::before { content:""; position:absolute; left:5px; top:6px; bottom:6px; width:1px; background:var(--line-strong); }
.tl-item { position:relative; padding-bottom:var(--sp-4); }
.tl-item:last-child { padding-bottom:0; }
.tl-item::before { content:""; position:absolute; left:calc(var(--sp-5) * -1 + 2px); top:5px; width:7px; height:7px;
  border-radius:50%; background:var(--ctrl); box-shadow:0 0 0 3px var(--surface); }
.tl-item.on::before { background:var(--accent-lit); }
.tl-item b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); display:block; }
.tl-item time { font-family:var(--f-num); font-weight:600; font-size:var(--fs-sm); color:var(--subtle-text);
  font-variant-numeric:tabular-nums; }
.tl-item p { font-size:var(--fs-sm); color:var(--muted); margin-top:3px; }

/* Tree nav. */
.tree-row { display:flex; align-items:center; gap:var(--sp-2); font-family:var(--f-body); font-size:var(--fs-sm);
  color:var(--muted); padding:5px var(--sp-2); border-radius:calc(var(--r-xs)*.7); cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.tree-row:hover { background:var(--hover); color:var(--text); }
.tree-row.on { color:var(--accent-text); background:var(--accent-tint); }
.tree-row .cv { width:11px; height:11px; color:var(--subtle-text); transition:transform var(--d-base) var(--ease-spring); }
.tree-row.open .cv { transform:rotate(90deg); }
.tree-kids { padding-left:var(--sp-4); margin-left:5px; border-left:1px solid var(--line); display:none; }
.tree-kids.open { display:block; }

/* Bulk-select toolbar. Appears when rows are checked. */
.bulk { display:flex; align-items:center; flex-wrap:wrap; min-width:0; max-width:100%;
  gap:var(--sp-2) var(--sp-4); padding:var(--sp-3) 0; border-bottom:1px solid var(--line-strong);
  background:none; border-radius:0; box-shadow:none; }
.bulk .n { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm); color:var(--text);
  font-variant-numeric:tabular-nums; }
.bulk .n small { font-family:var(--f-ui); font-weight:600; font-size:var(--fs-sm); color:var(--subtle-text);
  letter-spacing:.06em; text-transform:uppercase; margin-left:5px; }

/* ── `.float`: THE SELECTION BAR LEAVES THE PAGE ──────────────────────────
   ADDED 31-08-2026. Knox: "I need this to show up as a dialog box type of
   thing, like a status bar. Below, it should appear as we select, not on the
   page itself. Can we do that like an overlay?"

   `.bulk` in a band is the right answer for a surface where selecting is the
   main job - a table, a queue - because the bar belongs to the list and should
   live in its header. It is the wrong answer for a WALL, where selecting is
   something you do to pictures while looking at pictures: the bar takes a line
   off the top of the frames, and the thing you are acting on is what it pushed
   down.

   Floating fixes both. It sits over the wall near the bottom, nothing above it
   moves, and it is near the hand that has been clicking. `position:fixed`
   centred on the viewport rather than on the canvas: a rail is 197px and
   offsetting for it means an app measuring its own chrome, which is the kind of
   arithmetic that is right until somebody collapses the rail. Centre on the
   window, like every gallery that does this well.

   E5 and glass, because it is above everything on the surface and has to stay
   legible over a frame of any colour. It arrives from below, which is where it
   is: a bar that fades in place has no answer to "where did this come from". */
.bulk.float { position:fixed; left:50%; bottom:var(--sp-5); z-index:var(--z-drop);
  transform:translateX(-50%) translateY(0); width:auto; max-width:min(920px, calc(100vw - var(--sp-6)));
  flex-wrap:nowrap; gap:var(--sp-3); padding:var(--sp-3) var(--sp-4); border-bottom:none;
  border:1px solid var(--glass-border); border-radius:var(--r-pill);
  background:color-mix(in srgb, var(--seed-ink) 82%, transparent);
  -webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px); box-shadow:var(--e5);
  animation:bulk-rise calc(280ms * var(--dur)) var(--ease-entrance) both; }
@keyframes bulk-rise {
  from { opacity:0; transform:translateX(-50%) translateY(10px); }
  to   { opacity:1; transform:translateX(-50%) translateY(0); }}
/* A hairline between the verbs you use and the ones you regret, rather than a
   gap somebody has to notice. */
.bulk.float .sep { width:1px; align-self:stretch; margin:0 var(--sp-1);
  background:var(--line-strong); flex:none; }
@media (max-width:640px){
  .bulk.float { left:var(--sp-3); right:var(--sp-3); transform:none; max-width:none;
    flex-wrap:wrap; border-radius:var(--r); animation:none; }}

/* Command palette. E5, the highest surface in the system. */
.cmdk { border-radius:var(--r); background:var(--surface-raise); box-shadow:var(--e5); overflow:hidden; }
.cmdk .cin { display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-4);
  border-bottom:1px solid var(--line); }
.cmdk .cin input { font-family:var(--f-body); font-size:var(--fs-lg); color:var(--text); width:100%; }
.cmdk .cin input::placeholder { color:var(--disabled); }
.cmdk .cin svg { width:17px; height:17px; color:var(--subtle); flex-shrink:0; }
.cmdk .cgrp { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--subtle-text); padding:var(--sp-3) var(--sp-4) var(--sp-1); }
.cmdk .crow { display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-2) var(--sp-4);
  font-size:var(--fs-sm); color:var(--muted); cursor:pointer; }
.cmdk .crow:hover, .cmdk .crow.hi { background:var(--hover); color:var(--text); }
.cmdk .crow svg { width:14px; height:14px; color:var(--subtle); }
.cmdk .crow .k { margin-left:auto; font-family:var(--f-ui); font-size:var(--fs-caps); color:var(--subtle-text); }

/* Charts. V4 shipped only a sparkline. */
.bars { display:flex; align-items:flex-end; gap:var(--sp-2); height:120px; margin-bottom:26px; }
.bars > div { flex:1; border-radius:var(--r-xs) var(--r-xs) 3px 3px; position:relative;
  background:linear-gradient(180deg in oklab, var(--accent-lit) 0%, color-mix(in oklab, var(--seed-accent-hi) 55%, transparent) 100%);
  transform-origin:bottom; animation:grow calc(700ms * var(--dur)) var(--ease-entrance) both; }
@keyframes grow { from { transform:scaleY(0); }}
.bars > div span { position:absolute; bottom:-20px; left:0; right:0; text-align:center;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); }
.donut { width:120px; height:120px; }
/* BUTT caps, not round. A round cap adds half the stroke width of arc at each
   end, which on a 13px stroke over a 97px circumference is enormous: every
   segment overran its neighbour and the ring read as two arcs crossing. Round
   is only correct on a single-value ring with nothing beside it. */
.donut circle { fill:none; stroke-width:13; stroke-linecap:butt;
  transform:rotate(-90deg); transform-origin:center; }
.donut.single circle { stroke-linecap:round; }

/* Pagination */
.pager { display:inline-flex; align-items:center; gap:var(--sp-1); }
.pager button { min-width:32px; height:32px; padding:0 var(--sp-2); border-radius:var(--r-xs);
  font-family:var(--f-num); font-weight:600; font-size:var(--fs-sm); color:var(--muted);
  font-variant-numeric:tabular-nums; transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.pager button:hover { background:var(--hover); color:var(--text); }
.pager button svg { width:11px; height:11px; margin:0 auto; }
.pager button.on { background:var(--accent-lit); color:var(--on-accent); }
[data-theme="light"] .pager button.on { color:var(--on-accent); }

/* Toast. Severity is a status line inside, never a coloured bar. */
.toast { display:flex; gap:var(--sp-3); align-items:flex-start; padding:var(--sp-3) var(--sp-4);
  border-radius:var(--r-xs); background:var(--glass-bg); -webkit-backdrop-filter:blur(22px);
  backdrop-filter:blur(22px); border:1px solid var(--glass-border); box-shadow:var(--e4); max-width:360px; }
.toast b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); display:block; }
.toast p { font-size:var(--fs-sm); color:var(--muted); margin-top:2px; }

/* Modal */
.modal { border-radius:var(--r-lg); background:var(--grad-ground); box-shadow:var(--e4);
  padding:var(--sp-5);
  /* WIDTH, NOT MAX-WIDTH. A max-width here capped .w-md at 420 instead of
     560 and .w-xl at 420 instead of 1040, so the size scale below did
     nothing and the master's own demos rendered at the wrong size. */
  width:min(440px, 92vw); }

/* ── THE OVERLAY ── the real one, not the demo's ──────────────────────────
   ADDED 27-08-2026, AFTER IT COST AN APP A DAY. Knox Artiste Accounts moved to
   V5 and all 22 of its overlays laid themselves out down the page: every screen
   between 11,657 and 16,068 pixels tall on a 900px viewport. The cause was that
   this system described a modal and never shipped the thing that puts one on
   top of a page. `.modal` was a card, and every piece of overlay behaviour --
   fixed position, the backdrop, the centring, the entrance -- existed only
   inside `.scrim-demo`, which is a documentation wrapper on this page.

   An app cannot use a demo. So the overlay is a component now, and the demo
   below is what it always should have been: the same component, scoped to a box
   so it can be shown on this page without covering it.

   V4 shipped this and V5 dropped it in the port. Pulse and Grid have modals and
   would each have rediscovered it the same way. */
.scrim { position:fixed; inset:0; z-index:var(--z-modal); display:grid; place-items:center;
  background:var(--scrim); -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  opacity:0; pointer-events:none;
  transition:opacity calc(240ms * var(--dur)) var(--ease-hover); }
.scrim.open { opacity:1; pointer-events:auto; }
/* A VIEWER'S BACKDROP IS DEEPER THAN A DIALOG'S. A dialog sits over a page you
   are still working in and the page should stay legible behind it; a viewer
   REPLACES the page, and the one thing on screen is a picture. At the shared
   62% the wall behind stayed bright enough to compete with the frame in front
   of it, which is the opposite of what a viewer is for.

   Selected with `:has` rather than a class an app has to remember, because
   whether the backdrop is deep is a property of what is on it and not a
   decision each app should get to make differently. */
.scrim:has(> .viewer) { background:color-mix(in srgb, var(--seed-ink) 93%, transparent); }
/* `.modal` carries a max-width and no width, which is right for a card and not
   enough for an overlay: something has to say how wide it is before it is
   centred in a viewport. */
/* ── THE OVERLAY SIZE SCALE ────────────────────────────────────────────────
   ADDED 27-08-2026, and it is the biggest thing this system was missing.

   V5 defined 232 components and no composition. There was one overlay width
   and no way to ask for another, so every app hand-typed its own. Knox Artiste
   Accounts was measured carrying EIGHT: 440, 660, 680, 800, 96vw and three
   more. Knox's report was "the modal for the clients bit is way too wide when
   it doesn't need to be", and he was right: it was 680px because somebody
   typed 680 once, against nothing.

   Four sizes, and a dialog picks the smallest one its content fits in. The
   viewport cap rises with the size because a wide dialog on a phone should
   still breathe, and a narrow one should not stretch.

     (default)  a confirm, a short form, one decision          440
     .w-md      a record: a client, an expense, a subscription 560
     .w-lg      a working panel: notifications, a long form    760
     .w-xl      something that has to be READ: a document      1040

   THERE IS NO FIFTH. If content does not fit .w-xl it is a screen, not a
   dialog, and the answer is a route rather than a wider box. */
.scrim > .modal { width:min(440px, 92vw);
  transform:translateY(14px) scale(.97);
  transition:transform calc(360ms * var(--dur)) var(--ease-entrance); }
.scrim > .modal.w-md { width:min(560px, 92vw); }
.scrim > .modal.w-lg { width:min(760px, 92vw); }
.scrim > .modal.w-xl { width:min(1040px, 94vw); }
.scrim.open > .modal { transform:none; }
.modal h3 { font-family:var(--f-head); font-size:var(--fs-md); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-head); line-height:var(--lh-head); margin:0 0 var(--sp-3); color:var(--text); }
.modal p { font-size:var(--fs-sm); color:var(--muted); margin-bottom:var(--sp-5); }
.scrollx { overflow-x:auto; }
/* ── THE SEED CARDS ──
   Six cards on the same rhythm and the same corner as the six ramp swatches
   below them, because they are the same kind of object: a colour with its name
   under it. The old block was three tall pillars with the palette NAME painted
   over the colour, which failed twice. It carried hardcoded V4 names and hexes,
   so the document said "Royal Onyx, #2C5364 to #0F2027" over a red gradient and
   lied about its own system. And type sitting directly on the swatch can only
   stay readable if you know the colour in advance, which stopped being true the
   moment these became pickers.

   So the label sits on a --surface bar UNDER the swatch. That is readable
   against anything anybody picks, and it is the only version of this card that
   cannot break. Each card is a label wrapping a hidden colour input, so the
   whole card is the hit target and every hex is live.

   Roles, not names: Ink, Paper, Accent. A palette gets renamed; a role does
   not, and a name painted into a document drifts the day the seed changes. */
.seeds { display:grid; grid-template-columns:repeat(6,1fr); gap:var(--sp-2); margin-top:var(--sp-3); }
@media (max-width:900px){ .seeds { grid-template-columns:repeat(3,1fr); }}
@media (max-width:520px){ .seeds { grid-template-columns:repeat(2,1fr); }}
.seedcard { position:relative; display:block; border-radius:var(--r-xs); box-shadow:var(--e1);
  overflow:hidden; cursor:pointer; }
.seedcard.fixed { cursor:default; }
.seedcard .scw { display:block; height:70px; background:var(--sc, var(--ctrl)); position:relative; }
.seedcard .scm { display:block; padding:var(--sp-2); background:var(--surface); }
.seedcard b { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--text); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.seedcard .mono { display:block; margin-top:2px; font-family:var(--fm); font-size:var(--fs-caps);
  color:var(--subtle-text); letter-spacing:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.seedcard input[type=color] { position:absolute; inset:0; width:100%; height:100%; opacity:0;
  border:none; padding:0; cursor:pointer; }
.seedcard:has(input:focus-visible) { outline:2px solid var(--accent-live); outline-offset:2px; }


/* ═══ RESTORED FROM V4 ═════════════════════════════════════════════════════
   Fifteen components the first V5 pass dropped without saying so. Rebuilt on
   V5 tokens, so they ride the seeds and the density dial like everything else. */

/* Calendar. A native date input renders OS chrome and never ships. */
.cal { max-width:290px; }
.cal-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:var(--sp-3); }
.cal-head b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--text); }
.cal-nav { display:flex; gap:6px; }
.cal-nav button { width:26px; height:26px; border-radius:calc(var(--r-xs) * .9); display:grid; place-items:center;
  background:var(--surface-deep); color:var(--subtle); box-shadow:var(--e-sunk);
  transition:color calc(130ms * var(--dur)) var(--ease-hover); }
.cal-nav button:hover { color:var(--text); }
.cal-nav svg { width:11px; height:11px; }
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:2px; text-align:center; }
.cal-grid b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.08em;
  color:var(--subtle); padding:6px 0; }
.cal-grid span { font-family:var(--f-num-col); font-weight:600; font-size:var(--fs-sm); padding:8px 0;
  border-radius:calc(var(--r-xs) * .9); cursor:pointer; color:var(--muted); font-variant-numeric:tabular-nums;
  transition:background calc(130ms * var(--dur)) var(--ease-hover), color calc(130ms * var(--dur)) var(--ease-hover); }
.cal-grid span:hover { background:var(--hover); color:var(--text); }
.cal-grid span.dim { color:var(--disabled); }
/* The in-range fill is squared off so a range reads as one continuous band. */
.cal-grid span.inr { background:var(--accent-tint); border-radius:0; color:var(--text); }
.cal-grid span.on:has(+ span.inr) { border-radius:calc(var(--r-xs) * .9) 0 0 calc(var(--r-xs) * .9); }
.cal-grid span.inr + span.on { border-radius:0 calc(var(--r-xs) * .9) calc(var(--r-xs) * .9) 0; }
.cal-grid span.on { background:var(--accent-lit); color:var(--on-accent); box-shadow:var(--e2); }
[data-theme="light"] .cal-grid span.on { color:var(--on-accent); }

/* Time picker */
.timep { display:flex; gap:9px; align-items:center; }
.timep .cell { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-lg); color:var(--text);
  background:var(--surface-deep); border-radius:calc(var(--r-xs) * .9); padding:10px 15px;
  box-shadow:var(--e-sunk); font-variant-numeric:tabular-nums; }
.timep .colon { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-lg); color:var(--subtle); }
.timep .mer { display:flex; flex-direction:column; gap:4px; }
.timep .mer button { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.08em;
  padding:5px 11px; border-radius:calc(var(--r-xs) * .7); background:var(--surface-deep); color:var(--subtle);
  box-shadow:var(--e-sunk); transition:background calc(130ms * var(--dur)) var(--ease-hover), color calc(130ms * var(--dur)) var(--ease-hover); }
.timep .mer button.on { background:var(--accent-lit); color:var(--on-accent); box-shadow:var(--e2); }
[data-theme="light"] .timep .mer button.on { color:var(--on-accent); }

/* Filing index. Subgrid, so the three columns align across every row. */
.filing { display:grid; grid-template-columns:1fr max-content max-content;
  border-radius:var(--r); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e1); }
.frow { grid-column:1/-1; display:grid; grid-template-columns:subgrid; gap:var(--sp-4); align-items:center;
  padding:var(--sp-3) var(--sp-4); border-bottom:1px solid var(--line); cursor:pointer;
  transition:background calc(130ms * var(--dur)) var(--ease-hover); }
.frow:last-child { border-bottom:none; }
.frow:hover { background:var(--hover); }
.fname { font-family:var(--f-body); font-size:var(--fs-md); color:var(--text); }
.fname small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); margin-top:2px; }
.fmeta { font-family:var(--f-num); font-weight:600; font-size:var(--fs-sm); color:var(--subtle-text);
  white-space:nowrap; justify-self:end; font-variant-numeric:tabular-nums; }
/* Attention rows wear an inset bar, never a coloured background. */
.frow.attn { box-shadow:inset 3px 0 0 var(--danger); background:color-mix(in srgb, var(--danger) 6%, transparent); }
.frow.attn:hover { background:color-mix(in srgb, var(--danger) 10%, transparent); }

/* Accordion. Height animates on grid-template-rows, so no measured max-height. */
.acc { border-radius:var(--r); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e1); }
.acc-item + .acc-item { border-top:1px solid var(--line); }
/* An accordion header labels the block under it, so it belongs to the label
   layer. It stays at 13px rather than dropping to 11: this one is a hit target
   as well as a label, and it has to stay easy to aim at. */
.acc-trig { width:100%; display:flex; justify-content:space-between; align-items:center; gap:var(--sp-3);
  text-align:left; padding:var(--sp-4); color:var(--text); font-family:var(--f-ui); font-size:var(--fs-sm);
  font-weight:600; letter-spacing:var(--ls-caps); text-transform:uppercase;
  transition:background calc(130ms * var(--dur)) var(--ease-hover); }
.acc-trig:hover { background:var(--hover); }
.acc-trig svg { width:12px; height:12px; flex-shrink:0; color:var(--subtle);
  transition:transform calc(320ms * var(--dur)) var(--ease-spring); }
.acc-item.open .acc-trig svg { transform:rotate(180deg); }
.acc-body { display:grid; grid-template-rows:0fr; transition:grid-template-rows calc(320ms * var(--dur)) var(--ease-entrance); }
.acc-item.open .acc-body { grid-template-rows:1fr; }
.acc-body > div { overflow:hidden; }
/* The body had ZERO top padding, so it opened flush under the trigger and the
   trigger's hover fill ran straight into the first line of prose. The gap is
   not decoration: it is what separates the label from the thing it labels. */
.acc-body p { padding:var(--sp-3) var(--sp-4) var(--sp-4); font-size:var(--fs-sm);
  color:var(--muted); line-height:1.6; }

/* Step indicator */
.steps { display:flex; align-items:center; flex-wrap:nowrap; gap:var(--sp-3);
  /* The current step's ring is drawn outside its circle. Without room it is
     clipped by the scroll box, which is what sliced the top off step three. */
  padding:5px 2px; }
.step { display:flex; align-items:center; gap:10px; }
.step .sn { width:27px; height:27px; border-radius:50%; display:grid; place-items:center; flex-shrink:0;
  font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps); color:var(--subtle-text);
  background:var(--surface-deep); box-shadow:var(--e-sunk);
  transition:background calc(200ms * var(--dur)) var(--ease-hover), color calc(200ms * var(--dur)) var(--ease-hover), box-shadow calc(200ms * var(--dur)) var(--ease-hover); }
.step b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--subtle-text); }
.step.done .sn { background:var(--accent-lit); color:var(--on-accent); box-shadow:var(--e2); }
[data-theme="light"] .step.done .sn { color:var(--on-accent); }
.step.done b { color:var(--muted); }
.step.cur .sn { box-shadow:var(--e-sunk), 0 0 0 2px color-mix(in srgb, var(--seed-accent-hi) 50%, transparent);
  color:var(--accent-text); }
.step.cur b { color:var(--text); }
.step .sline { width:38px; height:1px; background:var(--line-strong); }

/* Media cards */
.media-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-4); }
.media-card { position:relative; isolation:isolate; border-radius:var(--r); overflow:hidden;
  background:var(--grad-ground); box-shadow:var(--e1); cursor:pointer;
  transition:background calc(130ms * var(--dur)) var(--ease-hover), box-shadow calc(130ms * var(--dur)) var(--ease-hover); }
.media-card:hover { box-shadow:var(--e2); }
.media-thumb { aspect-ratio:16/9; display:grid; place-items:center; color:rgba(255,255,255,.8); position:relative;
  background:radial-gradient(95% 115% at 72% 0%, color-mix(in srgb, var(--seed-accent-hi) 45%, transparent), transparent 65%), var(--surface-deep); }
.media-thumb svg { width:26px; height:26px; }
.media-thumb .dur { position:absolute; right:8px; bottom:8px; font-family:var(--f-num); font-weight:600;
  font-size:var(--fs-caps); background:rgba(0,0,0,.55); color:var(--on-accent); padding:2px 7px; border-radius:6px; }
.media-body { padding:var(--sp-3); }
.media-body b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); display:block; }
.media-body small { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--subtle-text); }

/* Icon grid, carved cells */
.iconrow { display:grid; grid-template-columns:repeat(8,1fr); gap:10px; }
.icell { aspect-ratio:1; display:grid; place-items:center; border-radius:calc(var(--r-xs) * 1.1); cursor:pointer;
  background:var(--surface-deep); box-shadow:var(--e-sunk); color:var(--muted);
  transition:color calc(130ms * var(--dur)) var(--ease-hover), background calc(130ms * var(--dur)) var(--ease-hover); }
.icell:hover { color:var(--accent-text); background:var(--accent-tint); }
.icell svg { width:16px; height:16px; }


/* Checklist. The whole row is the hit area, never just the box. */
.check { display:flex; align-items:center; gap:var(--sp-3); padding:11px 2px; border-bottom:1px solid var(--line);
  cursor:pointer; }
.check:last-child { border-bottom:none; }
.check .lbl { font-family:var(--f-body); font-size:var(--fs-md); color:var(--muted); flex:1;
  transition:color calc(160ms * var(--dur)) var(--ease-hover); }
.check:hover .lbl { color:var(--text); }
.check small { font-family:var(--f-num); font-weight:600; font-size:var(--fs-caps); color:var(--subtle-text); }

/* Toggle list row: the label answers to the switch. */
.vt-row { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-4);
  padding:14px 2px; border-bottom:1px solid var(--line); }
.vt-row:last-child { border-bottom:none; }
.vt-row span { font-size:var(--fs-md); color:var(--muted); transition:color calc(200ms * var(--dur)) var(--ease-hover); }
.vt-row.on span { color:var(--accent-text); font-weight:500; }

/* The accent hero band */
.accent-band { position:relative; border-radius:var(--r-lg); overflow:hidden; padding:30px;
  background:radial-gradient(85% 100% at 78% 0%, color-mix(in srgb, var(--seed-accent-hi) 75%, transparent), transparent 62%), var(--grad-accent);
  box-shadow:var(--e3); color:var(--on-accent); }
.accent-band::after { content:""; position:absolute; inset:0; background-image:var(--noise);
  background-size:130px; opacity:calc(.06 * var(--noise-o)); mix-blend-mode:overlay; pointer-events:none; }
.accent-band > * { position:relative; z-index:1; }
.accent-band h3 { font-family:var(--f-head); font-size:var(--fs-xl); font-weight:400; text-transform:uppercase;
  letter-spacing:var(--ls-head); line-height:var(--lh-head); color:var(--on-accent); margin:0 0 var(--sp-3); }
.accent-band p { color:color-mix(in srgb, var(--on-accent) 78%, transparent); max-width:52ch; }

.toast .ib { margin-left:auto; margin-top:-3px; margin-right:-6px; }
.pager .ell { color:var(--subtle); padding:0 4px; font-family:var(--f-num); }

@media (max-width:760px){ .media-grid { grid-template-columns:1fr; } .iconrow { grid-template-columns:repeat(4,1fr); }}



/* ═══ IDENTITY · THE RAIL FOOT ═════════════════════════════════════════════
   THERE IS NO USER CHIP. Knox, 23-08-2026: identity is not a tag floating in a
   layout, it is a fixed block at the bottom of the rail, and it is the same
   block in every app. This is lifted from Knox Artiste Accounts, which is where
   the pattern was settled and where Darkroom and Grid already follow it.

   Two rows, one block, pinned to the bottom of the rail above a hairline:
   the theme switch, then the account. Settings lives inside the account menu,
   never in the rail, because the rail is for the work and the account is not
   the work. The menu opens UPWARDS, since the block sits at the bottom. */
.rail-foot { margin-top:auto; padding-top:var(--sp-3); display:flex; flex-direction:column;
  gap:var(--sp-1); border-top:1px solid var(--line); }

/* THE LABEL NAMES THE MODE YOU ARE IN, not the one you might switch to.
   Knox, 19-08-2026, on the KAA build: "the toggle is always showing light mode,
   even if I'm going into dark mode." The old row was a switch called "Light
   mode" and the toggle said whether light was on, which is correct in principle
   and ambiguous by construction: half the people read it as a label about now
   and half as an offer. So the words and the glyph both state what is true
   right now. A moon and "Dark mode" in the dark, a sun and "Light mode" in the
   light. Do not re-litigate this. */
.rail-theme { display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:7px 12px; border-radius:var(--r-xs); cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover); }
.rail-theme:hover { background:var(--hover); }
.rail-theme .lbl { display:flex; align-items:center; gap:10px; min-width:0;
  font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted);
  transition:color var(--d-fast) var(--ease-hover); }
.rail-theme:hover .lbl { color:var(--text); }
.rail-theme .lbl svg { width:15px; height:15px; flex-shrink:0; }
.rail-theme .tgl { transform:scale(.82); transform-origin:right center; }

.who { position:relative; display:flex; align-items:center; gap:9px; width:100%;
  box-sizing:border-box; padding:7px 12px; border-radius:var(--r-xs); cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover); }
.who:hover { background:var(--hover); }
.who .whoname { display:block; min-width:0; font-family:var(--f-body); font-size:var(--fs-sm);
  color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  transition:color var(--d-fast) var(--ease-hover); }
.who:hover .whoname { color:var(--text); }
.who .ava { width:26px; height:26px; font-size:var(--fs-caps); }
.who .chev { margin-left:auto; width:10px; height:6px; flex-shrink:0; color:var(--subtle);
  transition:transform 240ms var(--ease-spring); }
.who.open .chev { transform:rotate(180deg); }

/* This menu overlaps its OWN container, so it cannot be glass. A 42% translucent
   panel over the canvas reads as depth; the same panel over the rail lets the
   nav links and their counts bleed straight through the account name. Opaque,
   with the blur kept only for the edge treatment. */
.who-menu { position:absolute; z-index:var(--z-drop); top:auto; bottom:calc(100% + 10px);
  left:0; right:auto; min-width:100%; width:max-content; max-width:min(320px, 88vw);
  transform-origin:bottom left;
  background:var(--surface-raise); -webkit-backdrop-filter:none; backdrop-filter:none;
  box-shadow:var(--e4);
  opacity:0; transform:translateY(6px) scale(.985); pointer-events:none;
  transition:opacity 180ms var(--ease-hover), transform 240ms var(--ease-spring); }
/* Twin of the ancestor gate. See the note on .drop-menu. 27-08-2026. */
.who.open .who-menu, .who-menu.open { opacity:1; transform:none; pointer-events:auto; }
/* The identity line is a caption, not a menu item, so it does not take the hover. */
.who-menu .who-head { display:block; padding:10px 12px 11px; border-radius:0; cursor:default; }
.who-menu .who-head:hover { background:transparent; }
.who-menu .who-head b { display:block; font-family:var(--f-body); font-size:var(--fs-sm);
  font-weight:600; color:var(--text); }
.who-menu .who-head span { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--muted);
  margin-top:4px; overflow-wrap:anywhere; }

/* Collapsed rail: the block stays, the words go. */
[data-rail="icons"] .rail-foot { align-items:center; }
[data-rail="icons"] .rail-theme { justify-content:center; padding:7px 0; width:42px; }
[data-rail="icons"] .rail-theme .lbl span:not(#themeIcon) { display:none; }
[data-rail="icons"] .rail-theme .tgl { display:none; }
[data-rail="icons"] .who { justify-content:center; padding:7px 0; width:42px; }
[data-rail="icons"] .who .whoname, [data-rail="icons"] .who .chev { display:none; }
[data-rail="icons"] .who-menu { min-width:210px; }


/* ═══ THE FRONT DOOR · AUTH SURFACES ═══════════════════════════════════════
   Sign in, sign up, password reset. Structure taken from Knox Artiste Accounts'
   own signin.html, which is where this was settled.

   A finance or production tool's front door is not a marketing page: no hero,
   no gradient wash, no illustration, no social buttons. One card on the ground
   gradient, the wordmark above it, the fields, one key. The page should look
   like the app it opens, because it is the first promise the app makes. */
.ground { position:relative; isolation:isolate; min-height:100dvh; display:grid;
  place-items:center; padding:var(--sp-6) var(--sp-5); background:var(--grad-ground); }
.ground > .grain { position:absolute; inset:0; pointer-events:none; z-index:0;
  background-image:var(--noise); background-size:120px; opacity:calc(.04 * var(--noise-o)); mix-blend-mode:overlay; }
[data-theme="light"] .ground > .grain { opacity:.025; mix-blend-mode:multiply; }

.gate { position:relative; z-index:1; width:min(400px, 100%); }
/* Sign-up carries eight fields. At 400px that is a scroll; at 560 it is a form. */
.gate.wide { width:min(568px, 100%); }

.mark { text-align:center; margin-bottom:var(--sp-5); }
.mark .wordmark { font-family:var(--f-hero); font-size:var(--fs-lg); text-transform:uppercase;
  letter-spacing:var(--ls-head); color:var(--text); }
.mark .wordmark span { color:var(--accent-lit); }
/* --subtle sits near the AA floor, and these captions have to be read, so they
   take --muted. Same finding as the app and as KAA's own front door. */
.mark .sub { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-top:9px; }

.gate .card { padding:var(--sp-5); }
.gate h1 { font-family:var(--f-head); font-size:var(--fs-lg); font-weight:400;
  text-transform:uppercase; letter-spacing:var(--ls-head); line-height:var(--lh-head);
  color:var(--text); margin:0 0 6px; }
.gate-lede { font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted);
  margin:0 0 var(--sp-5); line-height:1.55; max-width:38ch; }

.gate.wide .gate-lede { max-width:56ch; }
.rows { display:flex; flex-direction:column; gap:var(--sp-4); }
.rows.pair { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-4); }
@media (max-width:520px){ .rows.pair { grid-template-columns:1fr; }}
/* One action on the page is the definition of a primary, so the front door
   takes the accent key at full width. */
.gate .btn-accent, .gate .btn-key { width:100%; justify-content:center; margin-top:var(--sp-5); }
.gate .btn-accent[disabled] { pointer-events:none; }

/* The reveal lives INSIDE the well, the same way the shortcut hint lives inside
   the search well. It is never a checkbox below the field. */
.pw { display:flex; align-items:center; padding-right:6px; }
.pw .well-input { flex:1; }
.peek { display:grid; place-items:center; width:30px; height:30px; border-radius:calc(var(--r-xs)*.8);
  color:var(--subtle); cursor:pointer; flex-shrink:0; background:none; border:none;
  transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover); }
.peek:hover { color:var(--text); background:var(--accent-tint); }
.peek:active { transform:scale(.94); transition-duration:70ms; }
.peek svg { width:15px; height:15px; }

/* Requirements are a CHECKLIST, not a coloured strength bar. A bar says "weak"
   without saying what to change, and it signals with colour alone. */
.reqs { display:flex; flex-direction:column; gap:5px; margin-top:10px; }
/* The UNMET line is the one somebody needs to read, so it takes the readable
   tier and the met one recedes. The first pass had this the wrong way round,
   which put the only text that matters at 2.95:1. */
.req { display:flex; align-items:center; gap:8px; font-family:var(--f-body);
  font-size:var(--fs-caps); color:var(--muted); transition:color var(--d-fast) var(--ease-hover); }
.req .box { width:13px; height:13px; border-radius:4px; flex-shrink:0; display:grid;
  place-items:center; background:var(--surface-deep); box-shadow:var(--e-sunk); color:transparent;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.req .box svg { width:8px; height:8px; }
.req.met { color:var(--subtle); }
.req.met .box { background:var(--accent-lit); color:var(--on-accent); box-shadow:none; }
[data-theme="light"] .req.met .box { color:var(--on-accent); }

.gate .f-err { display:none; margin-top:var(--sp-4); line-height:1.5; }
.gate .f-err.on { display:block; }
/* The done state borrows the accent, not the status colour: finishing a reset is
   not an alert. */
.gate .f-done { display:none; margin-top:var(--sp-4); font-family:var(--f-ui);
  font-size:var(--fs-sm); font-weight:600; color:var(--accent-text); line-height:1.5; }
.gate .f-done.on { display:block; }

.authrow { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3);
  margin-top:var(--sp-4); }
.authlink { font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted);
  text-decoration:none; border-bottom:1px solid var(--line-strong); cursor:pointer;
  transition:color var(--d-fast) var(--ease-hover), border-color var(--d-fast) var(--ease-hover); }
.authlink:hover { color:var(--text); border-color:var(--accent-live); }

.foot { margin-top:var(--sp-5); text-align:center; font-family:var(--f-body);
  font-size:var(--fs-caps); color:var(--muted); line-height:1.7; }
.foot a { color:var(--muted); text-decoration:none; border-bottom:1px solid var(--line-strong); }
.foot a:hover { color:var(--text); }


/* ═══ FROM THE APPS ════════════════════════════════════════════════════════
   Built 23-08-2026 from an inventory of Pulse, Knox Artiste Accounts and
   Darkroom. Everything here already existed in at least one app, and several
   existed in two or three under different names, which is the whole argument
   for having it in one place. Nothing below is speculative. */

/* ── SEARCH · four shapes, because they are four different jobs ─────────────
   The apps had all four and the system had one, so each app kept re-inventing
   the other three. */
/* 1 · Inline filter. Lives above a table and filters it live. */
.srch { display:flex; align-items:center; gap:var(--sp-2); padding:0 var(--sp-3); }
.srch svg { width:14px; height:14px; color:var(--subtle); flex-shrink:0;
  transition:color var(--d-fast) var(--ease-hover); }
.srch:focus-within svg { color:var(--accent-text); }
.srch input { flex:1; min-width:0; background:none; border:none; outline:none; line-height:1.2;
  font-family:var(--f-body); font-size:var(--fs-md); color:var(--text); padding:12px 0; }
.srch input::placeholder { color:var(--disabled); }
.srch .clr { width:20px; height:20px; border-radius:50%; display:none; place-items:center;
  color:var(--subtle); flex-shrink:0; }
.srch.has .clr { display:grid; }
.srch .clr:hover { color:var(--text); background:var(--hover); }
/* 2 · The result line under a live filter. Says what the search did. */
.srch-found { display:flex; align-items:center; gap:var(--sp-3); margin-top:var(--sp-2);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); }
.srch-found b { font-family:var(--f-num); font-weight:var(--num-wt); color:var(--text);
  font-variant-numeric:tabular-nums; }
/* 3 · Grouped picker: sticky category heads, a value on the right, and a way
       to add what is not there yet. A flat list cannot do a price column. */
.optgrp { position:sticky; top:0; z-index:1; background:var(--surface-raise);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--subtle); padding:var(--sp-2) var(--sp-3) 5px; }
.optrow { display:flex; align-items:baseline; gap:var(--sp-3); width:100%; text-align:left;
  padding:9px var(--sp-3); border-radius:calc(var(--r-xs) * .8); font-family:var(--f-body);
  font-size:var(--fs-sm); color:var(--muted); }
.optrow:hover, .optrow.hi { background:var(--accent-tint); color:var(--text); }
.optrow .nm { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; }
.optrow .px { font-family:var(--f-num); font-weight:var(--num-wt); color:var(--text);
  font-variant-numeric:tabular-nums; flex-shrink:0; }
.optrow.none { color:var(--subtle-text); font-style:italic; pointer-events:none; }
.optrow.addnew { color:var(--accent-text); font-weight:600;
  border-top:1px solid var(--line); border-radius:0; margin-top:var(--sp-1); padding-top:var(--sp-3); }

/* ── FILTER BAR ── KAA and Darkroom each built this separately. */
.filters { display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap; }
.filters .well { flex:1 1 240px; min-width:0; }
.fchip { font-family:var(--f-ctrl); font-size:var(--fs-caps); font-weight:600; letter-spacing:.07em;
  text-transform:uppercase; line-height:1; color:var(--muted); background:none;
  border:1px solid var(--line-strong); border-radius:var(--r-pill); padding:8px 14px; cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover),
             border-color var(--d-fast) var(--ease-hover); }
.fchip:hover { color:var(--text); border-color:var(--ctrl-hi); }
.fchip.on { background:var(--accent-lit); color:var(--on-accent); border-color:transparent; }
[data-theme="light"] .fchip.on { color:var(--on-accent); }
.fchip .n { font-family:var(--f-num); font-weight:var(--num-wt); margin-left:6px; opacity:.75;
  font-variant-numeric:tabular-nums; }

/* ── DATE RANGE ── a start and an end, two months side by side, presets above.
   The single-month single-date .cal cannot express a range at all. */
.rangep { display:inline-block; }
.range-panel { background:var(--surface-raise); border-radius:var(--r); box-shadow:var(--e3);
  padding:var(--sp-4); width:max-content; max-width:100%; }
.range-presets { display:flex; flex-wrap:wrap; gap:var(--sp-1); padding-bottom:var(--sp-3);
  margin-bottom:var(--sp-3); border-bottom:1px solid var(--line); }
.range-months { display:flex; gap:var(--sp-5); }
@media (max-width:640px){ .range-months { flex-direction:column; gap:var(--sp-4); }}
.range-foot { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-3);
  margin-top:var(--sp-4); padding-top:var(--sp-3); border-top:1px solid var(--line); }
.range-read { font-family:var(--f-num); font-weight:600; font-size:var(--fs-sm); color:var(--text);
  font-variant-numeric:tabular-nums; }
.range-read .sep { color:var(--subtle-text); margin:0 6px; }
.range-read .un { color:var(--subtle-text); font-family:var(--f-body); font-weight:400; }
/* The two ends are solid; everything between them is a continuous tinted band,
   so a range reads as one thing rather than a row of separate marks. */
.cal-grid span.r-in { background:var(--accent-tint); border-radius:0; color:var(--text); }
.cal-grid span.r-a { border-radius:var(--r-xs) 0 0 var(--r-xs); }
.cal-grid span.r-b { border-radius:0 var(--r-xs) var(--r-xs) 0; }
.cal-grid span.r-a, .cal-grid span.r-b { background:var(--accent-lit); color:var(--on-accent); box-shadow:var(--e2); }
[data-theme="light"] .cal-grid span.r-a, [data-theme="light"] .cal-grid span.r-b { color:var(--on-accent); }
.month-nav { display:inline-flex; align-items:center; gap:var(--sp-1); }
.month-nav b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--text);
  min-width:112px; text-align:center; }

/* ── EXPORT ── five surfaces across two apps fired exports from bare buttons. */
.export .sp-menu button svg { width:13px; height:13px; flex-shrink:0; color:var(--subtle); }
.exform { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-4) var(--sp-5); }
@media (max-width:560px){ .exform { grid-template-columns:1fr; }}
.exform .ex-head { grid-column:1/-1; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text); }
.exacts { grid-column:1/-1; display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-3); padding-top:var(--sp-4); border-top:1px solid var(--line); }

/* ── LINE ITEMS AND TOTALS ──
   KAA has the editor, Pulse has the cart. Same component at opposite ends of
   the estate, in neither case in the system. */
.lines { display:flex; flex-direction:column; gap:var(--sp-2); }
.line { display:grid; grid-template-columns:minmax(0,1fr) 92px 116px 132px auto; gap:var(--sp-3);
  align-items:center; padding:var(--sp-3); border-radius:var(--r-xs);
  background:var(--surface-deep); box-shadow:var(--e-sunk); }
@media (max-width:820px){ .line { grid-template-columns:1fr 1fr; } .line .l-desc { grid-column:1/-1; }}
.line .l-desc b { display:block; font-family:var(--f-body); font-size:var(--fs-md); font-weight:600; color:var(--text); }
.line .l-desc small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-top:3px; }
.line .num { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); text-align:right; font-variant-numeric:tabular-nums; }
.line .l-rm { width:28px; height:28px; border-radius:calc(var(--r-xs)*.8); display:grid;
  place-items:center; color:var(--subtle-text); }
.line .l-rm:hover { color:var(--danger-text); background:color-mix(in srgb, var(--danger) 12%, transparent); }
.line .l-rm svg { width:13px; height:13px; }
.totals { margin-left:auto; width:min(320px,100%); background:var(--surface-deep);
  border-radius:var(--r-xs); box-shadow:var(--e-sunk); padding:var(--sp-4); }
.totals .row { display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-4);
  padding:6px 0; font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted); }
.totals .row .v { font-family:var(--f-num-col); font-weight:var(--num-wt-col); color:var(--text);
  font-variant-numeric:tabular-nums; }
.totals .row.grand { margin-top:var(--sp-2); padding-top:var(--sp-3);
  border-top:1px solid var(--line-strong); color:var(--text); }
.totals .row.grand .v { font-size:var(--fs-lg); }
/* Reserved height, so a live figure appearing under a field cannot shift the
   layout under somebody's cursor. */
.f-calc { min-height:1.4em; font-family:var(--f-num); font-weight:600; font-size:var(--fs-caps);
  color:var(--muted); font-variant-numeric:tabular-nums; }
.f-calc.bad { color:var(--danger-text); }

/* ── FIGURE STRIP ── subgrid, so labels, values and notes align across every
   card no matter how long the content is. */
/* `1fr` SPREADS THREE FIGURES ACROSS THE FULL WIDTH and turns a compact strip
   into a banner, while six figures at a fixed width clip. The same `1fr` cannot
   serve both, and both are real: Knox Artiste Accounts has strips of three
   (subscriptions, VAT) and a strip of six (who owes what).

   `max-content` with a 150px floor serves both. Three sit together at the left
   instead of spreading; six need 1,000px and get it; a genuinely huge figure
   pushes its own column wider, because a clipped number is worse than an uneven
   row. `overflow-x:auto` catches the rest.

   Found in KAA, fixed here 27-08-2026, because a strip of three is not a KAA
   problem. */
.figs { display:grid; grid-auto-flow:column; grid-auto-columns:minmax(150px, max-content);
  grid-template-rows:auto auto auto; gap:4px var(--sp-4); align-items:start; overflow-x:auto; }
.fig { display:grid; grid-row:1/4; grid-template-rows:subgrid; gap:4px; }
.fig .l { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--muted); }
.fig .v { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-2xl);
  color:var(--text); line-height:1.05; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
.fig .n { font-family:var(--f-body); font-size:var(--fs-caps); color:var(--subtle-text); }

/* ── ROW LIST ── all three apps built this. Name, sub, value, selectable. */
.rows-list { display:flex; flex-direction:column; }
/* NO HORIZONTAL PADDING. A row's padding is its hit height, not an indent, and
   15px of it on each side pushed the row's text 15px inboard of everything else
   in the same container while the divider underneath still spanned the full
   width. That is the whole fault: a rule wider than the text it sits under, and
   an invoice number that did not line up with the figure above it. Vertical
   padding stays, because that IS the target height. */
.rowitem { display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-3) 0;
  border-bottom:1px solid var(--line); cursor:pointer; text-align:left; width:100%;
  transition:background var(--d-fast) var(--ease-hover); }
.rowitem:last-child { border-bottom:none; }
/* The fill bleeds back out to where the padding used to be, so a row still
   reads as a band rather than a tight box around the words. */
.rowitem:hover { background:var(--hover); box-shadow:calc(var(--sp-3) * -1) 0 0 var(--hover),
  var(--sp-3) 0 0 var(--hover); }
.rowitem .rl { flex:1; min-width:0; }
.rowitem .rl b { display:block; font-family:var(--f-body); font-size:var(--fs-md);
  font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rowitem .rl small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-top:3px; }
.rowitem .rv { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; flex-shrink:0; }

/* ── TABLE ROW ACTIONS ── a fixed grid so the icons line up down the column
   whatever each row can actually do. A flex row wanders and looks broken. */
.acts { display:grid; grid-auto-flow:column; gap:2px; justify-content:end; }
.act { width:28px; height:28px; border-radius:calc(var(--r-xs)*.8); display:grid; place-items:center;
  color:var(--subtle-text); transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover); }

/* ── the touch floor for row actions ──────────────────────────────────────
   28px is right beside a mouse and a miss under a thumb, and a table cell can
   hold six of them. THIS BLOCK HAS TO SIT AFTER `.act`, not with the rest of
   the coarse-pointer floor near the top of the sheet: at equal specificity the
   later rule wins, so the one up there computed 28px anyway and measured as
   though nothing had been fixed. Found 27-08-2026 against Knox Artiste
   Accounts at 375px. The strip gives the width back by closing its own gap. */
@media (pointer: coarse) {
  .act { width:40px; height:40px; }
  .acts { gap:0; }}
.act:hover { color:var(--text); background:var(--hover); }
.act svg { width:14px; height:14px; }
.act.attn:hover { color:var(--danger-text); background:color-mix(in srgb, var(--danger) 12%, transparent); }
.act-gap { width:28px; }

/* ── NOTIFICATIONS ── KAA has one, Pulse builds it five times. */
.bell { position:relative; width:36px; height:36px; border-radius:var(--r-pill); display:grid;
  place-items:center; color:var(--muted); cursor:pointer;
  transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover); }
.bell:hover { color:var(--text); background:var(--hover); }
.bell svg { width:17px; height:17px; }
.bell .n { position:absolute; top:3px; right:3px; min-width:15px; height:15px; padding:0 4px;
  border-radius:var(--r-pill); background:var(--danger); color:var(--on-accent); font-family:var(--f-num);
  font-weight:var(--num-wt); font-size:calc(var(--fs-caps) * .82); line-height:15px; text-align:center;
  font-variant-numeric:tabular-nums; box-shadow:0 0 0 2px var(--surface); }
.bell.quiet .n { display:none; }
.nlist { display:flex; flex-direction:column; gap:var(--sp-3); }
.nitem { padding:var(--sp-4); border-radius:var(--r-xs); background:var(--surface-deep);
  box-shadow:inset 3px 0 0 var(--nsev, var(--line-strong)); }
.nitem.urgent { --nsev:var(--danger); } .nitem.attention { --nsev:var(--warning); }
.nitem.info { --nsev:var(--info); }
.nitem .nsev { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--nsev, var(--muted)); }
.nitem b { display:block; font-family:var(--f-body); font-size:var(--fs-md); font-weight:600;
  color:var(--text); margin-top:5px; }
.nitem p { font-size:var(--fs-sm); color:var(--muted); margin-top:4px; }
.nitem .nact { display:flex; gap:var(--sp-2); justify-content:flex-end; margin-top:var(--sp-3); }
/* Handled items stay visible and recede. Removing them destroys the audit trail. */
.nitem.was { opacity:.55; box-shadow:inset 3px 0 0 var(--line-strong); }
.nitem.was .nact { display:none; }

/* ── COLLAPSIBLE TABLE GROUP ── a tbody that opens and closes. */
.t6 tbody.mgroup > tr.mhead td { background:var(--surface-deep); cursor:pointer;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:var(--ls-caps);
  text-transform:uppercase; color:var(--muted); }
.t6 tbody.mgroup > tr.mhead td .mchev { display:inline-block; width:9px; height:9px; margin-right:8px;
  transition:transform var(--d-base) var(--ease-spring); }
.t6 tbody.mgroup.open > tr.mhead td .mchev { transform:rotate(90deg); }
/* Rows collapse by SHRINKING, never display:none: the height animates and the
   DOM stays intact for the audit trail. V6 law, from the Accounts accordion. */
.t6 tbody.mgroup > tr:not(.mhead) td { transition:padding calc(240ms * var(--dur)) var(--ease-entrance),
  opacity calc(160ms * var(--dur)) ease, line-height calc(240ms * var(--dur)) var(--ease-entrance); }
.t6 tbody.mgroup:not(.open) > tr:not(.mhead) td { padding-top:0; padding-bottom:0;
  border-bottom-width:0; font-size:0; line-height:0; opacity:0; }
.t6 tbody.mgroup:not(.open) > tr:not(.mhead) td > * { display:none; }
.t6 tbody.mgroup:not(.open) > tr:not(.mhead) { pointer-events:none; }

/* ── PROMPT COMPOSER ──
   REPLACED IN V6 by the console instrument (section: The console). The flat
   glass card is gone; the class name .composer survives on the new build so
   app markup survives. The new rules live in the V6 layer further down. */

/* A chip that opens a value list. It shows what is SET; the setting's name is on
   hover. The value takes the accent, because it is the thing that changed. The
   chevron sits in a fixed slot so the label is not shunted off centre by it. */
.pchip { display:inline-flex; align-items:center; gap:9px; height:var(--ctrl-h); font-family:var(--f-ctrl); /* carries its OWN height as of V6: it once took it from .composer .cbot > * and rendered half size anywhere else */
  font-size:var(--fs-caps); font-weight:600; letter-spacing:.06em; text-transform:uppercase;
  /* --accent-text is accent TYPE on a surface, for a link or an accent figure.
     It was never a control label colour, and using it here is why the model and
     ratio keys read light red on this system and light blue on Royal Onyx. A key
     label is --text, the same as every other key on the bar. */
  line-height:1; color:var(--text); background:var(--ctrl); border-radius:var(--bar-r);
  padding:0 12px 0 14px; cursor:pointer; box-shadow:var(--e1); white-space:nowrap;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.pchip:hover { background:var(--ctrl-hi); color:var(--text); }
.pchip svg { width:9px; height:9px; color:var(--subtle); flex-shrink:0;
  transition:transform var(--d-base) var(--ease-spring); }
.pchip.open svg { transform:rotate(180deg); }



/* A panel the composer docks into, for the demo and for any app that wants one. */
.sidepanel { display:flex; flex-direction:column; height:100%; min-height:0;
  background:var(--grad-ground); border-left:1px solid var(--line); }
.sidepanel .sp-head { flex-shrink:0; padding:var(--sp-4);
  border-bottom:1px solid var(--line); display:flex; align-items:center; gap:var(--sp-3); }
.sidepanel .sp-head b { flex:1; font-family:var(--f-head); font-size:var(--fs-md); font-weight:400;
  text-transform:uppercase; letter-spacing:var(--ls-head); }
.sidepanel .sp-scroll { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; padding:var(--sp-4); }

/* ── SPEND METER ── any metered app needs the balance where it can be seen. */
.spend { display:flex; align-items:center; gap:var(--sp-3); white-space:nowrap; }
.spend .m { width:96px; height:6px; border-radius:99px; background:var(--surface-deep);
  box-shadow:var(--e-sunk); overflow:hidden; }
.spend .m > i { display:block; height:100%; border-radius:99px; background:var(--accent-lit); }
.spend .v { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; }
.spend .v small { font-family:var(--f-ui); font-weight:600; font-size:var(--fs-caps);
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-left:5px; }

/* ── MODAL VARIANTS ──
   A modal that scrolls needs its own architecture: a head that stays, a body
   that scrolls and does not chain to the page, and a gutter so the rounded
   corner does not clip the scrollbar. */
.modal.tall { display:flex; flex-direction:column; max-height:min(78vh, 720px); padding:0; }
/* THE HEAD AND FOOT RESERVE THE SAME SCROLLBAR GUTTER AS THE BODY.
   The body is the only part that scrolls, so it was the only part reserving a
   gutter, and every right edge inside it therefore sat 10px inboard of the head
   and the foot. That is why the status read further right than the figures and
   the amounts below it: not a padding mistake, a gutter only one of the three
   was paying for.

   scrollbar-gutter applies to any scroll container, and overflow:hidden makes
   one, so hidden plus stable reserves EXACTLY what the body reserves, in
   whatever browser, with no hardcoded scrollbar width to drift. Measured: a
   200px box gives 190px of content under both overflow:hidden and overflow:auto
   with stable, and 200px without. */
/* STRUCTURE BELONGS TO EVERY MODAL WITH A HEAD. SCROLLING BELONGS TO .tall.
   These were one block scoped to .modal.tall, so a plain confirm dialog
   written as head/body/foot got no padding on the body and no alignment on
   the foot, and the only way to fix it was to add a class whose name says
   the dialog is tall when it is not. The two concerns are now separate and
   .tall means one thing: the body scrolls. */
.modal:has(> .m-head) { padding:0; }
.modal .m-head { flex-shrink:0; padding:var(--sp-5) var(--sp-5) var(--sp-4);
  border-bottom:1px solid var(--line); display:flex; align-items:center;
  gap:var(--sp-3); }
.modal .m-head h3 { margin:0; flex:1; min-width:0; font-size:var(--fs-md);
  text-wrap:balance; overflow-wrap:anywhere; }
.modal .m-head .status { flex-shrink:0; }
.modal .m-body { padding:var(--sp-5); }
/* THE HEAD AND FOOT RESERVE THE SAME GUTTER THE BODY DOES, and only when
   the body actually scrolls. overflow:hidden makes a scroll container, so
   hidden plus stable reserves exactly what auto plus stable reserves, with
   no hardcoded scrollbar width to drift. Without this the status in the
   head read 10px further right than the figures below it. */
.modal.tall .m-head, .modal.tall .m-foot { overflow:hidden; scrollbar-gutter:stable; }
.modal.tall .m-body { flex:1; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; }
.modal .m-foot { flex-shrink:0; padding:var(--sp-4) var(--sp-5);
  border-top:1px solid var(--line); display:flex; align-items:center;
  justify-content:flex-end; gap:var(--sp-2); }
.m-back { width:28px; height:28px; border-radius:calc(var(--r-xs)*.8); display:grid;
  place-items:center; color:var(--subtle-text); flex-shrink:0; }
.m-back:hover { color:var(--text); background:var(--hover); }
.m-back svg { width:14px; height:14px; }
/* A destructive confirm names the thing and asks twice. Colour is never the
   only signal here, because the status hue sits close to the brand accent. */
/* The stripe is an inset shadow on a square head sitting inside a 20px
   radius, so it ran straight across the curve instead of following it. */
.modal.destructive .m-head { box-shadow:inset 0 3px 0 var(--danger);
  border-top-left-radius:var(--r-lg); border-top-right-radius:var(--r-lg); }
.confirm-name { font-family:var(--f-num); font-weight:var(--num-wt); color:var(--text);
  background:var(--surface-deep); border-radius:calc(var(--r-xs)*.7); padding:2px 7px; }
.btn-danger { background:none; border-color:var(--danger); color:var(--danger-text); }
.btn-danger:hover { background:color-mix(in srgb, var(--danger) 14%, transparent); }
.btn-danger.armed { background:var(--danger); color:var(--on-accent); border-color:transparent; }
.btn-danger:active { transform:scale(.98); transition-duration:calc(70ms * var(--dur)); box-shadow:var(--e-sunk); }

/* ═══ V6 · WHAT V5 WAS MISSING ═════════════════════════════════════════════
   Added 27-08-2026 from three weeks of applying V5 to Darkroom, Knox Artiste
   Accounts and Pulse. Four laws, measured on real failures:
   1 · If the system ships it, the app calls it. An app layer is 400 lines or
       less, and nothing in it sets padding, height or font-size on a control.
   2 · No load-bearing rule is scoped to a demo wrapper or to one parent.
       Every surface, scrim and finish below is global, and every component
       carries its own height.
   3 · Keyboard keys render as symbols: ⌘K, ⇧⏎, ⌥drag. One keycap component.
   4 · State classes and required elements are part of each component's
       contract, documented beside its markup. */

/* ── THE INSTRUMENT ── the four-layer physics, named, so any hero component
   can use the recipe: ground (lit from the top left, falls away), bezel (1px
   masked ring, light catches the top edge), grain (fractal tile at 120px),
   rest shadow (--e-rest: it sits above the canvas, it does not float off it). */
/* A DATA CELL NEVER WRAPS MID-VALUE. An invoice id, a date, a name or a money
   figure reads as one unit or not at all; when the table is wider than its box,
   the .t6-wrap/.scrollx wrapper scrolls instead. DD-MM-YYYY in every data
   field is a law on this page — a date split across lines breaks it. */
.t6 th, .t6 td { white-space:nowrap; }

.instrument { position:relative; isolation:isolate; border-radius:var(--r-lg);
  background:var(--grad-ground); box-shadow:var(--e-rest); }
.instrument::before { content:""; position:absolute; inset:0; border-radius:inherit; padding:1px;
  pointer-events:none; z-index:3; background:var(--edge-ring);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite:exclude; }
.instrument::after { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background-image:var(--noise); background-size:120px; opacity:calc(.045 * var(--noise-o)); mix-blend-mode:overlay; }
[data-theme="light"] .instrument::after { opacity:.03; mix-blend-mode:multiply; }

/* ═══ THE CONSOLE ═══ the prompt instrument, replacing V5's flat composer.
   Ported from the original Darkroom creation bar, re-seeded to the locked
   studio accent. The class name .composer survives so app markup survives.
   CONTRACT · the root carries data-mode="stills" or "video". Row keys are
   <button>, never <div>. The engine menu carries .open on the MENU as well as
   the root (portal twin). Elements only one mode uses carry .still-only or
   .vid-only. The fire key is the one accent key on the screen. */
.composer { padding:var(--sp-4) var(--sp-4) var(--sp-3); }
.composer .crail { display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap;
  margin-bottom:var(--sp-3); position:relative; z-index:4; }
.composer .crail .sp, .composer .cset .sp { flex:1; min-width:0; height:auto; }

/* Recipe chip: a two-line preset. Thumb, caps eyebrow over the set value, chevron. */
.rchip { display:inline-flex; align-items:center; gap:9px; height:44px; padding:0 14px 0 7px;
  background:var(--surface); border:1px solid var(--line-strong); border-radius:var(--r-pill);
  box-shadow:0 1px 3px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.04); cursor:pointer;
  transition:background var(--d-fast) var(--ease-hover), transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
[data-theme="light"] .rchip { box-shadow:0 1px 3px rgba(15,32,39,.08), inset 0 1px 0 rgba(255,255,255,.85); }
.rchip:hover { background:var(--hover); }
.rchip:active { transform:scale(.97); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }
.rchip .thumb { width:20px; height:20px; border-radius:7px; flex:none;
  background:var(--tc, var(--accent-deep)); box-shadow:var(--e2); }
.rchip .tx { display:flex; flex-direction:column; align-items:flex-start; gap:3px; }
.rchip .tx small { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--subtle-text); line-height:1; }
.rchip .tx b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); line-height:1; }
.rchip svg { width:9px; height:6px; color:var(--subtle); flex-shrink:0; margin-left:2px; }

.refct { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text); white-space:nowrap; }
.refct b { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; }
.droppill { display:inline-flex; align-items:center; gap:7px; height:32px; padding:0 13px;
  border:1px dashed var(--line-strong); border-radius:var(--r-pill); color:var(--subtle-text);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.1em;
  text-transform:uppercase; white-space:nowrap; }
.droppill svg { width:11px; height:11px; }

/* The start-frame well. Video only. Candidates are generated frames, plates and
   dragged images, never the library — the well says so on its face. */
.startframe { display:flex; align-items:center; gap:var(--sp-3); margin-bottom:var(--sp-3);
  padding:9px 12px; background:var(--surface-deep); border:1px solid var(--line);
  border-radius:calc(var(--r) * .7); box-shadow:var(--e-sunk); position:relative; z-index:4; }
.startframe .sfthumb { width:62px; height:42px; border-radius:9px; flex:none;
  background:linear-gradient(150deg in oklab, var(--seed-accent-hi) 0%, var(--seed-accent-lo) 100%);
  box-shadow:var(--e2); }
.startframe .sfx { flex:1; min-width:0; }
.startframe .sfx small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.14em; text-transform:uppercase; color:var(--subtle-text); }
.startframe .sfx b { display:block; font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600;
  color:var(--text); margin-top:2px; }
.startframe .sfx b span { font-weight:500; color:var(--muted); }
.startframe .prov { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.09em; text-transform:uppercase; color:var(--subtle-text); text-align:right; line-height:1.7; }

/* Row 2. Mode keys, the prompt well, the tag keys. */
.composer .cprompt { display:flex; align-items:stretch; gap:10px; position:relative; z-index:3; }
.modekeys { display:flex; flex-direction:column; gap:4px; flex:none; justify-content:center; }
.mkey { width:38px; height:30px; border-radius:11px; display:grid; place-items:center;
  color:var(--subtle); background:var(--surface-deep); box-shadow:var(--e-sunk); cursor:pointer;
  transition:background calc(200ms * var(--dur)) var(--ease-hover), color calc(200ms * var(--dur)) var(--ease-hover), box-shadow calc(200ms * var(--dur)) ease; }
.mkey svg { width:14px; height:14px; }
.mkey:hover { color:var(--muted); }
.mkey.on { color:var(--on-accent); background:var(--accent-lit); box-shadow:var(--e2); }

/* THE PROMPT WELL. Prose and entity chips flow inline: a sentence you are
   building, not a form you are filling. The caret is drawn, so the well reads
   armed even in a spec. */
.pwell { flex:1; min-width:0; display:flex; align-items:center; flex-wrap:wrap; gap:8px;
  padding:11px 16px; background:var(--surface-deep); border:1px solid var(--line);
  border-radius:calc(var(--r) * .7); box-shadow:var(--e-sunk);
  font-family:var(--f-body); font-size:var(--fs-md); color:var(--text); cursor:text;
  transition:border-color var(--d-fast) var(--ease-hover), box-shadow var(--d-fast) var(--ease-hover); }
.pwell:focus-within, .pwell.armed { border-color:color-mix(in srgb, var(--seed-accent-hi) 50%, transparent);
  box-shadow:var(--e-sunk), inset 0 0 12px rgba(0,0,0,.10); }
.echip { display:inline-flex; align-items:center; gap:7px; font-family:var(--f-ui);
  font-size:var(--fs-caps); font-weight:600; letter-spacing:.04em; color:var(--accent-text);
  background:var(--surface); border-radius:var(--r-pill); padding:4px 6px 4px 5px; box-shadow:var(--e2); }
.echip .thumb { width:15px; height:15px; border-radius:5px; background:var(--tc, var(--accent-deep)); }
.echip i { font-style:normal; cursor:pointer; color:var(--subtle); width:15px; height:15px;
  border-radius:50%; display:grid; place-items:center; font-size:11px; line-height:1;
  transition:color var(--d-fast) ease, background var(--d-fast) ease; }
.echip i:hover { color:var(--text); background:var(--hover); }
.pcaret { width:2px; height:16px; background:var(--accent-live); border-radius:1px;
  animation:kcaret 1.1s steps(1) infinite; }
@keyframes kcaret { 0%,45% { opacity:1; } 55%,100% { opacity:0; }}
.ghost-at { display:inline-flex; align-items:center; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.06em; color:var(--subtle); border:1px dashed var(--line-strong);
  border-radius:var(--r-pill); padding:4px 10px; cursor:pointer;
  transition:color var(--d-fast) ease, border-color var(--d-fast) ease; }
.ghost-at:hover { color:var(--muted); border-color:var(--ctrl-hi); }
.ckeys { display:flex; align-items:center; gap:6px; flex:none; }
.ckey { width:38px; height:38px; border-radius:12px; display:grid; place-items:center;
  background:var(--surface); color:var(--muted); box-shadow:0 2px 4px rgba(0,0,0,.24); cursor:pointer;
  font-family:var(--fm); font-size:14px;
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover), transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
[data-theme="light"] .ckey { box-shadow:0 2px 5px rgba(15,32,39,.12); }
.ckey:hover { background:var(--hover); color:var(--text); }
.ckey:active { transform:scale(.96); box-shadow:var(--e-sunk); }
.ckey svg { width:14px; height:14px; }

/* Row 3. The settings rail. Everything is a real control with its own height. */
.composer .cset { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-top:var(--sp-3); position:relative; z-index:4; }
.ctdiv { width:1px; height:22px; background:var(--line); margin:0 3px; flex:none; }
.mchip { display:inline-flex; align-items:center; height:32px; font-family:var(--f-ui);
  font-size:var(--fs-sm); font-weight:500; color:var(--muted); background:none;
  border:1px solid transparent; border-radius:var(--r-pill); padding:0 14px; cursor:pointer; white-space:nowrap;
  transition:color var(--d-fast) var(--ease-hover), background var(--d-fast) var(--ease-hover), transform calc(90ms * var(--dur)) var(--ease-press); }
.mchip:hover { color:var(--text); }
.mchip:active { transform:scale(.97); }
.mchip.on { color:var(--text); background:var(--surface); border-color:var(--line-strong);
  box-shadow:0 1px 3px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.04); }
[data-theme="light"] .mchip.on { box-shadow:0 1px 3px rgba(15,32,39,.08), inset 0 1px 0 rgba(255,255,255,.85); }
.doct { display:inline-flex; align-items:center; gap:9px; cursor:pointer; background:none; border:none; }
.doct .dlab { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--muted);
  transition:color var(--d-base) var(--ease-hover); }
.doct .tgl { width:44px; height:24px; }
.doct .tgl::after { width:18px; height:18px; }
.doct .tgl.on::after { transform:translateX(20px); }
.doct.off .dlab { color:var(--subtle-text); }

/* The engine chip. Opens UPWARD into a glass menu; rows that do not apply to
   the current mode are dimmed, the active row is tinted. */
.engwrap { position:relative; }
.engchip { display:inline-flex; align-items:center; gap:9px; height:36px; padding:0 14px;
  background:var(--surface-deep); border:1px solid var(--line); border-radius:var(--r-pill);
  box-shadow:var(--e-sunk); cursor:pointer; white-space:nowrap;
  transition:border-color calc(200ms * var(--dur)) ease; }
.engchip:hover { border-color:var(--line-strong); }
.engchip b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); }
.engchip .res { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps);
  color:var(--subtle-text); font-variant-numeric:tabular-nums; }
.engchip svg { width:10px; height:6px; color:var(--subtle);
  transition:transform calc(240ms * var(--dur)) var(--ease-spring); }
.engwrap.open .engchip svg { transform:rotate(180deg); }
.engmenu { position:absolute; bottom:calc(100% + 9px); left:0;
  width:330px; max-width:calc(100vw - var(--sp-5) * 2); z-index:var(--z-drop-over);
  padding:6px; border-radius:calc(var(--r) * .7);
  background:color-mix(in srgb, var(--surface-raise) 92%, transparent);
  -webkit-backdrop-filter:blur(22px) saturate(160%); backdrop-filter:blur(22px) saturate(160%);
  border:1px solid var(--glass-border); box-shadow:var(--e4), inset 0 1px 0 var(--glass-hi);
  opacity:0; transform:translateY(6px) scale(.985); pointer-events:none; transform-origin:bottom left;
  transition:opacity calc(160ms * var(--dur)) ease, transform calc(240ms * var(--dur)) var(--ease-spring); }
/* Twin of the ancestor gate. See the note on .drop-menu. */
.engwrap.open .engmenu, .engmenu.open { opacity:1; transform:none; pointer-events:auto; }
.erow { display:flex; align-items:center; gap:10px; width:100%; padding:10px 12px;
  border-radius:10px; cursor:pointer; text-align:left; background:none; border:none;
  transition:background calc(110ms * var(--dur)) ease; }
.erow:hover { background:color-mix(in srgb, var(--seed-accent-hi) 18%, transparent); }
.erow.on { background:var(--accent-tint); }
.erow .ex { flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; }
.erow .ex b { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); }
.erow .ex small { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); }
.erow .px { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; }
.erow.dim { opacity:.4; pointer-events:none; }

/* Value pills: aspect, resolution, batch, duration. Numerals on a raised pill. */
.vpill { display:inline-flex; align-items:center; height:32px; padding:0 13px;
  font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps);
  color:var(--muted); background:var(--surface); border:1px solid var(--line-strong);
  border-radius:var(--r-pill); cursor:pointer; white-space:nowrap; font-variant-numeric:tabular-nums;
  box-shadow:0 1px 3px rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.04);
  transition:color var(--d-fast) ease, background var(--d-fast) ease, transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
[data-theme="light"] .vpill { box-shadow:0 1px 3px rgba(15,32,39,.08), inset 0 1px 0 rgba(255,255,255,.85); }
.vpill:hover { color:var(--text); background:var(--hover); }
.vpill:active { transform:scale(.97); box-shadow:var(--e-sunk); }
.vpill.on { color:var(--on-accent); background:var(--accent-lit); border-color:transparent; box-shadow:var(--e2); }

/* THE FIRE KEY. The one accent key on the screen, with the price on its face.
   The figure on the key is what will be charged. */
.fire { display:inline-flex; align-items:center; gap:10px; height:44px; padding:0 22px;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.07em;
  text-transform:uppercase; color:var(--on-accent); white-space:nowrap; cursor:pointer;
  background:linear-gradient(115deg in oklab, color-mix(in oklab, var(--seed-accent-hi) 88%, white) 0%,
    var(--seed-accent-hi) 55%, color-mix(in oklab, var(--seed-accent-hi) 82%, black) 100%);
  border:1px solid color-mix(in oklab, var(--seed-accent-lo) 70%, black);
  border-radius:calc(var(--r) * .8); position:relative; overflow:hidden; isolation:isolate;
  box-shadow:0 2px 5px rgba(0,0,0,.30), inset 0 1px 0 rgba(255,255,255,.20);
  transition:transform calc(90ms * var(--dur)) var(--ease-press), box-shadow calc(90ms * var(--dur)) ease; }
[data-theme="light"] .fire { box-shadow:0 2px 6px rgba(15,32,39,.22), inset 0 1px 0 rgba(255,255,255,.30); }
.fire::before { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:rgba(255,255,255,.10); opacity:0; transition:opacity calc(160ms * var(--dur)) ease; z-index:-1; }
.fire:hover::before { opacity:1; }
.fire:active { transform:scale(.98); box-shadow:var(--e-sunk); transition-duration:calc(70ms * var(--dur)); }
.fire svg { width:12px; height:12px; flex:none; }
.fire .price { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm);
  letter-spacing:0; font-variant-numeric:tabular-nums; }
.fire[disabled] { color:var(--disabled); background:var(--ctrl); box-shadow:none;
  border-color:var(--line); filter:saturate(.15); pointer-events:none; }

/* The honesty footer. Doctrine on: a micro caps line. Doctrine off: a status line. */
.cfoot { display:flex; align-items:center; justify-content:center; gap:var(--sp-4); margin-top:var(--sp-3); }
.cfoot .quiet { font-family:var(--f-brand); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.18em; text-transform:uppercase; color:var(--subtle); white-space:nowrap; }
.cfoot .manual { display:none; align-items:center; gap:8px; font-family:var(--f-ui);
  font-size:var(--fs-caps); font-weight:600; letter-spacing:.1em; text-transform:uppercase;
  color:var(--danger-text); white-space:nowrap; }
.cfoot .manual i { width:6px; height:6px; border-radius:50%; background:currentColor; }
.composer-zone.manual .cfoot .quiet { display:none; }
.composer-zone.manual .cfoot .manual { display:inline-flex; }

/* Mode gating. The root carries data-mode; elements one mode owns carry a class. */
.composer[data-mode="stills"] .vid-only { display:none !important; }
.composer[data-mode="video"] .still-only { display:none !important; }

/* DOCKED. The same component built into a panel: loses the float, the blur and
   the corner on the joined side, keeps the sunken well, the rows and the fire key. */
.composer.docked { border-radius:0; background:var(--surface); box-shadow:none; border-top:1px solid var(--line); }
.composer.docked::before, .composer.docked::after { display:none; }
.composer.docked .crail, .composer.docked .cset { flex-wrap:wrap; }

/* THE COMPILED SHOT CARD. Floats above the console, same physics one level lighter. */
.shotcard { position:relative; isolation:isolate; width:min(700px, 100%); border-radius:var(--r);
  background:var(--grad-ground); box-shadow:var(--e2); }
.shotcard::before { content:""; position:absolute; inset:0; border-radius:inherit; padding:1px;
  pointer-events:none; z-index:2; background:var(--edge-ring);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite:exclude; }
.shotcard::after { content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background-image:var(--noise); background-size:120px; opacity:calc(.04 * var(--noise-o)); mix-blend-mode:overlay; }
.shotcard .sc-head { position:relative; display:flex; align-items:baseline; justify-content:space-between;
  gap:var(--sp-3); padding:13px 18px; border-bottom:1px solid var(--line); }
.shotcard .sc-head .lft, .shotcard .sc-head .rgt { display:flex; align-items:baseline; gap:12px; min-width:0; }
.shotcard .sc-eyebrow { font-family:var(--f-brand); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.16em; text-transform:uppercase; color:var(--accent-text); white-space:nowrap; }
.shotcard .sc-eng { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); white-space:nowrap; }
.shotcard .sc-seed { font-family:var(--fm); font-size:var(--fs-caps); color:var(--subtle-text); white-space:nowrap; }
.shotcard .sc-px { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm); color:var(--text);
  font-variant-numeric:tabular-nums; }
.shotcard .sc-body { position:relative; display:flex; gap:16px; padding:14px 18px 12px; }
.shotcard .sc-read { flex:1; min-width:0; font-family:var(--fm); font-size:var(--fs-caps);
  line-height:1.6; color:var(--muted); display:-webkit-box; -webkit-line-clamp:3;
  -webkit-box-orient:vertical; overflow:hidden; }
.shotcard .sc-flag { position:relative; display:flex; align-items:center; gap:12px; margin:0 18px 4px;
  padding:10px 14px; border-radius:10px; box-shadow:inset 3px 0 0 var(--danger);
  background:color-mix(in srgb, var(--danger) 6%, transparent); }
.shotcard .sc-flag .fstat { display:inline-flex; align-items:center; gap:9px; font-family:var(--f-ui);
  font-size:var(--fs-caps); font-weight:600; letter-spacing:.1em; text-transform:uppercase;
  color:var(--danger-text); white-space:nowrap; }
.shotcard .sc-flag .fstat i { width:6px; height:6px; border-radius:50%; background:currentColor; font-style:normal; }
.shotcard .sc-flag p { flex:1; min-width:0; font-family:var(--f-body); font-size:var(--fs-sm);
  color:var(--muted); margin:0; }
.shotcard .sc-flag p b { color:var(--text); font-weight:600; }
.shotcard .sc-foot { position:relative; display:flex; align-items:center; justify-content:flex-end;
  gap:10px; padding:10px 18px 16px; }

/* Role-tagged reference thumbs: Identity, Product, Plate. Own size, 52px. */
.roleths { display:flex; gap:8px; flex:none; }
.roleth { text-align:center; }
.roleth .rt { width:52px; height:52px; border-radius:12px; background:var(--tc, var(--surface-deep));
  box-shadow:var(--e2); }
.roleth small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--subtle-text); margin-top:6px; }
/* AND THE THUMB TAKES A REAL PICTURE. ADDED 30-08-2026, the same gap `.vframe`
   had and for the same reason: `.rt` is a coloured plate, which is a stand-in
   for a reference and cannot BE one. An app showing what it was actually given
   puts its own <img> here. `cover`, not `contain`, because this is a 52px
   thumbnail and the rule the wall follows applies - a thumbnail is a picture
   and a letterboxed one at this size is two grey bars and a stamp.

   Selected at `.roleth img.rt` (0-2-1) and not `img.rt`, because `.roleth .rt`
   above is 0-2-0 and would win every declaration they share. */
.roleth img.rt { display:block; object-fit:cover; background:var(--surface-deep); }

/* ═══ SPOTLIGHT ═══ one search, one palette. The trigger is the top bar well;
   the overlay is the palette on the global scrim at the modal layer.
   CONTRACT · rows are <button class="crow">; the highlighted row carries .hi;
   a record row shows its type as a quiet caps label (.typ), never a pill;
   at most one shortcut per row, rendered as a symbol keycap. */
.scrim.spot { place-items:start center; padding-top:12vh; }
.scrim.spot > .cmdk { width:min(600px, 92vw); transform:translateY(14px) scale(.97);
  transition:transform calc(360ms * var(--dur)) var(--ease-entrance); }
.scrim.spot.open > .cmdk { transform:none; }
.cmdk .crow .typ { margin-left:auto; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); }
.cmdk .crow .k .kbd { color:var(--subtle-text); }
.cmdk .cfootr { display:flex; align-items:center; gap:var(--sp-4); padding:var(--sp-2) var(--sp-4);
  border-top:1px solid var(--line); font-family:var(--f-ui); font-size:var(--fs-caps);
  color:var(--subtle-text); letter-spacing:.06em; }
.cmdk .cfootr .kbd { margin-right:5px; }

/* ═══ MEDIA ═══ walls, strips, tiles, the viewer. Two laws, both measured:
   a frame is drawn at its own aspect or not at all, and sizing goes
   height-first, then clamps by width. */

/* The wall header: filters above, a zoom dial on the right. Composes existing parts. */
.wallhead { display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap; margin-bottom:var(--sp-3); }

/* ── A THUMBNAIL IS A FRAME, NOT A DASHBOARD · KNOX, 30-08-2026 ──────────────
   RECORDED OVERRIDE of `06-wall.html`, alongside the row-control one. The
   signed drawing puts three readouts on every tile: a filename, a "QA pass"
   badge, and a red / amber / green drift light. All three come off the wall and
   the strip.

   Knox: "We don't need to see the name of the file." And: "We don't need a QA
   pass: red, amber, green. That is something that we need to look at when we
   click on the image and it shows up, the status and everything on the right
   side. As a thumbnail, you don't need to see all of the stuff."

   He is right about where it belongs. Section 1.10 of the Darkroom framework
   already specifies the drift verdict as a Lightbox side-panel section: a score
   out of 100, a verdict word, a note, and a chip per moved facet. A dot on a
   130px thumbnail carries none of that, so it was never a summary of the
   verdict, it was a fourth thing to learn sitting where the picture is. Twelve
   per row means twelve dots and twelve filenames competing with twelve frames.

   The rules stay: other surfaces in this system put `.tmeta` and `.qa` over
   imagery. What changed is that the wall and the strip no longer use them, and
   `.dr-light` is off both. Nothing left the vocabulary; the wall stopped
   spending the frame on it. */

/* Hover meta on a tile: name, engine, cost. Not used by the wall or the strip.
   Meta on hover only. */
.tile .tmeta { position:absolute; left:8px; right:8px; bottom:7px; z-index:1; display:flex;
  align-items:baseline; gap:8px; opacity:0; transition:opacity var(--d-fast) var(--ease-hover); }
.tile:hover .tmeta { opacity:1; }
.tile .tmeta b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.06em; text-transform:uppercase; color:#F2F2F0; min-width:0; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; } /* knox-allow: over imagery, not a theme surface */
.tile .tmeta small { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps);
  color:rgba(255,255,255,.75); margin-left:auto; white-space:nowrap; font-variant-numeric:tabular-nums; }
.tile .qa { position:absolute; left:7px; top:7px; z-index:1; display:inline-flex; align-items:center; gap:6px;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.09em;
  text-transform:uppercase; color:#F2F2F0; background:rgba(0,0,0,.5); border-radius:6px; padding:3px 7px;
  opacity:0; transition:opacity var(--d-fast) var(--ease-hover); }
.tile:hover .qa { opacity:1; }

/* The review strip: fixed height, width follows aspect, scrolls horizontally. */
.rstrip { display:flex; gap:2px; overflow-x:auto; overscroll-behavior-x:contain; padding-bottom:4px; }
.rstrip .tile { flex:none; height:var(--strip-h, 150px); width:auto;
  aspect-ratio:var(--ar, 1.5); min-width:0; }

/* The building tile: a clip whose proxy is not ready. A quiet spinner and a
   caps label, never a broken image. */
.tile.building { cursor:default; }
.tile.building .art { opacity:.18; filter:saturate(.3); }
.tile.building .bmsg { position:absolute; inset:0; display:flex; flex-direction:column; gap:9px;
  align-items:center; justify-content:center; color:var(--subtle-text); font-family:var(--f-ui);
  font-size:var(--fs-caps); font-weight:600; letter-spacing:.1em; text-transform:uppercase; }
.tile.building .bmsg .spin { width:16px; height:16px;
  background:conic-gradient(from 0deg, transparent 0deg, var(--subtle) 300deg); }

/* THE VIEWER. The frame at its true aspect over the global scrim, and a side
   panel carrying "Why it looks this way". Its delete uses the danger pattern
   ABOVE the viewer's own layer, so the confirm can never render underneath it. */
/* THE DOCKED PANEL. --panel-w, never a typed number: the four apps between
   them shipped 320, 460, 400, 264, 478, 208 and 200 for the same idea. Below
   the PANEL breakpoint it goes under the canvas rather than beside it. */
/* ── ONE SURFACE, TWO SECTIONS. REWRITTEN 30-08-2026 ──────────────────────
   Knox, on the centred 1180px card this used to be:

     "The panel, the surface area that holds all the information, loads up in
      any space. It doesn't load up exactly in the same place every time.
      Actually I think it is loading up exactly in the same place and you just
      keep changing where it loads up. The actual image or the aspect ratio
      keeps changing with it... you need to have one single surface area with
      two sections."

   He is describing a real thing and the second sentence is the diagnosis. The
   card WAS a constant 1180 x 960 box, but it was CENTRED in the viewport, so a
   9:16 frame and a 3:2 frame put the panel in the same place while the picture
   under his eye moved by hundreds of pixels, and the whole thing read as
   drifting. A centred card is the wrong container for media whose shape is not
   knowable in advance.

   So the viewer is now the surface itself: it fills the scrim, the panel is
   snapped to the right edge at `--panel-w`, and the stage is whatever is left.
   Both regions are the same rectangle for every frame ever opened. The picture
   scales inside a stage that does not move, which is the only arrangement where
   the answer to "where is the close button" is the same twice.

   `position:absolute; inset:0` INSIDE THE SCRIM, and not `height:100%`. The
   percentage was the first attempt and the gate measured what it actually did:
   opening a 3:2 frame and then a 9:16 one grew the stage AND the panel by 514px
   each. `.scrim` is a grid with one auto row, an auto row is sized by its
   content, and a percentage height against an indefinite row is ignored - so
   the viewer sized to the panel, the panel sized to its own facts, and the
   surface Knox asked to hold still breathed with whatever was in it.

   That is the same circle as the `max-height` fault earlier the same day, one
   level further out, and a percentage cannot break it because both ends are
   asking each other. Absolute positioning against the scrim breaks it from
   outside: the box comes from the viewport, the rows inside it are definite,
   `.vside` is bounded, and `.vs-body` scrolls. Nothing in the panel can change
   the size of the thing containing it, which is the property being bought.

   HEIGHT, NOT MAX-HEIGHT, AND THAT ONE WORD WAS TWO BUGS. MEASURED IN THE LIVE
   APP 30-08-2026, on a 3008 x 1520 window, with the viewer open on a real
   frame:

     viewer  1180 x  820        the box it claims to be
     stage    840 x 1344        taller than the box that contains it
     side     320 x 1344        the panel, running off the bottom of the screen
     frame    448 x  560        in an 840 x 1344 stage

   `max-height` never resolves a height, it only clips one that exists. So the
   grid rows sized to the panel's CONTENT - 1344px of facts - and the panel ran
   past the viewer and off the window, because `.vs-body`'s `flex:1;
   min-height:0; overflow-y:auto` can only scroll inside a parent whose own
   height is bounded, and `.vside`'s was not. The scroller was correct the whole
   time and had nothing to scroll inside of.

   The same word cost the media its space: `max-height:100%` on the frame
   resolves against the stage, and a percentage against an indefinite height
   computes to none, so the frame fell back to the 560px in its own rule and
   left 784px of stage empty under it. Knox: "whatever the actual media is, it
   takes up the actual media."

   One definite height fixes both, because both were the same missing number.
   960 rather than 820: 820 was written for a demo inside a documentation card,
   and this is the component an operator looks at a frame in. */
.viewer { display:grid; grid-template-columns:minmax(0,1fr) var(--vside-w, var(--panel-w)); gap:var(--sp-3);
/* AND THE HEIGHT IS A NAMED TOKEN, BECAUSE TWO RULES NEED THE SAME NUMBER.
   The frame's cap is the stage's height and the stage's height is the viewer's,
   so it is written once. `max-height:100%` on the frame was the obvious way to
   say that and the two-engine gate refused it: on the same 9:16 frame in the
   same 840 x 860 stage, Chromium drew 1401px tall and WebKit drew 860. A
   percentage max-height on a grid item inside a grid item is resolved
   differently by the two, and Knox is on a Mac, so a Chromium-only answer is
   not an answer. `min(86vh, 960px)` is the same quantity with no percentage in
   it, and both engines agree on it. */
  position:absolute; inset:0; padding:var(--sp-3); align-items:stretch; }
/* AND IT IS `--vside-w`, NOT `--panel-w`, WITH A REASON. The panel-width comment
   above is emphatic that four apps once shipped seven numbers for one idea, and
   a second width token is exactly how that starts again - so this one has to
   earn itself. `--panel-w` is a DOCKED panel beside a working canvas: a form, a
   filter set, a detail drawer, and 320 is right because the canvas is the point
   and the panel is the margin. The viewer's panel is not a margin. It is one of
   the two halves the component is named for, it carries a prompt and a table of
   facts rather than fields, and at 320 the readout was four words a line and
   two buttons side by side were 135px each. Knox: "the panel can be a little
   bit wider in width."

   One token, defined here, used here, and any app that wants a different viewer
   sets it rather than typing a number into a grid template. */
.viewer { --vside-w:380px; }
@media (max-width:1180px){ .viewer { --vside-w:auto; }}
/* THE PANEL BREAKPOINT. Was 820px, now 1180px and shared. A 320px panel
   beside a canvas needs 1180px before the canvas stops being a slot. */
@media (max-width:1180px){ .viewer { grid-template-columns:1fr; }}
/* ── THE ENTRANCE IS THE PICTURE'S, NOT THE SURFACE'S ─────────────────────
   Knox: "The image opens up in high resolution, showcasing the image... it is
   kind of an animation flash type of a thing."

   For one build this carried `.modal`'s 14px rise on `.viewer` itself. That was
   right while the viewer was a card and wrong the moment it became the surface:
   a full-bleed surface that slides is the whole screen sliding, and a surface
   whose job is to be in the same place every time should not be the thing that
   moves. The two parts arrive instead.

   The frame leads and the panel follows it in by 60ms, so the eye lands on the
   picture and the information catches up. That order is the entire point: the
   picture is what was clicked. */
.scrim > .viewer .vframe { opacity:0; transform:scale(.985);
  transition:opacity calc(300ms * var(--dur)) var(--ease-hover),
             transform calc(420ms * var(--dur)) var(--ease-entrance); }
.scrim.open > .viewer .vframe { opacity:1; transform:none; }
.scrim > .viewer .vside { opacity:0; transform:translateX(14px);
  transition:opacity calc(260ms * var(--dur)) calc(60ms * var(--dur)) var(--ease-hover),
             transform calc(380ms * var(--dur)) calc(60ms * var(--dur)) var(--ease-entrance); }
.scrim.open > .viewer .vside { opacity:1; transform:none; }
/* `position:relative` so the frame below can be taken out of the grid. The
   grid and `place-items:center` stay for the `.vframe` SPAN placeholder, which
   is still an ordinary in-flow item. */
.viewer .vstage { position:relative; display:grid; place-items:center;
  min-width:0; min-height:0; }
.viewer .vframe { max-width:100%; max-height:100%; aspect-ratio:var(--ar, 1.5);
  height:min(62vh, 560px); border-radius:calc(var(--r-xs) * .8); box-shadow:var(--e5);
  background:linear-gradient(150deg in oklab, var(--seed-accent-hi) 0%, var(--seed-accent-lo) 100%); }
/* A REAL FRAME IN THE STAGE. ADDED 30-08-2026. The `.vframe` above is a
   gradient span, which is a placeholder and can never be a picture: an app
   opening a viewer has to put its own <img> or <video> in that box. Same
   aspect law, same corner, same shadow, and the gradient drops out because a
   plate showing through a transparent PNG is a bug in every app at once.

   `object-fit:contain`, and this is the one place in the system where that is
   the right answer. `.tile img` is `cover` on purpose: a wall wants equal
   frames and a centre crop is how it gets them. The VIEWER is where you go to
   see what the frame actually IS, so it may not crop, ever. If the delivered
   dimensions and `--ar` ever disagree the frame letterboxes against `--bg` and
   you can SEE that they disagree, which is worth more than a tidy box.

   `--bg` and not a surface token: the ground behind a frame is absence, not
   another panel sitting under this one. */
/* AND IT SIZES BY THE REPLACED-ELEMENT ALGORITHM, NOT BY THE SPAN'S RULE.
   The gate caught this before it shipped, which is what the gate is for: the
   span above takes `height:min(62vh,560px)` with `max-width:100%`, and that
   clamps the WIDTH while the height stays put - so a frame too wide for the
   stage got a box at 1.407 holding a picture at 1.5, with the difference paid
   in letterbox bars inside the corner radius and the shadow drawn around the
   bars. That is precisely the failure the media law names: "sizing goes
   height-first, then clamps by width, so a 9:16 gets shorter instead of
   breaking upward".

   `max-height:100%` could not save it because a percentage max-height against
   a stage whose own height is indefinite computes to none. So both caps here
   are absolute, and with width and height both auto a replaced element resolves
   them through CSS2.1 10.4, which shrinks to fit BOTH caps and keeps the ratio.
   The frame gets smaller. It never gets a different shape.

   OUT OF THE GRID AND ABSOLUTE AGAINST THE STAGE, and this is the third and
   last shape this cap has taken. The first two are worth naming because each
   looked correct in the file:

     min(62vh, 560px)   a typed number, and it left a 9:16 frame at 315 x 560
                        in an 840 x 860 stage with 300px of it empty
     max-height: 100%   the honest version, and the two-engine gate refused it
                        TWICE: a 9:16 frame in a definite 835 x 970 stage drew
                        1484 tall in Chromium and 970 in WebKit, because a
                        percentage height on a grid item nested in a grid item
                        is resolved differently by the two

   `position:absolute; inset:0; margin:auto` with both dimensions auto is the
   old centring trick and it is the right one here for a reason beyond
   centring: an absolutely positioned box has an explicit containing block, so
   its percentages resolve against a padding box both engines agree about
   rather than against a grid area they do not. The replaced-element algorithm
   still does the sizing - shrink to fit both caps, keep the ratio - it just
   finally has caps that mean the same thing in Safari and in Chrome.

   The 560px stays on the `.vframe` span above, because a span has no intrinsic
   size and would collapse to nothing on a percentage. The placeholder needs a
   number. Real media does not.

   AND IT IS SELECTED AT `.viewer img.vframe`, NOT `img.vframe`. `.viewer
   .vframe` above is 0-2-0 and `img.vframe` is 0-1-1, so the bare version loses
   every declaration it shares - height and max-width included, which are the
   two this rule exists to overturn. It read as correct in the file and was
   never applied. Third time this session that a rule I wrote lost on
   specificity or source order and I checked that it was PRESENT rather than
   that it WON. Presence is not application. */
.viewer img.vframe, .viewer video.vframe { display:block; object-fit:contain;
  background:var(--bg); width:auto; height:auto;
  position:absolute; inset:0; margin:auto;
  max-width:100%; max-height:100%; }
/* The panel is a card in its own right now rather than a column of the card
   the viewer used to be, so it carries the corner and the lift itself. */
.viewer .vside { display:flex; flex-direction:column; min-height:0; background:var(--surface-raise);
  border-radius:var(--r); box-shadow:var(--e4); overflow:hidden; }
.viewer .vs-head { flex-shrink:0; display:flex; align-items:center; gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-4); border-bottom:1px solid var(--line); }
.viewer .vs-head b { flex:1; min-width:0; font-family:var(--f-head); font-size:var(--fs-sm);
  font-weight:400; text-transform:uppercase; letter-spacing:var(--ls-head); }
.viewer .vs-body { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; padding:var(--sp-4); }
.viewer .vs-foot { flex-shrink:0; display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-2); padding:var(--sp-3) var(--sp-4); border-top:1px solid var(--line); }
/* `.stack` when the foot carries a GROUP of verbs rather than a left-and-right
   pair. The verbs live at the bottom of the panel and not under the prompt, so
   that where they are does not depend on how long the prompt is: a control that
   moves because a caption grew is a control an operator has to look for. That
   is the same rule the surface itself follows one level up. */
.viewer .vs-foot.stack { flex-direction:column; align-items:stretch;
  justify-content:flex-start; gap:var(--sp-2); padding:var(--sp-4); }
/* `auto` AND NOT 88px. A two-column grid with an auto first track sizes it to
   the WIDEST key, so every row still aligns - the alignment was never what the
   typed number bought. What it bought was 88px of air after COST in a 320px
   panel, which is the "way too broad, way too comfortable" Knox measured by
   eye. The keys decide their own column now, and a set of short ones gets a
   short one. */
.vs-kv { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px var(--sp-3);
  align-items:baseline; }
.vs-kv .k { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text); }
.vs-kv .v { font-family:var(--f-body); font-size:var(--fs-sm); color:var(--text); min-width:0; }

/* ── FULL SCREEN ──────────────────────────────────────────────────────────
   ADDED 30-08-2026. Knox: "you should be able to make the image full screen."
   The stage is already most of the window; this is for the moment the panel is
   in the way of the picture, which is the moment you are actually looking hard.
   It takes the STAGE fullscreen rather than the viewer, so the frame gets the
   whole display and the facts step aside, and Escape returns exactly the way it
   already closes the viewer.

   Same rest-and-return behaviour as the transport: an always-visible chrome
   button in the corner of a photograph is a watermark. */
/* IT SITS IN THE CORNER OF A STILL AND ON THE TRANSPORT OF A CLIP. Knox: "you
   can put the full screen on the bottom right of that under the playhead bit."
   He is right that a clip already has a control bar and a second floating
   button above it is two places to look. A still has no bar, so it keeps the
   corner. One class, two homes, and the delegated handler does not care which
   because it resolves the stage rather than the button's parent. */
.viewer .vzoom { position:absolute; right:var(--sp-3); top:var(--sp-3); z-index:3;
  opacity:0; transition:opacity calc(180ms * var(--dur)) var(--ease-hover); }
.viewer .vstage:hover .vzoom, .viewer .vzoom:focus-within { opacity:1; }
@media (pointer: coarse) { .viewer .vzoom { opacity:1; }}
/* On the bar it is one more control in a row, so it drops the floating
   position, the glass and the fade: the strip already carries all three. */
.viewer .vtrans .vzoom { position:static; opacity:1; }
.viewer .vtrans .vzoom .ib { background:none; border:none;
  -webkit-backdrop-filter:none; backdrop-filter:none; width:30px; height:30px; }
.viewer .vzoom .ib { background:color-mix(in srgb, var(--seed-ink) 74%, transparent);
  -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px);
  border:1px solid var(--glass-border); width:32px; height:32px; }
/* Fullscreen paints the element on a black ground of the browser's choosing, so
   the stage has to carry its own or a letterboxed frame sits in a void that is
   not this system's. */
/* AND IT HAS TO RETAKE ITS OWN GEOMETRY, WHICH IS THE WHOLE FAULT. The UA
   stylesheet gives `:fullscreen` position:fixed and inset:0, and an AUTHOR rule
   beats a UA rule - so `.viewer .vstage { position:relative }` at 0-2-0 quietly
   won, the stage stayed a cell of the viewer's grid, and fullscreen painted a
   grid cell in the middle of a black screen. Knox: "it's just putting that box
   in the center of my screen." Stated explicitly here rather than left to a
   cascade the element is on the wrong side of. */
.viewer .vstage:fullscreen { position:fixed; inset:0; width:100%; height:100%;
  background:var(--bg); padding:var(--sp-3); }
.viewer .vstage:fullscreen .vzoom .out { display:block; }
.viewer .vstage:fullscreen .vzoom .in { display:none; }
.viewer .vzoom .out { display:none; }

/* ── THE TRANSPORT ────────────────────────────────────────────────────────
   ADDED 30-08-2026. Knox: "for the video bit, there was a previous build I saw
   where the video bit was following the same kind of play button and the play
   head, which plays the video and everything as per the design system."

   There was no such component. `06-wall.html` never had one, the design pack
   never had one, and V7 shipped a viewer that put `controls` on the video and
   let the browser draw its own bar - which is the one part of a media viewer
   that is guaranteed to look like somebody else's product. What he remembers is
   Darkroom's own rebuild plan, which specified "a real transport: play, speed,
   volume, scrubber, and a timecode reading elapsed over total" and was never
   built. So it is built here, in the system, where the next app gets it too.

   AND IT IS ASSEMBLED, NOT INVENTED. Every part already existed:

     the playhead   `.sld`, whose `.fill` is `--accent-lit` - the accent Knox
                    says is missing from this panel is the played portion of
                    the bar, which is the right place for it
     the buttons    `.ib`, the system's 28px icon button
     the timecode   `--f-num` with tabular numerals, so 0:09 and 0:10 do not
                    change the width of the bar as it plays
     the ground     the same glass the tooltip and the command palette use

   Nothing here is a new control. It is five old ones on one rail. */
/* THE STRIP SPANS THE STAGE. REWRITTEN 30-08-2026 against Knox's reference:
   "the play head takes up the entire space of the full window area, not just
   where the video box sits."

   The first version was a 560px pill floating over the middle of the frame,
   which is a widget sitting on a picture. A transport is not a widget on the
   media, it is the bottom edge of the surface the media is on - so the scrub
   line runs the full width of the stage, edge to edge, and the controls sit in
   a row beneath it. The seek target becomes the whole window instead of a
   third of it, which is the practical half of why every player worth using is
   built this way.

   A gradient rather than a panel: a solid bar would be a second surface laid
   over the frame, and the frame is the point. The controls need contrast, the
   picture needs to keep its bottom edge, and a scrim that fades out gives both.

   Three regions on the control row, and the middle one is `auto` so the
   timecode stays dead centre no matter how many controls either side grows. */
.viewer .vtrans { position:absolute; left:0; right:0; bottom:0; z-index:3;
  display:flex; flex-direction:column; gap:2px; padding:var(--sp-4) var(--sp-4) var(--sp-3);
  /* A SHALLOW SCRIM, NOT A BOX. Knox: "there is some kind of vignette thing
     happening, it needs to just be a hover rather than this whole boxy vignette
     coming in place." The first version ran 86% opaque over 120px of frame,
     which is a black slab arriving on the picture - the eye reads the SLAB, not
     the controls on it. Half the depth over half the distance, and an ease so
     the top edge dissolves instead of ending on a line: enough to hold a 3px
     bar and 11px numerals legible over any frame, not enough to be a shape. */
  background:linear-gradient(to top,
    color-mix(in srgb, var(--seed-ink) 62%, transparent) 0%,
    color-mix(in srgb, var(--seed-ink) 26%, transparent) 46%,
    transparent 100%);
  opacity:0; transition:opacity calc(180ms * var(--dur)) var(--ease-hover); }
/* AN APP RENDERS THIS WITH `.is-waiting` ALREADY ON IT. The class is maintained
   by `vtSync` below, and `vtSync` only runs on a media event - so between the
   viewer opening and the first `loadedmetadata` there was no class at all and
   the scrim painted at full strength over an empty stage. On this library that
   window is fifteen seconds. The waiting state is the state a transport STARTS
   in; arriving is what has to be announced, not waiting. */
/* It rests hidden and comes back on any pointer inside the stage, and stays up
   while the clip is paused, because a paused clip with a hidden transport is a
   still nobody can restart. `:focus-within` so a keyboard never loses it. */
.viewer .vstage:hover .vtrans, .viewer .vtrans:focus-within,
.viewer .vstage.paused .vtrans { opacity:1; }
@media (pointer: coarse) { .viewer .vtrans { opacity:1; }}

/* THE PLAYHEAD, edge to edge and thin. `.sld` is built for a form row, where
   an 8px track under a 20px domed thumb is right for a value you set once. A
   playhead is read at a glance and dragged often, so the LINE gets thinner and
   the HIT AREA stays tall: the input keeps its height and the track shrinks
   under it, which is the opposite of shrinking the control. */
/* THICKER. Knox: "increase the thickness of the playhead." 3px was the
   reference's line and the reference is on a smaller window; on a 2500px stage
   a 3px rule reads as a hairline somebody forgot to finish. 5px still reads as
   a line rather than a slider, and the hit area is 18px regardless. */
.viewer .vtrans .vt-bar { width:100%; height:18px; }
.viewer .vtrans .vt-bar .track, .viewer .vtrans .vt-bar .fill { height:5px; border-radius:3px; }
.viewer .vtrans .vt-bar .track { border:none; box-shadow:none;
  background:color-mix(in srgb, var(--seed-paper) 34%, transparent); }
/* A bar, not a dome. The reference draws the head as a slim vertical marker and
   it is the better shape: a circle on a 3px line reads as a bead sitting on the
   track rather than a position within it. */
.viewer .vtrans .vt-bar input::-webkit-slider-thumb { width:5px; height:17px;
  border-radius:3px; border:none; box-shadow:0 0 0 1px rgba(0,0,0,.35); }
.viewer .vtrans .vt-bar input::-moz-range-thumb { width:5px; height:17px;
  border-radius:3px; border:none; }

.viewer .vtrans .vt-row { display:grid; grid-template-columns:1fr auto 1fr;
  align-items:center; gap:var(--sp-3); }
.viewer .vtrans .vt-left { display:flex; align-items:center; gap:var(--sp-2); min-width:0; }
.viewer .vtrans .vt-right { display:flex; align-items:center; gap:var(--sp-2);
  justify-content:flex-end; min-width:0; }
.viewer .vtrans .ib { flex:none; width:30px; height:30px; }
.viewer .vtrans .ib svg { width:14px; height:14px; }

/* Elapsed over total, in the middle, in one string. Two numbers at opposite
   ends of a bar is a measurement; one string in the centre is a position, which
   is the thing a person is actually reading. Tabular so the row does not
   breathe once a second. */
.vt-time { font-family:var(--f-num); font-variant-numeric:tabular-nums;
  font-size:var(--fs-caps); color:color-mix(in srgb, var(--seed-paper) 78%, transparent);
  white-space:nowrap; }

/* SPEED, as a label rather than a menu. Four steps, and the control says which
   one it is on: a rate picker that hides the current rate behind a click is a
   setting you have to open to read. */
.vt-rate { flex:none; min-width:30px; height:30px; padding:0 6px; display:grid;
  place-items:center; border-radius:calc(var(--r-xs) * .9);
  font-family:var(--f-num); font-variant-numeric:tabular-nums; font-size:var(--fs-caps);
  color:var(--muted); cursor:pointer;
  transition:color calc(130ms * var(--dur)) var(--ease-hover),
             background calc(130ms * var(--dur)) var(--ease-hover); }
.vt-rate:hover { color:var(--text); background:var(--hover); }

/* VOLUME. Knox: "for the volume button there's only mute and unmute, we need to
   be able to have a manual volume button here too." It rides out of the speaker
   on hover rather than taking 80px of the row permanently, because the number
   of times anybody sets a level is a fraction of the times they glance at the
   bar. Width, not display, so it slides instead of appearing. */
.viewer .vtrans .vt-vol { width:0; overflow:hidden; flex:none;
  transition:width calc(200ms * var(--dur)) var(--ease-entrance); }
.viewer .vtrans .vt-volwrap:hover .vt-vol,
.viewer .vtrans .vt-vol:focus-within { width:72px; }
.viewer .vtrans .vt-volwrap { display:flex; align-items:center; gap:var(--sp-2); }
.viewer .vtrans .vt-vol .sld { width:72px; height:30px; }
.viewer .vtrans .vt-vol .sld .track, .viewer .vtrans .vt-vol .sld .fill { height:3px; border-radius:2px; }
.viewer .vtrans .vt-vol .sld .track { border:none; box-shadow:none;
  background:color-mix(in srgb, var(--seed-paper) 34%, transparent); }
.viewer .vtrans .vt-vol .sld input::-webkit-slider-thumb { width:10px; height:10px; }
.viewer .vtrans .vt-vol .sld input::-moz-range-thumb { width:10px; height:10px; }

/* A TRANSPORT WITH NO DURATION SAYS SO. A clip whose metadata has not arrived
   shows a bar that looks operable and reads 0:00 of 0:00, and every part of
   that is a claim the player cannot back: the scrubber goes nowhere, the play
   button starts nothing, and two zeros look like a clip of no length rather
   than a clip that has not loaded. Found live against a library whose clips
   carry their moov atom at the tail, so no browser can report a duration until
   it has downloaded the whole file. The transport was correct and it was lying
   anyway. Waiting is a state and it gets drawn. */
/* AND IT DOES NOT PAINT ITS SCRIM WHILE IT WAITS. Knox: "upon loading of the
   video page I am still getting this vignetting-style box... when it figures
   out the aspect ratio that wrapper goes away."

   That is this gradient over an empty stage. Until the media reports a
   duration there is no frame under the strip yet, so the scrim is darkening
   nothing and reads as a box that arrived on its own and then left. The
   controls stay, dimmed, so there is evidence the player exists; the SHAPE goes
   until there is a picture for it to sit on. A scrim exists to keep controls
   legible over media, so with no media it has no work to do. */
.vtrans.is-waiting { background:none; }
.vtrans.is-waiting .vt-bar { opacity:.4; pointer-events:none; }
.vtrans.is-waiting .vt-play { opacity:.4; pointer-events:none; }

/* AND THE STAGE SAYS IT IS WORKING. Fifteen seconds of nothing is
   indistinguishable from broken, and Darkroom's clips take exactly that because
   their moov atom is at the tail. `.spin` is the system's. */
.viewer .vstage .vload { position:absolute; left:50%; top:50%; z-index:2;
  display:grid; place-items:center; transform:translate(-50%, -50%);
  pointer-events:none; }
/* BOTH GLYPHS SHIP AND ONE IS HIDDEN, rather than the label being rebuilt in
   script on every timeupdate, which is a button that flickers. The state is
   already on the element for the CSS to read. */
.vt-play .pa, .vt-play.is-playing .pl { display:none; }
.vt-play.is-playing .pa { display:block; }
.vt-mute.is-muted { color:var(--subtle); }

/* ── A LABEL THAT CARRIES ITS OWN ACTION ──────────────────────────────────
   `.c-lab` names a block. This is `.c-lab` with the one control that belongs to
   that block on the other end of the line - PROMPT and the button that copies
   it, and nothing else. It exists because the alternative kept being a second
   copy of the same verb further down the panel: a block with its own action
   does not need that action repeated in the action list, and Knox said so
   directly - "if you have a copy button for the prompt over there, then you
   don't need a separate copy button for the prompt anywhere."

   One action. If a block needs two, it is a section and it gets a heading. */
.vs-lab { display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-3); min-height:26px; }
.vs-lab > .c-lab { margin:0; }

/* ── A SUNKEN WELL FOR A BLOCK'S CONTENT ──────────────────────────────────
   ADDED 30-08-2026. Knox: "there should be a sunken surface where the prompt
   sits... we can replicate our sunken box, which has our actual image
   references, as a separate sunken box."

   The panel had been a column of prose on the panel's own ground, so a label
   and the thing it labels sat on the same plane and the only separator was a
   gap. `.pwell` already says how this system draws content you can act on:
   `--surface-deep` under `--e-sunk`, one step below the surface it sits on. The
   same recess here, minus the field behaviour, because these hold content
   rather than take it. */
/* THE PADDING IS `.pwell`'S, TO THE PIXEL. Corrected 30-08-2026: this shipped
   at `var(--sp-3)`, which is 12px at compact density against `.pwell`'s 11px
   and 16px, so a well and the prompt field it was modelled on recessed their
   content by different amounts and the panel had two ideas about what a sunken
   surface is. Knox saw it as the words inside not lining up. Copied rather than
   approximated: two recesses on one screen either match or they are noise. */
.vs-well { background:var(--surface-deep); border:1px solid var(--line);
  border-radius:calc(var(--r) * .7); box-shadow:var(--e-sunk); padding:11px 16px; }
.vs-well + .vs-well { margin-top:var(--sp-2); }
/* A well full of thumbnails wants its own breathing room rather than a text
   field's, and 11px above 52px thumbs with nothing under them reads as a
   mistake. Square it. */
.vs-well.roleths { padding:11px; gap:11px; flex-wrap:wrap; }

/* ── THE READOUT ──────────────────────────────────────────────────────────
   A long piece of machine text - a prompt, a response, a log line - shown in
   the panel without letting it own the panel. Four lines clamped, and `.all`
   opens it TO A CEILING and then scrolls. Knox: "it opens it up to a point,
   but then it goes into vertical scroll. It's not like it's going to take up
   the entirety of the section and push everything below."

   That last clause is the whole design. An expander with no ceiling is not an
   expander, it is a layout change: every control under it leaves the screen,
   and the operator who wanted to read one line has lost the buttons. The
   ceiling is a token so an app can raise it for a surface that is mostly text,
   and the scroll contains itself so a wheel over the prompt does not take the
   panel with it.

   `.shotcard .sc-read` is the same idea clamped to three lines and scoped to
   the card. It stays where it is: that one has no expander and never scrolls,
   because a card in a list must not change height under the cursor. */
/* AND IT IS SET IN THE SYSTEM'S PROSE VOICE, NOT IN MONO. CORRECTED
   30-08-2026, on Knox: "the prompt is in a different typography altogether,
   you've not followed the design system there."

   He is right and the system says so in its own file: `.pwell`, the surface an
   operator TYPES a prompt into, is `--f-body` at `--fs-md`. A readout of the
   same text in `--fm` at `--fs-caps` is a second opinion about what a prompt
   is. It came from `.shotcard .sc-read`, which is mono for a reason that does
   not travel - a card in a list is a machine record being scanned, and this is
   the prompt itself being read.

   `--fs-sm` rather than `--fs-md` because the panel is 320px wide and body size
   there is four words a line. Same family, same line height, one step down. */
.vs-read { font-family:var(--f-body); font-size:var(--fs-sm); line-height:var(--lh-body);
  color:var(--muted); display:-webkit-box; -webkit-line-clamp:4;
  -webkit-box-orient:vertical; overflow:hidden; }
/* THE HANDLES ARE THE ACCENT, and they are the only accent in the prose. Knox:
   "there is no accent color there." An `@handle` in a prompt is not decoration,
   it is the one part of those words that names something the system knows
   about, and the console already draws it as `.echip`. Inline here, stripped of
   the pill: the same idea the composer already applies to a chip sitting inside
   typed text, so a handle looks like a handle wherever it is read. */
.vs-read .echip { display:inline; height:auto; padding:0; gap:0; background:none;
  box-shadow:none; border-radius:0; font-family:inherit; font-size:inherit;
  font-weight:600; letter-spacing:0; color:var(--accent-text); }
.vs-read.all { display:block; -webkit-line-clamp:none;
  max-height:var(--read-max, 232px); overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; }
/* The expander reads as a link, not a button: it changes how much of one thing
   you can see, which is not the same kind of act as the verbs below it.

   IT LIVES INSIDE THE WELL, and it leaves when it has done its job. Knox: "the
   see more bit needs to be inside the box, not outside the box. Once the box
   opens up, I should not be able to see show less. It should just open up a
   bit, and then the vertical scroll should do the rest."

   He is describing a one-way control and he is right that it should be. Outside
   the well it read as a control on the panel rather than on the prompt. And a
   Show less that persists is a second thing to look at forever in exchange for
   an action almost nobody takes twice: the readout has a ceiling and a scroll,
   so opened is not a state anybody needs rescuing from. It resets when the
   frame closes, which is the only place a reset is worth anything. */
.vs-more { display:inline-flex; align-items:center; gap:5px; margin-top:var(--sp-2);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text);
  cursor:pointer; transition:color calc(130ms * var(--dur)) var(--ease-hover); }
.vs-more:hover { color:var(--text); }
.vs-more svg { width:10px; height:10px;
  transition:transform calc(320ms * var(--dur)) var(--ease-entrance); }
.vs-more[aria-expanded="true"] svg { transform:rotate(180deg); }
/* Gone once the readout is open, and the sibling selector is what makes that a
   fact of the markup rather than a flag an app has to remember to unset. */
.vs-read.all + .vs-more { display:none; }
/* Inside the well the expander is a footer to the text, not a line under a box:
   it sits on the well's own left edge and takes the full width so the hit area
   is the whole line rather than four small words. */
.vs-well > .vs-more { width:100%; justify-content:flex-start; }

/* THE VERB GRID. ADDED 30-08-2026, at Knox's instruction: "we cannot have it on
   the thumbnail... in the side panel of that space, we should have things like
   the four buttons that we currently have on the thumbnail. These are proper
   buttons that need to be there on the side panel."

   The system already agreed with him in writing - the media section says the
   verbs belong in the viewer - and then gave the viewer nowhere to put them.
   This is that place.

   Two columns, not one and not four. Four verbs stacked is a menu; four in a
   row inside a 320px panel is 68px a button, which is an icon with a caption
   under it and no label anybody can read. Two-up gives a real word on a real
   button. `.wide` spans both for the one action that leads - the primary verb
   a panel exists for - and matches how `.gate`, `.kb-col` and `.engwrap` all
   spell the same modifier. */
.vs-acts { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-2); }
.vs-acts > * { width:100%; min-width:0; }
.vs-acts > .wide { grid-column:1 / -1; }
/* AN ICON CLUSTER THAT MATCHES THE BUTTON BESIDE IT. Knox: "if you're
   sectioning it out by two, then let's section it out by two and increase the
   size of the archive and the delete icon."

   `.ib` is a fixed 28px square, which is right riding a table row and wrong
   next to a 39px button - the row reads as one control and two afterthoughts.
   The height is NOT retyped here: the grid row is already as tall as its
   tallest cell, so the cluster stretches into it and each icon takes an equal
   share of the width. Whatever the button size scale does next, these follow it
   without anybody editing a number. */
.vs-acts .ibs { display:flex; align-items:stretch; gap:var(--sp-2); min-width:0; }
.vs-acts .ibs .ib { flex:1 1 0; width:auto; height:auto; min-width:0;
  border-radius:calc(var(--btn-r) * .8); }
.vs-acts .ibs .ib svg { width:15px; height:15px; }

/* ═══ SYSTEM-WIDE GAP COMPONENTS ═══════════════════════════════════════════ */

/* Portal tooltip. One node, fixed, on its own z token, flips near an edge.
   The CSS .tip stays for simple cases; anything inside a scroll container
   uses this. CONTRACT · the element carries data-tipp="text"; the node is
   created once by the system script. */
.tipp { position:fixed; z-index:var(--z-tip); pointer-events:none; max-width:min(260px, 70vw);
  font-family:var(--f-ui); font-size:var(--fs-sm); color:var(--text); text-align:center; line-height:1.4;
  background:var(--surface-raise); border:1px solid var(--glass-border); border-radius:var(--r-xs);
  padding:5px 11px; box-shadow:var(--e3); opacity:0; transform:translateY(3px);
  transition:opacity calc(120ms * var(--dur)) ease, transform calc(120ms * var(--dur)) var(--ease-entrance); }
.tipp.on { opacity:1; transform:none; }

/* Connection strip. Quiet, full width, with a grace delay in the script so a
   blip never flashes it. GLOBAL and fixed: an app shows it by adding .on. */
.connbar { position:fixed; top:0; left:0; right:0; z-index:var(--z-toast);
  display:flex; align-items:center; justify-content:center; gap:10px; padding:9px var(--sp-4);
  background:var(--surface-raise); box-shadow:var(--e3), inset 0 -3px 0 var(--warning);
  font-family:var(--f-body); font-size:var(--fs-sm); color:var(--muted);
  transform:translateY(-104%); transition:transform var(--d-base) var(--ease-entrance); }
.connbar.on { transform:none; }
.connbar .spin { width:14px; height:14px; padding:2px; }
.connbar.back { box-shadow:var(--e3), inset 0 -3px 0 var(--success); }
.connbar b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--text); }

/* Failure screen. The error-boundary state, designed: what broke, what
   survives, one action. An app must never blank. */
.failscreen { text-align:center; max-width:460px; margin:0 auto; padding:var(--sp-7) var(--sp-4); }
.failscreen .ic { width:52px; height:52px; margin:0 auto var(--sp-4); border-radius:var(--r-xs);
  display:grid; place-items:center; background:var(--surface-deep); box-shadow:var(--e-sunk);
  color:var(--danger-text); }
.failscreen .ic svg { width:22px; height:22px; }
.failscreen h3 { font-family:var(--f-head); font-size:var(--fs-lg); font-weight:400;
  text-transform:uppercase; letter-spacing:var(--ls-head); line-height:var(--lh-head); margin:0 0 var(--sp-2); }
.failscreen p { font-size:var(--fs-sm); color:var(--muted); max-width:44ch; margin:0 auto var(--sp-2); }
.failscreen .keeps { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); display:block;
  margin-bottom:var(--sp-5); }
/* Busy keys. Any key that fires a mutation shows an in-flight state and refuses
   a second press. The label is HELD, never swapped for a spinner alone.
   CONTRACT · the key carries data-busy; the script inserts the spinner. */
.busy { pointer-events:none; }
.busy .spin { width:13px; height:13px; padding:2px;
  background:conic-gradient(from 0deg, transparent 0deg, currentColor 300deg); }

/* Board. Kanban for post-production. Extends .kb with the contract Pulse needs:
   a drag affordance, a WIP cap on the column, and cards that are <div draggable>. */
.kb-card .kgrip { position:absolute; right:9px; top:11px; color:var(--subtle); opacity:0;
  transition:opacity var(--d-fast) ease; cursor:grab; }
.kb-card:hover .kgrip { opacity:1; }
.kb-card .kgrip svg { width:10px; height:14px; }
.kb-col.full .kb-head .ct { color:var(--danger-text); }
.kb-col.full .kb-slot { display:grid; place-items:center; padding:var(--sp-3);
  border:1px dashed var(--line-strong); border-radius:var(--r-xs);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.09em;
  text-transform:uppercase; color:var(--subtle-text); }

/* Month calendar. A full-bleed month grid; the .cal picker is not this.
   Day cells hold two entries plus an overflow count. Entries are content,
   so they take a tinted ground and full-strength type, never a status pill. */
.mcal { border-radius:var(--r); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e1); }
.mcal .mc-dow { display:grid; grid-template-columns:repeat(7,1fr); border-bottom:1px solid var(--line-strong); }
.mcal .mc-dow b { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text);
  padding:var(--sp-2) var(--sp-3); text-align:left; }
.mcal .mc-grid { display:grid; grid-template-columns:repeat(7,1fr); }
.mcal .mc-day { min-height:92px; min-width:0; padding:7px; border-right:1px solid var(--line);
  border-bottom:1px solid var(--line); display:flex; flex-direction:column; gap:4px; }
.mcal .mc-day:nth-child(7n) { border-right:none; }
.mcal .mc-grid > .mc-day:nth-last-child(-n+7) { border-bottom:none; }
.mcal .mc-day .d { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--muted); font-variant-numeric:tabular-nums; }
.mcal .mc-day.dim .d { color:var(--disabled); }
.mcal .mc-day.today .d { display:inline-grid; place-items:center; width:22px; height:22px;
  border-radius:50%; background:var(--accent-lit); color:var(--on-accent); }
.mcal .ev { font-family:var(--f-body); font-size:var(--fs-caps); font-weight:600;
  color:var(--text); background:var(--accent-tint); border-radius:6px; padding:3px 7px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; }
.mcal .ev.q { background:var(--surface-deep); box-shadow:var(--e-sunk); color:var(--muted); }
.mcal .more { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); padding-left:2px; }

/* Segment sizing lives on the CONTAINER; buttons inherit. A seg-mini panel
   never restyles .seg-b. */
.seg.mini { padding:3px; }
.seg.mini .seg-b { padding:7px calc(14px - .055em) 7px calc(14px + .055em); }
.seg.mini .seg-pill { top:3px; bottom:3px; }

/* The wordmark lockup. A one-word product never splits; a long name breaks on
   two lines at the small step rather than shrinking. First <span>Second</span>
   is for two-word names only. */
.rail-logo.two { white-space:normal; line-height:1.25; }

/* ═══ THE PHONE ═══ the 390px dress: bottom nav of four tabs plus More, the
   bottom sheet, a 44px touch floor on every key and chip. */
.phoneframe { position:relative; isolation:isolate; width:min(390px, 100%); height:760px;
  border-radius:calc(var(--r-lg) * 1.15); overflow:hidden; background:var(--bg);
  border:1px solid var(--line-strong); box-shadow:var(--e4);
  display:flex; flex-direction:column; }
.phoneframe .pf-scroll { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  padding:var(--sp-4) var(--sp-4) calc(var(--dock-clear) + var(--sp-4)); }
.phoneframe .pf-dock { position:absolute; left:0; right:0; bottom:0; z-index:var(--z-chrome); }
.phoneframe .pf-dock .dock { width:100%; }
/* The touch floor: on the phone dress, every key, chip and row is at least 44px. */
.phoneframe .btn-accent, .phoneframe .btn-key, .phoneframe .btn-ghost,
.phoneframe .chip, .phoneframe .fchip, .phoneframe .rowitem { min-height:44px; }
.phoneframe .b-sm { height:44px; }

/* The bottom sheet. Everything More holds. CONTRACT · .open on the sheet AND
   the scrim; rows are <button class="mrow"> at the 44px floor. */
.msheet { position:absolute; left:0; right:0; bottom:0; z-index:var(--z-modal);
  background:var(--surface-raise); border-radius:var(--r-lg) var(--r-lg) 0 0; box-shadow:var(--e5);
  padding:10px var(--sp-4) calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  transform:translateY(102%); transition:transform var(--d-slow) var(--ease-entrance); }
.msheet.open { transform:none; }
.msheet .grab { width:36px; height:4px; border-radius:99px; background:var(--line-strong);
  margin:0 auto var(--sp-3); }
.msheet .mrow { display:flex; align-items:center; gap:var(--sp-3); width:100%; min-height:44px;
  padding:0 var(--sp-2); border-radius:var(--r-xs); text-align:left;
  font-family:var(--f-body); font-size:var(--fs-md); color:var(--muted);
  transition:background var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.msheet .mrow:hover { background:var(--hover); color:var(--text); }
.msheet .mrow svg { width:16px; height:16px; flex-shrink:0; color:var(--subtle); }
.msheet .mrow .ct { margin-left:auto; font-family:var(--f-num); font-weight:var(--num-wt);
  font-size:var(--fs-sm); color:var(--subtle-text); font-variant-numeric:tabular-nums; }
.msheet .msep { height:1px; background:var(--line); border:none; margin:var(--sp-2) 0; }

/* The call sheet block. Crew rows with initials; call time reads against start. */
.callsheet { border-radius:var(--r); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e1); }
.callsheet .cs-head { display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-4); border-bottom:1px solid var(--line); }
.callsheet .cs-head .st b { display:block; font-family:var(--f-body); font-size:var(--fs-md);
  font-weight:600; color:var(--text); }
.callsheet .cs-head .st small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-top:3px; }
.callsheet .cs-call { text-align:right; }
.callsheet .cs-call b { display:block; font-family:var(--f-num); font-weight:var(--num-wt);
  font-size:var(--fs-lg); color:var(--text); font-variant-numeric:tabular-nums; }
.callsheet .cs-call small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); margin-top:2px; }
.callsheet .cs-row { display:flex; align-items:center; gap:var(--sp-3); min-height:44px;
  padding:6px var(--sp-4); border-bottom:1px solid var(--line); }
.callsheet .cs-row:last-child { border-bottom:none; }
.callsheet .cs-row .nm { flex:1; min-width:0; font-family:var(--f-body); font-size:var(--fs-sm);
  color:var(--text); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.callsheet .cs-row .rl { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); white-space:nowrap; }
.callsheet .cs-row .tm { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; white-space:nowrap; }

/* Gear checkout row: the scan line, the custody line, the QC state, the flag. */
.gearrow { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:4px var(--sp-3);
  align-items:center; padding:var(--sp-3) 0; border-bottom:1px solid var(--line); }
.gearrow:last-child { border-bottom:none; }
.gearrow .gnm { font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text);
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.gearrow .gnm .mono { font-size:var(--fs-caps); margin-left:7px; }
.gearrow .gcust { grid-column:1; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.06em; text-transform:uppercase; color:var(--subtle-text); }

/* Element card (Darkroom characters and elements): hero thumb, handle, type,
   lock state, coverage as a quiet progress row. The "+ New" tile goes first. */
.elgrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:var(--sp-3); }
.elcard { border-radius:var(--r); overflow:hidden; background:var(--grad-ground); box-shadow:var(--e1);
  cursor:pointer; transition:box-shadow var(--d-fast) var(--ease-hover); }
.elcard:hover { box-shadow:var(--e2); }
.elcard .eth { aspect-ratio:4/5; background:linear-gradient(150deg in oklab, var(--tc, var(--seed-accent-hi)) 0%, var(--seed-accent-lo) 100%); }
.elcard .ebody { padding:10px 12px 12px; }
.elcard .ebody b { display:block; font-family:var(--fm); font-size:var(--fs-sm); font-weight:500;
  color:var(--accent-text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.elcard .ebody small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); margin-top:4px; }
.elcard .ecov { display:flex; align-items:center; gap:8px; margin-top:9px; }
.elcard .ecov .prog { flex:1; height:5px; }
.elcard .ecov small { margin:0; font-family:var(--f-num); font-weight:var(--num-wt);
  color:var(--subtle-text); letter-spacing:0; font-variant-numeric:tabular-nums; }
.elcard.new { display:grid; place-items:center; min-height:100%; background:none;
  border:1px dashed var(--line-strong); box-shadow:none; }
.elcard.new .plus { display:grid; place-items:center; gap:9px; text-align:center; color:var(--subtle-text);
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.1em; text-transform:uppercase; }
.elcard.new .plus svg { width:20px; height:20px; margin:0 auto; }
.elcard.new:hover { background:var(--accent-tint); border-color:var(--accent-live); }

/* Key vault row (Darkroom System): reveal and save. The secret is mono, masked. */
.vaultrow { display:flex; align-items:center; gap:var(--sp-3); padding:var(--sp-3) 0;
  border-bottom:1px solid var(--line); }
.vaultrow:last-child { border-bottom:none; }
.vaultrow .vk { flex:0 0 130px; min-width:0; font-family:var(--f-body); font-size:var(--fs-sm);
  font-weight:600; color:var(--text); }
.vaultrow .vk small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); margin-top:2px; }
.vaultrow .well { flex:1; min-width:0; }
.vaultrow .well input { font-family:var(--fm); font-size:var(--fs-sm); letter-spacing:.08em; }

/* Share-link row: the link is mono in a well; copy is a key; revoke is quiet. */
.sharerow { display:flex; align-items:center; gap:var(--sp-2); }
.sharerow .well { flex:1; min-width:0; }
.sharerow .well input { font-family:var(--fm); font-size:var(--fs-caps); color:var(--muted); }

/* ═══ PROOF FRAMES ═══ page furniture for section 15 only: a positioned box so
   a fixed overlay can be shown inside a document. The components inside are the
   real ones; nothing below changes how any component renders. */
.proof { position:relative; isolation:isolate; border-radius:var(--r-lg); overflow:hidden;
  box-shadow:var(--e3); background:var(--bg); }
/* Inside a proof frame the scrim dims WITHOUT blurring, and lightly: the whole
   point of a proof is that the screen behind the dialog stays readable. The
   real fixed .scrim keeps its full treatment. */
.proof .scrim { position:absolute;
  -webkit-backdrop-filter:none; backdrop-filter:none;
  background:color-mix(in srgb, var(--seed-ink) 28%, transparent); }
.phoneframe .scrim { -webkit-backdrop-filter:none; backdrop-filter:none;
  background:color-mix(in srgb, var(--seed-ink) 26%, transparent); }
.proof .connbar { position:absolute; }

.connbar.back .spin { display:none; }
.phoneframe .scrim { position:absolute; }


/* ═══ PRINT ═══════════════════════════════════════════════════════════════
   Chrome does not print background graphics by default, so a dark surface
   prints as nothing at all. Every export, statement and call sheet that leaves
   this system on paper goes to ink on white regardless of the theme on screen,
   and the chrome that only exists to navigate is dropped. V5 shipped without a
   print layer, which is a real bug the moment anyone hits Export PDF. */
@media print {
  :root, [data-theme="dark"], [data-theme="light"] {
    --bg:#FFFFFF; --surface:#FFFFFF; --surface-raise:#FFFFFF; --surface-deep:#F4F4F4; /* knox-allow: pure-white, this is the print sheet */
    --grad-ground:none; --text:#111111; --muted:#3A3A3A; --subtle:#5A5A5A; --subtle-text:#595959; /* knox-allow: pure-white, paper is white */
    --line:#D8D8D8; --line-strong:#B4B4B4;
    --e0:none; --e1:none; --e2:none; --e3:none; --e4:none; --e5:none; --e-sunk:none;
    --extrude:none; --edge-ring:none; }
  body { background:#FFFFFF !important; color:#111111 !important; } /* knox-allow: pure-white, print */
  body::after, .grain { display:none !important; }
  .rail, .topb, .dock, .console, .c-open, .vtabs, .bulk, .pager, .filters, .composer,
  .drawer-demo, .scrim-demo, .cmdk, .range-presets, .range-foot,
  .connbar, .tipp, .msheet, .pf-dock, .seedbar, .seedopen { display:none !important; }
  .card, .t6-wrap, .kb-card, .modal, .filing, .acc { box-shadow:none !important;
    border:1px solid #D8D8D8 !important; background:#FFFFFF !important; break-inside:avoid; } /* knox-allow: pure-white, print */
  .card::before, .card::after, .t6-wrap::before, .kb-card::before, .modal::before { display:none !important; }
  .t6 th { background:#F4F4F4 !important; }
  .t6 tr, .frow, .tl-item, .nitem, .line { break-inside:avoid; }
  .tip::after { display:none !important; }}

/* ═══ THE REST OF THE INVENTORY ═══════════════════════════════════════════
   The ten components named in "Still to build". Built 23-08-2026 so the system
   is complete rather than complete-ish. */

/* ── DONUT, PROPERLY ──
   The ONE chart allowed more than one colour, because it splits a whole into
   named parts and a legend has to be matchable to a wedge. Every other chart
   stays one accent. Segments are conic, not stroked arcs: a stroked arc has to
   be told its own circumference, and a cap or a rounding error makes wedges
   overlap. A conic gradient cannot overlap by construction. */
/* Conic segments cannot overlap, which is why the ring is built this way. The
   soft ends come from a mask that rounds the whole band rather than from caps on
   individual arcs, which is what made the stroked version overrun itself. */
/* ── THE DONUT ──
   Drawn as SVG arcs rather than a conic gradient, because a gradient can only
   ever butt one colour against the next: hard radial seams, and the whole thing
   reads as a slice of pie that has been cut. Arcs can have round ends and sit
   apart from each other, which is what makes it read as a set of measurements
   rather than a wedge diagram.

   THE GAP MATHS IS THE WHOLE TRICK. A round cap adds HALF a stroke width beyond
   each end of the dash, so an arc of the naive length overruns its share by a
   full stroke width and overlaps its neighbour. That is exactly what broke the
   first attempt. Every arc therefore gives back one stroke width plus the gap,
   and starts half of each back from its true position, so the PAINTED arc
   occupies precisely its own percentage and nothing else. */
.ring { --ring-d:184px; --ring-band:23px; --ring-sw:12.5; --ring-c:251.327; --ring-gap:5;
  display:block; width:var(--ring-d); height:var(--ring-d); flex-shrink:0; overflow:visible; }
.ring circle { fill:none; transform:rotate(-90deg); transform-origin:50% 50%; }
.ring .trk { stroke:var(--ctrl-hi); stroke-width:calc(var(--ring-sw) * 1px); opacity:.34; }
.ring .seg { stroke:var(--sc, var(--chart-1)); stroke-width:calc(var(--ring-sw) * 1px);
  stroke-linecap:round;
  stroke-dasharray:max(.01px, calc((var(--to) - var(--from)) * var(--ring-c) / 100 * 1px
                       - var(--ring-gap) * 1px - var(--ring-sw) * 1px)) 999px;
  stroke-dashoffset:calc(-1px * (var(--from) * var(--ring-c) / 100
                       + var(--ring-gap) / 2 + var(--ring-sw) / 2)); }
.ring .seg.a { --sc:var(--chart-1); }
.ring .seg.b { --sc:var(--chart-2); }
.ring .seg.c { --sc:var(--chart-3); }
.ring::after { content:none; }
/* The figure sits in the hole, on its own surface, so it is read as a total and
   not as part of the band. */
.ringwrap { --ring-band:23px; position:relative; display:inline-grid; place-items:center; }
/* Capped to the hole, less a real margin, so the figure physically cannot touch
   the band however long the number gets. */
.ringwrap .mid { position:absolute; inset:0; display:grid; place-items:center; text-align:center;
  line-height:1.15; pointer-events:none; padding:calc(var(--ring-band, 23px) + 16px); }
.ringwrap .mid b { display:block; font-family:var(--f-num); font-weight:var(--num-wt);
  font-size:var(--fs-lg); color:var(--text); font-variant-numeric:tabular-nums;
  letter-spacing:-.015em; white-space:nowrap; }
.ringwrap .mid small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:var(--ls-caps); text-transform:uppercase;
  color:var(--muted); margin-top:3px; }
.ring-key { display:flex; flex-direction:column; gap:var(--sp-2); }
.ring-key .k { display:flex; align-items:baseline; gap:var(--sp-3); font-family:var(--f-body);
  font-size:var(--fs-sm); color:var(--muted); }
.ring-key .k i { width:9px; height:9px; border-radius:3px; flex-shrink:0; align-self:center;
  background:var(--kc, var(--accent-lit)); box-shadow:inset 0 0 0 1px rgba(0,0,0,.25); }
.ring-key .k .nm { flex:1; min-width:0; }
.ring-key .k .pc { font-family:var(--f-num-col); font-weight:var(--num-wt-col); color:var(--text);
  font-variant-numeric:tabular-nums; }
/* Three DISTINCT hues, matched to the legend. See --chart-1..3 above. */
.ring-key .k.a { --kc:var(--chart-1); }
.ring-key .k.b { --kc:var(--chart-2); }
.ring-key .k.c { --kc:var(--chart-3); }

/* ── IMAGE GRID WITH A DENSITY DIAL ── Darkroom ── */
.tiles { display:grid; grid-template-columns:repeat(var(--cols, 4), 1fr); gap:2px; }
.tile { display:block; position:relative; aspect-ratio:var(--tile-ar, 3/2); overflow:hidden; cursor:pointer;
  background:var(--surface-deep); }
.tile img, .tile .art { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.tile .art { background:linear-gradient(150deg in oklab, var(--seed-accent-hi) 0%, var(--seed-accent-lo) 100%); }
.tile .vig { position:absolute; inset:0; background:linear-gradient(180deg, transparent 55%, rgba(0,0,0,.55) 100%);
  opacity:0; transition:opacity var(--d-fast) var(--ease-hover); }
.tile:hover .vig { opacity:1; }
.tile .stamp { position:absolute; left:7px; bottom:7px; z-index:1; opacity:0;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; letter-spacing:.06em;
  text-transform:uppercase; color:var(--on-accent); transition:opacity var(--d-fast) var(--ease-hover); }
.tile:hover .stamp { opacity:1; }
/* In flight: the art is there but dimmed, with the wait stated rather than implied. */
.tile.proc .art { opacity:.28; filter:saturate(.4); }
.tile.proc::after { content:""; position:absolute; inset:0;
  background:linear-gradient(100deg, transparent 20%, rgba(255,255,255,.07) 50%, transparent 80%);
  background-size:220% 100%; animation:shim calc(1400ms * var(--dur)) infinite; }
.tile .proch { position:absolute; left:7px; top:7px; z-index:2; display:flex; align-items:center;
  gap:7px; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.06em; text-transform:uppercase; color:var(--on-accent); }
.zoom { display:flex; align-items:center; gap:var(--sp-3); }
.zoom .sld { width:140px; }

/* ── MODEL SHEET ── Darkroom ── */
.sheet { border-radius:var(--r); padding:var(--sp-4); background:var(--surface-raise);
  box-shadow:var(--e4); }
.modgrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(128px,1fr)); gap:var(--sp-3); }
/* display:block matters. These are spans, and aspect-ratio does nothing on an
   inline box, so the thumbnail collapsed to zero height and the card rendered as
   text only. */
.mod { display:block; border-radius:var(--r-xs); overflow:hidden; cursor:pointer;
  background:var(--surface-deep); box-shadow:var(--e-sunk);
  transition:box-shadow var(--d-fast) var(--ease-hover); }
.mod:hover { box-shadow:var(--e2); }
.mod.on { box-shadow:var(--e2); outline:2px solid var(--accent-lit); outline-offset:-2px; }
.mod .mthumb { display:block; aspect-ratio:4/3;
  background:linear-gradient(150deg in oklab, var(--seed-accent-hi) 0%, var(--seed-accent-lo) 100%); }
.mod .mbody { display:block; padding:9px 11px; }
.mod b { display:block; font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600; color:var(--text); }
.mod small { display:block; font-family:var(--f-num); font-weight:600; font-size:var(--fs-caps);
  color:var(--subtle-text); margin-top:2px; font-variant-numeric:tabular-nums; }

/* ── INGEST ARRIVAL ROW ── Darkroom ── */
.arr { display:grid; grid-template-columns:74px minmax(0,1fr) auto; gap:var(--sp-3); align-items:center;
  padding:var(--sp-3); border-radius:var(--r-xs); background:var(--surface-deep); box-shadow:var(--e-sunk); }
/* A 1fr column still refuses to shrink past its content, and a camera filename
   is long and unbreakable. minmax(0,1fr) lets it, and the name truncates rather
   than pushing the row wider than the page. */
.arr > * { min-width:0; }
.arr .th { display:block; aspect-ratio:3/2; border-radius:calc(var(--r-xs) * .7); background:var(--ctrl); }
.arr b { display:block; font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600;
  color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.arr small { display:block; font-family:var(--f-num); font-weight:600; font-size:var(--fs-caps);
  color:var(--subtle-text); margin-top:3px; font-variant-numeric:tabular-nums; }
.sdrawer { position:absolute; top:0; right:0; bottom:0; width:min(var(--panel-w-lg), 94%);
  background:var(--surface-raise); box-shadow:var(--e4); transform:translateX(100%);
  transition:transform var(--d-slow) var(--ease-entrance);
  display:flex; flex-direction:column; }
/* ── THE OPEN STATE HAS TO LIVE ON THE ELEMENT · 30-08-2026 ──────────────────
   Found building Darkroom's Library against the real dist, which is the only
   way any of these have ever been found.

   `.sdrawer` ships `transform:translateX(100%)`, and the ONLY rule that brought it
   back was `.sdrawer-demo.open .sdrawer` - scoped to the GALLERY'S DEMO WRAPPER. `.sdrawer-demo`
   is in _extract.py's DEMO drop set, so the open state is dropped from every
   app's dist and the panel sits permanently off screen. It looks like a
   component that shipped. It is a component that can never open.

   This is the `.scrim-demo` fault of 29-08-2026 in a different part: a
   documentation wrapper carrying something load-bearing away with it when it is
   dropped. `.modal` escaped it only because someone had already written the
   real `.scrim.open > .modal` beside the demo's.

   So the element carries its own state, and the demo keeps its wrapper form.
   Both live, deliberately, exactly like the portalled-menu twins above. */
.sdrawer.open { transform:none; }
.sdrawer .sd-head { flex-shrink:0; padding:var(--sp-4); border-bottom:1px solid var(--line);
  display:flex; align-items:center; gap:var(--sp-3); }
.sdrawer .sd-head b { flex:1; font-family:var(--f-head); font-size:var(--fs-md); font-weight:400;
  text-transform:uppercase; letter-spacing:var(--ls-head); }
.sdrawer .sd-body { flex:1; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; padding:var(--sp-4); }

/* ── DETERMINATE IMPORT PROGRESS ── Pulse builds this six times ── */
.imp { display:flex; align-items:center; gap:var(--sp-3); }
.imp .bar { flex:1; min-width:0; }
.imp .n { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; white-space:nowrap; }
.imp .n small { font-family:var(--f-ui); font-weight:600; font-size:var(--fs-caps);
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-left:5px; }

/* ── FACETED FILTER ── Darkroom ── */
.facets { display:grid; grid-template-columns:repeat(auto-fit, minmax(150px,1fr)); gap:var(--sp-4); }
.facet .fh { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--subtle-text);
  padding-bottom:var(--sp-2); margin-bottom:var(--sp-2); border-bottom:1px solid var(--line); }
.facet .fl { display:flex; flex-direction:column; gap:2px; }

/* ── MONTHLY BAR ROW ── Knox Artiste Accounts ── */
.barrow { display:grid; grid-template-columns:64px minmax(0,1fr) 110px; gap:var(--sp-3); align-items:center;
  padding:7px 0; }
.barrow .l { font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
.barrow .v { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); text-align:right; font-variant-numeric:tabular-nums; }

/* ── FAILURE WITH A WAY OUT ──
   A banner states a problem. This one states it and offers the fix, which is
   what an error that a person can actually resolve should always do. */
.failure { display:flex; align-items:center; gap:var(--sp-4); padding:var(--sp-5);
  border-radius:var(--r-xs); background:var(--surface-deep);
  box-shadow:inset 3px 0 0 var(--danger); }
.failure .fx { flex:1; min-width:0; }
.failure b { display:block; font-family:var(--f-body); font-size:var(--fs-md); font-weight:600;
  color:var(--text); }
.failure p { font-size:var(--fs-sm); color:var(--muted); margin-top:4px; }

/* ── CURSOR-FOLLOWING TOOLTIP ──
   The CSS tip is anchored to its element, which is right almost everywhere and
   wrong on a dense row of icons, where the tip for the fourth icon lands under
   the third. This one follows the pointer. */
.ktip { position:fixed; z-index:var(--z-toast); pointer-events:none; white-space:nowrap;
  font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600; color:var(--text);
  background:var(--surface-raise); border-radius:calc(var(--r-xs) * .7); padding:5px 9px;
  box-shadow:var(--e3); opacity:0; transform:translate(-50%, -140%);
  transition:opacity calc(120ms * var(--dur)) ease; }
.ktip.on { opacity:1; }


/* ── MASONRY IMAGE GRID ──
   A generation grid holds 9:16, 3:4, 4:3 and 16:9 side by side. A fixed-ratio
   tile grid crops or letterboxes all of them to one shape, which is exactly what
   you do not want when the shape IS the output. Rows lock to a height and tiles
   pack along it, each keeping its own ratio, so nothing is cropped and no gaps
   are left. Justified rows, the way a contact sheet works. */
/* --row-h is declared on :root as 44px, meaning ONE TABLE ROW at default density.
   The tiles below read var(--row-h, 150px) meaning a media row, so the fallback
   could never fire and every wall in the estate came out 44px tall. Both wireframe
   pages patch it at page level; it belongs here, in the layer the fix can reach
   every app from. Set on the component so an inline --row-h on the element still
   wins, which is how the zoom stepper works. FIXED 29-08-2026. */
.mgrid { display:flex; flex-wrap:wrap; gap:3px; align-items:flex-start; --row-h:150px; }
.mgrid .tile { display:block; position:relative; overflow:hidden; cursor:pointer;
  background:var(--surface-deep);
  /* FIXED 29-08-2026. This is the justified row solved in CSS alone, and it needs
     all three of the lines below to be what they are.

     --row-h is the TARGET height, carried as the basis at the tile's true width
     (row-h x ar), so a row breaks where a row of real frames would break. Inside
     the row, flex-grow by aspect resolves to width = ar x h with
     h = (width - gaps) / sum(ar): the row lands flush on both edges, exactly the
     similarity transform. aspect-ratio then reads the height back off that width,
     so every tile in the row comes out at the same solved h and NOT at the target
     - which is the whole trick, because a row cannot both fill its width and keep
     its ratios while its height is pinned.

     What it replaces: flex-basis:0 with a min-width of 55% of the true width. A
     zero-basis tile has no natural width, so wrapping was driven entirely by that
     min-width and the tiles then stretched to share the row. A 9:16 measured 0.339
     against 0.5625 - every frame in the estate rendered about 40% too narrow, and
     every one of them was distorted rather than merely small.

     flex-shrink stays at its default 1, weighted by the basis, which is the same
     transform in reverse on an over-full row. */
  flex-grow:var(--ar, 1.5);
  flex-basis:calc(var(--row-h, 150px) * var(--ar, 1.5));
  height:auto; aspect-ratio:var(--ar, 1.5); min-width:0; }
.mgrid .tile > .art, .mgrid .tile > img { position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; }
.mgrid .tile .ratio { position:absolute; right:7px; top:7px; z-index:2; opacity:0;
  font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps);
  color:var(--on-accent); background:rgba(0,0,0,.5); border-radius:4px; padding:2px 6px;
  transition:opacity var(--d-fast) var(--ease-hover); }
.mgrid .tile:hover .ratio { opacity:1; }
/* Named ratios, so markup says 9:16 rather than 0.5625. */
.ar-9x16 { --ar:.5625; } .ar-3x4 { --ar:.75; } .ar-1x1 { --ar:1; }
.ar-4x3 { --ar:1.3333; } .ar-3x2 { --ar:1.5; } .ar-16x9 { --ar:1.7778; }
/* The narrow-sheet guard used to live here as `@media (max-width:600px){
   .mgrid .tile { --row-h:104px } }`, and it fought the count solve. The solve
   writes --row-h on the GRID; a rule on the TILE is more specific for the
   tile's own flex-basis, so on a phone the basis ignored the count entirely
   while the row height honoured it. Measured 30-08-2026 at 390px: rows broke at
   widths the solve never chose, and single frames stood 260px tall in a 290px
   sheet. Two mechanisms writing one token is the bug; the guard now lives in
   the solve as a floor, so the token has exactly one author. */

/* ── CHECKOUT ──
   Three placements, one component. The cart is the same rows and the same totals
   whether it is a panel, a modal or inline: only where it sits changes. */
.cart { display:flex; flex-direction:column; min-height:0; }
.cart .cart-head { flex-shrink:0; display:flex; align-items:center; gap:var(--sp-3);
  padding-bottom:var(--sp-3); border-bottom:1px solid var(--line); }
.cart .cart-head b { flex:1; font-family:var(--f-head); font-size:var(--fs-md); font-weight:400;
  text-transform:uppercase; letter-spacing:var(--ls-head); }
.cart .cart-n { font-family:var(--f-num); font-weight:var(--num-wt); font-size:var(--fs-caps);
  min-width:20px; height:20px; padding:0 6px; border-radius:var(--r-pill);
  background:var(--accent-lit); color:var(--on-accent); display:grid; place-items:center;
  font-variant-numeric:tabular-nums; }
[data-theme="light"] .cart .cart-n { color:var(--on-accent); }
.cart .cart-body { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  scrollbar-gutter:stable; padding:var(--sp-3) 0; }
.cart .cart-foot { flex-shrink:0; padding-top:var(--sp-3); border-top:1px solid var(--line); }
.cartline { display:grid; grid-template-columns:52px minmax(0,1fr) auto; gap:var(--sp-3);
  align-items:center; padding:var(--sp-3) 0; border-bottom:1px solid var(--line); }
.cartline:last-child { border-bottom:none; }
.cartline .th { display:block; aspect-ratio:1; border-radius:calc(var(--r-xs) * .8);
  background:var(--ctrl); }
.cartline b { display:block; font-family:var(--f-body); font-size:var(--fs-sm); font-weight:600;
  color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cartline small { display:block; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.08em; text-transform:uppercase; color:var(--subtle-text); margin-top:3px; }
.cartline .qty { display:flex; align-items:center; gap:var(--sp-2); }
.cartline .qty .stepper { height:30px; border-radius:var(--r-pill); background:var(--surface-deep);
  box-shadow:var(--e-sunk); overflow:hidden; }
.cartline .qty .stepper input { width:28px; font-size:var(--fs-caps); padding:0; }
.cartline .qty .stepper button { width:26px; font-size:var(--fs-sm); }
.cartline .amt { font-family:var(--f-num-col); font-weight:var(--num-wt-col); font-size:var(--fs-sm);
  color:var(--text); font-variant-numeric:tabular-nums; min-width:72px; text-align:right; }

/* ── THE ACCENT BAND, USED ──
   Knox called this the best read of the accent in the system, so it earns more
   than one appearance: a hero strip, and a surface for anything that should feel
   like a statement rather than a row. */
.accent-band.slim { padding:var(--sp-5); }
.accent-band .bandrow { display:flex; align-items:center; gap:var(--sp-5); flex-wrap:wrap; }
.accent-band .glass { flex:1; min-width:200px; }
.accent-band .fig .l { color:color-mix(in srgb, var(--on-accent) 72%, transparent); }
.accent-band .fig .v, .accent-band .fig .n { color:var(--on-accent); }
.accent-band .fig .n { opacity:.78; }


/* ═══════════════════════════════════════════════════════════════════════════
   MEDIA · THE FRAME ACTIONS AND THE DRIFT LIGHT
   Ported verbatim from 06-wall.html, 30-08-2026. The signed Library wall has
   both and the master had neither, so any surface built from the system alone
   would have shipped a wall of frames you cannot act on. Cut, not redrawn.
   ═══════════════════════════════════════════════════════════════════════════ */
.dr-sheet { gap:2px; }
.tile .dr-light { position:absolute; top:10px; left:10px; z-index:2; }
.dr-sheet .tile .dr-light { opacity:.92; }
.dr-light { display:inline-flex; width:10px; height:10px; border-radius:var(--r-pill);
  flex:none; box-shadow:0 0 0 2px rgba(0,0,0,.30); }
.dr-light.ok   { background:var(--success); }
.dr-light.warn { background:var(--warning); }
.dr-light.bad  { background:var(--danger); }

/* The action strip rides in from under the frame on hover. It is not a menu:
   four verbs an operator uses constantly, at the frame they apply to. */
/* KNOX, 30-08-2026: FADE, NOT A BOUNCE. The signed drawing rides the strip in
   from under the frame on `transform:translateY(115%)` with --ease-entrance,
   an ease-out-quint: a slide that snaps as it lands. On a wall where the
   pointer crosses twenty frames on the way to one, twenty strips snap. His
   words: "they can't be animating in like that, they should just kind of fade
   in / fade out." So the strip sits where it belongs and only opacity moves, on
   --d-fast and --ease-hover, the same fade this system uses everywhere else for
   something answering a pointer. pointer-events follow the opacity so a hidden
   strip cannot be clicked. */
.dr-strip { position:absolute; left:0; right:0; bottom:0; z-index:2;
  display:flex; align-items:center; gap:6px; padding:7px 9px;
  justify-content:flex-end; opacity:0; pointer-events:none;
  transition:opacity var(--d-fast) var(--ease-hover); }
.dr-sheet .tile:hover .dr-strip, .dr-sheet .tile:focus-within .dr-strip {
  opacity:1; pointer-events:auto; }
.dr-strip .spx { flex:1; font-family:var(--f-num); font-variant-numeric:tabular-nums;
  font-size:var(--fs-caps); color:rgba(253,253,253,.85); }
.dr-strip .spx::before { content:"$"; opacity:.6; }
/* Smaller, on his read of the render, and the verbs are five now that Download
   is in: 24px boxes at 12px icons crowded a 130px frame. */
.dr-strip .fact { width:20px; height:20px; border-radius:calc(var(--r-xs) * .8); }
.dr-strip .fact svg { width:11px; height:11px; }
.fact { display:inline-flex; align-items:center; justify-content:center;
  width:28px; height:28px; flex:none; padding:0; cursor:pointer;
  background:rgba(253,253,253,.13); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  border:1px solid rgba(253,253,253,.10); border-radius:var(--r-xs);
  color:rgba(253,253,253,.88);
  transition:background calc(140ms * var(--dur)) var(--ease-hover),
             color calc(140ms * var(--dur)) var(--ease-hover),
             border-color calc(140ms * var(--dur)) var(--ease-hover); }
.fact:hover { background:var(--seed-accent-hi); border-color:transparent; color:var(--on-accent); }
.fact:focus-visible { outline:2px solid var(--accent-lit, var(--accent-text)); outline-offset:2px; }
.fact:active { transform:scale(.96); }
.fact svg { width:14px; height:14px; flex:none; }
.dr-sheet .tile.sel::after { content:""; position:absolute; inset:0; z-index:3;
  box-shadow:inset 0 0 0 2px var(--seed-accent-hi); pointer-events:none; }

/* ── THE STRIP SHEDS LOAD AS THE FRAME NARROWS ───────────────────────────────
   Twelve per row in a 920px sheet puts a frame at 91px, and four buttons plus a
   price do not fit in 91px: they overlap, which is worse than being absent.
   The tile is its own container, so the strip answers to the FRAME's width and
   not the window's. It drops in order of how often each is wanted: the price
   first, since it is on the card anyway, then the two rarer verbs, leaving
   Recreate and Reference, which is what an operator reaches for at a glance. */
.dr-sheet .tile { container-type:inline-size; }
@container (max-width: 230px) { .dr-strip .spx { display:none; }}
@container (max-width: 170px) { .dr-strip .fact:nth-of-type(n+5) { display:none; }}
@container (max-width: 150px) { .dr-strip .fact:nth-of-type(n+4) { display:none; }}
@container (max-width: 115px) { .dr-strip .fact:nth-of-type(n+3) { display:none; }}
@container (max-width: 96px)  { .dr-strip { display:none; }}

/* ── FRAMES PER ROW IS A COUNT, NOT A HEIGHT ─────────────────────────────────
   Settled by Knox 29-08-2026, and it is the one place this section overrides
   the signed drawing. 06-wall.html draws a row-height slider; the decision is a
   count of three to twelve.

   Why the count wins: the wall solves each row to fill its width, so the height
   you get is a RESULT, not a setting. A height slider therefore asks for
   something the layout will not honour, and the frame count drifts with the
   window while the number under your hand stays still. Asking for the count
   asks for the thing the solve actually takes.
   The row height stays capped at half the sheet so a frame can never be driven
   taller than the window. */
.percount { display:inline-flex; align-items:center; gap:var(--sp-2); }
.percount .pc-n { font-family:var(--f-num); font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm); color:var(--text); min-width:1.4em; text-align:center; }

/* ── THE REVIEW STRIP ────────────────────────────────────────────────────────
   06-wall.html declares `.rstrip { --strip-h:150px; }` immediately beside
   `.mgrid { --row-h:150px; }`, and the master carried only the wall's half.
   Both lines exist for one reason: a component whose single sizing knob is
   reachable ONLY as an inline fallback cannot be found by reading the token
   layer and cannot be set by a parent. Declared here so a review room on a
   large display raises the strip without anyone editing the tile rule. */
.rstrip { --strip-h:150px; scrollbar-width:thin; }

/* The strip is WALKED, not worked. Its tiles carry the frame and nothing else:
   no action strip, no meta line, no light. The verbs belong in the viewer the
   strip opens rather than in the navigator, and a 9:16 frame in a 150px strip
   is 84.4px wide, under the 96px cut where the wall's own action strip hides
   itself, so shipping them here would ship them hidden. The light came off with
   the wall's on 30-08-2026: a review reads the drift verdict in the viewer's
   side panel, where the score, the word and the moved facets actually fit. */

/* One frame is on stage. That is NOT the wall's `.sel`, which is a multi-select
   an operator accumulates and then acts on; this is a single position in an
   ordered list. Same ring, different word, so the two can never be read as the
   same state by anyone maintaining either. */
.rstrip .tile.cur::after { content:""; position:absolute; inset:0; z-index:3;
  box-shadow:inset 0 0 0 2px var(--seed-accent-hi); pointer-events:none; }

/* A scroll region with no focusable child is a keyboard trap: 1417px of frames
   in a 920px box that tab skips entirely. The strip takes focus itself and
   answers to the arrows, which is also how a director walks it with a hand off
   the mouse. Same finish as the reference tray, the system's other horizontal
   media row: a thin scrollbar, no edge fade. */
.rstrip:focus-visible { outline:2px solid var(--accent-lit, var(--accent-text));
  outline-offset:3px; border-radius:var(--r-xs); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE CONSOLE
   Templated from 02-stills.html, then corrected by an Adam pass, 29-08-2026.
   Everything here is either a value the signed console already established, or
   a fix the pass named. No new appearance is invented.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── THE MISSING SLIMMING OVERRIDE ───────────────────────────────────────────
   Carried verbatim from 02-stills.html, "console slimmed 28-08-2026". Without
   it the echip renders 25.59px inside a 24px line box, so line one of a brief
   is 33.6px and line two is 24px: uneven leading inside a single sentence, and
   exactly the "tags bigger than the sentence" fault this build was meant to
   end. It was left behind twice. */
/* ── A TAG IS HIGHLIGHTED TEXT, NOT A PILL ───────────────────────────────────
   Knox, 30-08-2026: a tag reads as part of the sentence, in the accent, with
   its @ visible. No plate, no border, no thumbnail. The remove control appears
   on hover and is not there the rest of the time.

   Why this is better than the pill it replaces: a reference is a WORD in the
   brief. Boxing it made every reference a foreign object sitting in the prose,
   the line height had to grow to hold it, and a thumbnail 16px tall inside a
   15px line is a picture nobody can read carrying a caption nobody needs. The
   colour is the same --tc the reference square carries, so the correspondence
   the row exists for survives the box being removed. */
.composer .pwell .echip { display:inline; height:auto; padding:0; gap:0;
  background:none; border:0; box-shadow:none; border-radius:0;
  font-family:inherit; font-size:inherit; font-weight:600; letter-spacing:0;
  color:var(--accent-text);
  /* The highlight is the ACCENT, not the reference's plate colour. Tried the
     plate first and it fails: @image3's plate is a dark navy, and 34% of a dark
     navy on a dark ground is invisible, so one tag in three had no highlight at
     all. The plate colour stays where it works, on the square, and the link
     between the two is carried by the hover instead, which lights both. */
  background-color:color-mix(in srgb, var(--seed-accent-hi) 20%, transparent);
  box-decoration-break:clone; -webkit-box-decoration-break:clone;
  padding:1px 2px; border-radius:calc(var(--r) * .35); }
.composer .pwell .echip .thumb { display:none; }
.composer .pwell .echip i { display:none; margin-left:3px; width:auto; height:auto;
  background:none; border-radius:0; font-size:inherit; color:var(--subtle-text);
  vertical-align:baseline; }
.composer .pwell .echip:hover i, .composer .pwell .echip:focus-within i { display:inline; }
.composer .pwell .echip i:hover { color:var(--text); background:none; }
.composer .pwell .echip.lit {
  background-color:color-mix(in srgb, var(--seed-accent-hi) 45%, transparent);
  color:var(--text); }
/* NO OUTLINE ON A REFERENCE. Removed 30-08-2026, Knox: the armed console must
   read the same as the tray and the card, and those never had one.
   It was added to show the tag-to-square correspondence on hover, but the
   number already does that job and does it better: an outline says "selected",
   which is a state this square does not have, while the number says "this is
   the one @image2 points at", which is the actual question. The .lit class
   stays and still reveals the number when the TAG is hovered, so pointing at a
   tag still lights its square. Only the ring goes. */

/* ── THE REFERENCE ROW ───────────────────────────────────────────────────────
   The one genuinely new part of this instrument. The signed console hid
   references behind a paperclip; Knox moved them above the field, which is the
   right reading order: you see what the sentence points at before you read it. */
/* NOT ONE HARDCODED RADIUS IN THIS SECTION. Every corner below derives from
   --r, the house radius, which is 20px and is a dial in the panel. The first
   build wrote 12px on the squares and 3px on the tags, so the radius slider
   moved the whole system except this instrument. A component that ignores the
   dial is a component that has quietly forked. */
/* ── THE TRAY IS A SUNKEN SURFACE ────────────────────────────────────────────
   Knox, 30-08-2026: the references should sit in a sunken well above the field,
   snapped in like bricks, and scroll when there are many.

   He is right, and it fixes something the row did not have: a container. Loose
   squares floating on the instrument ground had no edge, so nothing said where
   the set of references ended and the instrument began. A well says "these
   belong together and they belong to the field below". It is the same --e-sunk
   the prompt field already uses, so the two read as one stacked group, which is
   what they are.

   It scrolls on OVERFLOW rather than at a count. Knox suggested twenty or
   twenty-five; the honest trigger is the window, since the same twelve
   references fit on a desktop and do not on a laptop. The engine caps run from
   14 to 50, so both cases are real. */
/* The tray takes the PROMPT WELL'S recess, exactly. Knox, 30-08-2026: the two
   surfaces had different corners, 8px against 14px, and once they are stacked
   eight pixels apart that reads as two unrelated boxes rather than one
   instrument. It is not only the radius: the well is --surface-deep with a
   line and --e-sunk, and the tray was --bg-2 with no border, so the recesses
   did not match either. All four properties now come from the same place, and
   the radius derives from --r like everything else in this section. */
.creftray { margin-bottom:8px; padding:var(--sp-2);
  border-radius:calc(var(--r) * .7);
  background:var(--surface-deep); border:1px solid var(--line);
  box-shadow:var(--e-sunk); }
.crefs { display:flex; align-items:center; gap:6px; flex-wrap:nowrap;
  overflow-x:auto; overflow-y:hidden; scrollbar-width:thin;
  /* Bricks snap: the row scrolls to a whole square, never half of one. */
  scroll-snap-type:x proximity; }
.crefs > * { scroll-snap-align:start; }
.crefs::-webkit-scrollbar { height:6px; }
.crefs::-webkit-scrollbar-track { background:none; }
.crefs::-webkit-scrollbar-thumb { background:var(--line-strong); border-radius:999px; }

/* ONE SIZE AND ONE SHAPE, in all four places: image empty, image armed, video,
   and the compiled card. Knox, 30-08-2026.

   This reverses an earlier decision, and the reasoning is worth keeping. The
   Adam pass argued the box should take the console's own aspect, so a 4:5 shot
   showed 4:5 thumbnails and a 16:9 clip showed 16:9. That is right if the box
   is a PREVIEW of the output. It cannot hold once the same component appears on
   an image console, a video console and a card in one page: the boxes then
   change shape between panels for a reason no operator can see, and the card's
   were a different size again.

   So the box stops claiming to be a preview and becomes what it always was: a
   uniform thumbnail slot. The image inside is cover-cropped. The aspect claim
   now lives on the aspect dropdown alone, which is the control that actually
   sets it, and one value is stated in one place. */
.cref { position:relative; display:inline-flex; align-items:center; justify-content:center;
  width:44px; height:44px; flex:0 0 auto;
  border-radius:calc(var(--r) * .6); overflow:hidden;
  transition:transform calc(90ms * var(--dur)) var(--ease-press); }
.cref:active { transform:scale(.96); }

/* The plate is the tag's OWN colour. Square two and @image2 are one object, and
   that correspondence is the entire reason the row sits above the sentence. The
   first build coloured them by hue-rotate into red, green and violet: three
   saturated accent blocks above the one key that is meant to be the only accent
   on the screen. */
/* The plate is the media seed rotated by the slot's index. --i is set on the
   element, so nothing here is a hand-picked colour. */
.cref .art { position:absolute; inset:0; background:var(--seed-media);
  filter:hue-rotate(calc(var(--i, 0) * var(--media-step))); }
.cref .art::after { content:""; position:absolute; inset:0; background-image:var(--noise);
  background-size:120px; opacity:calc(.05 * var(--noise-o)); mix-blend-mode:overlay; }

/* ── THE INDEX IS A WATERMARK, NOT A BADGE ───────────────────────────────────
   Knox, 30-08-2026: no box inside a box, and the number is not burnt in. It
   behaves exactly like the remove control: absent until you hover the square,
   or hover the tag that points at it.

   Why this is right beyond the look: the number is a lookup, not a label. You
   need it at the moment you are about to type @image2, and never otherwise. A
   permanent badge spends contrast every second on information wanted for one.
   It sits centred on a scrim over the whole plate, so it is legible on any
   image without a plate of its own. */
.cref .n { position:absolute; inset:0; z-index:1; font-style:normal;
  display:grid; place-items:center; margin:0; padding:0;
  background:color-mix(in srgb, var(--seed-ink) 55%, transparent);
  font-family:var(--fm); font-size:15px; font-weight:600; letter-spacing:.06em;
  color:var(--paper-text, #FFF); opacity:0;
  transition:opacity var(--d-fast) var(--ease-hover); }
.cref:hover .n, .cref.lit .n, .cref:focus-within .n { opacity:1; }

.cref .rm { position:absolute; right:3px; top:3px; z-index:2; display:grid;
  place-items:center; width:16px; height:16px; padding:0; border:0; border-radius:50%;
  background:color-mix(in srgb, var(--surface-deep) 82%, transparent);
  color:var(--subtle-text); cursor:pointer; opacity:0;
  transition:opacity var(--d-fast) var(--ease-hover), color var(--d-fast) var(--ease-hover); }
.cref .rm svg { width:9px; height:9px; }
.cref:hover .rm, .cref .rm:focus-visible { opacity:1; }
.cref .rm:hover { color:var(--text); }
.cref .rm:focus-visible { outline:2px solid var(--accent-lit, var(--accent-text)); outline-offset:1px; }

/* The empty square IS the drop target. It fills with the first reference and
   stops being an empty square; a fresh one waits at the end. */
.cref.add { border:1px dashed var(--line-strong); background:none;
  color:var(--subtle-text); cursor:pointer;
  transition:color var(--d-fast) var(--ease-hover), border-color var(--d-fast) var(--ease-hover),
             transform calc(90ms * var(--dur)) var(--ease-press); }
.cref.add svg { width:14px; height:14px; }
.cref.add:hover, .cref.add.over { border-style:solid; color:var(--text); }
/* Focus keeps its ring. Killing it was a straight hit on Banned Forever, and
   this is the only interactive element in the row. */
.cref.add:focus-visible { outline:2px solid var(--accent-lit, var(--accent-text));
  outline-offset:2px; color:var(--text); }

.crefnote { font-size:var(--fs-sm); color:var(--subtle-text); min-width:0; }

/* ── THE MENU MUST BE OPAQUE ─────────────────────────────────────────────────
   Reported 30-08-2026: opening a dropdown showed the prompt well's border
   through it. The cause is real and not a z-index slip. The menu ships at 92%
   of --surface-raise over a backdrop-filter, and the field it opens across has
   a 1px accent border. Blur does not remove that border, it SMEARS it, so an
   accent line reads straight through the panel. Any translucent surface over a
   high-contrast edge does this.
   A menu is a reading surface, not a scrim: it is opaque. The blur stays, so
   the character is unchanged where it overhangs the page ground, but nothing
   behind it can be read through it any more. This also covers the glass finish,
   which took the same panel down to 58%. */
.composer .engmenu, [data-finish="glass"] .composer .engmenu {
  background:var(--surface-raise);
  /* The blur did nothing once the panel went opaque, and it was costing a
     compositing layer on every open. */
  -webkit-backdrop-filter:none; backdrop-filter:none; }

/* ── WHICH WAY THE MENU OPENS ────────────────────────────────────────────────
   The signed console is docked at the bottom of the viewport, so the menu was
   written to open upward and only upward: bottom:100%, transform-origin at the
   bottom. On this page the console sits mid-document, so upward meant a 412px
   panel flying over the heading and three paragraphs of unrelated text. That is
   the "it covers everything" fault, and it is not a z-index problem.
   The direction is now decided at open time from the space actually available,
   which is also correct in the app: a composer near the top of a short window
   has nowhere to go up. */
/* The composer ships a document-level handler that strips .open off every
   .engwrap on any click. It was written for one chip and it fights any second
   opener. Rather than race it, this rail drives its own state class, which
   nothing else touches. */
.composer .engwrap.vopen > .engmenu { display:block; opacity:1; transform:none;
  pointer-events:auto; }
.engwrap .engmenu.down { bottom:auto; top:calc(100% + 9px);
  transform-origin:top left; transform:translateY(-6px) scale(.985); }
.engwrap.open .engmenu.down, .engmenu.down.open { transform:none; }

/* Value dropdowns are the engine chip at value size: one component, five uses. */
.engwrap.val .engchip { font-family:var(--f-num); font-variant-numeric:tabular-nums; }
.engwrap.val .engmenu { width:238px; }
.engwrap.val.wide .engmenu { width:300px; }
/* ── A VALUE MENU IS A LIST OF VALUES ────────────────────────────────────────
   Knox, 30-08-2026: "we don't need an explanation of what the aspect ratio is,
   just an option to select the correct aspect ratio."

   He is right and it is the same fault three times. An aspect ratio, a
   resolution and a duration are self-describing: 4:5 means 4:5 to anyone who
   asked for it, and a sentence beside it is the menu explaining a thing the
   reader already knows. Six ratios with stacked subtitles came to 412px, which
   is why the panel covered the page whichever way it opened.

   So: values only, in caps, one compact line each. The ONE exception is shot
   structure, where the label genuinely does not carry the meaning, and the
   engine, which carries the price because the choice is made against the cost. */
.engwrap.val .erow { grid-template-columns:1fr; padding:8px 10px; }
.engwrap.val .ex { display:flex; align-items:baseline; gap:10px; min-width:0; }
.engwrap.val .ex b { flex:0 0 auto; font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; }
.engwrap.val .ex small { min-width:0; text-transform:none; letter-spacing:0;
  font-size:var(--fs-sm); line-height:1.45; }
/* Narrow, because a list of values needs no room for prose. */
.engwrap.val .engmenu { width:182px; }
/* Wide, and the sub sits UNDER the label rather than beside it, because these
   three labels are the only ones on the rail that do not explain themselves. */
.engwrap.val.wide .engmenu { width:330px; }
.engwrap.val.wide .ex { display:block; }
.engwrap.val.wide .ex b { display:block; margin-bottom:3px; }
.engwrap.val.wide .ex small { display:block; white-space:normal; }

/* The engine row: name left, price right, nothing else. */
.engwrap:not(.val) .erow .ex b { font-family:var(--f-ui); font-size:var(--fs-caps);
  font-weight:600; letter-spacing:.1em; text-transform:uppercase; }
.engwrap:not(.val) .engmenu { width:250px; }

/* ── THE FIELD GROWS FROM ONE LINE TO EIGHT ──────────────────────────────────
   Knox, 30-08-2026. Empty is ONE line, not four: a box claiming four lines of
   height while holding nothing is the instrument pretending. It grows a line at
   a time as you type, stops at eight, then scrolls inside itself, so the
   control row and the fire key never leave the view and nothing below the
   console moves while you are writing.

   Measured in lines rather than pixels, so it follows the type scale dial.
   1.55 is .pwell's own line-height and 22px is its vertical padding. */
.composer .pwell { position:relative; align-items:flex-start; display:block;
  min-height:calc(1.55em * 1 + 22px);
  max-height:calc(1.55em * 8 + 22px);
  overflow-y:auto; }

/* ── THE EMPTY FIELD IS EXACTLY ONE LINE, IN EVERY ENGINE ────────────────────
   Knox, 30-08-2026: "the box is holding two lines at all times, and when I
   start typing it becomes one."

   Two things were each adding a line box and neither was needed.
   1. The empty field shipped with a literal <br> inside it, the usual trick for
      giving a contenteditable somewhere to put the caret. Chromium collapses a
      trailing <br> after generated content; WebKit does not, so on a Mac the
      empty field was two lines and dropped to one the moment the <br> was
      replaced by a character. Measuring in Chromium showed no jump, which is
      why I reported this fixed when it was not. The <br> is removed.
   2. The placeholder was an inline ::before, so it took part in layout. It is
      absolutely positioned now: it paints, it never occupies a line.

   min-height alone holds the single line open, so there is nothing left whose
   presence or absence can change the height. Empty and one character are the
   same box by construction rather than by measurement. */
/* .pcaret is deleted from every editable field. It was a drawn caret in a
   static wireframe, which is right there and wrong the moment the field is
   real: it paints a second caret that never moves, and it is one more node
   whose empty-inline treatment differs between engines. */
.pwell.is-empty::before { content:attr(data-ph); color:var(--subtle-text);
  position:absolute; left:16px; right:16px; top:11px; pointer-events:none;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* ── THE CARD ────────────────────────────────────────────────────────────────
   Built from .sc-head / .sc-body / .sc-read / .sc-foot, which were designed for
   exactly this and were sitting unused while the first build reassembled the
   card out of .cset and .engchip. That rebuild spread the header across four
   baselines, because .sc-head is baseline-aligned and .cset is centre-aligned. */
.shotcard .sc-read mark { background:none; color:var(--accent-text); }
.shotcard .creftray { margin:0; flex:0 0 auto; max-width:100%; }
.shotcard .crefs { align-items:flex-start; }
.shotcard .sc-verdict { display:inline-flex; align-items:center; gap:9px;
  margin-right:auto; font-family:var(--f-ui); font-size:var(--fs-caps); font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--subtle-text); }
.shotcard .sc-verdict i { width:6px; height:6px; border-radius:50%;
  background:var(--accent-text); font-style:normal; flex:0 0 auto; }
.shotcard .sc-foot { flex-wrap:wrap; }
/* .sc-eng and .sc-seed are nowrap by design, so at 390px the engine line and
   the price ran off the card. The head wraps into two rows instead: identity
   above, money below, each group still internally baseline-aligned. */
@media (max-width:640px){
  .shotcard .sc-head { flex-wrap:wrap; }
  .shotcard .sc-head .lft, .shotcard .sc-head .rgt { flex-wrap:wrap; }
  .shotcard .sc-eng { white-space:normal; }}
@media (max-width:640px){
  .shotcard .sc-body { flex-direction:column; }
  .composer .cset { flex-wrap:wrap; }}
