/* ============================================================
   Proof — text-first cut
   "Marking up a galley proof in a darkroom."
   Warm near-black, cream type, one amber accent, rust for cuts.
   ============================================================ */

/* Self-hosted faces (P6 §4, W11) — variable woff2 vendored under fonts/, all
   OFL. The two MEASURING faces (Literata = paper, Spline Sans Mono = chips)
   are font-display: block — a fallback swap mid-measure is the lie pretext
   can't survive (G5); boot awaits them before the first measurement anyway,
   so block never shows. Hanken is chrome only and may swap freely. */
@font-face {
  font-family: 'Literata';
  font-style: normal;
  font-weight: 300 700;
  font-display: block;
  src: url(/fonts/literata-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Literata';
  font-style: normal;
  font-weight: 300 700;
  font-display: block;
  src: url(/fonts/literata-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Literata';
  font-style: italic;
  font-weight: 300 700;
  font-display: block;
  src: url(/fonts/literata-italic-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Literata';
  font-style: italic;
  font-weight: 300 700;
  font-display: block;
  src: url(/fonts/literata-italic-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Spline Sans Mono';
  font-style: normal;
  font-weight: 300 600;
  font-display: block;
  src: url(/fonts/spline-sans-mono-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Spline Sans Mono';
  font-style: normal;
  font-weight: 300 600;
  font-display: block;
  src: url(/fonts/spline-sans-mono-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url(/fonts/hanken-grotesk-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Spectral SemiBold — the WORDMARK face only (the ¶Proof brand note). Chrome, not
   a measuring face, so it may swap freely (like Hanken); not preloaded. Single
   latin subset (covers the pilcrow U+00B6); missing glyphs fall back to serif. */
@font-face {
  font-family: 'Spectral';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/fonts/spectral-600-latin.woff2) format('woff2');
}

:root {
  /* TONE PASS (Jesse 2026-06-29: "the yellow and brown has lost its charm — fresh eyes").
     The dark fills carried too much chroma (0.010–0.014 at hue 65–70), which on large
     surfaces read as muddy yellow/brown. Slashed the chroma toward the canonical browse-
     mockup's restraint (its darks #100f0e/#161514 are ~0.004) while HOLDING every lightness
     value — contrast is governed by L in oklch, so the WCAG floors (contrast.test.ts) are
     untouched. COOLED (Jesse 2026-06-29 round 2: "still a little browney, a little cooler"):
     hue swung 70→255 so the residual chroma reads cool-slate, harmonizing with the blue
     --accent (also hue ~250) instead of brown. The saturated life still comes from the
     accents (blue/amber/status), not the walls. */
  --bg:        oklch(0.165 0.006 255);
  --bg-deep:   oklch(0.135 0.005 255);
  --surface:   oklch(0.205 0.007 255);
  /* a SUBTLE semi-opaque lift over the warm-dark bg (Jesse 2026-06-29) — a low-opacity
     CREAM wash (the browse-mockup's #ece6d6 ink at ~5%), NOT a heavier warm slab like
     --surface (which read as an ugly light-brown band on large surfaces). Matches the
     canonical darkroom-browser mock's tone-shift architecture: surfaces lift via gentle
     semi-opaque cream over the page, never a hard jump. Reuse app-wide. */
  --lift-1:    rgb(236 230 216 / 0.05);
  --line:      oklch(0.30 0.012 255);
  /* inks settle to a near-neutral, faintly-cool off-white (round 2) — the warm cream read
     as part of the "browney" cast against the now-cool walls, so they swung cool too, at a
     whisper of chroma so text stays soft, not clinical-blue. */
  --ink:       oklch(0.91 0.010 255);
  --ink-dim:   oklch(0.62 0.010 255);
  /* the faint split (P2 audit P0): --ink-faint is TEXT — anything read as
     words — and must clear WCAG AA 4.5:1 on --bg AND --surface; --ink-deco is
     decorative strokes/glyphs only (dotted fill underline, fdel line-through,
     grip, disclosure chevron) and holds the 3:1 non-text floor. The whisper
     register lives in --ink-deco now; never put text on it.
     tests/contrast.test.ts pins the floors — retune freely above them. */
  --ink-faint: oklch(0.60 0.010 255);
  --ink-deco:  oklch(0.51 0.010 255);
  /* THE accent — renamed from --amber the day it turned blue (Jesse ruling:
     the token names a ROLE, not a color). The cool INTERACTION/wayfinding role.
     SINGLE KNOB (Jesse 2026-06-29): --accent is the ONLY value to edit — the soft
     FILL and the selection tint now DERIVE from it via relative-color (like
     --accent-wash/-sel-hot already do), so retinting the blue propagates to the
     tab/button fills + selection in one edit instead of nine hardcoded literals.
     NOT the only loud role anymore — the call-to-action is its own
     --primary-action (DECISIONS #27(1) retired "one accent only"). */
  --accent:     oklch(0.72 0.10 272);
  --accent-soft:oklch(from var(--accent) l c h / 0.18);
  --accent-sel: oklch(from var(--accent) l c h / 0.40);  /* selection reads stronger than the playback wash */
  /* THE primary-action role — the call-to-action fill (the mockup's amber "New").
     DECISIONS #27(1): Jesse retired "one accent only" in favour of a small CODIFIED
     SET of role tokens, each globally swappable. This is the CTA role, deliberately
     DISTINCT from the two neighbours it must never collapse into:
       · --accent (cool wayfinding/interaction; SWAPS teal under the library profile)
         — a call-to-action is not a wayfinding mark, so it does NOT ride the profile
         swap: profile-constant, like the status trio below;
       · --working amber (a STATUS, "processing") — borrowing it for an ACTION is the
         --accent overload sin (token names a role, Law 1). Its own token, own value.
     Amber by Jesse's mockup; the exact value is his live feel-tune. Ink on the fill
     is --bg-deep (a light fill) — contrast.test.ts pins the 4.5:1 floor. */
  --primary-action: oklch(0.78 0.145 70);
  --rust:      oklch(0.58 0.085 38);
  /* THE stricken role — rust is the color, stricken is the ROLE (the --accent
     lesson): ghosts, line-throughs, the section/chip ×-hover, and the CUT
     clock all speak it. New stricken-meaning rules use --stricken. */
  --stricken:  var(--rust);
  --rust-dim:  oklch(0.46 0.060 38);
  --moved:     oklch(0.72 0.085 145);
  /* THE working role — semantic STATUS amber (brief §3: green=ready · amber=
     working · grey=archived; "every mark means something"). DISTINCT from the
     interaction --accent: a processing/ingesting/transcribing cue is a status,
     not a wayfinding mark, so it gets its OWN role — and stays amber across
     profiles (status is profile-constant; the cool library swap re-values only
     the accent triad, NOT this). Before this token the working cues borrowed
     --accent and silently turned teal under the cool profile, conflating two
     roles. Non-text (progress fill / spinner head), so no contrast gate; the
     exact amber is Jesse's live feel-tune. Soft = the spinner track. */
  --working:      oklch(0.80 0.135 80);
  --working-soft: oklch(from var(--working) l c h / 0.20);
  /* THE comment-position role (COMMENT-WORD-ANCHOR-1) — a guest comment's marker on a
     recipient scrub bar. Its OWN role, not a borrow (the --accent lesson): the marker
     must read against BOTH the cool --accent playback fill AND the neutral --line track,
     on BOTH the dark room and the light review card — a warm pip is the only value that
     contrasts with all four, so annotation gets a warm role distinct from the cool
     interaction accent. Profile-constant (a note means the same on any surface) and
     non-text (a 6px pip), so no contrast gate; Jesse feel-tunes the exact amber. */
  --comment-mark: oklch(0.80 0.14 68);
  /* THE ready role — the green that closes the brief §3 status trio (green=ready ·
     amber=working · grey=archived). A POSITIVE "settled, openable" mark, its OWN
     role: distinct in hue from --working amber (a status must never read as still-
     working) AND from the cool library --accent teal (a status is not a wayfinding
     mark) AND from the editor's reorder --moved green (a different role on a
     different surface — never share a token across roles, the --accent lesson).
     Profile-constant like --working: status means the same everywhere, so the cool
     swap (interaction triad only) leaves it untouched. Non-text dot ⇒ no contrast
     gate; the exact green is Jesse's live feel-tune. */
  --ready:        oklch(0.72 0.115 150);
  /* THE archived role — the GREY that completes the brief §3 status trio (green=
     ready · amber=working · grey=archived). The trio's NEUTRAL member: a status
     that means "set aside, no active state" is deliberately DESATURATED (near-zero
     chroma) so it never competes with the saturated ready/working marks. Its OWN
     named role (not a borrowed --wash/--ink-dim) so "grey = archived" is a law, not
     an accident of which neutral it happened to reuse — the same lesson --working
     taught when it silently turned teal borrowing --accent. Profile-constant: a
     status means the same in every profile, so the cool library swap leaves it
     untouched. Non-text mark (a dim + a grey pill) ⇒ no contrast gate; the exact
     grey is Jesse's live feel-tune. Wash = the pill surface, derived so a retune
     carries it. */
  --archived:      oklch(0.60 0.010 250);
  --archived-wash: color-mix(in oklab, var(--archived) 14%, transparent);
  /* the set-aside RECEDE — how far an archived item dims without disappearing.
     A named micro-token (taxonomy gap #2), not a magic 0.6 buried in a rule. */
  --set-aside-opacity: 0.6;

  /* washes — hover/selection tints DERIVED from the inks above, so a retheme
     carries them for free. Never hand-mix an overlay color in a rule. */
  --wash:       color-mix(in oklab, var(--ink) 7%, transparent);
  --wash-soft:  color-mix(in oklab, var(--ink) 5%, transparent);
  --accent-wash: color-mix(in oklab, var(--accent) 7%, transparent);
  --working-wash: color-mix(in oklab, var(--working) 8%, transparent);
  --rust-wash:  color-mix(in oklab, var(--rust) 12%, transparent);
  /* T9 combination layer (states.md laws 2+3): hover-on-washed and sel∧cur read
     as the SAME wash one lightness step hotter. The step writes the wash ITSELF
     (channel A only — filter:brightness would lift ink and tint the mark,
     channel B/D crosstalk, so it's banned; tests/states.test.ts pins that).
     Hot pairs derive from their wash token + the shared delta, so a wash retune
     carries them for free. Step size (and sign) is a look-lane placeholder. */
  --hover-delta: 0.05;
  --accent-sel-hot:  oklch(from var(--accent-sel) calc(l + var(--hover-delta)) c h);
  --accent-soft-hot: oklch(from var(--accent-soft) calc(l + var(--hover-delta)) c h);
  /* T10's presence tint, tokenized (the sheet's own law: never hand-mix in a rule) */
  --presence-wash: color-mix(in oklab, var(--accent) 3.5%, transparent);
  --scrim:      oklch(0.1 0.008 255 / 0.45);
  /* the DEFAULT project color — every project HAS a color (Jesse 2026-07-09: no
     colorless state, no Clear-color verb); an UNSET color renders as this slate
     wherever a project color shows (swatch · rails · tile faces). ≈ the recolor
     picker's Slate (#64748b), so "default" and "picked Slate" read identically. */
  --project-slate: oklch(0.554 0.046 257);
  /* ink that reads OVER a scrimmed dark backdrop (the --scrim is dark in every theme,
     so its over-text is a constant near-white in every theme — NOT --ink, which flips
     with the surface). Used by the grid tile's blurred poster hero (PT-2); a named
     role so "text on media" is a token, not a hand-typed white in a rule. */
  --ink-on-media: oklch(0.97 0.015 85);

  /* AK-3 — the audio-only thumbnail's decorative waveform amber. Its OWN role token,
     NOT --working (status) or --primary-action (CTA): borrowing either would be the
     --accent-overload sin (a token names a role, Law 1). Non-text/decorative (the card
     name carries identity; the svg is aria-hidden) ⇒ no contrast gate. Profile-constant
     like the status trio; the exact amber is Jesse's live feel-tune lane. */
  --audio-wave: oklch(0.72 0.15 55);

  /* speaker palette — equal lightness/chroma, hue-spread (extend in order) */
  --spk1: oklch(0.74 0.080 80);
  --spk2: oklch(0.74 0.070 150);
  --spk3: oklch(0.74 0.075 280);
  --spk4: oklch(0.74 0.075 20);

  /* clip provenance — shared by the strip canvas (strip.js reads these) and
     the legend; the canvas and its key can't drift apart */
  --clip-ord: oklch(0.78 0.05 85);
  --clip-reo: oklch(0.72 0.12 145);
  --clip-dup: oklch(0.66 0.13 300);

  --sp1: 4px; --sp2: 8px; --sp3: 12px; --sp4: 16px;
  --sp5: 24px; --sp6: 32px; --sp7: 48px; --sp8: 64px;

  /* type ramp — every font-size in the app is one of these roles.
     (--geo-* sizes are NOT here: transcript geometry is published by
     js/layout.js, the single source of truth pretext measures with.)
     P3(b): the chrome ramp is THREE steps (micro/standard/body) — the old
     9/10/11/12/13 ladder was 1px noise pretending to be hierarchy. Roles
     keep their names (usage stays semantic; a future re-split is one line)
     but their values converge. tests/states.test.ts pins the step COUNT,
     never the values — sizes stay Jesse's lane. */
  --fs-label:   10px;  /* uppercase micro-labels: clock/panel/strip headings (tracking carries the smallness) */
  --fs-chip:    12px;  /* chips, tabs, kbd, small buttons — bumped 10→12 (Jesse 2026-07-09: button legibility too low everywhere; the canonical mockup's toolbar text is 12px) */
  --fs-note:    12px;  /* annotations: hints, speakers, counts, sublines */
  --fs-ui:      12px;  /* standard chrome text: filename, toasts, scrub values */
  --fs-control: 14px;  /* interactive controls: toggles, inputs, selection meta — interactive text earns body size */
  --fs-base:    14px;  /* body default */
  --fs-clock:   18px;  /* the big TRT readout */
  --fs-brand:   21px;  /* wordmark */

  --f-ui: "Hanken Grotesk", sans-serif;
  --f-body: "Literata", serif;
  --f-brand: "Spectral", "Literata", serif;  /* the wordmark face — Spectral SemiBold; falls back to the prior Literata wordmark */
  --f-tc: "Spline Sans Mono", monospace;
}

/* P9(c) — HIGH CONTRAST: a token-TIER override, never a redesign. The tier
   re-values ink-side tokens only; the grounds (--bg/--bg-deep/--surface) keep
   the darkroom, and every wash/hot/presence tint derives from these tokens
   (color-mix / oklch-from), so the whole state system rethemes from this one
   block — no rule below ever mentions the tier. Floors the gate pins
   (tests/contrast.test.ts): text inks AAA 7:1 on both grounds, --ink-deco and
   the stricken rust ≥ 4.5:1, --bg-deep ink ≥ 4.5:1 on the accent fill, and
   every name here must already exist in :root (a swap, never a fork). Exact
   values are Jesse's lane — retune freely above the floors. main.ts applies
   the tier as html[data-contrast="high"] from the proof:ui envelope. */
:root[data-contrast="high"] {
  --line:      oklch(0.42 0.018 70);
  --ink:       oklch(0.97 0.015 85);
  --ink-dim:   oklch(0.78 0.020 78);
  --ink-faint: oklch(0.76 0.020 76);
  --ink-deco:  oklch(0.68 0.018 75);
  --accent:     oklch(0.82 0.100 250);
  --accent-soft:oklch(from var(--accent) l c h / 0.26);  /* derives hue; HC keeps a stronger fill alpha */
  --accent-sel: oklch(from var(--accent) l c h / 0.42);
  --rust:      oklch(0.68 0.100 38);
  --rust-dim:  oklch(0.58 0.075 38);
  --moved:     oklch(0.78 0.085 145);
  --working:   oklch(0.85 0.120 80);
  --ready:     oklch(0.80 0.110 150);
  --archived:  oklch(0.74 0.014 250);
  --audio-wave: oklch(0.78 0.15 55);
  --hover-delta: 0.07;
  --scrim:      oklch(0.1 0.01 65 / 0.65);
  --spk1: oklch(0.80 0.080 80);
  --spk2: oklch(0.80 0.070 150);
  --spk3: oklch(0.80 0.075 280);
  --spk4: oklch(0.80 0.075 20);
  --clip-ord: oklch(0.84 0.05 85);
  --clip-reo: oklch(0.78 0.12 145);
  --clip-dup: oklch(0.74 0.13 300);
}

/* browser-shell §3 — the COOL PROFILE. A "theme = a role-override set" (Jesse,
   2026-06-18): ONE token system across both surfaces, the editor is the WARM
   profile (the default :root, blue --accent) and the library/app-shell runs a
   touch cooler — amber/teal is the thread tying the two. The interaction note
   on the library side is the RATIFIED teal (~#56a892, the cool complement to
   amber, in the editor's moved-green family); selection, the breadcrumb's lone
   climbable crumb, the openable-card focus ring, the grouped-lane divider all
   already speak --accent, so re-valuing the interaction TRIAD here turns the
   whole library cool with zero per-site change — and makes every "--accent
   (teal)" comment in the browse block (the breadcrumb, the home shell) TRUE.

   Keyed on the EXISTING surface marker (shell.ts writes body[data-route] on the
   single load path): "page" = the non-editor app-shell (browse/projects/signin),
   "editor" = the warm page-inside-the-book. One source of truth, no parallel
   writer to drift from the route. (A user-chosen profile axis decoupled from
   route is an additive attribute later — same swap-not-fork law, no rework.)

   SWAP, NEVER A FORK (theme-token-taxonomy law 6, as the HC tier above): only
   the interaction triad is re-valued; the derived washes/hot-pairs (--accent-
   wash/-sel-hot/-soft-hot, defined once in :root off var(--accent)) re-resolve
   to teal for free under the cascade. The GROUNDS and INKS are deliberately
   UNTOUCHED — cooling the chrome itself re-values grounds, which is a separate
   APPEARANCE AXIS (the light-theme door, DECISIONS #24 §9 / taxonomy gap #1),
   not this swap. The teal clears the SAME AA floor the base accent does
   (tests/contrast.test.ts pins it), so an HC user on the library surface still
   gets a legal selection fill. Exact shade is Jesse's live feel-tune lane. */
body[data-route="page"],
body[data-route="landing"] {
  /* SINGLE KNOB: only --accent is re-valued (teal); --accent-soft/-sel derive from it in
     :root, so they re-resolve to teal here under the cascade — no per-scope literals.
     The landing (ENTRY-DOOR-2) shares the profile so porch → door reads as one house. */
  --accent:     oklch(0.70 0.10 178);
}

/* §look-lane — the DESCEND-INTO-EDITOR motion. shell.ts sets body[data-nav=
   "descend"] for one browse→editor hop (ephemeral, like layout-snap); the editor
   stage rises+fades forward so the browse→editor relationship reads as "one app,
   two depths," not two pages. Transform/opacity only — compositor-cheap, no
   layout thrash, no new hex (a SPATIAL cue, never a colored one; the status/
   interaction hues are untouched). A keyframe (not a transition) so it plays
   the instant the editor surface mounts, with no from→to reflow to coordinate.
   Reuses the established 220ms cubic-bezier(0.2,0.8,0.2,1) family. */
@keyframes nav-descend-rise {
  from { opacity: 0; transform: translateY(10px) scale(0.994); }
  to   { opacity: 1; transform: none; }
}
body[data-route="editor"][data-nav="descend"] main {
  animation: nav-descend-rise 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* Reduced-motion SNAPS to today's hard cut — the safety floor, the existing
   :root[data-motion="reduced"] convention (style.css ~877). */
:root[data-motion="reduced"] body[data-route="editor"][data-nav="descend"] main {
  animation: none;
}

/* §look-lane — the ASCEND (editor→browse): the symmetric inverse of the descend.
   Leaving the canvas, the library OVERVIEW settles back into view from slightly
   above + larger — you rise back OUT to the wider depth, the mirror of the canvas
   rising forward on the way in. shell.ts sets body[data-nav="ascend"] for the one
   editor→browse hop; the WHOLE browse shell (rail + content together = the
   overview) plays the settle. Same transform/opacity-only, no-new-hex,
   spatial-not-colored discipline as the descend; reduced-motion SNAPS it. */
@keyframes nav-ascend-settle {
  from { opacity: 0; transform: translateY(-8px) scale(1.006); }
  to   { opacity: 1; transform: none; }
}
body[data-nav="ascend"] .browse-shell {
  animation: nav-ascend-settle 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
:root[data-motion="reduced"] body[data-nav="ascend"] .browse-shell {
  animation: none;
}

* { box-sizing: border-box; }

/* P2(b) — FOCUS, channel E (states.md law 8): ONE additive accent ring, offset
   clear of any wash, on every focusable in the app. Never substituted, never
   suppressed — a focused ghost still shows its ring (focus says where the
   keyboard is, not what the element is). :focus-visible only: pointer clicks
   never park focus (keyrouter blur hygiene), so the ring appears exactly when
   the keyboard is driving. No rule anywhere may write `outline: none`
   (tests/states.test.ts pins the ban). Width/offset are look-lane placeholders. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

html { height: 100%; }
body {
  margin: 0;
  /* dvh: track the REAL visual viewport (mobile URL bars, folds, rotation) */
  height: 100vh;
  height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: var(--fs-base);
  display: grid;
  grid-template-rows: auto 1fr auto;
  overflow: hidden;
}

button {
  font: inherit; color: inherit; background: none;
  border: 1px solid var(--line); border-radius: 4px;
  padding: 6px 10px; cursor: pointer;
}
button:hover { border-color: var(--ink-dim); }
button:active { background: var(--surface); }
/* ★ SELECTED-CONTROL LAW (RFC active-tab-selected-control / DECISIONS #49):
   a control that marks itself "active/selected" (segmented tabs, mode pickers,
   rate picks, dock tabs, view/handle toggles) reads as a selected AREA —
   `background: var(--accent-soft)` + `color: var(--accent)` — and NEVER as a
   line. Lines (underline/edge accents) are reserved for the TEMPORAL marks
   (playhead/scrubber/progress); an "active" border-line collides with them.
   Keep the control's RESTING border; the active state fills, it does not
   escalate the edge. New active/selected controls copy THIS, not a one-off. */
/* capability-hidden chrome stays hidden no matter what display a reveal rule
   sets — the author-display-beats-UA-[hidden] gotcha, solved once for all */
button[hidden] { display: none !important; }
/* …except the peek handle: its BOX stays reserved so the header never
   reflows when it appears/disappears (geometric inertness, Jesse) */
/* P3(b) — BUTTON VERB HIERARCHY, three registers (audit P1): QUIET
   (borderless inline glyphs — grip, ×, ⤒) for verbs living inside content;
   STANDARD (the bordered default above) for panel verbs; PRIMARY (accent
   fill) for THE verb a panel exists to deliver — at most one on screen, and
   today exactly one in the app: the render command, because the cut itself
   is what Proof exports. Hierarchy is treatment, never new behavior; hover/
   active ride the same --hover-delta as every other hot state (channel A's
   one knob). Ink is --bg-deep on the accent fill — contrast gate pins it. */
button.primary {
  background: var(--accent); border-color: transparent;
  color: var(--bg-deep); font-weight: 600;
}
button.primary:hover {
  background: oklch(from var(--accent) calc(l + var(--hover-delta)) c h);
  border-color: transparent;
}
button.primary:active {
  background: oklch(from var(--accent) calc(l - var(--hover-delta)) c h);
}
kbd {
  font-family: var(--f-tc); font-size: var(--fs-chip); color: var(--ink-faint);
  border: 1px solid var(--line); border-radius: 3px; padding: 0 4px; margin-left: 4px;
}
.tc { font-family: var(--f-tc); font-variant-numeric: tabular-nums; }
.dim { color: var(--ink-dim); }
.count {
  font-family: var(--f-tc); font-size: var(--fs-note); color: var(--accent);
  margin-left: var(--sp2);
}

/* ============================================================
   Proof — the shared ROW primitive (ROW-PRIMITIVE-PILOT · BROWSER-DESIGN-COHERENCE)
   The app has ~15 list/row surfaces (version history, org members, browse
   projects, account workspaces, api-keys, billing, …) and, before this sheet,
   ~15 bespoke naming schemes doing the SAME leading·title·meta·trailing job —
   each re-improvising its own kebab placement, hover rule and keyboard reach
   (the RFC audit, investigations/browser-row-primitive.md). This sheet names the
   pattern the surfaces independently rediscovered: it is a codification, NOT a
   redesign — nearly every text row is already `display:flex; align-items:baseline`
   with a `flex:1` title and a pushed-right trailing edge, so a surface adopts the
   slot classes with ZERO geometry change (RFC finding #2).

   CSS-class contract FIRST, TS builder DEFERRED (RFC decision 1): each surface
   opts into the slot classes incrementally, one file per shift, verified in
   isolation. Pilot = browse projects (`.project-item`) — see browse.css, where
   the orphaned ⋯ kebab finally lands in a real `.row-trail` slot.

   Canonical inset: a surface that pads the ROW itself uses `var(--sp2) var(--sp3)`
   (the value `.project-link`/`.recent-link` already carry). This sheet does NOT
   impose padding on `.row` — many surfaces (the pilot included) pad their inner
   interactive child and keep the border/rail on the wrapper, so a base padding
   here would DOUBLE the inset. Padding stays a per-surface concern; the token is
   the shared law, the placement is the surface's.
   ============================================================ */

/* the row itself — the convergent idiom, named once. baseline so a tall leading
   glyph and small meta text sit on the same type baseline; the gap is the
   inter-slot rhythm (only in-flow siblings pay it — an absolutely-placed
   `.row-trail`, the table-row case, contributes none). */
.row {
  display: flex;
  align-items: baseline;
  gap: var(--sp2);
}

/* optional LEADING slot — avatar / thumb / icon / status disc. Fixed size, never
   grows or shrinks. OMIT it entirely and the title starts at the row's edge (the
   pilot's list row has no leading glyph — its identity is the accent rail border). */
.row-lead { flex: 0 0 auto; }

/* the TITLE — the primary label, the one growing slot. min-width:0 is load-bearing:
   without it a long title refuses to shrink below its content width and SHOVES the
   trailing edge off-row; with it the title truncates and the trailing actions stay
   pinned. (The pilot's `.project-link` is itself a grid of table cells — a `.row`
   child can carry its own internal layout; `.row-title` only governs how it flexes.) */
.row-title { flex: 1; min-width: 0; }

/* the META slot — secondary text (counts, dates, roles). Recedes to the cool chrome
   register. The middot `::before` separator (lifted from the account/billing
   convention, RFC's cleanest of the six meta dialects) lets multi-part meta compose
   without bespoke spans: "Created May 3 · last used today" is two `.row-meta`
   spans, each self-separating. `--nums` opts into tabular figures for numeric reads
   (billing/quota) so columns of digits align. */
.row-meta {
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink-dim);
}
.row-meta::before {
  content: "·";
  margin-right: var(--sp2);
  color: var(--ink-faint);
}
.row-meta--nums { font-variant-numeric: tabular-nums; }

/* the TRAILING slot — the actions/kebab edge, pinned to the far side (margin-left:auto
   so it hugs the trailing edge regardless of how many meta slots precede it). This is
   the kebab's new HOME: before this primitive, the browse kebab was the ONLY one in the
   app, an absolutely-placed overlay belonging to no convention. A grid-table row (the
   pilot) overrides the in-flow pin with absolute placement so the trailing actions claim
   NO column track and the colhead stays aligned — that override lives with the surface,
   not here (analogous to how `.browse-card > .row-actions` was already absolute). */
.row-trail {
  flex: 0 0 auto;
  margin-left: auto;
}

/* the trailing-reveal — the single behavior the whole app was missing. On a pointer
   device the trailing actions rest hidden and surface on row hover / focus-within, so a
   dense list reads clean until you reach for a row (the comment-card spotlight is the
   adjacent precedent — hover already means "reveal affordances"). opacity, NOT display,
   so the slot keeps its width at rest: no reflow on hover, and a grid-table row's columns
   never twitch. GATED under @media (hover:hover) on purpose: a coarse / no-hover pointer
   can't hover-reveal, so on touch the trailing actions stay VISIBLE. Ungating this (a
   plain always-on rule) breaks the touch tier — the design-audit test bites that. */
@media (hover: hover) {
  .row-trail {
    opacity: 0;
    transition: opacity 0.12s ease;
  }
  .row:hover .row-trail,
  .row:focus-within .row-trail {
    opacity: 1;
  }
}

/* opt-in SELECTABLE behaviour — a row that acts as a listbox option. The role/tabindex
   and the Enter/Space→activate handler (lifted from history-row, the app's one correct
   keyboard row) are applied in JS by the DEFERRED `.row` TS builder (RFC decision 1); no
   surface opts in yet (the pilot's project rows carry wireMasterSelect's own roving-tabindex
   grammar, which this must not fight). This sheet ships the STYLING hooks the future opt-in
   reads — the pointer affordance + the current/selected mark — so a migrating surface gets
   the look for free the moment it adds the class. */
.row--selectable { cursor: pointer; }
.row--selectable[aria-current="true"],
.row--selectable.selected { background: var(--wash); }
/* ------------------------------------------------ header
   The header must have NO intrinsic min-width beyond a phone's width —
   content that can't shrink is what makes device emulation scale-zoom the
   whole page. Chrome wraps; labels hide progressively on narrow screens. */
.top {
  display: flex; align-items: baseline;
  flex-wrap: wrap;
  gap: 2px clamp(8px, 2vw, 32px);
  padding: var(--sp2) clamp(10px, 2vw, 24px);
  border-bottom: 1px solid var(--line);
  background: var(--bg-deep);
}
.top > * { min-width: 0; }
/* TOOLS-IA slice 1 — the header menubar button (Edit; File/Transcript later). A
   quiet desktop-menu label: dim ink at rest, wash + full ink on hover/open, a
   hairline focus ring. flex:none so it never competes with the crumbtrail's
   stretch; sits just right of the brand as the first menubar item. */
.menubtn {
  flex: none;
  font: inherit; font-size: var(--fs-ui);
  color: var(--ink-dim);
  background: none; border: 1px solid transparent; border-radius: 6px;
  padding: var(--sp1) var(--sp2); cursor: pointer;
}
.menubtn:hover, .menubtn[aria-expanded="true"] { color: var(--ink); background: var(--wash-soft); }
.menubtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* EDITOR A11Y item 1a — skip-to-transcript link. Off-screen until focused (the
   standard bypass-blocks recipe: it must stay reachable by Tab, so it is clipped
   off-screen, NOT display:none which would drop it from the tab order). On :focus
   it snaps to a visible chip pinned top-left over the header. Reuses the house
   focus-ring tokens; the accent fill makes it unmissable when a keyboard user
   tabs into the editor. */
.skiplink {
  position: absolute; left: -9999px; top: 0; z-index: 50;
  padding: var(--sp2) var(--sp3);
  background: var(--accent); color: var(--bg-deep);
  border-radius: 0 0 6px 0; font: inherit; text-decoration: none;
}
.skiplink:focus { left: 0; outline: 2px solid var(--accent); outline-offset: 2px; }
/* L3.b — the canvas tab-bar ("tabs = cuts"): the Cut SWITCHER, presented as
   Google-Sheets/Chrome-style tabs at the TOP of the transcript region (Jesse
   2026-06-29). Tabs sit on a hairline baseline; the active cut reads as a filled,
   top-rounded tab. Painted by mount from cardinality ≥1; [hidden] keeps it out of
   the flow on the home/single-canvas editor. Token-only. */
.canvastabs { display: flex; min-width: 0; }
.canvastabs[hidden] { display: none; }
/* the top band — a flex:none row above the scroller (sibling BEFORE .tscroll, so the
   paper's scroll origin is untouched). PRES-IN-TRANSCRIPT-1: the nav is now a two-column
   row — the cuts scroll-column (flexible middle) + the presence seats (right-pinned).
   The nav itself no longer scrolls; each column owns its OWN overflow (below). */
.transcript-header .canvastabs {
  flex: none;
  align-items: stretch; gap: var(--sp3);
  padding: var(--sp2) var(--sp3) 0;
  border-bottom: 1px solid var(--line);
}
/* "your cuts" — the flexible middle. Takes the free width, hangs its tabs on the
   bottom baseline, and scrolls HORIZONTALLY on its own when the cuts overrun (20 cuts
   scroll here without ever shoving the seats). min-width:0 lets it actually shrink. */
.canvastabs-scroll {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: flex-end; gap: 2px;
  overflow-x: auto; scrollbar-width: none;
}
.canvastabs-scroll::-webkit-scrollbar { display: none; }
.version-control {
  position: relative;
  flex: none;
  align-self: center;
  margin-bottom: var(--sp1);
}
.crumbtrail .version-control {
  align-self: center;
  margin: 0;
}
.version-control[hidden] { display: none; }
.version-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--sp2);
  min-height: 30px;
  padding: 0 var(--sp2);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font: 650 var(--fs-ui)/1 var(--f-ui);
  cursor: pointer;
}
.version-trigger:hover,
.version-trigger[aria-expanded="true"] {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--line));
  background: var(--accent-soft);
}
.version-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.version-popover {
  position: absolute;
  z-index: 40;
  inset: calc(100% + var(--sp2)) auto auto 0;
  width: min(25rem, 78vw);
  max-height: min(32rem, 70vh);
  overflow-y: auto;
  padding: var(--sp2);
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: var(--lift-1);
}
.version-popover[hidden] { display: none; }
.version-popover-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp3);
  padding: var(--sp2) var(--sp2) var(--sp3);
  color: var(--ink);
  font-family: var(--f-ui);
}
.version-popover-head > span { color: var(--ink-dim); font-size: var(--fs-chip); }
.version-error {
  margin: 0 var(--sp2) var(--sp2);
  padding: var(--sp2);
  border: 1px solid color-mix(in oklab, var(--strike) 48%, var(--line));
  border-radius: 7px;
  background: color-mix(in oklab, var(--strike) 10%, transparent);
  color: var(--strike);
  font: var(--fs-chip)/1.35 var(--f-ui);
}
.version-row-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  border-radius: 7px;
}
.version-row-shell:hover { background: var(--wash-soft); }
.version-row-shell.active { background: var(--accent-soft); }
.version-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp3);
  width: 100%;
  min-height: 52px;
  padding: var(--sp2);
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.version-row:hover { background: transparent; }
.version-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.version-row:disabled { opacity: var(--set-aside-opacity); cursor: wait; }
.version-ordinal {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 1.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-dim);
  font: 650 var(--fs-chip)/1 var(--f-ui);
  font-variant-numeric: tabular-nums;
}
.version-row-shell.active .version-ordinal {
  border-color: color-mix(in oklab, var(--accent) 50%, var(--line));
  color: var(--accent);
}
.version-copy { display: grid; gap: 2px; min-width: 0; }
.version-copy > strong {
  overflow: hidden;
  color: var(--ink);
  font: 600 var(--fs-ui)/1.25 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.version-copy > span {
  color: var(--ink-dim);
  font: var(--fs-chip)/1.25 var(--f-ui);
}
.version-actions {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-inline-end: var(--sp1);
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--ink-dim);
  font: 700 var(--fs-chip)/1 var(--f-ui);
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease-out, color 120ms ease-out, background 120ms ease-out;
}
.version-row-shell:hover .version-actions,
.version-actions:focus-visible,
.version-actions[aria-expanded="true"] { opacity: 1; }
.version-actions:hover {
  background: var(--surface-raised);
  color: var(--ink);
}
.version-actions:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.version-actions:disabled { cursor: wait; opacity: var(--set-aside-opacity); }
.canvastabs .tab {
  font: inherit; color: var(--ink-dim);
  background: none; border: 1px solid transparent; border-bottom: none;
  border-radius: 7px 7px 0 0;
  padding: var(--sp1) var(--sp3); cursor: pointer;
  max-width: 20ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.canvastabs .tab:hover { color: var(--ink); background: var(--wash-soft); }
/* the active cut — a filled, named tab (Sheets/Chrome): --accent text on the soft
   --accent fill, a hairline top+sides, bottom open onto the page so it reads as the
   foreground tab connected to the content below. */
.canvastabs .tab.active {
  color: var(--accent); font-weight: 600;
  background: var(--accent-soft);
  border-color: var(--line);
}
.canvastabs .tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* "+ New cut" (L3.c-1): a quiet create affordance after the tabs (Chrome's "+"). */
.canvastabs .newcut {
  font: inherit; color: var(--ink-dim);
  background: none; border: none; border-radius: 6px;
  padding: var(--sp1) var(--sp2); margin-bottom: 2px; cursor: pointer;
  align-self: center;
}
.canvastabs .newcut:hover { color: var(--ink); background: var(--wash-soft); }
.canvastabs .newcut:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.canvastabs .newcut:disabled { opacity: 0.5; cursor: default; }
/* L3.c-2: the inline rename input that replaces the active tab's label on dblclick.
   Sized to the tab so the bar doesn't reflow mid-edit; token-only, .tab precedent. */
.canvastabs .tab .tab-rename {
  font: inherit; color: var(--ink); background: none;
  border: none; padding: 0; margin: 0;
  width: 14ch; max-width: 14ch;
}
.canvastabs .tab .tab-rename:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* L3.d-2: a tab and its delete × ride in a slot so the × groups with its own tab
   (the bar's gap separates slots; this tighter gap separates a tab from its ×). */
.canvastabs .tabslot { display: inline-flex; align-items: center; gap: 2px; }
/* PRES-IN-TABS-1 (b): the per-cut seat slot — the peers editing THIS cut, seated INSIDE
   its tab (◆ Jesse: "expand how big a tab is and plop their icons inside"). Empty by
   default (zero width, no gap eaten); the beat fills it and the tab visibly widens. The
   discs here shrink a touch from the fallback cluster's 20px so a crowded cut stays a
   compact stripe on the tab. Sits between the tab and its × in the slot. */
.canvastabs .tab-seats { display: inline-flex; align-items: center; gap: 2px; }
.canvastabs .tab-seats:not(:empty) { margin-left: 3px; }
.canvastabs .tab-seats .seat { width: 17px; height: 17px; font-size: 8px; }
.canvastabs .tab-seats .seat.overflow { font-size: 7px; }
/* The per-tab delete ×, painted only at >1 (never the sole cut). Arms on first click
   (× → ✓?), commits on the second; token-only, the .tab/.newcut precedent. */
.canvastabs .tab-close {
  font: inherit; color: var(--ink-dim);
  background: none; border: none; padding: 0 2px; cursor: pointer; line-height: 1;
}
.canvastabs .tab-close:hover { color: var(--ink); }
.canvastabs .tab-close.confirming { color: var(--accent); }
.canvastabs .tab-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 1100px) { .brand .tag { display: none; } }
@media (max-width: 900px)  { .clock.dim { display: none; } }
@media (max-width: 700px)  { .filename { display: none; } }
@media (max-width: 700px) {
  .transcript-header .canvastabs { gap: var(--sp2); padding-inline: var(--sp2); }
  .version-popover {
    position: fixed;
    inset: auto var(--sp2) var(--sp2);
    width: auto;
    max-height: min(72vh, 34rem);
    border-radius: 14px;
  }
  .version-row { min-height: 52px; }
  .version-actions {
    width: 44px;
    height: 44px;
    opacity: 1;
  }
}
/* phone tier (MOBILE-3a) — the last step of the progressive-hide ladder above.
   At phone width the header's fixed-size controls (mode toggle · Share · avatar)
   plus the breadcrumb plus the save-state no longer share one 380px row: the
   shrinkable .crumbtrail (flex:1) collapses below its content and its nowrap legs
   overflow, so the .syncstatus label paints OVER .crumb-proj ("Sav[T]ed"). The
   two lowest-priority LABELS give way — the routine save-state reassurance (the
   actionable .warn state STAYS, it borrows the strike rust and must stay seen)
   and the project crumb (the ‹ back button, primary nav, stays). No forked
   layout, no JS — the same display:none progressive-hide as .filename above. */
@media (max-width: 430px) {
  .syncstatus:not(.warn) { display: none; }
  .crumb-proj, .crumb-slash { display: none; }
}
.brand {
  font-family: var(--f-brand); font-weight: 600; font-size: var(--fs-brand);
  letter-spacing: 0.01em;
  /* the wordmark is an <a href="/browse"> (logo = home); neutralize the default
     anchor chrome so it reads as the brand, not a link, but still invites a click */
  color: inherit; text-decoration: none; cursor: pointer;
}
/* the editor wordmark is an <a class="brand"> injected into #route-page, so the broad
   `.route-page a { color:--accent }` (specificity 0,1,1) out-paints `.brand`'s
   `color:inherit` (0,1,0) and LEAKS interaction-teal onto the brand. Reclaim it to the
   cream --ink with a descendant selector that out-specifies that rule — the exact
   "brand is never accent" temperature split the browser .app-header-brand already
   carries (§3). Route/profile-invariant: .brand only ever lives inside #route-page. */
.route-page .brand, .route-page .brand .brand-mark { color: var(--ink); }
.brand .tag {
  font-family: var(--f-ui); font-weight: 400; font-size: var(--fs-note);
  color: var(--ink-dim); margin-left: var(--sp3);
  text-transform: uppercase; letter-spacing: 0.14em;
}
/* EDITOR-CRUMB — the header's enclosing-context trail: ‹ Project / filename.
   Frame-style STRATEGIC truncation: both text crumbs ellipsize, but the PROJECT
   gives way 8× faster (flex-shrink 8 vs 1) so the leaf stays readable down to
   narrow widths — the middle collapses first, exactly Frame's `3BPS2 / … / file`
   behavior with one middle segment. The nav owns .filename's old flex:1 stretch;
   on the local-file pair the back/project/slash legs are [hidden] and this is
   byte-identical to the old filename-only header. */
.crumbtrail {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--sp2);
  font-size: var(--fs-ui);
}
.crumb-back {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 22px; block-size: 22px;
  border-radius: 5px;
  color: var(--ink-dim); text-decoration: none;
  font-size: var(--fs-clock); line-height: 1;
  cursor: pointer;
}
.crumb-back:hover { color: var(--accent); background: var(--wash); }
.crumb-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.crumb-proj {
  flex: 0 8 auto; min-width: 4ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink-dim); text-decoration: none;
}
.crumb-proj:hover { color: var(--accent); text-decoration: underline; }
.crumb-proj:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.crumb-slash { flex: none; color: var(--ink-faint); user-select: none; }
.filename {
  flex: 0 1 auto; min-width: 0; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* save-state indicator (main.ts syncStatusLabel) — flex:none so it never
   competes with .filename's ellipsis for the row's stretch space; .warn
   (offline/error) borrows the same rust the strike-through uses (--stricken)
   so a stalled sync reads as the SAME "something needs you" color, not a new one. */
.syncstatus {
  flex: none; font-size: var(--fs-ui); white-space: nowrap;
}
.syncstatus.warn { color: var(--stricken); }
/* the CUTS/TRT readout follows your attention (Jesse, CHAPTERS note 4):
   WATCHING ⇒ it rides the picture (one quiet line above the video, ≈20px);
   EDITING ⇒ workspace.ts reparents it to band the transcript's top. The single
   node moves; these base rules ride it to either home. */
.clocks { display: flex; gap: var(--sp4); align-items: baseline; padding: 0 2px var(--sp2); }
.clocks .big { font-size: inherit; }  /* TRT sits level with cut/source (Jesse) — accent alone carries the emphasis */
.clocks .stricken { color: var(--stricken); }
/* the readout BAND above the video (edit mode) — over the picture it measures,
   not over the transcript (Jesse 2026-06-29: the Cut switcher took the transcript's
   top band). A direct flex:none child of .monitor, so it's the row above the <video>;
   in watch mode the readout lives in .transport instead, so this never matches there. */
.monitor > .clocks {
  flex: none;
  padding: 0 0 var(--sp2);
}
/* the transport-control row (watch mode) — the readout SHARES the strip with
   play/mute/rate (Jesse, note-4 follow-up). margin-auto anchors it past the
   rates at the row's right edge; padding zeroed so it sits balanced on the
   centered row rather than carrying the above-video band's bottom gap. */
main[data-mode="watch"] .transport > .clocks {
  margin-left: auto;
  padding: 0;
}

/* MOCKUP: account avatar — identity chrome, far-right anchor of the header */
.avatar {
  margin-left: auto; align-self: center;
  width: 26px; height: 26px; border-radius: 50%;
  padding: 0; border: 1px solid var(--line);
  /* flat fill (P5 design-audit) — the gradient was the only one in the app;
     identity chrome reads as a quiet tinted disc, not a rendered sphere */
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
  color: var(--accent);
  font-family: var(--f-ui); font-size: var(--fs-chip); font-weight: 600;
  letter-spacing: 0.04em;
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; cursor: pointer;
}
.avatar:hover { border-color: var(--accent); }
.avatar .identity-chip,
.avatar .identity-disc { width: 100%; height: 100%; }
.avatar .identity-chip { display: block; }
.avatar .identity-disc { font-size: var(--fs-chip); }

/* MP-1c: the live-presence avatar cluster — the seats of everyone editing this cut
   right now, painted from the MP-1b beat (presenceSeats.ts). PRES-IN-TRANSCRIPT-1
   (◆ Jesse) relocated it from the header into the Cut-tabs strip's right column: it
   shrinks to its content, pins to the right (the scroll column takes the free width),
   and wears a subtle divider fencing "who's here" from "your cuts". Hidden entirely
   when you're alone (the element ships `hidden`), and the divider hides with it. */
.presence-seats { display: flex; align-items: center; gap: 3px; flex: none; }
/* the seats' home inside the strip — right-pinned, vertically centred on the band, a
   hairline divider on its leading edge (present only when the seats are), and a
   max-width so a crowded roster stays a compact cluster (presenceSeats caps at +N). */
.canvastabs > .presence-seats {
  align-self: center; margin-bottom: 2px;
  padding-left: var(--sp3);
  border-left: 1px solid var(--line);
  max-width: 40%;
}
/* phone (◆ Jesse): the discs give way to a single "N here" pill rendered off the
   count presenceSeats stamps on data-here — one glanceable fact, no disc row to crowd
   a narrow strip. The pill IS the divider's neighbor, so the fence still reads. */
@media (max-width: 640px) {
  .canvastabs > .presence-seats .seat { display: none; }
  .canvastabs > .presence-seats::after {
    content: attr(data-here) " here";
    font-family: var(--f-ui); font-size: var(--fs-chip); font-weight: 600;
    color: var(--ink-dim); white-space: nowrap;
    padding: 1px var(--sp2); border-radius: 999px;
    background: color-mix(in oklab, var(--accent) 8%, var(--surface));
  }
}
/* one seat — an initials face over the person's stable hue (commentHue, keyed off the
   immutable membership id), same disc recipe as the dock's .cmt-avatar shrunk to the
   header scale. Constant over-media ink for contrast on any hue. */
.seat {
  flex: 0 0 auto;
  width: 20px; height: 20px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media);
  background: oklch(0.56 0.072 var(--seat-hue, 250));
  user-select: none;
}
/* your own seat wears an accent ring so "which one is me" is answered at a glance */
.seat.is-you { box-shadow: 0 0 0 1.5px var(--surface), 0 0 0 3px var(--accent); }
/* TRAVEL-1: a peer on a word is a button — click to jump to them. The pointer +
   hover lift is the affordance; hover brightens by lifting L on the seat's own hue
   (channel-A law: never filter:brightness). The focus ring keeps keyboard travel visible. */
.seat.has-loc { cursor: pointer; transition: transform 0.1s ease, background 0.1s ease; }
.seat.has-loc:hover { transform: translateY(-1px); background: oklch(0.63 0.08 var(--seat-hue, 250)); }
.seat.has-loc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* the +N overflow counter — neutral, not a person (no hue) */
.seat.overflow {
  background: color-mix(in oklab, var(--accent) 8%, var(--surface));
  color: var(--ink-faint); font-size: 8px; letter-spacing: 0;
}
.clock label {
  text-transform: uppercase; letter-spacing: 0.14em; font-size: var(--fs-label);
  color: var(--ink-faint); margin-right: var(--sp2);
}
.clock .big { color: var(--accent); }
/* ------------------------------------------------ main split
   Layout is SOLVED by js/workspace.js (template + sizes from viewport and
   video aspect ratio); these are the finite templates it chooses between. */
main {
  display: grid;
  min-height: 0;
  position: relative;   /* anchors the P8 seam dividers */
  --vidW: 390px; --vidH: 280px; --toolsW: 360px;
}
main[data-layout="side"] {
  grid-template-columns: minmax(0, 1fr) var(--vidW);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas: "transcript monitor" "transcript rail";
}
main[data-layout="top"] {
  grid-template-columns: minmax(0, 1fr) var(--toolsW);
  grid-template-rows: var(--vidH) minmax(0, 1fr);
  grid-template-areas: "monitor rail" "transcript rail";
}
main[data-layout="tall"] {
  /* monitor hugs the transcript — the eye line is read ↔ glance; tools are reference */
  grid-template-columns: minmax(0, 1fr) var(--vidW) var(--toolsW);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "transcript monitor rail";
}
/* DEMOTED: tools summoned as a sheet — single column, monitor banners the text */
main[data-layout="top"][data-tools="sheet"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: var(--vidH) minmax(0, 1fr);
  grid-template-areas: "monitor" "transcript";
}
/* WATCH mode: video-primary, the dock stays DOCKED (notes tab by default —
   tools are one tab away); only the transcript is summoned */
main[data-layout="watch-side"] {
  grid-template-columns: minmax(0, 1fr) var(--comW);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "monitor rail";
}
main[data-layout="watch-stack"] {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: var(--vidH) minmax(0, 1fr);
  grid-template-areas: "monitor" "rail";
}
.transcript { grid-area: transcript; }
.monitor { grid-area: monitor; }
.rail { grid-area: rail; }

/* P8 dividers — drag a seam to PIN its size over the solver (resize.md §6).
   Invisible at rest, a hairline in --line on hover/drag — the seam is the
   affordance, no grip furniture. 8px hit area straddling the seam; each
   divider positions on the SAME var its grid template reads, so it tracks
   the seam with zero JS. Present only when its template is active and the
   rail is docked — a demoted layout reads no pins (resize.md decision #6). */
.divider { display: none; position: absolute; z-index: 9; touch-action: none; user-select: none; }
.divider[data-axis="x"] { top: 0; bottom: 0; width: 8px; cursor: col-resize; }
.divider[data-axis="y"] { left: 0; right: 0; height: 8px; cursor: row-resize; }
.divider::after { content: ""; position: absolute; background: transparent; transition: background 120ms ease-out; }
.divider[data-axis="x"]::after { top: 0; bottom: 0; left: 50%; width: 1px; }
.divider[data-axis="y"]::after { left: 0; right: 0; top: 50%; height: 1px; }
.divider:hover::after, .divider.dragging::after { background: var(--line); transition: none; }
body[data-tier-rail="docked"] main[data-layout="side"] .divider[data-pin="side.vidW"]   { display: block; right: calc(var(--vidW) - 4px); }
body[data-tier-rail="docked"] main[data-layout="top"]  .divider[data-pin="top.vidH"]    { display: block; top: calc(var(--vidH) - 4px); right: var(--toolsW); }
body[data-tier-rail="docked"] main[data-layout="top"]  .divider[data-pin="top.toolsW"]  { display: block; right: calc(var(--toolsW) - 4px); }
body[data-tier-rail="docked"] main[data-layout="tall"] .divider[data-pin="tall.vidW"]   { display: block; right: calc(var(--toolsW) + var(--vidW) - 4px); }
body[data-tier-rail="docked"] main[data-layout="tall"] .divider[data-pin="tall.toolsW"] { display: block; right: calc(var(--toolsW) - 4px); }
body[data-tier-rail="docked"] main[data-layout="watch-side"] .divider[data-pin="watch.comW"] { display: block; right: calc(var(--comW) - 4px); }

/* no display:none for whole regions — a non-docked region is SUMMONED
   (lives in the shelf with a visible handle), never gone */
main[data-mode="watch"] .monitor video { flex: 1; min-height: 0; object-fit: contain; }

/* ------------------------------------------------ transcript
   The REGION is a column: .tscroll scrolls the paper; docked rows (the verb
   bar) and edge chrome (the follow pill) belong to the region, not the page. */
.transcript {
  position: relative;
  display: flex; flex-direction: column;
  overflow: hidden; min-height: 0;
}
/* TRANSCRIPT-ZONE-1 (Jesse) — the region is three stacked bands. The HEADER (cut
   tabs) and FOOTER (verb bar) are flex:none rails; the BODY is the flex:1 middle and
   the SOLE position:relative context the edge lanes anchor to. Because the lanes live
   in .transcript-body, their `top:0; bottom:0` spans only this band — they can never
   paint up over the header's tabs (the swallow bug) by construction. The history
   drawer stays a direct .transcript child (its own overlay, above all three bands). */
.transcript-header { flex: none; }
.transcript-body {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  overflow: hidden;
}
.transcript-footer { flex: none; }
.tscroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: auto;
  scrollbar-color: var(--line) transparent;
  /* A touch of symmetric side breathing so the text never sits flush to the pane edge
     (Jesse 2026-06-29) — ~20px, mirroring the comments' gap off the scrollbar. The
     open-lane rule below overrides the RIGHT to the reserve strip; the left touch stays. */
  padding-inline: 1.25rem;
}
/* CM-1 a1/a2 — the comment MARGIN lane. When open (view.ts toggles data-margin-open
   only when the pane carries enough slack — marginFits), reserve a real MARGIN_W strip
   on the RIGHT: the `.paper` (margin: 0 auto) re-centers in the reduced box, shifting
   left so the comment gutter (anchored at the paper's right edge) lands IN the slack.
   RESERVE, never overlay (RFC synthesis A). Closed ⟹ no padding ⟹ the paper reclaims
   the full measure (the a2 reclaim). `--margin-w` is published by render() from the JS
   MARGIN_W (one source of truth). Width-only; no motion (Q4 instant). */
.transcript[data-margin-open] .tscroll { padding-right: var(--margin-w, 320px); }
/* CHAPTERS INDEX lane (RFC slice 1) — the LEFT mirror of the comment margin. Docked to
   the .transcript region's LEFT edge, width --chap-lane-w (published by render() from the
   JS CHAP_LANE_W seat). Shown only when data-chap-open; the .tscroll gains left padding so
   the centered paper shifts RIGHT into the reserved slack (RESERVE, never overlay). */
.chapters-index { display: none; }
.transcript[data-chap-open] .chapters-index {
  display: flex; flex-direction: column; gap: var(--sp2);
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 2;
  width: var(--chap-lane-w, 260px);
  padding: 56px var(--sp4) var(--sp4) 1.25rem;
  box-sizing: border-box;
  overflow-y: auto;
  background: color-mix(in oklab, var(--bg) 50%, var(--bg-deep));
  scrollbar-color: var(--line) transparent;
}
.transcript[data-chap-open] .tscroll { padding-left: calc(1.25rem + var(--chap-lane-w, 260px)); }

.chapters-index-list { display: flex; flex-direction: column; }
.chapters-index-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp3);
  padding: var(--sp2) var(--sp1);
  border-bottom: 1px solid color-mix(in oklab, var(--line) 45%, transparent);
  border-radius: 4px; cursor: pointer;   /* the outline is live navigation — a row click seeks */
}
.chapters-index-actions {
  display: flex; flex-direction: column;
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--sp3);
  margin-bottom: var(--sp2);
}
.chapters-index-row--new,
.chapters-index-row--fold {
  align-items: center;
}
.chapters-index-row--new .chapters-index-tick,
.chapters-index-row--fold .chapters-index-tick {
  display: grid; place-items: center;
  width: 24px; height: 24px; flex: 0 0 24px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--accent) 15%, transparent);
  color: var(--accent);
  font-size: 16px;
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
}
.chapters-index-row:hover { background: color-mix(in oklab, var(--ink) 6%, transparent); }
/* current-chapter WASH (RFC slice 1, #49) — the index twin of .block.cur-section:
   the row whose chapter holds the playhead wears the SAME quiet presence wash (zero
   new color, AT-1 discipline), never a line. Hover composites over it in the cascade. */
.chapters-index-row.cur { background: var(--presence-wash); }
/* Slice 3 — drag-reorder the outline. The dragged row dims (it's travelling); the
   whole lane wears a grabbing cursor for the gesture. The insertion bar is an
   --accent hairline drawn as an inset shadow (zero layout shift, so the list never
   reflows mid-drag): `.drop-above` lights the TOP edge of the target row (insert
   before it), `.drop-tail` the BOTTOM edge of the list (append past the last). */
.transcript.chap-dnd .chapters-index-row { cursor: grabbing; }
.chapters-index-row.dragging { opacity: 0.3; }   /* DRAG-GHOST-1: match the block drag ghost (one drag-dim rule) */
.chapters-index-row.drop-above { box-shadow: inset 0 2px 0 0 var(--accent); }
.chapters-index-list.drop-tail { box-shadow: inset 0 -2px 0 0 var(--accent); }
.chapters-index-title {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* CIX-A — the synthetic leading row (the orphan lead-in before the first real
   chapter): a quiet placeholder, never mistaken for a named chapter. Not draggable
   (see the JS row markup) — dropping ONTO it, not dragging it, is how it re-sorts. */
.chapters-index-title--placeholder { color: var(--ink-faint); font-style: italic; }
/* Slice 2b — the inline rename editor: an input that stands in for the title span
   (dblclick a row). Matches the title's UI type + fills the row's flex measure so the
   editor sits exactly where the name read. */
.chapters-index-input {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 3px; padding: 0 4px;
  min-width: 0; flex: 1;
}
.chapters-index-dur {
  font-family: var(--f-tc); font-variant-numeric: tabular-nums;
  font-size: var(--fs-note); color: var(--ink-dim); flex: none;
}
/* CH4 twin — the row's REMOVE control (mirror of .chead-del): persistent + de-emphasized
   (faint at rest, warming to --rust on hover/focus — destructive, no fill at rest, never
   hover-only per the F-D hover trap so it stays keyboard-tabbable + touch-reachable). It
   sits AFTER the duration; align-self:center against the row's baseline flex. */
.chapters-index-del {
  flex: none; align-self: center;
  border: none; background: none; padding: 0 2px;
  line-height: 1; color: var(--ink-faint); cursor: pointer;
  transition: color 120ms ease-out;
}
.chapters-index-del:hover, .chapters-index-del:focus-visible { color: var(--rust); }
.chapters-index-del:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.chapters-index-empty {
  font-family: var(--f-ui); font-size: var(--fs-note); line-height: 1.5;
  color: var(--ink-dim); margin: 0;
}
.chapters-index-empty b { color: var(--ink); font-weight: 600; }

/* INDEX-IA-1 slice 2 — the MINIMIZED tick rail (the left mirror of the comment avatar lane).
   When render() publishes data-chap-minimized (the user's arrow press OR a pane too narrow for
   the full list), the lane reserves only CHAP_RAIL_W and the rows collapse to a column of
   numbered ticks: the title/duration/remove controls fall away, each row becomes a centred tick
   disc that still SEEKS (the same data-chap-row, so the region-delegated handler is untouched).
   The transcript re-centers into the reclaimed slack, exactly as the comment lane's avatar form. */
.chapters-index-tick { display: none; }
.transcript[data-chap-minimized] .chapters-index {
  align-items: center;
  padding-left: var(--sp2); padding-right: var(--sp2);
}
.transcript[data-chap-minimized] .chapters-index-empty,
.transcript[data-chap-minimized] .chapters-index-title,
.transcript[data-chap-minimized] .chapters-index-dur,
.transcript[data-chap-minimized] .chapters-index-del { display: none; }
.transcript[data-chap-minimized] .chapters-index-row {
  justify-content: center; padding: var(--sp1) 0; border-bottom: none;
}
.transcript[data-chap-minimized] .chapters-index-tick {
  display: grid; place-items: center;
  width: 24px; height: 24px; border-radius: 50%;
  font-family: var(--f-tc); font-variant-numeric: tabular-nums;
  font-size: var(--fs-note); color: var(--ink-dim);
  background: var(--surface); border: 1px solid var(--line);
}
.transcript[data-chap-minimized] .chapters-index-row--new .chapters-index-tick,
.transcript[data-chap-minimized] .chapters-index-row--fold .chapters-index-tick {
  border-radius: 6px;
  font-size: 16px;
  background: color-mix(in oklab, var(--accent) 15%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
}
/* the current-chapter tick wears the presence wash (the row .cur already carries it; lift it
   onto the disc so the wash reads in the tiny rail too) */
.transcript[data-chap-minimized] .chapters-index-row.cur .chapters-index-tick {
  background: var(--presence-wash); color: var(--ink);
}

/* INDEX-IA-1 slice 2 — the chapters-lane minimize/expand toggle, the LEFT mirror of
   the comment lane's affordance. Anchors to the top center of the .chapters-index lane. */
.chap-lane-toggle { display: none; }
.transcript[data-chap-open] .chap-lane-toggle {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  position: absolute; top: var(--sp4); z-index: 7;
  /* Centered within the expanded lane (260px) or minimized rail (48px) */
  left: calc(var(--chap-lane-w, 260px) / 2); transform: translateX(-50%);
  width: auto; min-width: 22px; height: 22px; padding: 0 8px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-dim);
  cursor: pointer; transition: color 140ms ease, border-color 140ms ease;
}
.chap-lane-toggle:hover { color: var(--ink); border-color: var(--ink-deco); }
/* the arrow glyph */
.chap-lane-arrow::before { content: "\2039"; font-size: 13px; line-height: 1; }
.transcript[data-chap-minimized] .chap-lane-arrow::before { content: "\203A"; }

/* LANE-ICON-1 — the lane IDENTITY on the docked-top toggle: the lane glyph +
   a live count badge. Now always visible as a unified pill. */
.lane-ident { display: inline-flex; align-items: center; gap: 4px; }
.lane-ident .icn { width: 14px; height: 14px; }
.lane-label { font-family: var(--f-ui); font-size: var(--fs-chip); font-weight: 500; }
.lane-count {
  font-family: var(--f-ui); font-size: var(--fs-chip); line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--ink-dim);
}
.lane-count:empty { display: none; }   /* no count (0 or unpainted) → glyph alone, no empty pill */

/* When minimized, hide the label */
.transcript[data-chap-minimized] .chap-lane-toggle--top .lane-label,
.transcript[data-margin-minimized] .comment-lane-toggle--top .lane-label {
  display: none;
}
/* When expanded, hide the count */
.transcript:not([data-chap-minimized]) .chap-lane-toggle--top .lane-count,
.transcript:not([data-margin-minimized]) .comment-lane-toggle--top .lane-count {
  display: none;
}

/* ── LANES-COLLAPSED-VISUAL (LANES-FINAL-STATE slice 2) — the collapsed-tier edge icon ──────
   When a lane is STARVED to zero width (view.ts stamps data-tier-*='collapsed' — wanted open
   but no room even for the collapsed rail), its docked-top opener has no lane to sit atop, so
   the base `.chap-lane-toggle { display:none }` would erase the ONLY way back. This RELOCATES
   that same opener to a floating edge icon pinned to the region edge (chapters LEFT, comments
   RIGHT), the terminal honest rung below the collapsed-rail: the lane's glyph + live count
   badge (the LANE-ICON-1 identity), one coherent affordance with the minimized rail's pill.
   Clicking it re-summons the lane (main.ts: a collapsed toggle always drives EXPAND, so a true
   no-room squeeze answers with the honest toast, never a dead button). The reveal is hover-
   gated (transcript-flow.css @media hover:hover) so a touch tap just summons, never traps. */
.transcript[data-tier-chap='collapsed'] .chap-lane-toggle--top,
.transcript[data-tier-comment='collapsed'] .comment-lane-toggle--top {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  position: absolute; top: var(--sp4); z-index: 7; transform: none;
  min-width: 22px; height: 22px; padding: 0 8px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-dim);
  cursor: pointer; transition: color 140ms ease, border-color 140ms ease;
}
.transcript[data-tier-chap='collapsed'] .chap-lane-toggle--top { left: var(--sp2); }
.transcript[data-tier-comment='collapsed'] .comment-lane-toggle--top { right: var(--sp2); }
/* the edge icon is glyph + count (mirror of the minimized rail) — no "Minimize" label, no
   collapse arrow (there is no lane to collapse; it is already at zero). */
.transcript[data-tier-chap='collapsed'] .chap-lane-toggle--top .lane-label,
.transcript[data-tier-comment='collapsed'] .comment-lane-toggle--top .lane-label,
.transcript[data-tier-chap='collapsed'] .chap-lane-arrow,
.transcript[data-tier-comment='collapsed'] .comment-lane-arrow { display: none; }
.transcript[data-tier-chap='collapsed'] .chap-lane-toggle--top .lane-count,
.transcript[data-tier-comment='collapsed'] .comment-lane-toggle--top .lane-count { display: inline; }

/* FEEL-0710-3(c) LANE-HOVER-INFO — the collapsed-lane hover PEEK. A floating card the
   minimized toggle hosts; on hover/focus it lists the lane's content (chapter titles /
   comment snippets) so a glance reads the lane without re-opening it. Hidden by default,
   revealed only when the lane is COLLAPSED and only on a hover-capable pointer, so the
   touch path (where :hover sticks after a tap) never traps it open — a tap just toggles
   the lane. pointer-events:none keeps it a true peek: it can't be hovered to stay alive
   nor eat a click, so leaving the toggle box dismisses it. */
.lane-peek {
  display: none; position: absolute; top: calc(100% + 6px); z-index: 20;
  min-width: 160px; max-width: 240px; padding: var(--sp2) var(--sp3);
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink-dim);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  font-family: var(--f-ui); font-size: var(--fs-chip); line-height: 1.5;
  text-align: left; pointer-events: none;
}
.chap-lane-toggle .lane-peek { left: 0; }        /* left lane → open down-right */
.comment-lane-toggle .lane-peek { right: 0; }    /* right lane → open down-left */
.lane-peek-row { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lane-peek-row + .lane-peek-row { margin-top: 2px; }
/* the hover/focus REVEAL of .lane-peek lives in the single hover-gated block below
   (design-audit consolidation pin) — collapsed-only, hover-capable pointers only. */

/* ── VERSION-HISTORY drawer (VH-1 Stage A) ─────────────────────────────────────
   A right-docked OVERLAY (z-index above the paper, absolutely positioned to the
   .transcript region), NOT the reserve-lane mechanism the chapters/comment lanes
   use — so opening it never reflows the live editor. Two panes: the version LIST
   and the read-only PREVIEW of the doc as-of the selected point. */
.history-panel {
  /* z-index above the region chrome it overlays — the sticky viewbar (8) and the
     comment-lane divider (9) — but below the app-global dialogs/toasts. */
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 15;
  display: flex; flex-direction: column;
  width: min(720px, 82%);
  background: var(--bg);
  border-left: 1px solid var(--line);
  box-shadow: -12px 0 32px -18px rgba(0, 0, 0, 0.45);
}
.history-panel[hidden] { display: none; }
.history-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp3); padding: var(--sp3) var(--sp4);
  border-bottom: 1px solid var(--line);
}
.history-title {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint);
}
.history-close {
  font-family: var(--f-ui); font-size: var(--fs-base); line-height: 1;
  color: var(--ink-dim); background: none; border: none; cursor: pointer;
  padding: 4px 6px; border-radius: 4px;
}
.history-close:hover { color: var(--ink); background: color-mix(in oklab, var(--ink) 6%, transparent); }
.history-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.history-body { flex: 1; display: flex; min-height: 0; }
/* The left pane: a column stacking the status note above the scrolling listbox.
   The pane owns the fixed width + divider; the listbox scrolls within it. */
.history-listpane {
  flex: none; width: 240px; min-height: 0;
  display: flex; flex-direction: column;
  border-right: 1px solid var(--line);
}
.history-note:not(:empty) {
  padding: var(--sp3) var(--sp2); border-bottom: 1px solid color-mix(in oklab, var(--line) 40%, transparent);
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp2);
}
.history-retry {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--accent);
  background: none; border: 1px solid var(--accent); border-radius: 5px;
  padding: 2px var(--sp2); cursor: pointer;
}
.history-retry:hover { background: color-mix(in oklab, var(--accent) 10%, transparent); }
.history-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.history-list {
  flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp2);
  scrollbar-color: var(--line) transparent;
}
.history-row {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp2) var(--sp2); border-radius: 5px; cursor: pointer;
  border-bottom: 1px solid color-mix(in oklab, var(--line) 40%, transparent);
}
.history-row:hover { background: color-mix(in oklab, var(--ink) 6%, transparent); }
.history-row.cur { background: var(--presence-wash); }
.history-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.history-row-when {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--ink);
}
.history-row-sub {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.history-row-when .history-row-live {
  font-size: var(--fs-chip); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--accent); margin-left: var(--sp2);
}
.history-preview {
  flex: 1; min-width: 0; overflow-y: auto; padding: var(--sp4);
  scrollbar-color: var(--line) transparent;
}
.history-hint { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); margin: 0; }
.history-preview-head {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-faint);
  margin: 0 0 var(--sp3) 0; padding-bottom: var(--sp2); border-bottom: 1px solid var(--line);
}
.history-preview-para {
  font-family: var(--f-body, var(--f-ui)); font-size: var(--fs-base); line-height: 1.6;
  color: var(--ink); margin: 0 0 var(--sp3) 0; white-space: pre-wrap;
}
.history-preview-para .history-preview-spk { color: var(--ink-dim); font-weight: 600; }
/* VH-1 Stage B — the restore affordance, sat below the read-only preview. */
.history-restore { margin-top: var(--sp4); padding-top: var(--sp3); border-top: 1px solid var(--line); }
.history-restore-btn {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--accent);
  background: none; border: 1px solid var(--accent); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-restore-btn:hover { background: color-mix(in oklab, var(--accent) 10%, transparent); }
.history-restore-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.history-restore-q {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
  line-height: 1.5; margin: 0 0 var(--sp3) 0;
}
.history-restore-actions { display: flex; gap: var(--sp2); }
.history-restore-go {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--bg);
  background: var(--accent); border: 1px solid var(--accent); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-restore-go:hover { background: color-mix(in oklab, var(--accent) 88%, var(--ink)); }
.history-restore-cancel {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--ink-dim);
  background: none; border: 1px solid var(--line); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-restore-cancel:hover { color: var(--ink); background: color-mix(in oklab, var(--ink) 6%, transparent); }
.history-restore-go:focus-visible,
.history-restore-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* EDIT-LINK-1c: the per-burst revert bar. Mirrors the restore bar's ceremony but wears
   --rust (the palette's negative/reversal role) so undoing a burst reads distinct from
   restoring a version. Sits below restore on an earlier point; stands alone on the head. */
.history-revert { margin-top: var(--sp3); }
.history-revert-btn {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--rust);
  background: none; border: 1px solid var(--rust); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-revert-btn:hover { background: var(--rust-wash); }
.history-revert-btn:focus-visible { outline: 2px solid var(--rust); outline-offset: 1px; }
.history-revert-q {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
  line-height: 1.5; margin: 0 0 var(--sp3) 0;
}
.history-revert-actions { display: flex; gap: var(--sp2); }
.history-revert-go {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--bg);
  background: var(--rust); border: 1px solid var(--rust); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-revert-go:hover { background: color-mix(in oklab, var(--rust) 88%, var(--ink)); }
.history-revert-cancel {
  font-family: var(--f-ui); font-size: var(--fs-base); color: var(--ink-dim);
  background: none; border: 1px solid var(--line); border-radius: 5px;
  padding: var(--sp2) var(--sp3); cursor: pointer;
}
.history-revert-cancel:hover { color: var(--ink); background: color-mix(in oklab, var(--ink) 6%, transparent); }
.history-revert-go:focus-visible,
.history-revert-cancel:focus-visible { outline: 2px solid var(--rust); outline-offset: 1px; }
/* HISTORY-DRAWER-UX-1 (c): while a restore/revert is committing, the sibling confirm
   bar is disarmed — its buttons go disabled so the write can't be double-fired. */
.history-restore button:disabled,
.history-revert button:disabled { opacity: 0.45; cursor: default; }
/* GEOMETRY CONTRACT: every layout-affecting value below CONSUMES a --geo-*
   var published by js/layout.js (publishGeo) — pretext computes positions
   analytically from those numbers and the browser, reading the same vars,
   cannot disagree. Fallbacks only cover the pre-module first paint. */
.paper {
  position: relative;            /* sections are absolutely positioned */
  max-width: var(--geo-paper-max, 70ch);
  margin: 0 auto;
  font-family: var(--geo-font-family, var(--f-body));
  font-size: var(--geo-font-size, 17px);
  font-weight: var(--geo-font-weight, 350);
  user-select: none;
  -webkit-user-select: none;
}
.boot { color: var(--ink-faint); font-style: italic; padding-top: var(--sp8); text-align: center; }

/* view options — the transcript's own display settings (V.*), living ON the
   region they display. Zero-height sticky bar: the chip holds the top-right
   corner of the scroller — and travels with the region into the shelf —
   without displacing the paper below. */
.viewbar {
  position: sticky; top: var(--sp3); height: 0; z-index: 8;
  display: flex; justify-content: center;
  align-items: flex-start;   /* the 0-height bar must not stretch the chip */
  padding: 0 var(--sp4);
}
.viewbtn {
  padding: 4px 10px; font-size: var(--fs-chip); border-radius: 99px;
  color: var(--ink-dim); background: var(--surface);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.viewbtn:hover { color: var(--ink); }
/* icon doctrine (specs/icons.md): hand-kept SVG glyphs ride the text line.
   The defs block is reference-only — it must never take layout space. */
.icndefs { position: absolute; width: 0; height: 0; }
.icn { width: 12px; height: 12px; vertical-align: -2px; }
/* the gear icon sits in the old glyph's em box — keep its slot quiet */
.viewbtn .g { line-height: 1; opacity: 0.9; }
/* any non-default view = amber, same "deviates from policy" voice as pins */
.viewbtn.mod { color: var(--accent); background: var(--accent-soft); opacity: 1; }

/* the transcript's recall chip — header chrome, shown only while it's away */

.viewpop {
  position: absolute; top: 30px; left: 50%; transform: translateX(-50%); right: auto;
  width: 250px; padding: var(--sp3) var(--sp4) var(--sp4);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 10px 30px oklch(0 0 0 / 0.45);
  overflow-y: auto;   /* main.ts caps max-height to the region at open — past that, scroll, never clip */
}
.viewpop[hidden] { display: none; }
.viewpop h3 {
  margin: 0 0 var(--sp2); font-size: var(--fs-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.18em; color: var(--ink-faint);
}
.viewpop .hint { margin-top: var(--sp3); }

/* SHARE-FACE 1b-i — the Share panel (share.ts renders into #sharepop; the design
   brief is the notebook's investigations/share-face-brief.md). The viewpop idiom
   at panel width: a --surface card anchored under the header's Share handle
   (.top becomes its positioning context — its only other absolute child is the
   skiplink, pinned to the same top-left either way). Each row is a bordered
   mini-card whose TITLE is the derived link type — the consequential fact leads;
   the /s/:token address is a quiet copyable inset with Copy docked in its edge;
   Revoke is an --ink-faint text act that goes --rust when armed. Zero new color:
   --accent on interaction only, no amber (minting is not the global create verb). */
.top { position: relative; }
/* the CARD is shared with /look-share's .share-panel harness root (fidelity by
   construction — the harness shows the same pixels); only the ANCHORING under
   the header's Share handle is .sharepop's own. */
.sharepop, .share-panel {
  width: min(400px, calc(100vw - 2 * var(--sp4)));
  padding: var(--sp3) var(--sp4) var(--sp4);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 10px 30px oklch(0 0 0 / 0.45);
}
.sharepop {
  position: absolute; top: calc(100% + 6px); right: var(--sp4); z-index: 40;
  max-height: min(70vh, 560px); overflow-y: auto;
}
.sharepop[hidden] { display: none; }
.share-panel { margin: var(--sp4) auto; }
.share-scope {
  margin: 0 0 var(--sp2); font-size: var(--fs-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.18em; color: var(--ink-faint);
}
.share-empty { margin: var(--sp2) 0 0; color: var(--ink-dim); font-size: var(--fs-note); }
.share-note { margin: var(--sp2) 0 0; color: var(--rust); font-size: var(--fs-chip); }
.share-row {
  margin-top: var(--sp2); padding: var(--sp3);
  border: 1px solid var(--line); border-radius: 6px;
}
.share-row-head { display: flex; align-items: baseline; gap: var(--sp2); }
.share-meter { font-weight: 600; color: var(--ink); font-size: var(--fs-note); }
.share-kind { margin: 2px 0 var(--sp2); color: var(--ink-dim); font-size: var(--fs-chip); }
.share-address {
  display: flex; align-items: stretch;
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
}
.share-url {
  flex: 1; min-width: 0; padding: 5px var(--sp2);
  background: none; border: 0; color: var(--ink-dim);
  font-family: 'Spline Sans Mono', monospace; font-size: var(--fs-chip);
  text-overflow: ellipsis;
}
.share-url:focus { color: var(--ink); }
.share-copy {
  flex: none; padding: 0 var(--sp3);
  background: none; border: 0; border-left: 1px solid var(--line); border-radius: 0;
  color: var(--ink); font-size: var(--fs-chip);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.share-copy:hover { color: var(--accent); border-color: var(--line); }
.share-revoke {
  margin-left: auto; padding: 0;
  background: none; border: 0; color: var(--ink-faint); font-size: var(--fs-chip);
}
.share-revoke:hover { color: var(--ink); border-color: transparent; }
.share-revoke.confirming, .share-revoke.confirming:hover { color: var(--rust); font-weight: 600; }
.share-meta { margin: var(--sp2) 0 0; color: var(--ink-faint); font-size: var(--fs-chip); }
.share-create {
  display: block; width: 100%; margin-top: var(--sp3); padding: var(--sp2) var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink); font-size: var(--fs-note);
}
.share-create:hover, .share-create:focus-visible { border-color: var(--accent); color: var(--accent); }
.share-create:disabled { opacity: 0.5; cursor: default; }
/* SHARE-FACE 1b-ii — the inline create card (brief §3): the preset leads (the
   consequential decision, three radio-rows, Review default), then the light
   attrs OFF by default (ruling #4: expiry is opt-in). Same --surface-in---line
   card family as the rows; --accent on interaction only, zero new color. */
.share-createcard {
  margin-top: var(--sp3); padding: var(--sp3);
  border: 1px solid var(--line); border-radius: 6px;
}
.share-preset {
  display: flex; align-items: baseline; gap: var(--sp2);
  min-height: 44px; padding: var(--sp1) 0; cursor: pointer;
}
.share-preset input { accent-color: var(--accent); }
.share-preset-name { font-weight: 600; color: var(--ink); font-size: var(--fs-note); }
.share-preset-desc { color: var(--ink-dim); font-size: var(--fs-chip); }
.share-allow-editing {
  display: flex; align-items: center; gap: var(--sp2);
  min-height: 44px; padding: 0 0 var(--sp1) calc(1rem + var(--sp2));
  color: var(--ink-dim); font-size: var(--fs-chip); cursor: pointer;
}
.share-allow-editing[hidden] { display: none; }
.share-allow-editing input { accent-color: var(--accent); }
.share-presentation-title {
  display: grid; gap: var(--sp1); color: var(--ink-dim); font-size: var(--fs-note);
}
.share-presentation-title[hidden] { display: none; }
.share-presentation-title-input {
  width: 100%; min-width: 0; padding: var(--sp2); box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--bg-deep); color: var(--ink); font: inherit;
}
.share-presentation-title-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.share-content-access {
  display: grid; gap: var(--sp1);
  margin: var(--sp2) 0 0; padding: var(--sp2) 0 0;
  border: 0; border-top: 1px solid var(--line);
}
.share-content-access-title {
  padding: 0; color: var(--ink-dim); font-size: var(--fs-chip);
  font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
}
.share-content-option {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: start; gap: var(--sp2); min-height: 44px;
  padding: var(--sp1) 0; cursor: pointer;
}
.share-content-option input { margin-top: 3px; accent-color: var(--accent); }
.share-content-copy { display: grid; gap: 2px; }
.share-content-copy strong { color: var(--ink); font-size: var(--fs-note); font-weight: 600; }
.share-content-copy small { color: var(--ink-dim); font-size: var(--fs-chip); }
.share-expiry {
  display: flex; align-items: baseline; gap: var(--sp2);
  margin-top: var(--sp2); padding-top: var(--sp2); border-top: 1px solid var(--line);
  color: var(--ink-dim); font-size: var(--fs-note); cursor: pointer;
}
.share-expiry input { accent-color: var(--accent); }
.share-expiry-date {
  display: block; margin-top: var(--sp2); padding: 4px var(--sp2);
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-chip);
  color-scheme: dark;   /* the UA's date-picker chrome matches the slate surface */
}
.share-expiry-date[hidden] { display: none; }
/* SHARE-FACE 1b-iii — the Password checkbox-reveal: the Expires grammar twin
   (opt-in, OFF by default, no default value). A visible text field — the maker
   is choosing a password to hand out, not proving one. */
.share-passwordrow {
  display: flex; align-items: baseline; gap: var(--sp2);
  margin-top: var(--sp2); padding-top: var(--sp2); border-top: 1px solid var(--line);
  color: var(--ink-dim); font-size: var(--fs-note); cursor: pointer;
}
.share-passwordrow input { accent-color: var(--accent); }
.share-password-text {
  display: block; width: 100%; margin-top: var(--sp2); padding: 4px var(--sp2);
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-chip);
}
.share-password-text::placeholder { color: var(--ink-faint); }
.share-password-text[hidden] { display: none; }
.share-create-actions {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: var(--sp2); margin-top: var(--sp3);
}
.share-create-cancel, .share-mint {
  min-height: 44px; padding: var(--sp2) var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink); font-size: var(--fs-note);
}
.share-create-cancel { color: var(--ink-dim); }
.share-create-cancel:hover, .share-create-cancel:focus-visible,
.share-mint:hover, .share-mint:focus-visible { border-color: var(--accent); color: var(--accent); }
.share-mint:disabled { opacity: 0.5; cursor: default; }
/* SHARE-FACE 1b-iii — the recipient PASSWORD CHALLENGE at /s/:token. One quiet
   card, deliberately GENERIC (the server said nothing but "password" — the page
   matches that silence: no title, no org, no scope). The full recipient face is
   1c's own brief; this page only needs to be honest and calm. Zero new color:
   --surface-in---line, --accent on interaction, --rust for the refusal note. */
.share-viewer-challenge {
  max-width: 360px; margin: 18vh auto 0; padding: var(--sp4);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
}
.share-viewer-challenge-title { color: var(--ink); font-weight: 600; font-size: var(--fs-note); }
.share-viewer-challenge-hint { margin-top: var(--sp1); color: var(--ink-dim); font-size: var(--fs-chip); }
.share-viewer-challenge-row { display: flex; gap: var(--sp2); margin-top: var(--sp3); }
.share-viewer-password {
  flex: 1; min-width: 0; padding: 4px var(--sp2);
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note);
}
.share-viewer-password::placeholder { color: var(--ink-faint); }
.share-viewer-unlock {
  padding: 4px var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note);
}
.share-viewer-unlock:hover, .share-viewer-unlock:focus-visible { border-color: var(--accent); color: var(--accent); }
.share-viewer-unlock:disabled { opacity: 0.5; cursor: default; }
.share-viewer-challenge-note { margin-top: var(--sp2); color: var(--rust); font-size: var(--fs-chip); }
.share-viewer-challenge-note[hidden] { display: none; }

/* EDIT-LINK-1b — the guest editor's NAME GATE. A blocking scrim over the mounted cut:
   the guest names themselves once before the first op can drain (RFC D5). Same calm
   register as the challenge card — --surface card, --line, --accent on interaction, no
   new color; the scrim is the app's shared --scrim. Fixed-cover so it blocks the editor
   behind it regardless of scroll/stacking context (mounted as a body overlay). */
.guest-gate-scrim {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: flex-start; justify-content: center;
  background: var(--scrim);
}
.guest-gate-card {
  max-width: 380px; width: calc(100% - 2 * var(--sp4)); margin-top: 18vh; padding: var(--sp4);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--lift-1);
}
.guest-gate-title { color: var(--ink); font-weight: 600; font-size: var(--fs-note); }
.guest-gate-hint { margin-top: var(--sp1); color: var(--ink-dim); font-size: var(--fs-chip); }
.guest-gate-row { display: flex; gap: var(--sp2); margin-top: var(--sp3); }
.guest-gate-input {
  flex: 1; min-width: 0; padding: 4px var(--sp2);
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note);
}
.guest-gate-input::placeholder { color: var(--ink-faint); }
.guest-gate-input:disabled { opacity: 0.5; }
.guest-gate-go {
  padding: 4px var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note); white-space: nowrap;
}
.guest-gate-go:hover, .guest-gate-go:focus-visible { border-color: var(--accent); color: var(--accent); }
.guest-gate-go:disabled { opacity: 0.5; cursor: default; }
.guest-gate-note { margin-top: var(--sp2); color: var(--rust); font-size: var(--fs-chip); }
.guest-gate-note[hidden] { display: none; }

/* The one-line terms acceptance (TOS-ACCEPTANCE-SEAM-1 door (b)) — a click-wrap line
   under the name row: "Start editing" IS the acceptance, so this is a quiet notice, not
   a checkbox (the account signup carries the explicit box; a guest gets the lighter form). */
.guest-gate-terms { margin-top: var(--sp2); color: var(--ink-dim); font-size: var(--fs-chip); line-height: 1.4; }

/* EDIT-LINK item (a) — the RETURNING-guest continue banner. Non-modal (no scrim): the
   guest is already identified off the cookie, so editing is live behind it. A quiet
   bottom-anchored pill in the shared calm register (--surface/--line, --accent on the
   re-identify escape only), never blocking the cut. */
.guest-continue {
  position: fixed; z-index: 55; left: 50%; bottom: var(--sp4); transform: translateX(-50%);
  display: flex; align-items: center; gap: var(--sp3);
  max-width: calc(100% - 2 * var(--sp4)); padding: var(--sp1) var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px; box-shadow: var(--lift-1);
}
.guest-continue-identity, .guest-continue-account { display: flex; align-items: center; gap: var(--sp2); }
.guest-continue-label { color: var(--ink-dim); font-size: var(--fs-chip); white-space: nowrap; }
.guest-continue-notyou {
  padding: 2px var(--sp2);
  background: transparent; border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink); font-size: var(--fs-chip); white-space: nowrap;
}
.guest-continue-notyou:hover, .guest-continue-notyou:focus-visible { border-color: var(--accent); color: var(--accent); }
.guest-continue-account {
  padding-left: var(--sp3); border-left: 1px solid var(--line);
  color: var(--ink-faint); font-size: var(--fs-chip); white-space: nowrap;
}
.guest-continue-create, .guest-continue-signin { color: var(--ink); text-underline-offset: 3px; }
.guest-continue-create:hover, .guest-continue-create:focus-visible,
.guest-continue-signin:hover, .guest-continue-signin:focus-visible { color: var(--accent); }

@media (max-width: 680px) {
  .guest-continue {
    left: var(--sp3); right: var(--sp3); bottom: var(--sp3); transform: none;
    max-width: none; align-items: stretch; flex-direction: column; gap: var(--sp1);
    border-radius: 8px; padding: var(--sp2) var(--sp3);
  }
  .guest-continue-identity { justify-content: space-between; }
  .guest-continue-notyou, .guest-continue-create, .guest-continue-signin {
    min-height: 44px; display: inline-flex; align-items: center;
  }
  .guest-continue-account {
    padding: var(--sp1) 0 0; border-left: 0; border-top: 1px solid var(--line);
    flex-wrap: wrap; white-space: normal;
  }
}

/* EDIT-LINK item (f), MOBILE-FALL — the honest line above the read-only recipient viewer
   when an edit link is opened on a coarse/narrow device (the editor is a desktop surface).
   Calm, not an error: --ink-dim on --wash, sitting inside the reading column's rhythm. */
.guest-edit-mobilefall {
  margin: 0 auto var(--sp3); max-width: 60ch; padding: var(--sp2) var(--sp3);
  background: var(--wash); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink-dim); font-size: var(--fs-chip);
}

/* EDIT-LINK-1b guest dressing — the guest editor (body[data-guest], set by main.ts)
   changes ACCOUNT chrome, never document chrome. An edit grant can author the same op
   kinds as a member, including chapter operations, so Chapters must remain the real
   editor lane rather than a guest-only approximation. Comments likewise stay on the
   shared dock; its token transport gates the narrower reply/resolve/delete verbs. */

/* EDIT-LINK item (b) — the SERVER-export destinations (the metered MP4 render + the
   Premiere cut-JSON) are org-scoped, owner-only compute a session-less guest must never
   spend (main.ts skips initExportControl on !opsBase, so they never un-hide today). This
   is the DEFENCE-IN-DEPTH twin of the item-2h audience pin: even a programmatic reveal
   (a future refactor, a stray classList flip) stays suppressed for a guest. The
   client-derived destinations (Captions / Chapters / Data) round-trip nothing, so they
   stand — a guest may still take the cut's text/subtitles home. */
body[data-guest] #dest-mp4, body[data-guest] #dest-premiere { display: none; }

/* SHARE-PROJECT-ROOM-1 — the project-link ROSTER at /s/:token. A prj_ link has no
   single cut, so the recipient lands on a quiet pick-list of the project's cuts,
   grouped by their owning upload; a pick opens the SAME room/review surface a canvas
   link mounts. Same calm register as the challenge card: --surface card, --line
   borders, --accent on interaction only. Zero new color. */
.share-viewer-roster {
  --room-accent: var(--accent);   /* PROJECT-ROOM-BRANDING: org accent overrides via inline style */
  max-width: 460px; margin: 12vh auto 0; padding: var(--sp4) var(--sp3) var(--sp5);
}
/* PROJECT-ROOM-BRANDING: the org logo leads the picker head when set — the SAME
   decorative-mark treatment the room header uses (capped small mark above the name;
   the org name stays the accessible identity). */
.share-viewer-roster-logo {
  display: block; height: 30px; width: auto; max-width: 160px;
  object-fit: contain; object-position: left center; margin-bottom: var(--sp2);
}
.share-viewer-roster-org {
  color: var(--ink-dim); font-size: var(--fs-chip); letter-spacing: 0.02em;
}
.share-viewer-roster-title {
  margin-top: var(--sp1); color: var(--ink); font-weight: 600; font-size: var(--fs-clock);
}
.share-viewer-roster-list { margin-top: var(--sp3); display: flex; flex-direction: column; gap: 2px; }
.share-viewer-roster-group {
  margin-top: var(--sp3); margin-bottom: var(--sp1);
  color: var(--ink-faint); font-size: var(--fs-chip); letter-spacing: 0.04em; text-transform: uppercase;
}
.share-viewer-roster-group:first-child { margin-top: 0; }
.share-viewer-roster-cut {
  display: block; width: 100%; text-align: left; padding: var(--sp2) var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink); font-size: var(--fs-note); cursor: pointer;
}
.share-viewer-roster-cut:hover, .share-viewer-roster-cut:focus-visible {
  border-color: var(--room-accent); color: var(--room-accent);   /* org accent on interaction */
}

/* Mixed-content shares reuse the roster's branded entry grammar, but widen it
   enough for filenames and file metadata. Every row is one 52px touch target;
   video hands off to the canonical shared editor, retained files to the shared
   Asset Viewer preview renderer. */
.share-collection { max-width: 720px; }
.share-collection-trail {
  min-width: 0; display: flex; align-items: center; gap: var(--sp1);
  margin: var(--sp3) 0 var(--sp1); overflow-x: auto;
  color: var(--ink-faint); font-size: var(--fs-chip);
  scrollbar-width: none;
}
.share-collection-trail::-webkit-scrollbar { display: none; }
.share-collection-trail button {
  flex: 0 0 auto; min-height: 32px; padding: 0 var(--sp1);
  border: 0; background: transparent; color: var(--room-accent);
  font: inherit; cursor: pointer;
}
.share-collection-trail button:disabled {
  color: var(--ink-dim); cursor: default;
}
.share-collection-heading {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp3);
}
.share-collection-count { color: var(--ink-faint); font-size: var(--fs-chip); white-space: nowrap; }
.share-collection-list {
  display: flex; flex-direction: column; gap: 2px; margin-top: var(--sp3);
  border-top: 1px solid var(--line);
}
.share-collection-item {
  width: 100%; min-height: 58px; display: grid;
  grid-template-columns: 42px minmax(0, 1fr) auto; align-items: center;
  gap: var(--sp3); padding: var(--sp2);
  border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  background: transparent; color: var(--ink); text-align: left; cursor: pointer;
}
.share-collection-item:hover,
.share-collection-item:focus-visible { background: var(--surface); }
.share-collection-item.is-folder .share-collection-mark {
  border-color: color-mix(in srgb, var(--room-accent) 35%, var(--line));
  color: var(--room-accent);
}
.share-collection-mark {
  width: 42px; height: 42px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--surface-raised); color: var(--ink-dim);
  font: 600 var(--fs-label)/1 var(--font-ui); letter-spacing: 0.04em;
}
.share-collection-copy { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.share-collection-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-note); font-weight: 600;
}
.share-collection-meta { color: var(--ink-faint); font-size: var(--fs-chip); }
.share-collection-open { color: var(--room-accent); font-size: var(--fs-chip); }

.route-page:has(.share-collection-file) {
  width: 100%; max-width: none; margin: 0; padding: var(--sp4);
  justify-self: stretch; align-self: stretch;
}
.share-collection-file { min-height: 100%; }
.share-collection-file-header {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: var(--sp4); max-width: 1200px; margin: 0 auto var(--sp3);
}
.share-collection-back {
  min-height: 40px; padding-inline: var(--sp2); background: transparent;
  border-color: transparent; color: var(--ink-dim);
}
.share-collection-back:hover,
.share-collection-back:focus-visible { color: var(--ink); border-color: var(--line); }
.share-collection-file-identity { min-width: 0; }
.share-collection-file-identity h1 {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink); font-size: var(--fs-clock); font-weight: 600;
}
.share-collection-file-kind { color: var(--ink-faint); font-size: var(--fs-chip); }
.share-collection-download {
  display: inline-flex; align-items: center; min-height: 40px;
  padding: 0 var(--sp3); border: 1px solid var(--line); border-radius: 7px;
  color: var(--ink); text-decoration: none;
}
.share-collection-file-body {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
  gap: var(--sp3); max-width: 1440px; margin: 0 auto; align-items: start;
}
.share-collection-file-body > .asset-viewer-stage {
  min-height: min(72dvh, 800px);
}
.share-collection-discussion {
  min-width: 0; max-height: min(72dvh, 800px); overflow: auto;
  padding: var(--sp3); border: 1px solid var(--line); border-radius: 9px;
  background: var(--surface);
}
.share-collection-discussion-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp2); padding-bottom: var(--sp2); border-bottom: 1px solid var(--line);
}
.share-collection-discussion-head h2 {
  color: var(--ink); font: 600 var(--fs-note)/1.2 var(--font-ui);
}
.share-collection-discussion-count {
  color: var(--ink-faint); font-size: var(--fs-chip);
}

@media (max-width: 900px) {
  .share-collection-file-body { grid-template-columns: 1fr; }
  .share-collection-discussion { max-height: none; }
}

@media (max-width: 640px) {
  .share-viewer-roster { margin-top: var(--sp4); padding-inline: var(--sp2); }
  .share-collection-item { grid-template-columns: 42px minmax(0, 1fr); }
  .share-collection-open { display: none; }
  .route-page:has(.share-collection-file) { padding: var(--sp2); }
  .share-collection-file-header {
    grid-template-columns: 1fr auto; gap: var(--sp2);
  }
  .share-collection-back { grid-column: 1 / -1; justify-self: start; }
  .share-collection-download { padding-inline: var(--sp2); font-size: var(--fs-chip); }
  .share-collection-file-body > .asset-viewer-stage { min-height: 48dvh; }
}

/* SHARE-FACE 1c-ii — the PRESENTATION room (shareroom.ts; the brief's §2 layout,
   now LIVE over a composed stage). The room COMMITS to the deep register: the
   projection well is --bg-deep (the darkest wall in the house), chrome recedes to
   the faint inks, --accent on interaction only. Zero new color — the darkroom the
   app already is, one shade deeper. The header + transport idle-fade during
   playback (data-idle); the frame holds the ONE <video> the stage drives. */
/* PRESENT-FULLBLEED-1 — the present room is full-VIEWPORT cinema, not a centered
   920px card (revises 1c-iv's "reclaim 920px": that solved "too narrow", this fills
   the screen). The video well IS the page; the header, transport, and comments float
   OVER it and idle-fade during playback (the projection-room grammar the darkroom
   already speaks). This is presentation-ONLY — the review reading surface
   (.sharereview-card, below) keeps its measure. Zero new color: bg-deep well, black
   letterbox, the chrome scrims are flat translucent veils (not gradients — P5). */
.shareroom {
  position: relative; width: 100%; height: 100%;
  margin: 0; padding: 0; overflow: hidden;
  background: var(--bg-deep); border: 0; border-radius: 0;
  /* PRESENT-BRANDING-1 (#77): the room's interaction accent. Defaults to the house
     --accent; a branded grant overrides this ONE property inline (root.style) with
     the org's sanitized hex, so ONLY interaction chrome recolors — never text/bg
     (brief §5: accent on interaction). Unbranded rooms are byte-identical to before. */
  --room-accent: var(--accent);
}
/* The room mounts INTO #route-page, which also carries .route-page (the 460px door
   column, pushed down 22vh). A screening room is not a door — this compound override
   (0,2,0 beats .route-page's 0,1,0, order-independent) lets the room STRETCH to fill
   the whole body grid track edge-to-edge (no centered cap, no top gap), WITHOUT
   touching the shared .route-page rule every other page leans on. */
.route-page.shareroom {
  width: 100%; max-width: none; margin: 0; padding: 0;
  justify-self: stretch; align-self: stretch;
}
/* Header — org + title, top-left over the well on a flat scrim for legibility over a
   bright frame (a translucent veil, not a gradient — the house is flat; P5). Pointer-
   events:none at rest (it holds no controls) so nothing on the video is blocked; the
   idle rule fades it during playback. */
.shareroom-head {
  position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
  padding: clamp(var(--sp4), 2.4vw, 36px);
  background: oklch(0 0 0 / 0.38);
}
.shareroom-brand { display: flex; align-items: center; gap: var(--sp3); min-width: 0; }
.shareroom-brand-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.shareroom-org {
  color: var(--ink-faint); font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: 0.18em;
}
.shareroom-title { color: var(--ink); font-size: 16px; font-weight: 600; margin: 0; }
/* PRESENT-BRANDING-1 (#77): a deliberate brand slot accepts either a 1:1 mark or a
   horizontal lockup without distorting either. Text identity remains beside it, so an
   abstract logo never becomes the only orientation cue. */
.shareroom-logo-slot {
  display: flex; align-items: center; justify-content: flex-start; flex: none;
  width: auto; max-width: min(220px, 24vw); height: clamp(48px, 5vw, 64px);
}
.shareroom-logo {
  display: block; width: auto; height: 100%; max-width: 100%;
  object-fit: contain; object-position: left center;
}
/* A narrow projection-room gutter keeps the frame legible as an object rather than
   making every presentation look accidentally fullscreen. Fullscreen remains available
   as an explicit transport action. */
.shareroom-stage { position: absolute; inset: clamp(var(--sp4), 3vw, 48px); z-index: 0; margin: 0; }
.shareroom-frame {
  position: relative; width: 100%; height: 100%; aspect-ratio: auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  overflow: hidden; background: #000; border-radius: 10px;
}
.shareroom-playhint { color: var(--ink-deco); font-size: 40px; line-height: 1; }
.shareroom-skeleton-note { margin-top: var(--sp2); color: var(--ink-faint); font-size: var(--fs-chip); }
/* the ONE <video> the composed stage drives — letterboxed on the near-black well */
.shareroom-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background: #000;
}
/* WR-SIGNPLAY-ERR-1 — the transient "reload to continue" notice, centered over the
   near-black well ABOVE the letterboxed video. A signed-playback URL expiring
   mid-session is reload-fixable, not the permanent media-dead state. */
.shareroom-medianote {
  position: absolute; z-index: 2; left: 50%; top: 50%; transform: translate(-50%, -50%);
  max-width: 80%; text-align: center;
  padding: var(--sp2) var(--sp3); border-radius: 8px;
  background: rgba(0, 0, 0, 0.72); color: #fff; font-size: var(--fs-chip);
}
/* Header + transport (and the closed comments door) recede on idle-during-playback
   (brief §2). Gated on data-sidecar="closed": a recipient reading the comments is not
   idle, so an OPEN sidecar never fades its chrome out from under them. Faded chrome
   goes pointer-events:none so the first move REVEALS rather than blindly hitting an
   invisible button (the projection-room grammar — move to wake, then act); the cursor
   hides with it for the screening feel. */
.shareroom-head, .shareroom-transport, .shareroom-comments-door { transition: opacity 0.5s ease; }
.shareroom[data-idle="true"][data-sidecar="closed"] .shareroom-head,
.shareroom[data-idle="true"][data-sidecar="closed"] .shareroom-transport,
.shareroom[data-idle="true"][data-sidecar="closed"] .shareroom-comments-door {
  opacity: 0; pointer-events: none;
}
/* PRESENT-BRANDING-1 (#77): the honest "Made with Proof" mark — a quiet credit at the
   transport's right edge, always present (the room is Proof-made regardless of org
   branding). Lives IN the transport row, so it inherits its scrim + idle-fade. */
.shareroom-madewith {
  flex: none; margin-left: var(--sp2);
  color: var(--ink-deco); font-size: var(--fs-label); letter-spacing: 0.04em;
  white-space: nowrap; text-decoration: none;
  transition: color 120ms ease;
}
.shareroom-madewith:hover,
.shareroom-madewith:focus-visible { color: var(--room-accent); }
.shareroom[data-idle="true"][data-sidecar="closed"] { cursor: none; }
/* Transport — floats along the bottom edge on a flat scrim (the controls stay legible
   over a bright final frame; a translucent veil, not a gradient — P5 flat). */
.shareroom-transport {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: var(--sp3);
  margin: 0; padding: var(--sp4);
  background: oklch(0 0 0 / 0.45);
}
.shareroom-play {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 6px; flex: none;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); font-size: 13px; line-height: 1;
}
.shareroom-play:disabled { color: var(--ink-deco); cursor: default; }
.shareroom-play:not(:disabled) { cursor: pointer; }
.shareroom-play:not(:disabled):hover,
.shareroom-play:focus-visible { border-color: var(--room-accent); color: var(--room-accent); }
.shareroom-seek {
  position: relative;   /* the comment-marker anchor (COMMENT-WORD-ANCHOR-1) */
  flex: 1; height: 28px; background: transparent;
  cursor: pointer; touch-action: none;
}
.shareroom-seek::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: 6px; transform: translateY(-50%); border-radius: 999px;
  background: var(--line);
}
/* keyboard-operable slider (SHAREROOM-KEYBOARD-SEEK-1) — a visible focus ring, offset so
   it clears the 4px rail (WCAG 2.4.7). Pointer focus stays quiet via :focus-visible. */
.shareroom-seek:focus-visible { outline: 2px solid var(--room-accent); outline-offset: 3px; }
.shareroom-seek-fill {
  position: absolute; z-index: 1; left: 0; top: 50%;
  height: 6px; transform: translateY(-50%);
  background: var(--room-accent); border-radius: 999px;
}
.shareroom-time {
  flex: none; color: var(--ink-faint); font-size: var(--fs-chip);
  font-variant-numeric: tabular-nums;
}
/* SHAREROOM-FULLSCREEN-VOLUME-1 — the media chrome (mute + volume slider + fullscreen).
   The glyph buttons share the play button's square-tile idiom (--surface tile, accent on
   interaction); the icons are stroke SVG (currentColor, theme-aware — no emoji glyph). */
.shareroom-mute, .shareroom-fs {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 6px; flex: none; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); color: var(--ink);
}
.shareroom-mute svg, .shareroom-fs svg { width: 16px; height: 16px; display: block; }
.shareroom-mute:hover, .shareroom-mute:focus-visible,
.shareroom-fs:hover, .shareroom-fs:focus-visible { border-color: var(--room-accent); color: var(--room-accent); }
/* a muted glyph dims to signal the off state (the cross carries the meaning; the dim reinforces) */
.shareroom-mute.is-muted { color: var(--ink-deco); }
/* an unsupported-fullscreen environment self-hides the glyph — never a dead control */
.shareroom-fs[hidden] { display: none; }
/* volume: the mute glyph plus, on a fine pointer, a compact slider (absent from the DOM
   on a coarse pointer — hardware volume only). */
.shareroom-volume {
  display: flex; align-items: center; gap: var(--sp2); flex: none;
  padding-right: var(--sp2); border: 1px solid transparent; border-radius: 999px;
}
.shareroom-volume:hover, .shareroom-volume:focus-within {
  background: var(--wash); border-color: var(--line);
}
.shareroom-vol-slider {
  appearance: none; width: 58px; height: 20px; flex: none; cursor: pointer;
  background: transparent;
}
.shareroom-vol-slider::-webkit-slider-runnable-track {
  height: 2px; border-radius: 999px; background: var(--ink-deco);
}
.shareroom-vol-slider::-webkit-slider-thumb {
  appearance: none; width: 11px; height: 11px; margin-top: -4.5px;
  border: 2px solid var(--surface); border-radius: 50%; background: var(--room-accent);
}
.shareroom-vol-slider::-moz-range-track {
  height: 2px; border-radius: 999px; background: var(--ink-deco);
}
.shareroom-vol-slider::-moz-range-thumb {
  width: 9px; height: 9px; border: 2px solid var(--surface);
  border-radius: 50%; background: var(--room-accent);
}
.shareroom-vol-slider:focus-visible { outline: 2px solid var(--room-accent); outline-offset: 3px; }
/* Element fullscreen paints the room root full-screen — style the surface + its top-layer
   backdrop EXPLICITLY dark (backdrop inheritance is unreliable across engines; brief §1). */
.shareroom:fullscreen { width: 100vw; height: 100vh; background: var(--bg-deep); }
.shareroom:fullscreen::backdrop { background: #000; }
/* The comments affordance floats top-right, clear of the header (top-left) and the
   transport (bottom). It idle-fades with the rest of the chrome during playback. */
.shareroom-comments-door {
  position: absolute; top: var(--sp4); right: var(--sp4); z-index: 3;
  margin: 0; padding: 4px var(--sp3);
  background: var(--surface); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink-dim); font-size: var(--fs-note);
}
.shareroom-comments-door:hover, .shareroom-comments-door:focus-visible { border-color: var(--accent); color: var(--accent); }
/* The sidecar docks as a right-edge drawer OVER the cinema (a bright inner shadow rims
   it off the video) — reading comments never reflows the well. It becomes a bottom
   sheet on a phone (the narrow block below). */
.shareroom-sidecar {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 4;
  width: min(380px, 92vw); margin: 0; padding: var(--sp4); overflow-y: auto;
  background: var(--surface); border: 0; border-left: 1px solid var(--line); border-radius: 0;
  box-shadow: -12px 0 40px oklch(0 0 0 / 0.5);
  display: flex; flex-direction: column; gap: var(--sp3);
}
.shareroom-sidecar[hidden] { display: none; }
.shareroom-sidecar-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp3);
  padding-bottom: var(--sp2); background: var(--surface); border-bottom: 1px solid var(--line);
}
.shareroom-sidecar-close {
  min-width: 44px; min-height: 36px; padding: var(--sp1) var(--sp2);
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--wash-soft); color: var(--ink-dim); font: inherit;
}
.shareroom-sidecar-close:hover,
.shareroom-sidecar-close:focus-visible { border-color: var(--accent); color: var(--accent); }
/* The thread list scrolls WITHIN the sidecar so a long conversation never pushes the
   composer off-screen (the composer stays reachable — brief §3). */
.shareroom-comment-list {
  display: flex; flex-direction: column; gap: var(--sp3);
  max-height: 42vh; overflow-y: auto;
}
.shareroom-comment-status { color: var(--ink-faint); font-size: var(--fs-note); }
.shareroom-comment { border-bottom: 1px solid var(--line); padding-bottom: var(--sp2); }
.shareroom-comment:last-of-type { border-bottom: 0; padding-bottom: 0; }
/* A reply is indented one level under its root — a hairline rail marks the thread
   (one level only; the display never nests deeper, brief §3). */
.shareroom-comment-reply {
  margin-left: var(--sp3); padding-left: var(--sp3);
  border-left: 2px solid var(--line); border-bottom: 0;
}
.shareroom-comment-meta { display: flex; gap: var(--sp2); align-items: baseline; }
.shareroom-comment-author { color: var(--ink); font-size: var(--fs-note); font-weight: 600; }
.shareroom-comment-when { color: var(--ink-faint); font-size: var(--fs-chip); }
.shareroom-comment-body { margin-top: 2px; color: var(--ink-dim); font-size: var(--fs-note); }
/* The composer is progressive: identity is a one-time welcome step, then recedes into
   a quiet byline. The writing surface becomes the primary action on every return. */
.shareroom-composer { display: flex; flex-direction: column; gap: var(--sp2); }
.shareroom-composer[hidden] { display: none; }
.shareroom-composer-identity {
  padding: var(--sp3); border: 1px solid var(--line); border-radius: 8px;
  background: var(--wash-soft);
}
.shareroom-composer-prompt { color: var(--ink); font-size: var(--fs-note); font-weight: 600; }
.shareroom-composer-hint { margin-top: 2px; color: var(--ink-faint); font-size: var(--fs-chip); }
.shareroom-composer-identity-row, .shareroom-composer-row {
  display: flex; align-items: center; gap: var(--sp2);
}
.shareroom-composer-identity-row { margin-top: var(--sp3); }
.shareroom-composer-name, .shareroom-composer-body {
  min-height: 38px; padding: var(--sp2) var(--sp3);
  background: var(--wash); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note);
}
.shareroom-composer-name { flex: 1; min-width: 0; width: auto; }
.shareroom-composer-body { flex: 1; min-width: 0; }
.shareroom-composer-name::placeholder, .shareroom-composer-body::placeholder { color: var(--ink-faint); }
.shareroom-composer-send, .shareroom-composer-continue {
  min-height: 38px; padding: var(--sp2) var(--sp3); flex: none;
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  color: var(--ink); font-size: var(--fs-note);
}
.shareroom-composer-send:hover, .shareroom-composer-send:focus-visible,
.shareroom-composer-continue:hover, .shareroom-composer-continue:focus-visible {
  border-color: var(--accent); color: var(--accent);
}
.shareroom-composer-send:disabled, .shareroom-composer-continue:disabled {
  color: var(--ink-deco); cursor: default; border-color: var(--line);
}
.shareroom-composer-byline {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp2);
  color: var(--ink-faint); font-size: var(--fs-chip);
}
.shareroom-composer-change {
  color: var(--ink-dim); font-size: inherit; text-decoration: underline; text-underline-offset: 2px;
}
.shareroom-composer-change:hover, .shareroom-composer-change:focus-visible { color: var(--accent); }
.shareroom-composer-identity[hidden], .shareroom-composer-row[hidden],
.shareroom-composer-byline[hidden] { display: none; }
/* The honest cap/error line — full-width under the composer row, in the refusal hue. */
.shareroom-composer-note {
  flex-basis: 100%; margin: 0; color: var(--rust); font-size: var(--fs-chip);
}
.shareroom-composer-note[hidden] { display: none; }
/* Narrow (phones — the recipient's PRIMARY context, brief §3). The 480px breakpoint is
   a deliberate widening of the brief's literal "≤380px": modern phones report 390–430
   CSS px (iPhone 12–15 = 390–393, Pro Max = 430, Pixel = 412) — a 380px gate would miss
   nearly every real handset the mobile pass exists to serve. Intent (phones get the
   phone layout) over the spec's placeholder number. The room is already full-bleed at
   every size (PRESENT-FULLBLEED-1); here the RIGHT drawer flips to a bottom SHEET (a
   side drawer wastes a phone's width) and the play target grows touch-comfortable. */
@media (max-width: 480px) {
  .shareroom-head { padding: var(--sp3); }
  .shareroom-logo-slot { width: auto; max-width: min(150px, 34vw); height: 44px; }
  .shareroom-stage { inset: var(--sp3); }
  .shareroom-transport { gap: var(--sp2); padding: var(--sp3); }
  .shareroom-seek { height: 36px; }
  .shareroom-madewith { display: none; }
  /* a touch-comfortable play target on a phone (still the same minimal glyph) */
  .shareroom-play { width: 36px; height: 36px; }
  .shareroom-sidecar {
    top: auto; left: 0; right: 0; bottom: 0; width: auto;
    border-left: 0; border-top: 1px solid var(--line); border-radius: 12px 12px 0 0;
    max-height: 64vh; box-shadow: 0 -12px 40px oklch(0 0 0 / 0.5);
  }
  .shareroom-comment-list { max-height: none; }
}

/* SHARE-VIEWER-2a — the REVIEW card (sharereview.ts): the editor-skin recipient face.
   Where the room (above) is full-bleed cinema, THIS is a reading CARD — a player over a
   scrollable transcript with clickable word-seek + speaker chrome. Zero new color: the
   room's transport idiom (--surface/--wash/--line, --accent on interaction), and the
   editor's karaoke wash (--accent-sel) for the playhead's current word. */
/* The card mounts INTO #route-page (the 460px door column). A reading transcript + a
   16:9 player want more measure, so reclaim ~760 — scoped with :has() to the card's
   presence so the loading/miss/challenge states (card absent) keep the 460 door column,
   no JS class to leak. (0,2,0 beats .route-page's 0,1,0, order-independent.) */
.route-page:has(.sharereview-card) {
  width: 100%; max-width: 760px; margin: 0;
  padding: var(--sp5) var(--sp4); justify-self: center;
}
.sharereview-card {
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
  padding: var(--sp4);
}
.sharereview-head { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--sp3); }
.sharereview-org {
  color: var(--ink-faint); font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: 0.18em;
}
.sharereview-title { color: var(--ink); font-size: 16px; font-weight: 600; margin: 0; }
/* the player — a 16:9 well the composed stage's <video> letterboxes on */
.sharereview-stage { margin-bottom: var(--sp2); }
.sharereview-frame {
  position: relative; aspect-ratio: 16 / 9;
  background: oklch(0.10 0.004 255); border-radius: 8px; overflow: hidden;
}
.sharereview-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background: #000;
}
/* WR-SIGNPLAY-ERR-1 — the transient "reload to continue" notice, centered over the
   video well. Same reload-fixable framing as the room; distinct from media-dead. */
.sharereview-medianote {
  position: absolute; z-index: 2; left: 50%; top: 50%; transform: translate(-50%, -50%);
  max-width: 80%; text-align: center;
  padding: var(--sp2) var(--sp3); border-radius: 8px;
  background: rgba(0, 0, 0, 0.72); color: #fff; font-size: var(--fs-chip);
}
/* the transport — the room's minimal chrome (never native controls, the no-download
   posture), always visible here (a reading card does not idle-fade its chrome). */
.sharereview-transport {
  display: flex; align-items: center; gap: var(--sp3);
  margin-bottom: var(--sp3); padding-bottom: var(--sp3);
  border-bottom: 1px solid var(--line);
}
.sharereview-play {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 6px; flex: none;
  background: var(--wash); border: 1px solid var(--line);
  color: var(--ink); font-size: 13px; line-height: 1;
}
.sharereview-play:disabled { color: var(--ink-deco); cursor: default; }
.sharereview-play:not(:disabled) { cursor: pointer; }
.sharereview-play:not(:disabled):hover,
.sharereview-play:focus-visible { border-color: var(--accent); color: var(--accent); }
.sharereview-seek {
  position: relative;   /* the comment-marker anchor (COMMENT-WORD-ANCHOR-1) */
  flex: 1; height: 4px; border-radius: 2px; background: var(--line);
  cursor: pointer; touch-action: none;
}
/* keyboard-operable slider (SHAREROOM-KEYBOARD-SEEK-1) — the focus ring, offset off the rail. */
.sharereview-seek:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.sharereview-seek-fill { height: 100%; background: var(--accent); border-radius: 2px; }

/* COMMENT-WORD-ANCHOR-1 + SHARE-MARKER-SEEK-1 — a guest comment's position on the
   recipient scrub bar, now an OPERABLE pip. One warm dot sitting just ABOVE the bar (so it
   never collides with the accent fill sliding under it), centered on its edited-time
   fraction (left set inline). A click seeks to the comment's anchor; a hover reveals the
   mini tooltip below. It owns its own pointerdown in JS (stopPropagation) so it never
   scrubs the bar under it — the pip is interactive, the bare rail still scrubs. One class,
   BOTH surfaces (the funnel line): the room defines --room-accent, the review card
   doesn't, but the marker is --comment-mark on both — a warm, surface-independent hue. */
.share-scrub-marker {
  position: absolute; z-index: 2; bottom: calc(50% + 6px); margin-bottom: 3px; margin-left: -3px;
  width: 6px; height: 6px; padding: 0; border: none; border-radius: 50%;
  background: var(--comment-mark); box-shadow: 0 0 0 1px var(--bg-deep);
  cursor: pointer; transition: transform 90ms ease;
}
.share-scrub-marker:hover, .share-scrub-marker:focus-visible { transform: scale(1.4); }
.share-scrub-marker:focus-visible { outline: 2px solid var(--comment-mark); outline-offset: 2px; }
/* The mini tooltip (author + first words), floated above the pip, revealed on hover/focus.
   pointer-events:none so it never blocks the pip's own click; nowrap + a max-width keep it
   a tidy label, not a wrapping block. */
.share-scrub-marker-tip {
  position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
  margin-bottom: 7px; padding: 5px 8px; border-radius: 6px;
  background: var(--bg-deep); border: 1px solid var(--line);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  font-size: var(--fs-chip); line-height: 1.3; white-space: nowrap;
  max-width: 220px; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none; opacity: 0; visibility: hidden; transition: opacity 90ms ease;
  z-index: 2;
}
.share-scrub-marker:hover .share-scrub-marker-tip,
.share-scrub-marker:focus-visible .share-scrub-marker-tip { opacity: 1; visibility: visible; }
.share-scrub-marker-tip-author { display: block; color: var(--ink); font-weight: 600; }
.share-scrub-marker-tip-body { display: block; color: var(--ink-faint); }
.sharereview-time {
  flex: none; color: var(--ink-faint); font-size: var(--fs-chip);
  font-variant-numeric: tabular-nums;
}
/* SHAREROOM-FULLSCREEN-VOLUME-1 — the review card's mute + volume + fullscreen chrome,
   the room's idiom in the card's --wash/--accent palette. Fullscreen here targets the
   player FRAME (the card keeps its reading measure). */
.sharereview-mute, .sharereview-fs {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 6px; flex: none; cursor: pointer;
  background: var(--wash); border: 1px solid var(--line); color: var(--ink);
}
.sharereview-mute svg, .sharereview-fs svg { width: 16px; height: 16px; display: block; }
.sharereview-mute:hover, .sharereview-mute:focus-visible,
.sharereview-fs:hover, .sharereview-fs:focus-visible { border-color: var(--accent); color: var(--accent); }
.sharereview-mute.is-muted { color: var(--ink-deco); }
.sharereview-fs[hidden] { display: none; }
.sharereview-volume { display: flex; align-items: center; gap: var(--sp2); flex: none; }
.sharereview-vol-slider { width: 64px; height: 4px; flex: none; cursor: pointer; accent-color: var(--accent); }
/* element fullscreen of the player frame — explicit dark surface + backdrop (brief §1) */
.sharereview-frame:fullscreen { width: 100vw; height: 100vh; background: #000; border-radius: 0; }
.sharereview-frame:fullscreen::backdrop { background: #000; }
/* the transcript — a reading column that scrolls within the card (the player stays put) */
.sharereview-transcript { max-height: 56vh; overflow-y: auto; }
/* a speaker label opens each voice change (editedTextOf's speaker prefix, presented) */
.sharereview-speaker {
  color: var(--ink-faint); font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: 0.14em; margin-top: var(--sp3);
}
.sharereview-speaker:first-child { margin-top: 0; }
.sharereview-para {
  margin-top: var(--sp2); color: var(--ink-dim);
  font-size: var(--fs-note); line-height: 1.7;
}
/* each word is a click target that seeks the player (the transcript-as-transport idiom,
   the FEEL-0710-1a/G12 word-seek the editor holds) */
.sharereview-word { cursor: pointer; border-radius: 3px; }
.sharereview-word:hover { color: var(--ink); background: var(--accent-soft); }
/* the playhead's current word — the karaoke wash the editor transcript carries */
.sharereview-word.is-active { background: var(--accent-sel); color: var(--ink); }
/* SHARE-VIEWER-2b — the guest comment lane. A PERSISTENT lane below the transcript (not
   the room's collapsible sidecar: you read AND comment on a review link, so the thread
   stays open), separated by a hairline rule. It reuses the shared .shareroom-comment* /
   .shareroom-composer* treatment (the same cards + composer the room paints) — only the
   lane frame + header are review-specific. */
.sharereview-comments {
  margin-top: var(--sp3); padding-top: var(--sp3); border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: var(--sp3);
}
.sharereview-comments-head {
  margin: 0; color: var(--ink); font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: 0.14em;
}
/* APPROVE-VERB-1 — the sign-off panel. A persistent block above the discussion lane
   (decide, then discuss), the formal review verbs + the cut's decision history. It reads
   in the house language (the reviewer card idiom) — the verbs borrow the two semantic
   status hues (--ready green for Approve, --rust for Request-changes) so the affordance is
   unmistakable without a new palette. Present ONLY on a Review/Edit link (the approve cap);
   the Present screening room never paints it. */
.share-signoff {
  margin-top: var(--sp3); padding-top: var(--sp3); border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: var(--sp2);
}
.share-signoff-title {
  margin: 0; color: var(--ink); font-size: var(--fs-label);
  text-transform: uppercase; letter-spacing: 0.14em;
}
.share-signoff-namerow { display: flex; }
.share-signoff-name {
  flex: 1; padding: var(--sp1) var(--sp2); font-size: var(--fs-note);
  color: var(--ink); background: var(--bg-deep); border: 1px solid var(--line); border-radius: 6px;
}
.share-signoff-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.share-signoff-verbs { display: flex; gap: var(--sp2); }
.share-signoff-btn {
  flex: 1; padding: var(--sp2); font-size: var(--fs-note); font-weight: 600;
  border-radius: 6px; border: 1px solid var(--line); background: transparent; color: var(--ink-dim);
  cursor: pointer; transition: border-color 120ms, color 120ms, background 120ms;
}
.share-signoff-btn:disabled { opacity: 0.5; cursor: default; }
.share-signoff-approve:hover:not(:disabled), .share-signoff-approve:focus-visible {
  border-color: var(--ready); color: var(--ready); background: color-mix(in oklab, var(--ready) 10%, transparent);
}
.share-signoff-changes:hover:not(:disabled), .share-signoff-changes:focus-visible {
  border-color: var(--rust); color: var(--rust); background: var(--rust-wash);
}
.share-signoff-note { margin: 0; color: var(--ink-faint); font-size: var(--fs-note); }
.share-signoff-list { display: flex; flex-direction: column; gap: var(--sp1); }
.share-signoff-empty { margin: 0; color: var(--ink-faint); font-size: var(--fs-note); }
.share-signoff-row {
  display: flex; gap: var(--sp2); align-items: baseline; font-size: var(--fs-note);
  padding-left: var(--sp2); border-left: 2px solid var(--line);
}
.share-signoff-row.share-signoff-approved { border-left-color: var(--ready); }
.share-signoff-row.share-signoff-changes { border-left-color: var(--rust); }
.share-signoff-author { color: var(--ink); font-weight: 600; }
.share-signoff-verb { color: var(--ink-dim); }
.share-signoff-approved .share-signoff-verb { color: var(--ready); }
.share-signoff-changes .share-signoff-verb { color: var(--rust); }
.share-signoff-when { color: var(--ink-faint); margin-left: auto; }

/* Asset Status — one compact metadata control shared by review and edit links. */
.share-status {
  margin-top: var(--sp3); padding-top: var(--sp3); border-top: 1px solid var(--line);
  display: flex; align-items: end; gap: var(--sp2);
}
.share-status-label {
  display: grid; gap: var(--sp1); color: var(--ink-faint); font-size: var(--fs-note);
  text-transform: uppercase; letter-spacing: .12em;
}
.share-status-select {
  min-width: 12rem; min-height: 40px; padding: var(--sp1) 2.25rem var(--sp1) var(--sp2);
  border: 1px solid var(--line); border-radius: 999px; background: var(--bg-deep); color: var(--ink);
  font: inherit; text-transform: none; letter-spacing: 0;
}
.share-status-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.share-status-note { margin: 0 0 var(--sp1); color: var(--rust); font-size: var(--fs-note); }

@media (max-width: 480px) {
  .route-page:has(.sharereview-card) { padding: var(--sp3); max-width: none; }
  .sharereview-play { width: 36px; height: 36px; }   /* touch-comfortable on a phone */
  .share-status { align-items: stretch; flex-direction: column; }
  .share-status-select { width: 100%; min-height: 44px; }
}
/* FACE-F5 — the ⌘F find bar. A floating pill docked to the transcript's
   top-right, above the sticky viewbar (z 9 > the viewbar's 8) so it never fights
   the layout chip. Absolutely positioned within .transcript (its positioning
   context), so revealing it shifts nothing — the transport-row lesson. */
.findbar {
  position: absolute; top: var(--sp3); right: var(--sp4); z-index: 9;
  display: flex; align-items: center; gap: var(--sp2);
  padding: 5px 6px 5px 10px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 10px 30px oklch(0 0 0 / 0.45);
}
.findbar[hidden] { display: none; }
.findbar .find-icn { width: 14px; height: 14px; color: var(--ink-faint); flex: none; }
.findbar #findinput {
  width: 190px; padding: 2px 0; border: 0; background: none;
  color: var(--ink); font-size: var(--fs-body);
}
.findbar #findinput::placeholder { color: var(--ink-faint); }
/* the pill itself carries the focus signal (accent border) when the field is
   focused — the input keeps its own accessible focus ring (never suppressed). */
.findbar:focus-within { border-color: var(--accent); }
.findcount {
  min-width: 4.5em; text-align: right; font-size: var(--fs-chip);
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
.findnav, .findclose {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 5px;
  color: var(--ink-dim); background: none; font-size: 15px; line-height: 1;
}
.findnav:hover, .findclose:hover { color: var(--ink); background: var(--accent-soft); }
.findclose { color: var(--ink-faint); }
/* the current match — a single warm highlight, distinct from the playback wash
   (--accent-soft on .word.cur) and the selection (--accent-sel). The all-matches
   highlight is Jesse's non-blocking look-lane feel-check, deliberately unbuilt. */
.word.findcur {
  background: var(--find-hot, oklch(0.78 0.14 90 / 0.42));
  border-radius: 3px; box-shadow: 0 0 0 1px var(--find-hot, oklch(0.78 0.14 90 / 0.42));
}

.block {
  position: absolute;
  left: var(--geo-pad-x, 32px); right: var(--geo-pad-x, 32px);
  margin: 0;
}
/* FOLD ANIMATION slice 1 (RFC fold-animation) — FLIP survivor-slide. The layout
   snaps instantly (analytic SSOT untouched, §22); view.ts foldWithFlip inverts a
   survivor to its pre-fold top then clears the transform under this class, so it
   rides a compositor-only translateY to its new slot. Transform-only → 60fps, no
   layout thrash. The reduced-motion floor lives in the JS gate (the class is never
   added under :root[data-motion="reduced"]) — ONE source of truth, no @media fork. */
.block.fold-slide { transition: transform 200ms cubic-bezier(0.22, 0.61, 0.36, 1); }
/* FOLD ANIMATION slice 2 (RFC fold-animation) — the collapsing band's fade. A
   member section folds to ZERO height (sectionHTML returns '' — its element is
   GONE), so the vanish is dressed with a short-lived CLONE ghost pinned at the
   band's pre-fold top (view.ts fadeVanishingBands), fading 1→0 while the survivors
   slide up over it. Pointer-inert and a11y-hidden (set in JS). The reduced-motion
   floor lives in the same JS gate as slice 1 — the ghost is never created under
   :root[data-motion="reduced"] (ONE source of truth, no @media fork). */
.block.fold-ghost { pointer-events: none; opacity: 1; transition: opacity 200ms ease-out; }
.block.fold-ghost.fold-ghost-leaving { opacity: 0; }
.line {
  position: absolute; left: 0; right: 0;
  height: var(--geo-line-h, 32px); line-height: var(--geo-line-h, 32px);
  white-space: nowrap;                           /* layout.js owns line breaks */
}
.bhead {
  position: absolute; top: 0; left: 0; right: 0;
  height: var(--geo-header-h, 20px);
  display: flex; align-items: center; gap: var(--sp3);
  font-family: var(--f-ui);
  margin: 0;
}
/* §9 3b — a chapter section reserves the .chead band ABOVE its .bhead
   (layout.ts CHEAD_H), so the section header drops by exactly the band height.
   The geometry contract: CSS reads the same --geo-chead-h pretext reserved. */
.block.chap .bhead { top: var(--geo-chead-h, 44px); }
/* the document-outline header — reads a clear tier ABOVE a section break, in the
   brand serif voice, and BREAKS OUT past the measure-capped text column to the paper edge
   (left/right:-PAD_X negates the .block inset — a pure constant, no DOM read). */
.chead {
  position: absolute; top: 0;
  left: calc(-1 * var(--geo-pad-x, 32px)); right: calc(-1 * var(--geo-pad-x, 32px));
  height: var(--geo-chead-h, 44px);
  display: flex; align-items: flex-end;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.chead-title {
  font-family: var(--f-brand); font-weight: 600;
  font-size: 22px; line-height: 1.1;   /* a tier above the 21px wordmark + 17px body — reads as a document header */
  color: var(--ink); letter-spacing: 0.005em;
  cursor: pointer;   /* CH3 — single-click / Enter SEEKS to the chapter; double-click still renames */
  transition: color 120ms ease-out;
}
/* CH3 — the seek affordance: hover + keyboard focus shift the serif header toward
   the accent so the title reads as the seekable target it is (the triangle owns
   the fold next to it). focus-visible mirrors .chead-tri so the tabindex stop is seen. */
.chead-title:hover { color: var(--accent); }
.chead-title:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
/* CV-2a — a chapter whose every member word is removed reads as removed at the
   document-outline tier: the title derives the EXACT word-strike voice (.word.del)
   — rust-dim ink + a rust line-through, no new token, no new color. view.ts paints
   the .del class from chapterAllStruck (derive-only); the title is still a seekable
   landmark, so hover still warms it (toward --rust here, mirroring .word.del:hover). */
.chead-title.del {
  color: var(--rust-dim);
  text-decoration: line-through;
  text-decoration-color: var(--rust);
  text-decoration-thickness: 1px;
}
.chead-title.del:hover { color: var(--rust); }
/* §22 inline rename — the title-edit input wears the SAME serif header voice so
   the swap is seamless; only a hairline border + surface say "now editable". */
.chead-input {
  font: 600 22px/1.1 var(--f-brand); letter-spacing: 0.005em;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 3px; padding: 0 4px;
}
/* §22.6 #1 — the disclosure triangle: the chead's OWN toggle target, left of the
   title, baseline-aligned to it. The title still seeks; only this flips the fold. */
.chead-tri {
  display: grid; place-items: center;
  width: 16px; height: 16px; flex: 0 0 16px;
  border: none; background: none; padding: 0; margin-right: var(--sp2);
  color: var(--ink-dim); cursor: pointer;
  transition: color 120ms ease-out;
}
.chead-tri:hover { color: var(--ink); }
.chead-tri:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.chead-tri svg,
.chapters-index-row--fold .chapters-index-tick svg {
  transition: transform 0.2s ease;
}
.chead-tri[aria-expanded="false"] svg,
.chapters-index-row--fold[aria-pressed="true"] .chapters-index-tick svg {
  transform: rotate(-90deg);
}
.chead-tri svg,
.chapters-index-row--fold .chapters-index-tick svg {
  transition: transform 0.2s ease;
}
.chead-tri[aria-expanded="false"] svg,
.chapters-index-row--fold[aria-pressed="true"] .chapters-index-tick svg {
  transform: rotate(-90deg);
}
/* §22.6 #4 — the faint "N sections · M words" a folded chapter wears, right of
   the title at the meta tier; never competes with the serif header voice. */
.chead-count {
  margin-left: var(--sp3); font-family: var(--f-ui);
  font-size: var(--fs-note); color: var(--ink-faint); letter-spacing: 0;
}
/* COLLAPSED-CHAPTER-COMMENTS-ROLLUP — the "💬 N" pill a folded head wears when it
   hides comments (the shipped overflow-pill idiom): quiet at rest at the meta tier,
   warms to --accent on hover/focus (it is a live opener — clicking unfolds the
   chapter). position:relative anchors its LANE-HOVER-INFO peek popover. */
.chead-comments {
  position: relative; margin-left: var(--sp2);
  display: inline-flex; align-items: center; gap: 3px;
  border: none; background: none; padding: 0 2px;
  font-family: var(--f-ui); font-size: var(--fs-note); line-height: 1;
  color: var(--ink-faint); cursor: pointer;
  transition: color 120ms ease-out;
}
.chead-comments:hover, .chead-comments:focus-visible { color: var(--accent); }
.chead-comments:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.chead-comments svg { width: 13px; height: 13px; }
.chead-comments-n { font-variant-numeric: tabular-nums; }
/* the peek opens down-left from the pill (it sits mid-header, right of the count) */
.chead-comments .lane-peek { left: auto; right: 0; }
/* CH4 — the chapter delete control. margin-left:auto floats it to the chead's
   right edge, well clear of the left triangle and the center title so a misclick
   can't destroy a chapter. PERSISTENT but de-emphasized (the .manage-remove
   register): faint at rest, warming to --rust on hover/focus — destructive, so it
   wears no fill at rest and never goes hover-only (F-D hover trap; stays keyboard-
   tabbable + touch-reachable). No dedicated --error token; --rust is the app's
   failure register. */
.chead-del {
  margin-left: auto; align-self: center;
  border: none; background: none; padding: 0 2px;
  font-size: 14px; line-height: 1; color: var(--ink-faint); cursor: pointer;
  transition: color 120ms ease-out;
}
.chead-del:hover, .chead-del:focus-visible { color: var(--rust); }
.chead-del:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.grip {
  /* T4(e) + Jesse 2026-06-11 v2: the BUTTON is the full-height rail (whole
     strip clickable, hover answers it); the DOTS are a fixed 2×6 cluster,
     centered — same clean vibe as the original ⠿, just bigger. The cluster
     box is an EXACT multiple of the dot tile (12=2×6px, 42=6×7px), so a
     partial/cut-off dot is geometrically impossible. */
  position: absolute; left: -28px; top: 0; bottom: 0; width: 16px;
  border: none; padding: 0; border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-deco); cursor: grab;
  opacity: 0; transition: opacity 120ms ease-out, color 120ms ease-out;
}
.grip::after {
  content: ""; width: 12px; height: 42px;
  background-image: radial-gradient(currentColor 1px, transparent 1.5px);
  background-size: 6px 7px;
}
/* the rail's OWN hover answers "is this whole strip clickable?" — yes */
.grip:hover { color: var(--ink-dim); background-color: var(--wash-soft); }
.grip:active { cursor: grabbing; color: var(--accent); }
/* §22.7 — on a chapter section the grip drops below the .chead band (the same
   --geo-chead-h drop .bhead takes, l.695), so its full-height hit-box never sits
   on top of the chead's left edge and steal the chapter title's caret/click.
   Pure constant, no DOM read — honors the §22 in-transcript-chrome law. */
.block.chap .grip { top: var(--geo-chead-h, 44px); }

.secmenu, .seckill {
  border: none; padding: 1px 5px; border-radius: 3px;
  color: var(--ink-faint); font-size: var(--fs-note); line-height: 1;
  opacity: 0; transition: opacity 120ms ease-out;
}
/* DESIGN-0710 SECTION-HAMBURGER — the trailing control group floats right: the ☰
   carries the margin-left:auto (it's the first of the pair now that the inline ⤒
   merge is gone), the × follows it on the flex gap. Both stay opacity-revealed, so
   the × never reflows when the ☰ shows/hides. */
.secmenu { margin-left: auto; }
/* §W1.6 P1 — the transcript reveals fire ONLY on a true-hover pointer. On
   coarse/touch the base opacity:0 gives way to the coarse rest-fallback
   (l.~2198: .grip/.secmenu/.seckill rest at 0.55) — no sticky-tap reveal, no
   wasted first tap (Jesse bug #2). Pinned by the "no ungated reveal" audit. */
@media (hover: hover) {
  .block:hover .grip { opacity: 1; }
  .block:hover .secmenu, .block:hover .seckill { opacity: 1; }
  /* FEEL-0710-3(c) LANE-HOVER-INFO — the collapsed-lane peek reveals ONLY on a hover-
     capable pointer (never the touch path, where :hover sticks after a tap) and ONLY when
     the lane is collapsed. Consolidated in the single hover-gated block (design-audit pin). */
  .transcript[data-chap-minimized] .chap-lane-toggle:hover .lane-peek,
  .transcript[data-chap-minimized] .chap-lane-toggle:focus-visible .lane-peek,
  .transcript[data-margin-minimized] .comment-lane-toggle:hover .lane-peek,
  .transcript[data-margin-minimized] .comment-lane-toggle:focus-visible .lane-peek,
  /* LANES-COLLAPSED-VISUAL slice 2 — the collapsed edge icon peeks its content on hover too,
     so a glance reads the starved lane without re-summoning it (same hover-only gate). */
  .transcript[data-tier-chap='collapsed'] .chap-lane-toggle:hover .lane-peek,
  .transcript[data-tier-chap='collapsed'] .chap-lane-toggle:focus-visible .lane-peek,
  .transcript[data-tier-comment='collapsed'] .comment-lane-toggle:hover .lane-peek,
  .transcript[data-tier-comment='collapsed'] .comment-lane-toggle:focus-visible .lane-peek,
  /* COLLAPSED-CHAPTER-COMMENTS-ROLLUP — the folded-head comment pill peeks its rolled-up
     threads on hover/focus too (same hover-only gate; a folded chapter is always the
     "collapsed" case here, so no data-tier guard is needed). */
  .chead-comments:hover .lane-peek,
  .chead-comments:focus-visible .lane-peek {
    display: block;
  }
}
.secmenu:hover { color: var(--ink); }
.seckill:hover { color: var(--rust); }

.speaker {
  border: none; padding: 0;
  font-size: var(--fs-note); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.13em;
  cursor: text;
}
.speaker.sp1 { color: var(--spk1); }
.speaker.sp2 { color: var(--spk2); }
.speaker.sp3 { color: var(--spk3); }
.speaker.sp4 { color: var(--spk4); }
.speaker-input {
  font: 600 var(--fs-note) var(--f-ui); letter-spacing: 0.13em; text-transform: uppercase;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 3px; padding: 1px 4px;
}
/* HEADER-TC RETIRED (Jesse 2026-07-20): the per-section header timecode is no longer
   rendered (view.ts keeps the template as a commented restore path). This rule stays as
   the inert look half of that reference — it costs nothing with no .btc in the DOM. */
.btc { font-size: var(--fs-chip); color: var(--ink-faint); }

.bp { margin: 0; }

.word { border-radius: 2px; cursor: pointer; transition: background .14s ease, color .18s ease, opacity .25s ease; }
:root[data-motion="reduced"] .word { transition: none; }
.word:hover { background: var(--wash); }
/* SELECTION SPOTLIGHT (Jesse 2026-06-29) — a word-range selection dims everything that
   isn't selected so the eye lands on the picked run; the soft opacity settle reads as the
   selection "finalizing". SUPPRESSED while a move-drag is active (.paper.dnd) — dragging the
   selected words must not fade their surroundings (you're placing them, not focusing them).
   Opacity only (no reflow); tunable via --sel-dim-opacity. */
.paper[data-sel-focus]:not(.dnd) .word:not(.sel),
.paper[data-sel-focus]:not(.dnd) .sil:not(.sel) { opacity: var(--sel-dim-opacity, 0.4); }
/* FEEL-0710-3(a) SPEAKER-DIM — the speaker labels are part of the transcript body, so they
   ride the SAME spotlight the words do: a live selection / comment-hover dims every speaker
   label to --sel-dim-opacity so the eye lands on the picked run instead of snagging on the
   still-bright block headers. A label is never `.sel`/`.cmt-lit`, so all of them dim (the
   header isn't the thing you picked). Opacity only, suppressed on .dnd like the word dim. */
.paper[data-sel-focus]:not(.dnd) .speaker { opacity: var(--sel-dim-opacity, 0.4); }
/* FEEL-0710-1(b) COMMENT HOVER-SPAN SPOTLIGHT — hovering a comment (dock card / margin dot)
   dims the transcript down to the words its span covers, the SAME spotlight the selection
   uses (--sel-dim-opacity, --accent-sel), on a PARALLEL flag so it never collides with a
   live selection. Opacity only (no reflow); the lit words wear the selection wash. */
.paper[data-cmt-focus]:not(.dnd) .word:not(.cmt-lit),
.paper[data-cmt-focus]:not(.dnd) .sil:not(.cmt-lit) { opacity: var(--sel-dim-opacity, 0.4); }
.paper[data-cmt-focus]:not(.dnd) .speaker { opacity: var(--sel-dim-opacity, 0.4); }
.word.cmt-lit { background: var(--accent-sel); }
/* filler candidate — dotted underline says "the policy may remove this" */
.word.fill { text-decoration: underline dotted var(--ink-deco); text-underline-offset: 4px; }
/* pinned to stay — amber underline says "protected from the policy" */
.word.fill.kept { text-decoration-color: var(--accent); }
.word.sel { background: var(--accent-sel); }
/* R15 s2 — long-press selection-entry progress (touch-input.md §4). While a
   finger/pen rests on a word, an accent ring grows over the long-press window
   (--lp-ms, published from the resolver's LONG_PRESS_MS so the two can't drift);
   entering selection or yielding to a pan clears the class. A CLASS, never an
   inline style — Spooky's long-press indicator mutated style.removeProperty in
   the gesture handler, the exact sin this investigation exists to retire. */
.word.lp-hold { animation: lp-hold-grow var(--lp-ms, 500ms) ease-out forwards; }
@keyframes lp-hold-grow {
  from { box-shadow: 0 0 0 0 var(--accent-sel); }
  to   { box-shadow: 0 0 0 2px var(--accent-sel); }
}
/* the karaoke/caret word keeps only the wash — the unified .playhead mark (one
   element in the paper layer) owns both the playing underline and the paused
   left-edge stroke now */
.word.cur { background: var(--accent-soft); }
/* CURSOR-1 — a live peer's caret, tinted in their identity hue (the SAME hue as their
   presence seat + their comments, sin #5: one face everywhere). A left-edge bar marks the
   exact word; a faint wash of the same hue tints the word so a peer's position reads at a
   glance. The hue rotates inside the darkroom register (chroma/lightness fixed here), never
   garish. Word-id anchored — repainted on every remount, so the caret follows the WORD
   through reflow and can never land on the wrong text. Applies to words AND silence chips. */
.peer-cursor {
  position: relative;
  /* CURSOR-LIVE-1 — a STRONGER caret post: a 3px saturated bar at the word's leading edge
     (was a 2px, 0.11-chroma inset that read "too subtle" on the first two-up) plus a firmer
     wash. Still the identity register (chroma/lightness fixed, hue rotates) — legible, never
     garish. */
  box-shadow: inset 3px 0 0 0 oklch(0.6 0.15 var(--cursor-hue, 250));
  background: color-mix(in oklab, oklch(0.62 0.14 var(--cursor-hue, 250)) 22%, transparent);
  border-radius: 2px;
}
/* PRES-WS-2 receiver glide — a caret that MOVED this paint eases onto its new word (a brief
   wash+bar fade rising from the leading edge) instead of popping. The word-tint is discrete
   (word-id anchored, not a positioned element — the floating x/y glide is CURSOR-XY-1), so
   the "interpolation" here is an entry ease on the tint itself. Applied only on a real move
   (presenceCursors keys off the last-seen word), so a stationary peer's every-beat re-paint
   never re-pulses. The name flag rides along via the same element. */
@keyframes peer-caret-in {
  from { box-shadow: inset 3px 0 0 0 oklch(0.6 0.15 var(--cursor-hue, 250) / 0); background: transparent; }
  to   { box-shadow: inset 3px 0 0 0 oklch(0.6 0.15 var(--cursor-hue, 250)); background: color-mix(in oklab, oklch(0.62 0.14 var(--cursor-hue, 250)) 22%, transparent); }
}
.peer-cursor.peer-cursor-moved { animation: peer-caret-in 180ms ease-out; }
/* Reduced-motion floor (project SSOT — :root[data-motion="reduced"], NOT the OS media query:
   the pref/OS tier owns that key so gating here keeps ONE source of truth). The caret still
   moves — instantly, no ease. */
:root[data-motion="reduced"] .peer-cursor.peer-cursor-moved { animation: none; }
/* the peer's selection range. CURSOR-VIS-1 (SEL-PROP-1 legibility half, Jesse two-up
   2026-07-03): the 13% wash read "too subtle to notice" — the caret word got a legibility
   bump but the selection run did not. A firmer tint (13%→20%, chroma 0.13→0.15) plus a
   bottom hairline in the identity hue so the run reads as a deliberate BAND, not a faint
   ghost. Still lighter than the caret word's own 22% + 3px bar, so the focus word stays the
   focal point. Bottom-only inset (no side/top ticks) stays continuous across wrapped words. */
.peer-sel {
  background: color-mix(in oklab, oklch(0.62 0.15 var(--cursor-hue, 250)) 20%, transparent);
  box-shadow: inset 0 -1.5px 0 0 color-mix(in oklab, oklch(0.6 0.15 var(--cursor-hue, 250)) 55%, transparent);
}
/* CURSOR-LIVE-1 — a PERSISTENT name flag rooted at the caret post (was hover-only, so you
   couldn't tell WHO was there without hovering). A small tag pinned to the word's top-left,
   its square bottom-left corner sitting on the caret bar like a pennant on its pole. Quiet at
   rest (0.9 alpha), full on hover; never intercepts pointer. Few-peers v1 — one tag per peer
   caret, so no clutter; live carets/labels-de-clutter is a v2 concern. */
.peer-cursor::after {
  content: attr(data-peer);
  position: absolute; left: -3px; bottom: 100%; transform: translateY(-1px);
  font-size: 9.5px; line-height: 1; font-weight: 600; letter-spacing: 0.01em;
  white-space: nowrap; padding: 1.5px 5px; border-radius: 4px 4px 4px 0;
  background: oklch(0.56 0.11 var(--cursor-hue, 250)); color: var(--ink-on-media);
  pointer-events: none; z-index: 5; opacity: 0.9;
}
.peer-cursor:hover::after { opacity: 1; }

/* ============================ CURSOR-XY-1 — the floating peer pointer ============================
   The OTHER half of a peer's presence: not where they're READING (the .peer-cursor word-tint
   above) but where their MOUSE points. Element-anchored, not raw x/y — presencePointers.ts places
   each pointer's tip at a fractional offset inside a hovered word's box (reflow-safe: the anchor's
   box, not a pixel), so it lands on the same content in every viewport. The layer is a positioned
   child of .paper (scrolls with the text); the pointers never eat a click. */
.peer-pointer-layer { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
.peer-pointer {
  position: absolute; left: 0; top: 0;               /* placed via inline left/top per paint */
  pointer-events: none; will-change: left, top;
  /* The glide: a pointer that moves eases to its new spot (Figma's cursor feel). A first
     placement is snapped (.peer-pointer-fresh suppresses this for one paint) so it appears,
     not slides in from the corner. */
  transition: left 180ms cubic-bezier(0.22, 0.61, 0.36, 1), top 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.peer-pointer-fresh { transition: none; }
.peer-pointer-arrow { display: block; filter: drop-shadow(0 1px 1.5px oklch(0 0 0 / 0.35)); }
.peer-pointer-arrow path {
  fill: oklch(0.62 0.16 var(--cursor-hue, 250));
  stroke: var(--ink-on-media, #fff); stroke-width: 1; paint-order: stroke;
}
/* The name flag rides just off the arrow tip, in the peer's identity hue (the caret's twin). */
.peer-pointer-name {
  position: absolute; left: 12px; top: 12px;
  font-size: 9.5px; line-height: 1; font-weight: 600; letter-spacing: 0.01em;
  white-space: nowrap; padding: 1.5px 5px; border-radius: 4px 4px 4px 0;
  background: oklch(0.56 0.13 var(--cursor-hue, 250)); color: var(--ink-on-media);
}
/* Reduced-motion floor (project SSOT — :root[data-motion="reduced"], the same key the caret
   glide uses): the pointer still MOVES, instantly, no slide. */
:root[data-motion="reduced"] .peer-pointer { transition: none; }

/* COMMENTS Shift-3b — the at-rest in-text anchor affordance (synthesis E/C). A
   commented word wears a QUIET underline in the accent register (reuses --accent,
   NOT a second highlighter — sin #5). A STRAIGHT, continuous underline via
   text-decoration (Jesse 2026-06-29): a box-shadow/background underline follows the
   word's border-radius → janky curves + bracket ticks at the span ends; text-decoration
   decorates the GLYPHS, so it ignores border-radius (the hover/selection HIGHLIGHT stays
   rounded while the underline is dead straight). Each word span carries its trailing space
   (view.ts:1173 `…text </span>`), so the underline bridges word-to-word with NO gaps across
   the run — the bracket-tick span ends are retired (a clean continuous rule reads better).
   Recessed at rest (--accent-soft) so it never competes with the playhead wash or selection
   fill; on the rare struck/filler word the line-through/dotted rule wins (the comment still
   shows in the margin), an accepted trade for a straight, gap-free underline. */
.word.cmt-anchor {
  text-decoration: underline;
  text-decoration-color: var(--accent-soft);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.word.cmt-anchor:hover { text-decoration-color: var(--accent); }
.word.del {
  color: var(--rust-dim);
  text-decoration: line-through;
  text-decoration-color: var(--rust);
  text-decoration-thickness: 1px;
}
.word.del:hover { color: var(--rust); background: var(--rust-wash); }
/* T11 hover unity — .span-hot lights EVERY member of the hovered span with the
   stricken hover wash, channel A ONLY (no ink shift — law 5's lift stays on the
   element under the pointer). Members are click targets now (span-select). */
.word.del.span-hot { background: var(--rust-wash); }
.sil-ghost.span-hot, .sil-ghost.span-hot:hover { background: var(--rust-wash); }
.sil-ghost[data-span], .sil-ghost[data-span]:hover { cursor: pointer; }
/* filler removed by the global policy — lighter than a hand cut, click to keep */
.word.fdel {
  color: var(--ink-faint);
  text-decoration: line-through dotted var(--ink-deco);
  text-decoration-thickness: 1px;
}
.word.fdel:hover { color: var(--ink-dim); background: var(--wash-soft); }
/* T9 laws 2+3 — combination cells write channel A ONLY: the fill winner's wash
   one step hotter. Placed AFTER the provenance hovers above so a selected
   stricken word hovers the SELECTION wash (its fill winner) while del's rust
   ink-lift (channel B) still applies — channels compose, fills never stack. */
/* pretext-lab: current + selected reads as JUST selected — the playhead never
   recolours a live selection (Jesse). A drilled single word IS both .cur and
   .sel; making the combo a special hot cell made it look unlike a plain selected
   word, hiding that the scope narrowed section→word. So .sel.cur = .sel, and a
   real hover still lifts (the :hover rule follows, so it wins when hovered). */
.word.sel.cur { background: var(--accent-sel); }
.word.sel:hover { background: var(--accent-sel-hot); }
.word.cur:hover { background: var(--accent-soft-hot); }
/* T11: a SELECTED span member under span-hot shows its fill winner (selection)
   one step hotter — same cell as sel ∧ hover, stated so span-hot can't paint
   rust-wash over a live selection by specificity accident. */
.word.sel.span-hot { background: var(--accent-sel-hot); }
/* pretext-lab: a STRUCK word in a selection keeps the BLUE selection (background =
   selection state ONLY, per the T9 channel laws — no dynamic per-content highlight).
   The clash was rust TEXT on blue; rejection now lives in the TEXT + the STRIKE, not
   the wash. ── KNOBS (Jesse): all clean, channel-safe — they touch only the word's
   own ink + strike, never the selection background:
     • color                       text brightness → --ink (bright) / --ink-dim / --ink-faint
     • text-decoration-color       the strike colour → --rust (red) / --rust-dim
     • text-decoration-thickness   the strike line width → 1px / 2px / 3px
   (NOTE: do NOT use `opacity` here — it would fade the blue selection too, not just
   the text. `color` is the clean text-dim knob.) Placed last so it wins the cascade. */
.word.del.sel {
  color: var(--ink-dim);                  /* dimmer than a live selected word → reads as "rejected", still legible */
  text-decoration-color: var(--rust);     /* red strike */
  text-decoration-thickness: 1px;
}
/* hiding struck content is done by EXCLUSION FROM LAYOUT (layout.js
   showCuts), never display:none — geometry and DOM must always agree. */

/* The inline RIP — a vertical torn edge at every splice. A moved span gets one
   on each side (the bookend); supersedes the old ⤷ arrow ("go here" was the wrong
   metaphor — a splice is a CUT, not a jump). Always-on, derived in layout from the
   one spliceBefore predicate; the heavier "Section at moved joins" tear stays a
   separate opt-in. Box = var(--geo-seam-size) wide + 2px margin = layout's L.seamW
   EXACTLY (both are GEO.SEAM_SIZE + 2), so text stays pixel-aligned; --moved carries
   the (themed) colour via mask. The jaggedness/thickness is Jesse-tunable in the ONE
   --rip-mask data-URI + its stroke-width below — the whole visual knob is right here. */
.word.seam::before {
  content: ""; display: inline-block;
  width: var(--geo-seam-size, 12px); height: 0.92em; vertical-align: -0.16em;
  margin-right: 2px; background: var(--moved);
  -webkit-mask: var(--rip-mask) center / contain no-repeat;
          mask: var(--rip-mask) center / contain no-repeat;
}
/* K2: a DISPLAYED struck word KEEPS the seam's reserved width (layout marks .seam
   on shown cuts too, so striking a moved span doesn't reflow — its box is frozen),
   but the move-squiggle is meaningless on rejected content, so HIDE the mark while
   keeping its box. visibility:hidden (not display:none) preserves the ::before's
   var(--geo-seam-size)+2px so the painted geometry still matches the analytic
   L.seamW — no drift, no shift, no green rip on a corpse. */
.word.del.seam::before, .word.fdel.seam::before { visibility: hidden; }
/* RUST CUT-SEAM (Jesse ruling 2026-07-02): the SAME rip, the removed-voice colour.
   Green (--moved) = content arrived; rust (--stricken) = content LEFT (a hidden cut,
   duration-gated in layout). Identical box → identical L.seamW reserve, so toggling
   Show Removed swaps the cause without reflowing a pixel. Only ever painted on an
   ALIVE arriving word, but mirror the struck-hides rule for symmetry if it's later cut. */
.word.cutseam::before {
  content: ""; display: inline-block;
  width: var(--geo-seam-size, 12px); height: 0.92em; vertical-align: -0.16em;
  margin-right: 2px; background: var(--stricken);
  -webkit-mask: var(--rip-mask) center / contain no-repeat;
          mask: var(--rip-mask) center / contain no-repeat;
}
.word.del.cutseam::before, .word.fdel.cutseam::before { visibility: hidden; }
/* RUST-SEAM-1 Stage 3 — the TRAILING half of the inline rip. The full-width serrated
   `.tear` band is retired; a splice now reads as a rip on BOTH edge words: the leading
   ::before on the arriving word (above) + this trailing ::after on the upper block's
   last shown word. Absolute + zero flow width (no L.seamW reserve → no reflow), so it
   hangs just past the word into the trailing gutter. Same --rip-mask + box as the
   leading mark; --moved for a move-join, --stricken for a hidden cut, matching the
   leading colour across the junction. Hidden on a struck corpse (the ::before rule's
   twin) — no rip on rejected content. */
.word.seamtail, .word.cutseamtail { position: relative; }
.word.seamtail::after, .word.cutseamtail::after {
  content: ""; position: absolute; left: 100%; top: 50%; transform: translateY(-50%);
  width: var(--geo-seam-size, 12px); height: 0.92em; margin-left: 2px; pointer-events: none;
  -webkit-mask: var(--rip-mask) center / contain no-repeat;
          mask: var(--rip-mask) center / contain no-repeat;
}
.word.seamtail::after { background: var(--moved); }
.word.cutseamtail::after { background: var(--stricken); }
.word.del.seamtail::after, .word.fdel.seamtail::after,
.word.del.cutseamtail::after, .word.fdel.cutseamtail::after { display: none; }
:root {
  --rip-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 48'%3E%3Cpath d='M8.5 0 L3 7 L9.5 14 L2.8 21 L9.2 28 L3.2 35 L9 42 L4 48' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* silence chip — the duration is the LABEL, and only the label (Jesse 2026-07-01):
   the chip's width used to also grow proportionally with pause length (a second,
   width-encoded duration signal), but at rest that reserved room just read as
   unexplained blank space before the × rather than a legible meter — retired.
   Every chip is SNUG now (label + × + fixed breathing room), regardless of
   duration or treat. The old amber rule was a redundant THIRD encoding (T2 a)
   and is long gone too. Transport on the body, mutation on the button. */
.sil {
  /* pretext-lab: the chip is WORD-SCALE text on the WORD BASELINE — that's what
     aligns the strike + selection with words for free (no per-marker calibration).
     Mono family + ink-dim keep its "meta" voice so it never reads as a spoken word.
     Selection box matched to a word's PIXEL-FOR-PIXEL (measured in a browser with
     the REAL Literata face: the word inline box is 25.0px at the 17px reading face —
     Literata is a tall design; this flex box lands 24.98px, top/bottom within
     0.01px). 1.47 ≈ Literata's content-area ratio, so it scales with the S/M/L
     presets; the 0.088em nudge re-centers the box on the word baseline (relative,
     not a fixed px, so it scales too). */
  line-height: 1.47; vertical-align: 0.088em;
  display: inline-flex; align-items: baseline;
  gap: var(--geo-chip-gap, 5px);
  overflow: hidden; justify-content: flex-start;
  cursor: pointer; user-select: none;
  font-family: var(--f-tc); color: var(--ink-dim);
  font-size: var(--geo-font-size, 17px);   /* WORD size — pretext measures the chip at this size too */
  padding: 0 var(--geo-chip-pad, 4px);   /* no vertical pad — line-height carries the word-matching height */
  border-radius: 3px;
}
.sil:hover { background: var(--wash-soft); color: var(--ink); }
/* T4(f): a chip inside a span selection wears the WORD selection token — derived
   membership, painted by the view iff both flanking words are selected. Ordered
   like words: sel beats hover; the stricken rust voice (.sil-tight, below) stays
   readable inside a selection. */
/* pretext-lab: a selected chip bridges its trailing joiner (--join-w, the inter-
   token space) with a flat offset box-shadow — the next word is always selected
   too (chip-sel ⇒ both flanks selected), so the span paints gap-free. Box-shadow
   is paint-only: no layout, no width drift. The bridge's left corners hide under
   the chip, so it reads as a few-px wider selection, not a second rounded blob. */
.sil.sel { background: var(--accent-sel); box-shadow: var(--join-w, 0) 0 0 0 var(--accent-sel); }
.sil b { font-weight: var(--geo-font-weight, 350); font-variant-numeric: tabular-nums; }  /* pretext-lab: match WORD weight — 500 mono read heavier/bigger than the 350 serif words */
/* the edit button: ✂ tightens a kept pause, ↺ restores a tightened one. Pinned
   to the chip's right edge (margin-left:auto), larger + legible now the rule's
   room is freed (T2 a), reveal on hover. */
.sil-x {
  margin-left: auto; flex: none;
  border: none; background: none; cursor: pointer;
  padding: 0 2px; line-height: 1; color: var(--ink-dim);
  /* prominent at rest (Jesse): the chip's one action should read without hover */
  font-size: 17px; font-weight: 500; transition: color 120ms;
}
.sil-x:hover { color: var(--rust); }                     /* × = remove → red on hover only */
.sil-x[data-untight]:hover { color: var(--moved); }      /* + = restore → green on hover */
/* UNRAZOR-DOOR-1 (b): the transcript razor mark — a thin inline glyph that names a
   `cut` entry's hard clip edge AND is the un-razor focus target. Reset button chrome
   to an inline, quiet mark that sits in the prose flow before its word. Structural
   only — glyph/weight/hue are look-lane placeholders (Jesse's pass). */
.razor-mark {
  border: none; background: none; cursor: pointer; padding: 0; margin: 0 1px 0 0;
  font-size: 0.72em; line-height: 1; color: var(--ink-faint); vertical-align: 0.05em;
  transition: color 120ms;
}
.razor-mark:hover, .razor-mark:focus-visible { color: var(--accent); }   /* join is a wayfinding verb */
/* a TIGHT pause — hand or policy, same state: a struck label. Click the body to
   hear it; ↺ restores the full pause (the pause-ghost analog of restoring a
   policy-removed filler). */
.sil-tight { color: var(--rust); }              /* same stricken voice as .word.del */
.sil-tight b { text-decoration: line-through var(--rust-dim); text-decoration-thickness: 1px; }  /* solid — same stricken voice + thickness as words (the small chip font thinned it) */
.sil-tight:hover { color: var(--rust); background: var(--rust-wash); }  /* match .word.del:hover */
/* any hand override (kept-whole OR hand-tightened): amber tick, policy-immune */
.sil.pinned b { border-bottom: 1px solid var(--accent); }
/* …but a STRICKEN chip never wears the pin underline — strike + underline is
   visual noise; stricken silences style identically to stricken words (Jesse).
   The pin still counts in the release-pins UI; it just doesn't double-mark. */
.sil-tight.pinned b { border-bottom: none; }

/* ── T2 (c) / TB3: a kept silence the playhead is currently SOUNDING ─────────
   A kept pause plays through as continuous source absorbed into the clip, so
   the chip becomes the current object. It wears the SAME playhead grammar a
   word does — amber wash + the inside-perimeter stroke HERE on the chip, and the
   growing fill + pause caret are the ONE unified .playhead SVG mark, now mounted
   on the chip box (view.applyGapChip → placeMark). The old ::after run/caret
   morph is gone (TB3): one corner-travel implementation, zero copies, so pausing
   mid-silence corners exactly like pausing on a word. Feel lives in the .playhead
   block below (Jesse's lane); this rule is just the chip's current-object wash. */
.sil.cur {
  background: var(--accent-soft);
  /* NO perimeter stroke (Jesse, 2026-06-10): current = wash + the bottom run,
     same grammar as words — the full outline read as ugly chrome, not state */
  color: var(--ink);
}
/* T9 laws 2+3 for chips — the IDENTICAL matrix (T7d), same hot tokens; placed
   after .sil-tight:hover so a selected tight chip hovers its fill winner */
/* pretext-lab: same rule as words — current+selected chip reads as just selected */
.sil.sel.cur { background: var(--accent-sel); box-shadow: var(--join-w, 0) 0 0 0 var(--accent-sel); }
.sil.sel:hover { background: var(--accent-sel-hot); box-shadow: var(--join-w, 0) 0 0 0 var(--accent-sel-hot); }
.sil.cur:hover { background: var(--accent-soft-hot); }
/* T12: a GHOST chip — the silence a strike removed, kept for shape. Provenance,
   not affordance: the stricken-word voice (rust-dim ink, struck label), default
   cursor, and NO hover wash or ink lift even selected — hover never animates a
   corpse (states.md). Placed after every chip hover rule so cascade order wins
   the specificity ties. Values are placeholders per the states.md row (Jesse's
   lane). .cur is unreachable on a ghost by construction (severed gaps aren't in
   clips), so only hover/sel need silencing. */
/* pretext-lab: a ghost has NO button but keeps the live chip's reserved width (a
   strike must not reflow). So CENTER the label in that width (kills the left-heavy
   imbalance) and draw the strike as a full-width line across the whole box — it
   crosses the label AND the empty button space, one continuous red rule, instead
   of a short strike that stops at the text. */
.sil-ghost, .sil-ghost:hover { color: var(--rust-dim); background: none; cursor: default; justify-content: center; position: relative; }
.sil-ghost b { text-decoration: none; }   /* the line is the full-width ::after now, not a text strike */
.sil-ghost::after {
  content: ''; position: absolute; pointer-events: none;
  left: var(--geo-chip-pad, 4px); right: var(--geo-chip-pad, 4px); top: 50%;
  border-top: 1px solid var(--rust-dim);   /* stays RED even when selected (Jesse) — not currentColor, which goes ink on selection */
}
/* T11: still selectable — the span's wash applies, rust stays readable in it
   (law 4); no hot step on hover (same corpse rule) */
/* pretext-lab: same as struck words — a selected ghost chip keeps the BLUE
   selection; only the TEXT goes legible ink, the red strike (from .sil-ghost b)
   stays. No rust/green highlight — selection background is one channel. */
.sil-ghost.sel, .sil-ghost.sel:hover { background: var(--accent-sel); box-shadow: var(--join-w, 0) 0 0 0 var(--accent-sel); color: var(--ink-dim); }  /* match the struck-WORD ink (.word.del.sel) — was --ink, read brighter than its neighbours */
/* T11: selection beats span-hot on chips too (fill winner, no hot step — the
   chip's quieter-by-design asymmetry holds) */
.sil-ghost.sel.span-hot, .sil-ghost.sel.span-hot:hover { background: var(--accent-sel); }

/* the unified playhead mark — ONE svg element holding ONE L-path (T1 slice B):
   playing → the lit dash parks over the underline; paused → it retreats around
   the bottom-left corner onto the inside-left edge, then blinks. Geometry + the
   dash metrics (--ph-edge/--ph-line/--ph-total) are set inline from Layout
   (analytic, virtualization-proof). The CSS owns only the motion + the feel —
   durations/curve/blink rhythm are intentionally tunable here (jesse's lane). */
.playhead {
  position: absolute;
  overflow: visible;
  pointer-events: none;
  z-index: 2;
}
.playhead[hidden] { display: none; }
.playhead path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: butt;   /* the spec's flat ends */
  stroke-linejoin: miter; /* a crisp square corner, not a bevel */
}
/* the lit dash is a <length> window followed by a gap longer than the whole
   path (so it never repeats); its length + offset park it over one of the two
   ends. Animating both at once slides it AROUND the corner between them. */
.playhead.playing path {
  stroke-dasharray: var(--ph-line) var(--ph-total);
  stroke-dashoffset: calc(-1 * var(--ph-edge));   /* parked over the underline */
  animation: ph-untravel 220ms cubic-bezier(.4, 0, .2, 1);
}
.playhead.paused path {
  stroke-dasharray: var(--ph-edge) var(--ph-total);
  stroke-dashoffset: 0;                            /* retreated onto the left edge */
  animation:
    ph-travel 220ms cubic-bezier(.4, 0, .2, 1),
    ph-blink 1100ms 220ms infinite;                /* blink ONLY after the travel lands */
}
@keyframes ph-travel {   /* underline → corner → left edge, on pause */
  from { stroke-dasharray: var(--ph-line) var(--ph-total); stroke-dashoffset: calc(-1 * var(--ph-edge)); }
  to   { stroke-dasharray: var(--ph-edge) var(--ph-total); stroke-dashoffset: 0; }
}
@keyframes ph-untravel { /* and back the way it came, on play */
  from { stroke-dasharray: var(--ph-edge) var(--ph-total); stroke-dashoffset: 0; }
  to   { stroke-dasharray: var(--ph-line) var(--ph-total); stroke-dashoffset: calc(-1 * var(--ph-edge)); }
}
/* hard on/off — a planted caret blink, not a fade (the 0.01% ramp is instant).
   Asymmetric duty cycle (Jesse, 2026-06-10): the caret DWELLS then winks —
   ~65% on / 35% off at a calmer 1.1s period beats a 50/50 strobe. */
@keyframes ph-blink { 0%, 64.99% { opacity: 1; } 65%, 100% { opacity: 0; } }
/* P9(d) — the ONE reduced-motion tier. The attribute (not the media query)
   is the key: main.ts resolves pref-or-OS through applyMotion, so the OS
   default and the in-app toggle land on the same selector instead of forking
   into two rule sets that drift. MOTION dies here; informational fades stay —
   the caret blink (it carries play-state), the word-flash locator, hover
   reveals (color/opacity, not movement). */
:root[data-motion="reduced"] .playhead.playing path { animation: none; }                  /* snap, no travel */
:root[data-motion="reduced"] .playhead.paused path { animation: ph-blink 1100ms infinite; } /* snap, but the blink persists */
:root[data-motion="reduced"] .rail,
:root[data-motion="reduced"] .transcript { transition: none !important; }  /* summoned sheets snap, like layout-snap holds */
@keyframes flashin { from { background: var(--accent-soft); } to { background: none; } }
.word.flash { animation: flashin 1.2s ease-out; }

/* word-granular drop: a caret line in the word gap the move lands in */
.word.drop-left { box-shadow: -2px 0 0 var(--accent); }
.word.drop-right { box-shadow: 2px 0 0 var(--accent); }
.block.dragging { opacity: 0.3; }   /* DRAG-GHOST-1: the drag ghost reads lighter (Jesse feel-tune) */
/* CDR-1 — a folded chapter band is a drag handle (reorder the collapse-all index):
   signal it with the grab cursor, and grabbing flips it to grabbing. */
.chap.collapsed[draggable="true"] { cursor: grab; }
.chap.collapsed.dragging { cursor: grabbing; }

/* TRAIL-RETIRE-1: the `.trail` append-dropzone (+ its `.drop-into` affordance) is
   retired. It was always pointer-events:none, so it never caught a real drop; the
   interior merge it named is resolved analytically (a drop at a section's tail →
   end+1 word-granular). Nothing styles it any more. */

/* T10: current-section presence — the playhead's section, quieter than any
   selection. Placeholder value derived from the accent token; the look lane
   owns the final voice, T9's matrix owns compositing (sel-section follows in
   the cascade, so selection paints over presence when both apply). */
.block.cur-section {
  background: var(--presence-wash);
  border-radius: 5px;
}

/* section-scope: the broad selection tints the whole paragraph. The 8px halo gives
   the section content a touch of breathing room inside its highlight (still short of
   the gap between blocks, so two selected sections never bump). */
.block.sel-section {
  background: var(--accent-wash);
  border-radius: 5px;
  box-shadow: 0 0 0 8px var(--accent-wash);
}

/* FEEL-0710-2 (Jesse, flagged 3×): a word-span that ends mid-paragraph gives its
   boundary section(s) a fill too — but a SUBTLER one than a fully-covered section:
   the accent-wash at reduced strength, and NO halo (the 8px halo is the full-cover
   tier's tell). Both the opening and the closing boundary carry it, so the band
   never fills one edge paragraph and drops the other. Zero new color — a weaker mix
   of the SAME --accent-wash token (AT-1). */
.block.sel-section-partial {
  background: color-mix(in oklab, var(--accent-wash) 55%, transparent);
  border-radius: 5px;
}

/* chapter-scope (CS-2): the chapter is the BROADER tier — same accent-wash token
   (zero new color, the AT-1 discipline), a WIDER 12px halo so a whole-chapter
   span reads as one bigger unit than a section (kept 4px wider than .sel-section's
   8px so the tier contrast holds). Placed AFTER .sel-section so a chapter span
   composites over any incidental section paint on a member block. */
.block.sel-chapter {
  background: var(--accent-wash);
  border-radius: 5px;
  box-shadow: 0 0 0 12px var(--accent-wash);
}

/* section-granular drop: ONE full-width rule per boundary, drawn below the
   upper block ('above' survives only at the topmost boundary — TB5). No
   position override here: .block is already absolutely positioned, and a
   `position: relative` on the hovered block yanks it into document flow. */
.block.drop-above::before, .block.drop-below::after {
  content: ""; position: absolute; left: -8px; right: -8px; height: 2px;
  background: var(--accent); border-radius: 2px;
}
.block.drop-above::before { top: -10px; }
.block.drop-below::after { bottom: -10px; }

/* DRAG-TACTILE-1 — the drop boundary opens a REAL gap: the sections at/after the
   insertion point slide down so there's visible room to drop into. Pure transform
   (compositor-only, no reflow), and gated to an ACTIVE drag (`.paper.dnd`) so it
   never touches the FLIP transforms outside a drag (which set their own inline
   transition and win regardless). Coherence rests on two things done in JS, NOT here:
   (1) the resolver reads the static pretext (dropTargetAt staticOnly) so the moving
   content can't feed back — no judder; (2) the indicator repaints only when the
   boundary changes (gestureBind lastDropKey) so this transition completes instead of
   resetting every frame. Because the analytic resolver reports a between-sections gap
   as 'below' the UPPER section, the insertion line (::after on that upper, unmoving
   block) stays glued to the cursor while the space opens beneath it — no line drift. */
/* The base-state transition governs the RETURN (block eases back when the boundary
   leaves it) — 160ms on the FLIP easing. */
.paper.dnd .block { transition: transform 160ms cubic-bezier(.22, .9, .27, 1); }
/* Only the ONE block adjacent to the insertion scoots aside — "this one's getting
   out of the way for you" — the rest of the doc holds still. 18px stays inside the
   24px SEC_GAP so it never collides with the block below. Between-sections gaps
   resolve as 'below' the upper block, so its immediate next sibling is the one that
   moves; the topmost boundary is the lone 'above' case (the first block itself).
   `transition: none` on this active state makes the OPEN instant — the eased return
   above only applies once the class is gone (asymmetric in/out). */
.paper.dnd .block.drop-above,
.paper.dnd .block.drop-below + .block { transform: translateY(18px); transition: none; }

/* CDR-2 — the collapse-all index reorders a WHOLE chapter, a coarser move than a
   section nudge, so its drop boundary wears a bolder insertion bar than the section
   hairline above: a taller 3px accent pill running a touch wider, wrapped in a soft
   accent-wash glow — the SAME tier-contrast language the sel-chapter halo speaks
   (12px vs the section's 8px). Pure restyle of the drop-above/drop-below classes the
   one DnD mechanism already sets on a band; no new token (accent + accent-wash), no
   new class. */
.block.chap.collapsed.drop-above::before,
.block.chap.collapsed.drop-below::after {
  height: 3px; left: -12px; right: -12px;
  box-shadow: 0 0 0 3px var(--accent-wash);
}

/* ------------------------------------------------ rail */
.rail {
  border-left: 1px solid var(--line);
  background: var(--bg-deep);
  overflow-y: auto; min-height: 0;
  display: flex; flex-direction: column;
}
/* SUMMONED tier — the shelf. ONE consistent home for any non-docked region,
   parked off-canvas until its handle summons it. The shelf's EDGE is solved
   (body[data-shelf-edge]): a right side-sheet on wide screens, a bottom sheet
   on narrow ones — same tier, same handles, different attachment. Demotion
   moves a region here; nothing is ever display:none'd away.
   Transitions run ONLY on user open/close — body.layout-snap suppresses them
   during tier/edge flips so resizing never shows panels flying around. */
body[data-tier-rail="summoned"] .rail,
body[data-tier-transcript="summoned"] .transcript {
  position: fixed; z-index: 30;
  background: var(--bg-deep);
  transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
body.layout-snap .rail,
body.layout-snap .transcript { transition: none !important; }
body[data-tier-transcript="summoned"] .transcript { background: var(--bg); }   /* .tscroll scrolls */

/* edge: right — side sheet */
body[data-shelf-edge="right"][data-tier-rail="summoned"] .rail,
body[data-shelf-edge="right"][data-tier-transcript="summoned"] .transcript {
  top: 49px; right: 0; bottom: 58px;
  border-left: 1px solid var(--line);
  transform: translateX(103%);
  box-shadow: -16px 0 40px oklch(0 0 0 / 0.35);
}
body[data-shelf-edge="right"][data-tier-rail="summoned"] .rail { width: min(360px, 92vw); }
body[data-shelf-edge="right"][data-tier-transcript="summoned"] .transcript { width: min(560px, 94vw); }

/* edge: bottom — bottom sheet with a grab notch (the phone presentation) */
body[data-shelf-edge="bottom"][data-tier-rail="summoned"] .rail,
body[data-shelf-edge="bottom"][data-tier-transcript="summoned"] .transcript {
  left: 0; right: 0; bottom: 58px; top: auto;
  height: min(62vh, 560px);
  border-top: 1px solid var(--line);
  border-radius: 10px 10px 0 0;
  /* park FULLY off-screen: 100% + the bottom anchor + shadow reach */
  transform: translateY(calc(100% + 120px));
  box-shadow: 0 -16px 40px oklch(0 0 0 / 0.4);
  padding-top: 14px;
}
body[data-shelf-edge="bottom"][data-tier-rail="summoned"] .rail::before,
body[data-shelf-edge="bottom"][data-tier-transcript="summoned"] .transcript::before {
  content: ""; position: absolute; top: 6px; left: 50%;
  width: 36px; height: 4px; border-radius: 2px;
  transform: translateX(-50%);
  background: var(--line);
}

body[data-shelf="rail"] .rail,
body[data-shelf="transcript"] .transcript { transform: none !important; }

.toolsback {
  position: fixed; inset: 0; z-index: 29; display: none;
  background: var(--scrim);
}
body[data-shelf="rail"] .toolsback,
body[data-shelf="transcript"] .toolsback { display: block; }

/* handles — ONE consistent affordance at every width: a floating cluster in
   the thumb zone, offset past the docked dock if there is one. tools/notes
   are shortcuts into the dock\'s panels; visible only when not docked. */
.handles {
  position: fixed; right: calc(var(--handleRight, 0px) + 10px); bottom: 70px;
  z-index: 31; display: flex; gap: 6px;
}
.handles button {
  display: none;
  padding: 11px 14px; font-size: var(--fs-note); border-radius: 8px;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); box-shadow: 0 6px 20px oklch(0 0 0 / 0.45);
  text-transform: uppercase; letter-spacing: 0.08em;
}
body[data-tier-rail="summoned"] [data-handle="tools"],
body[data-tier-rail="summoned"] [data-handle="notes"] { display: inline-block; }
.handles button.on { color: var(--accent); background: var(--accent-soft); }

/* layout pins are moot when the solver has no docked choices / in watch */
body[data-tier-rail="summoned"] #t-arrange { display: none; }

/* ------------------------------------------------ the dock
   One chrome region; tabs pick the primary PANEL (tools | notes). */
.dock-tabs {
  display: flex; flex: none;
  border-bottom: 1px solid var(--line);
}
body[data-mode="watch"] .dock-tabs button[data-tab="tools"] { display: none; }
/* A review grant is the full editor document in a read-only posture. Keep playback,
   chapters, timeline, transcript navigation, and Notes; remove controls whose only
   meaning is to mutate the cut. The model gate remains the security boundary. */
body[data-editor-access="review"] .dock-tabs button[data-tab="tools"],
body[data-editor-access="review"] .handles [data-handle="tools"],
body[data-editor-access="review"] .dock-pane[data-pane="tools"],
body[data-editor-access="review"] #editmenu,
body[data-editor-access="review"] #addchapter,
body[data-editor-access="review"] .grip,
body[data-editor-access="review"] .chead-del,
body[data-editor-access="review"] .chapters-index-del,
body[data-editor-access="review"] .chapters-index-row--new { display: none; }
body[data-editor-access="review"] .speaker { pointer-events: none; }
body[data-editor-access="review"] .modepick button[data-mode="edit"] { display: none; }

/* Token guests never inherit the account's client/server export affordances. Export
   requires an explicit grant; current review/edit presets confer none. */
body[data-guest] .dock-pane[data-pane="tools"] [data-sec="export"] { display: none; }
.dock-tabs button {
  flex: 1; border: none; border-radius: 0;
  padding: 9px 8px; font-size: var(--fs-chip);
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.1em;
  border-bottom: 2px solid transparent;
}
.dock-tabs button.on { color: var(--accent); background: var(--accent-soft); }
.dock-tabs button:hover { color: var(--ink); }
.dock-pane { display: none; }
#dock[data-tab="tools"] .dock-pane[data-pane="tools"],
#dock[data-tab="notes"] .dock-pane[data-pane="notes"],
#dock[data-tab="fields"] .dock-pane[data-pane="fields"] {
  display: flex; flex-direction: column; flex: 1; min-height: 0;
  overflow-y: auto;
}

.editor-fields { padding: var(--sp4); gap: var(--sp3); }
.editor-fields-state { margin: 0; color: var(--ink-faint); font: var(--fs-ui)/1.5 var(--f-ui); }
.editor-field-row { display: flex; flex-direction: column; gap: var(--sp1); }
.editor-field-label { color: var(--ink-dim); font: 600 var(--fs-label)/1.2 var(--f-ui); text-transform: uppercase; letter-spacing: .08em; }
.editor-field-control {
  width: 100%; color: var(--ink); background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; padding: 9px 10px; font: var(--fs-ui)/1.2 var(--f-ui);
}
.editor-field-control:focus { border-color: var(--accent); outline: 1px solid var(--accent); }
.editor-field-control:disabled { color: var(--ink-dim); opacity: .78; }
.editor-field-note { color: var(--rust-dim); font: var(--fs-note)/1.3 var(--f-ui); }

/* notes panel — placeholder content, real PANEL */
.comments { padding: var(--sp4); }
.cmt-empty { flex: 1; font-size: var(--fs-ui); color: var(--ink-faint); line-height: 1.6; }
/* the READ-failure note (CMT-1/2/3) — DISTINCT from .cmt-empty: a null load means
   "we couldn't ask", not "no notes". The rust idiom (the charter's error hue, off the
   loud-red ban), with a retry on its own line — the home/Recents couldn't-load grammar. */
.cmt-loaderr { flex: 1; font-size: var(--fs-ui); color: var(--rust-dim); line-height: 1.6; }
.cmt-retry {
  display: block; margin-top: var(--sp1);
  background: none; border: none; padding: 0;
  font: inherit; color: var(--accent); cursor: pointer;
}
.cmt-retry:hover, .cmt-retry:focus-visible { text-decoration: underline; }
.cmt-field input {
  width: 100%; font: var(--fs-control) var(--f-ui); color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  padding: 8px 10px;
}

/* the Notes dock feed (COMMENTS W1) — the Frame.io "all comments" view. The
   feed scrolls; the field (W2) stays pinned beneath it. */
.cmt-feed {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp3);
}
.cmt-thread {
  display: flex; flex-direction: column; gap: var(--sp2);
  padding: var(--sp3); border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface);
}
/* resolved threads fade in place — present in the record, visually settled */
.cmt-thread[data-status="resolved"] { opacity: 0.55; }
/* a margin dot click jumps here + pulses the thread once (W3 slice-2b) — the same
   one-shot attention grammar the transcript word-flash uses (reuses `flashin`) */
.cmt-thread.cmt-flash { animation: flashin 1.2s ease-out; }
:root[data-motion="reduced"] .cmt-thread.cmt-flash { animation: none; } /* snap, no pulse */
/* an optimistic note in flight — dimmed AND inert until the server reconciles it.
   The renderOne gate already withholds Reply/Resolve from a provisional row (CD-1) so
   they can't fire its `provisional:` id at the server; pointer-events:none is the belt
   to that suspenders — any stray affordance on the row stays unclickable until the
   real server id swaps in and the dimming clears. */
.cmt[data-pending] { opacity: 0.6; pointer-events: none; }
/* head → body breathing room: stack the identity head over the body with a small
   gap so the note never hugs the avatar/name above it. A reply carries no
   Reply/Resolve buttons to pad its head, so without this its body read cramped
   right under the avatar. Scoped to the dock thread — the inline margin card's
   height is arithmetic (CI-1 density model), so it keeps its own tight spacing. */
.cmt-thread .cmt { display: flex; flex-direction: column; gap: var(--sp2); }
/* center (not baseline) so the WARMTH avatar disc sits level with the name — the
   identity reads as one unit; the small badges/buttons that wrap after still align. */
.cmt-head { display: flex; align-items: center; gap: var(--sp2); flex-wrap: wrap; }
.cmt-author { font-size: var(--fs-ui); font-weight: 600; color: var(--ink); }
/* the conversational "when" (COMMENTS-WARMTH) — a quiet relative stamp beside the
   author so a note reads as something someone said at a time, not a cold record.
   Faint + small so it recedes behind the name; the full ISO rides as a hover title. */
.cmt-time { font-size: var(--fs-chip); color: var(--ink-faint); }
/* the COMMENTS-WARMTH identity avatar (DECISIONS #53) — an initials disc giving
   every author a face. The hue rotates per-person via --cmt-hue (stable, keyed off
   the author id in JS); chroma + lightness are theme-fixed here so it stays in the
   darkroom register and never goes garish. Initials in the constant near-white
   over-media ink for contrast on any hue. Decorative — the name announces. */
.cmt-avatar {
  flex: 0 0 auto;
  width: 22px; height: 22px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media);
  background: oklch(0.56 0.072 var(--cmt-hue, 250));
  user-select: none;
}
/* the cut timecode — a real SEEK button (C3), not decoration: --accent is the
   actionable hue, so it must read as clickable. Stripped of button chrome to keep
   the prior inline-timecode look; underline-on-hover + a focus ring make the
   affordance honest and keyboard-reachable. */
.cmt-tc {
  font-family: var(--f-tc); font-variant-numeric: tabular-nums;
  font-size: var(--fs-chip); color: var(--accent);
  background: transparent; border: 0; padding: 0; cursor: pointer;
}
.cmt-tc:hover { text-decoration: underline; }
.cmt-tc:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.cmt-resolved {
  font-size: var(--fs-chip); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.08em;
}
/* GLOBAL-COMMENT scope label — a dock-only head chip naming a whole-cut ('general')
   note, so it reads distinctly from a timecode-less point note. Same muted register as
   the resolved/visibility chips (the head's quiet metadata voice). */
.cmt-scope {
  font-size: var(--fs-chip); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.08em;
}
/* the internal-note badge (0024 §G) — a 'team' note the review-link guest can't
   see; a lock glyph + label so it reads distinctly from a public note */
.cmt-visibility {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-chip); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.08em;
}
/* COMMENTS-WARMTH — the lock is a token-colored SVG glyph, never the 🔒 emoji (the
   Spooky study found they use SVG glyphs throughout, never emoji — an emoji ships a
   foreign OS-vendor pixel into the darkroom register). A masked stroke-path padlock
   inherits the label's currentColor (--ink-faint), so it recedes with the chip and
   stays AT-distinct: a 'team' note still reads "Internal" + lock, never a public one. */
.cmt-visibility::before {
  content: ''; flex: 0 0 auto; width: 9px; height: 9px;
  background-color: currentColor;
  -webkit-mask: var(--lock-mask) center / contain no-repeat;
          mask: var(--lock-mask) center / contain no-repeat;
  --lock-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='11' width='14' height='10' rx='2'/%3E%3Cpath d='M8 11V7a4 4 0 0 1 8 0v4'/%3E%3C/svg%3E");
}
/* CM-LANE (Jesse 2026-06-29) — the SHARED bottom ACTION ROW: reply + resolve as Frame.io
   icon buttons, right-aligned at the bottom of every comment (dock + inline card). Was a
   pair of text buttons pinned in the head; the icon row is quieter and consistent. */
/* ONE footer row (Jesse 2026-06-29): show/hide-replies toggle on the LEFT, reply/resolve
   icons on the RIGHT. Tight to the body above (the old row felt over-padded). */
.cmt-actions {
  display: flex; align-items: center; gap: var(--sp2);
  margin-top: 2px; min-height: 22px;
}
.cmt-action-icons { margin-left: auto; display: flex; align-items: center; gap: 2px; }
.cmt-reply-btn, .cmt-resolve, .cmt-delete {
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; padding: 0;
  color: var(--ink-dim); background: transparent;
  border: 1px solid transparent; border-radius: 6px; cursor: pointer;
  transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.cmt-reply-btn:hover, .cmt-resolve:hover { color: var(--ink); background: var(--surface); border-color: var(--line); }
.cmt-reply-btn:focus-visible, .cmt-resolve:focus-visible, .cmt-delete:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* a RESOLVED thread's check reads brighter (done), and the thread's data-status fade
   already recedes the rest — so the green-free affirmative is the icon going full-ink. */
.cmt-resolve[data-resolved="true"] { color: var(--ink); }
/* DELETE (COMMENT-DELETE-1) — the trash tones toward the danger hue on hover, and the
   ARMED (`.confirming`) second-click state fills solid so "click again to delete" reads
   unmistakably (the share-revoke danger register). */
.cmt-delete:hover { color: var(--rust); background: var(--surface); border-color: var(--line); }
.cmt-delete.confirming {
  color: var(--rust); background: transparent; border-color: var(--rust);
  width: auto; padding: 0 6px; gap: 4px; font-weight: 600;
}
.cmt-delete.confirming::after { content: 'Remove?'; font-size: var(--fs-chip); }
/* COMMENTS-WARMTH (Jesse #1) — the note body reads in FULL ink, not the dim small
   register it wore as a cold log line. The Spooky study (comments-design.md) confirmed
   their body is full text-foreground, never dimmed; the dim read made a thread feel
   like metadata. --ink lets what someone SAID carry the row, the head recede behind it. */
/* a comment body is reading prose, so it earns --fs-base (the 14px body default),
   not the 12px chrome size — the note is content, legible like body copy */
.cmt-body { font-size: var(--fs-base); color: var(--ink); line-height: 1.5; white-space: pre-wrap; }
/* a comment's BODY is selectable/copyable text (Jesse 2026-06-29) — the inline card lives
   in `.paper` (user-select:none), so the body must opt BACK IN, and the text cursor signals
   it. Applies to the dock too (one rule, DRY). The card's open/reply/resolve controls stay
   pointer; only the prose is a text target. */
.cmt-body { user-select: text; -webkit-user-select: text; cursor: text; }
/* CLAMP long bodies (Jesse 2026-06-29) — an incredibly long note caps at 6 lines in every
   surface so it never dominates; the dock un-clamps in place via [data-expanded] (the
   "Show more" toggle below), the margin stays clamped (clicking the card opens the dock). */
.cmt-body { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6; overflow: hidden; }
.cmt-body[data-expanded] { -webkit-line-clamp: unset; overflow: visible; }
.cmt-more {
  align-self: flex-start; margin-top: var(--sp1); padding: 0;
  background: none; border: 0; cursor: pointer;
  font-size: var(--fs-chip); color: var(--accent);
}
.cmt-more:hover { text-decoration: underline; }
.cmt-replies {
  display: flex; flex-direction: column; gap: var(--sp2);
  margin-left: var(--sp3); padding-left: var(--sp3); border-left: 1px solid var(--line);
}
.cmt-reply .cmt-author { font-weight: 500; }

/* the live mount's feed zone grows + scrolls; the compose field (W2) pins beneath */
.cmt-zone { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* the compose field (COMMENTS W2) — the Slack-like input that authors a note at
   the playhead, pinned below the scrolling feed */
/* the compose field — a 2-col grid (Frame.io compose-Popover idiom): the reply
   banner, input, and error each SPAN both columns (full width); the audience toggle
   and the post button auto-place onto the final row, toggle leading / Comment
   trailing, so the visibility flip is no longer a stranded full-width line of its own */
.cmt-compose {
  display: grid; grid-template-columns: 1fr auto;
  column-gap: var(--sp2); row-gap: var(--sp2); align-items: center;
  padding-top: var(--sp3); margin-top: var(--sp3); border-top: 1px solid var(--line);
}
/* CM-LANE (Jesse 2026-06-29) — the inline REPLY composer: the same composer DOM, but
   relocated UNDER the comment it replies to. Indented behind a reply rail (the thread-
   depth cue) and de-coupled from the bottom border so it reads as belonging to the
   comment above, not the create field below. */
.cmt-compose--reply {
  margin: var(--sp2) 0 var(--sp2) var(--sp3);
  padding: var(--sp2) 0 0 var(--sp3);
  border-top: 0; border-left: 2px solid var(--accent-soft);
}
.cmt-reply-context, .cmt-compose-target, .cmt-compose-input, .cmt-compose-error { grid-column: 1 / -1; }
.cmt-compose-target {
  font: 600 var(--fs-chip) var(--f-ui);
  color: var(--ink-dim);
  letter-spacing: 0.01em;
}
.cmt-compose[data-target="range"] .cmt-compose-target { color: var(--accent); }
.cmt-compose-input {
  /* match the posted body's --fs-base — what you type is the prose you'll read,
     so the size never jumps from composer to feed */
  width: 100%; resize: vertical; font: var(--fs-base) var(--f-ui); color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 4px;
  padding: 8px 10px; line-height: 1.4;
}
.cmt-compose-submit {
  grid-column: 2; justify-self: end; font: var(--fs-control) var(--f-ui); color: var(--bg);
  background: var(--accent); border: none; border-radius: 4px;
  padding: 6px 14px; cursor: pointer;
}
/* the leading toggle row — General-note + audience pills share ONE flex cell (grid
   column 1) so they read as a row rather than colliding in the same grid slot; the post
   button keeps column 2. justify-self:start so the pills hug their content. */
.cmt-compose-controls {
  grid-column: 1; justify-self: start;
  display: inline-flex; align-items: center; gap: var(--sp2);
}
/* the audience toggle (0024 §G) + the General-note scope toggle (GLOBAL-COMMENT-COMPOSE)
   — quiet pills mirroring the filter-bar chip idiom; each reads its rest state, then picks
   up the accent edge when pressed so the chosen audience/scope is legible before you post. */
.cmt-compose-visibility, .cmt-compose-general {
  display: inline-flex; align-items: center; gap: 5px;
  font: var(--fs-control) var(--f-ui); color: var(--ink-dim);
  background: transparent; border: 1px solid var(--line); border-radius: 4px;
  padding: 5px 10px; cursor: pointer;
}
.cmt-compose-visibility:hover, .cmt-compose-general:hover { color: var(--ink); }
.cmt-compose-visibility[aria-pressed="true"], .cmt-compose-general[aria-pressed="true"] {
  color: var(--ink); border-color: var(--accent);
}
/* the dock's failure register is --rust, the app's stricken hue (browse/manage
   error notes all speak it) — NOT the undefined --danger token whose hardcoded
   crimson #c0392b fallback was off the 3-hue status language and the loud-red ban */
.cmt-compose-error { font-size: var(--fs-chip); color: var(--rust); }
/* the reply-context banner (W2) — shown above the input when the field is retargeted
   at a thread; names the author the reply lands under, with a Cancel back to a root note */
/* :not([hidden]) so the hidden attribute still wins — a bare `display:flex` would
   override the UA `[hidden]{display:none}` (author specificity) and strand a visible
   Cancel on every fresh field; the banner only lays out once the mount reveals it. */
.cmt-reply-context:not([hidden]) {
  display: flex; align-items: center; gap: var(--sp2);
  font-size: var(--fs-chip); color: var(--ink-dim);
}
.cmt-reply-label { flex: 1; min-width: 0; }
.cmt-reply-cancel {
  font-size: var(--fs-chip); font-family: var(--f-ui); color: var(--ink-faint);
  background: transparent; border: none; padding: 0; cursor: pointer; text-decoration: underline;
}
.cmt-reply-cancel:hover { color: var(--ink); }
/* the status-write failure surface (W2) — mirrors the compose error, below the feed */
.cmt-status-error, .cmt-delete-error { font-size: var(--fs-chip); color: var(--rust); margin-top: var(--sp2); }

/* the open/all feed filter (W2) — a small segmented toggle above the feed */
.cmt-filter { display: flex; gap: 2px; margin-bottom: var(--sp2); }
.cmt-filter-btn {
  padding: 3px 10px; font-size: var(--fs-chip); font-family: var(--f-ui);
  color: var(--ink-dim); background: transparent;
  border: 1px solid var(--line); border-radius: 3px; cursor: pointer;
}
.cmt-filter-btn[aria-pressed="true"] {
  color: var(--bg); background: var(--accent); border-color: var(--accent);
}

.modepick { display: flex; gap: 2px; }
.modepick button {
  padding: 3px 10px; font-size: var(--fs-chip); border-radius: 3px;
  color: var(--ink-faint); text-transform: uppercase; letter-spacing: 0.08em;
}
.modepick button.on { color: var(--accent); background: var(--accent-soft); }
.monitor {
  padding: var(--sp4);
  background: var(--bg-deep);
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
}
.monitor video {
  width: 100%; background: black;
  border-radius: 4px; display: block;
}
/* side: monitor row is auto-height — the video's own aspect ratio sets it */
main[data-layout="side"] .monitor { border-left: 1px solid var(--line); border-bottom: 1px solid var(--line); }
main[data-layout="side"] .monitor video { aspect-ratio: var(--ar, 16/9); height: auto; }
/* top / tall: the monitor AREA is solved — the video fills it, letterboxing
   whatever sub-pixel remainder the solve leaves */
main[data-layout="top"] .monitor { border-bottom: 1px solid var(--line); }
main[data-layout="tall"] .monitor { border-left: 1px solid var(--line); }
main[data-layout="top"] .monitor video,
main[data-layout="tall"] .monitor video { flex: 1; min-height: 0; object-fit: contain; }
/* MOBILE-2e-video (B1, the OPEN half) — a PORTRAIT clip no longer letterboxes as a
   tiny strip inside the full-width phone banner. In the single-column `top` layout
   the video BOX itself takes the media aspect (aspect-ratio: --ar) and centres in
   the solved band, capped by BOTH dims (max-width/height 100%): a portrait clip
   renders as a tall centred frame that reclaims the wasted vertical band, a 16:9
   clip stays width-bound exactly as before (max-width wins, same box as flex:1
   gave). `top` is the phone single-column stack only — `tall`/`side` (the desktop
   3-/2-column layouts) keep the rule above untouched, so desktop is unaffected. */
main[data-layout="top"] .monitor video {
  flex: 0 1 auto; width: auto; height: auto;
  max-width: 100%; max-height: 100%;
  aspect-ratio: var(--ar, 16 / 9);
  margin: auto;
}
main[data-layout="tall"] .rail { border-left: 1px solid var(--line); }
/* media-dead notice (P6 §3) — the .boot register worn in the monitor: quiet,
   text-first, no theater. Numbers are placeholders in the look lane. */
.medianote {
  color: var(--ink-faint); font-style: italic; font-size: var(--fs-ui);
  padding: var(--sp2) 0; text-align: center;
}
.transport {
  display: flex; align-items: center; gap: var(--sp2);
  margin-top: var(--sp3);
}
.playbtn {
  /* FULLY fixed box — ❚❚ and ▶︎ have different glyph metrics; width alone
     left the HEIGHT free to bump the whole transport row (Jesse) */
  width: 38px; height: 28px; padding: 0; line-height: 26px;
  font-size: var(--fs-control); flex: none; text-align: center;
}
.mutebtn {
  /* same fixed-box discipline — speaker glyphs swap, the box never moves */
  width: 34px; height: 28px; padding: 0; line-height: 26px;
  flex: none; display: inline-flex; align-items: center; justify-content: center;
}
.mutebtn.muted { color: var(--stricken); border-color: var(--stricken); }
.transport .tc {
  font-size: var(--fs-note); flex: none;
  /* reserve the widest timecode (mm:ss.t) so 9:59→10:00 never resizes the
     transport row; right-align so digits grow leftward into the reserve.
     (h:mm:ss.t content will need 9ch — revisit with long-asset support) */
  min-width: 7ch; text-align: right;
}
.rates { display: flex; gap: 2px; flex: none; }
.rates button { padding: 3px 6px; font-size: var(--fs-chip); border-radius: 3px; color: var(--ink-dim); }
.rates button.on { color: var(--accent); background: var(--accent-soft); }

.panel { padding: var(--sp4); border-bottom: 1px solid var(--line); }
.panel h3 {
  margin: 0 0 var(--sp3);
  font-size: var(--fs-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.18em;
  color: var(--ink-faint);
}
/* T6 — collapsible tools sections. The h3 holds a real button (Tab + Enter +
   aria-expanded); collapsing hides every sibling of the h3, which also drops
   the body's controls out of the tab order for free. Height snaps — nothing
   animates, nothing for reduced-motion to disable. Glyphs (⌄/›) and ink are
   PLACEHOLDERS — weight and voice are Jesse's lane. */
.sec-toggle {
  border: 0; border-radius: 0; padding: 0; background: none;
  text-transform: inherit; letter-spacing: inherit;
}
.sec-toggle::before { content: '⌄ '; color: var(--ink-deco); }
.sec-toggle[aria-expanded="false"]::before { content: '› '; }
.sec-toggle:hover { color: var(--ink-dim); }
.sec-toggle:active { background: none; }
.panel.closed > :not(h3) { display: none; }
.panel.closed h3 { margin-bottom: 0; }
.btnrow { display: flex; gap: var(--sp2); flex-wrap: wrap; }
.btnrow.stack { flex-direction: column; }
.wide { width: 100%; text-align: left; display: flex; justify-content: space-between; align-items: baseline; }
.hint { margin-top: var(--sp3); font-size: var(--fs-note); color: var(--ink-faint); line-height: 1.5; }
.hint b { color: var(--ink-dim); }

/* The MP4 export's transient ACTION line — the "Starting…" flash and the refusal the door
   gave THIS click. A quiet note under the Export MP4 button, same note-scale vocabulary as
   .hint. (The render's progress and its download used to pile up under here as a job strip;
   JOB-PROGRESS-DOCK-1 moved them to the app-shell dock, where an export that outlives this
   panel keeps its row. What's left is only the answer to the click you just made.) */
.mp4state { margin-top: var(--sp2); font-size: var(--fs-note); color: var(--ink-dim); line-height: 1.5; }

/* FACE-F2b — the export panel as a DESTINATIONS surface. Each `.dest` is one place the cut
   can go (MP4 · Premiere · Captions · Data): its buttons stack under a quiet uppercase label,
   tight within the group so the outer stack gap reads as the between-destination separation.
   MP4 + Premiere carry `hidden` until exportControl reveals them for an exportable canvas. */
.dest { display: flex; flex-direction: column; gap: var(--sp1); }
.dest-label {
  font-size: var(--fs-note); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.dest .hint { margin-top: var(--sp1); }

.toggle {
  display: grid; grid-template-columns: auto 1fr; gap: 2px var(--sp3);
  align-items: baseline; margin-top: var(--sp3); cursor: pointer; font-size: var(--fs-control);
}
.toggle input { accent-color: var(--accent); margin: 0; position: relative; top: 1px; }
.toggle em {
  grid-column: 2; font-style: normal; font-size: var(--fs-note); color: var(--ink-faint);
}
/* slider row (T8 max-chars): label over a full-width range, same voice as the checkboxes */
.toggle.lslider { grid-template-columns: 1fr; }
.toggle.lslider input[type="range"] { width: 100%; accent-color: var(--accent); top: 0; }
/* text-size segment (P9a): the pressed preset wears the accent fill — the
   primary register's voice at chip scale; hovers ride the shared delta */
.toggle.tsize { grid-template-columns: 1fr auto; cursor: default; }
.tsz-btns { display: inline-flex; gap: 4px; }
.tsz-btns button {
  min-width: 26px; padding: 2px 0; font-size: var(--fs-chip);
  border-radius: 3px; color: var(--ink-dim);
}
.tsz-btns button:hover { color: var(--ink); }
/* word-label presets (P9b width) need a breathing lane; min-width still
   governs the single-letter S/M/L, so the shipped control is untouched */
.pw-btns button { padding: 2px 6px; }
.tsz-btns button[aria-pressed="true"] {
  background: var(--accent); border-color: transparent;
  color: var(--bg-deep); font-weight: 600;
}
/* solver chips (P5 demotion): word-label lane like pw-btns; "solved" is the
   auto-pick's answer, voiced dashed-quiet — informative, never pressed */
.lay-btns button { padding: 2px 6px; }
.lay-btns button.solved { color: var(--ink-dim); border-style: dashed; }
.ctl { margin-top: var(--sp3); }
.ctl .toggle { margin-top: 0; }
.ctlrow { display: flex; align-items: baseline; gap: var(--sp2); }
.subline {
  margin: 2px 0 0 24px; font-size: var(--fs-note); color: var(--ink-faint); min-height: 14px;
}
.subline .link {
  border: none; padding: 0; font-size: var(--fs-note); color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px;
}

/* After Effects-style scrubbable value: drag ↔ to adjust, click to type.
   When the value differs from the applied one, apply/cancel appear inline. */
.scrubval {
  font-family: var(--f-tc); font-size: var(--fs-ui); color: var(--accent);
  font-variant-numeric: tabular-nums;
  border-bottom: 1px dashed color-mix(in oklab, var(--accent) 45%, transparent);
  cursor: ew-resize; user-select: none; -webkit-user-select: none;
  touch-action: none;
}
.scrubval.pending { background: var(--accent-soft); border-radius: 2px; padding: 0 3px; }
.scrubval .scrub-input {
  font: var(--fs-ui) var(--f-tc); width: 4ch; background: var(--surface);
  color: var(--ink); border: 1px solid var(--accent); border-radius: 2px; padding: 0 2px;
}
.confirm { display: inline-flex; gap: 4px; }
.confirm[hidden] { display: none; }   /* author display beats UA [hidden] otherwise */
.confirm button { padding: 1px 7px; font-size: var(--fs-chip); border-radius: 3px; color: var(--ink-dim); }
.confirm .ok { color: var(--accent); border-color: var(--accent); }

/* the verb bar — selection actions DOCKED to the region's bottom edge: a real
   row of the transcript column, so it never floats over text and never fights
   other floaters. Grows into the text-correction / content editor later. */
/* the .selbar / #selpanel rules are RETIRED with the docked verb bar
   (popover-only verbs — Jesse 2026-07-09); the popover carries its own skin. */

/* T11 §4 — the selection popover: a quiet RANGE HANDLE at the selection's focus
   end. It lives IN the paper (so it scrolls with the transcript natively) and
   opens a single lifted action tray toward whichever side has room. The mark is
   semantic selection geometry — a span with two bounds — rather than a generic
   mystery-meat kebab. */
.selpop { position: absolute; z-index: 12; }
.selpop[hidden] { display: none; }   /* author display beats UA [hidden] */
.selpop-icon {
  width: 26px; height: 26px; padding: 0;
  display: inline-grid; place-items: center;
  color: var(--ink-dim); background: var(--surface);
  border: 1px solid var(--line); border-radius: 6px;
  box-shadow: 0 5px 16px var(--scrim);
}
.selpop-icon:hover { color: var(--accent); border-color: var(--ink-dim); background: var(--accent-wash); }
.selpop-icon[hidden] { display: none; }
.selpop-range-mark {
  position: relative; display: block;
  width: 13px; height: 8px;
}
.selpop-range-line {
  position: absolute; left: 0; right: 0; top: 50%;
  height: 1px; background: currentColor;
}
.selpop-range-mark::before,
.selpop-range-mark::after {
  content: ""; position: absolute; top: 0;
  width: 1px; height: 8px; background: currentColor;
}
.selpop-range-mark::before { left: 0; }
.selpop-range-mark::after { right: 0; }
.selpop-verbs {
  display: flex; align-items: center; gap: 2px;
  padding: 3px;
  color: var(--ink-dim); background: var(--surface);
  border: 1px solid var(--line); border-radius: 7px;
  box-shadow: 0 8px 24px var(--scrim);
  animation: selpop-in 100ms ease-out;
}
.selpop[data-align="end"] .selpop-verbs {
  transform-origin: right center;
  translate: calc(-100% + 26px) 0;
}
.selpop-verbs[hidden] { display: none; }
.selpop-verbs button {
  white-space: nowrap; min-height: 28px; padding: 4px 9px;
  font-size: var(--fs-control); color: inherit;
  background: transparent; border-color: transparent; border-radius: 5px;
}
.selpop-verbs button:hover { color: var(--ink); background: var(--wash); border-color: transparent; }
.selpop-verbs [data-verb="remove"]:hover { color: var(--rust); background: var(--rust-wash); }
.selpop-verbs [data-verb="restore"]:hover { color: var(--moved); background: var(--wash); }
@keyframes selpop-in {
  from { opacity: 0; scale: .98; }
  to { opacity: 1; scale: 1; }
}
:root[data-motion="reduced"] .selpop-verbs { animation: none; }
/* R14 §8.2 — the suggestion margin: a recessed gutter docked in the paper's right
   margin (paper is position:relative, so this anchors to it like .selpop does), one
   dot per anchored suggestion. NEUTRAL/non-final feel this slice — recessed neutral
   hue (NOT the teal accent), narrow width, simple disc; the dot hue / width / shape
   pass is BANKED to feel-check-queue.md, not tuned blind. A dot graduates to a live
   button when a caller wires onOpen (thread-open slice) — clicking it opens the
   .suggestion-thread panel naming the proposed ops; clicking again toggles it shut.
   Off-screen markers emit no dot (the engine's locate→null), so this never floats. */
.suggestion-gutter {
  position: absolute; top: 0; right: -1.5rem;
  width: 0.5rem; height: 100%;
  pointer-events: none;          /* the gutter itself is inert; live dots/threads opt back in below */
}
.suggestion-rail { position: absolute; inset: 0; }
.suggestion-dot {
  position: absolute; left: 50%; width: 6px; height: 6px; margin-left: -3px;
  padding: 0; border: 0;
  border-radius: 50%;
  background: var(--ink-deco);   /* recessed neutral, feel-gated — never the accent */
  opacity: 0.6;
}
/* a clickable dot (thread-open slice) is a real button — opt back into pointers + a
   pointer cursor; the inert <div> form stays untouched by the gutter's none above. */
button.suggestion-dot { pointer-events: auto; cursor: pointer; }
button.suggestion-dot:hover, button.suggestion-dot:focus-visible { opacity: 1; }
/* a COMMENT dot rides the SAME rail/gutter/geometry (sin #5) and differs only by
   skin: a hollow RING vs the suggestion's solid disc — structurally distinct at a
   glance without committing a final hue (feel-banked, neutral — never the accent).
   Its click opens the dock thread, so it's always the live <button> form. */
.comment-dot {
  position: absolute; left: 50%; width: 6px; height: 6px; margin-left: -3px;
  padding: 0; border: 1.5px solid var(--ink-deco); /* ring, not fill — the distinct read */
  border-radius: 50%;
  background: transparent;
  opacity: 0.6;
}
button.comment-dot { pointer-events: auto; cursor: pointer; }
button.comment-dot:hover, button.comment-dot:focus-visible { opacity: 1; }

/* CM-LANE close/open (Jesse 2026-06-29) — the minimize/expand toggle at the comments lane's
   top center. */
.comment-lane-bg { display: none; }
.transcript[data-margin-open] .comment-lane-bg {
  display: block; position: absolute; top: 0; bottom: 0;
  left: var(--lane-x, 100%); right: 0; z-index: 1; pointer-events: none;
  background: color-mix(in oklab, var(--bg) 50%, var(--bg-deep));
}

.comment-lane-toggle { display: none; }
.transcript[data-margin-open] .comment-lane-toggle {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  position: absolute; top: var(--sp4); z-index: 7;
  /* Centered within the right lane */
  left: calc(var(--lane-x, 100%) + (100% - var(--lane-x, 100%)) / 2); transform: translateX(-50%);
  width: auto; min-width: 22px; height: 22px; padding: 0 8px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-dim);
  cursor: pointer; transition: color 140ms ease, border-color 140ms ease;
}
.comment-lane-toggle:hover { color: var(--ink); border-color: var(--ink-deco); }
/* the arrow glyph */
.comment-lane-arrow::before { content: "\203A"; font-size: 13px; line-height: 1; }
.transcript[data-margin-minimized] .comment-lane-arrow::before { content: "\2039"; }
/* CM-LANE (Jesse 2026-06-29) — the COLLAPSED comment marker is the author AVATAR disc
   (same recipe as the dock's .cmt-avatar), sitting in the reserved lane where its card
   would (left:100% = .paper's right edge, into the --margin-w strip), push-down-slotted
   by AVATAR_PITCH so two never overlap. Identity-bearing, not the neutral ring — a
   person at a glance; click opens the dock thread. This is the right-lane half of the
   symmetric collapsed-form (the left chapter lane's collapsed form is a marker icon). */
.comment-avatar.comment-marker {
  /* RIGHT-ANCHORED to the scroll area's right edge, same as the card (Jesse 2026-06-29) —
     the mini avatar hugs the scrollbar in the narrow collapsed strip, never floats inward. */
  position: absolute; left: auto; right: calc(-1 * var(--lane-pull, 0px));
  width: 22px; height: 22px; border-radius: 50%; padding: 0; border: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media); background: oklch(0.56 0.072 var(--cmt-hue, 250));
  cursor: pointer; user-select: none;
  pointer-events: auto;   /* opt back into the inert gutter so a real click opens the thread */
}
.comment-avatar.comment-marker:hover, .comment-avatar.comment-marker:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* COMMENTS-INLINE CI-2/CI-4 — a margin comment's EXPANDED inline card (RFC §4): the
   density chooser's 'card' verdict swaps the dot for this, the dock's own row shown
   in the gutter. A READ surface + single entry point (one <button>): no composer ever,
   so its height stays the CI-1 analytic value and the gutter stays decoupled.
   READABLE-WIDTH pass (CI-4): the 0.5rem rail can't hold a legible card, so the card
   FLOATS — a fixed readable width anchored at the rail's right edge, expanding LEFT
   over the paper's right margin as an absolutely-positioned, shadowed, z-raised layer
   (the SAME decoupled-overlay pattern .suggestion-thread already uses on this gutter,
   sin #5). Out of flow ⇒ it perturbs NOTHING in the word column (Probe B 0-delta holds).
   The dot's solved Y rides in via inline `top`. Expansion DIRECTION + exact width stay
   a Jesse feel-gate (banked to feel-check-queue.md); 14rem matches the sibling thread. */
.comment-card {
  /* CM-LANE (Jesse 2026-06-29): the card lives in the RESERVED lane to the RIGHT of
     the text, never over it. `left: 100%` plants its left edge at `.paper`'s right edge
     (the rail spans `.paper`), so it expands RIGHTWARD into the `.tscroll` padding-right
     strip render() reserved (`--margin-w`) — two disjoint regions, word-overlap is
     impossible by construction. Width sizes to the lane (responsive squish) so it never
     spills past the reserved strip; the lane only opens when it clears MARGIN_MIN. */
  /* RIGHT-ANCHORED to the scroll area's right edge (Jesse 2026-06-29): the card's right
     edge sits at the scrollbar via `right: -(--lane-pull)` (published by render() = how far
     right of paper's edge the strip is), NOT at the centered paper's edge. So full cards
     and mini avatars both hug the scrollbar; the text just gains margin when the pane is
     wide. left:auto so the fixed width grows leftward from the anchored right edge. */
  position: absolute; left: auto; right: calc(-1 * var(--lane-pull, 0px)); z-index: 2;
  /* The gutter is pointer-events:none (words underneath stay clickable); every live
     child must opt BACK IN or real clicks fall through to the transcript. Without this
     the card's open-button + replies-toggle look interactive but a real click misses
     them (a synthetic .click() still fires — the trap that hid this). CM-LANE bite. */
  pointer-events: auto;
  /* RESPONSIVE width (Jesse 2026-06-29): render() picks a width in [MIN,MAX] from the pane
     size and publishes it as --card-w; the card MUST render at exactly that width because
     cardHeight() derives its wrap-chars (and thus line count) from the SAME width — render
     at any other width and the prediction drifts and cards overlap. Falls back to 14rem
     (the MIN) before the first publish. */
  width: var(--card-w, 14rem);
  border: 1px solid var(--line);
  border-radius: 6px; background: var(--bg-deep);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);   /* floats above the paper — a decoupled layer, like .suggestion-thread */
  font: inherit; color: var(--ink);
  overflow: hidden;   /* the open/toggle rows' hover wash clips to the rounded corners */
}
/* CM-LANE: the card is now a CONTAINER (div), not a single button — a root open-button,
   a show/hide-replies toggle, and the expanded reply rows are siblings (no nested
   interactive). focus-within so keyboarding into any child lights the card border. */
.comment-card:hover, .comment-card:focus-within { border-color: var(--ink-deco); }

/* GLOBAL-ANCHOR-COMMENT-SURFACE — the pinned "general" slot at the comment lane's HEAD:
   the cut-level (global-anchor) notes the word-Y rail can't place (a note posted with no
   word under the playhead). Sticky to the top of the scroll so it rides the lane head as
   the transcript scrolls; anchored into the SAME reserved strip the cards use (right:
   -lane-pull, width --card-w) so it never overlaps the text — the disjoint-column
   invariant, reused. The gutter is pointer-events:none, so the slot opts its rows back in. */
.cmt-general {
  /* GENERAL-SLOT-CAP (ux board P2): the slot is sticky at the lane head, so an
     unbounded stack of general notes glued a 300px+ block over the transcript and its
     z-index:3 ECLIPSED the word-anchored cards (z:2) scrolling past underneath. Two
     teeth: (1) cap the block at 40vh + scroll internally so it never dominates the
     lane no matter how many globals exist; (2) drop below the word-card z (z:1 < the
     .comment-card z:2) so a word-anchored card always scrolls OVER the slot, never
     behind it — the sticky head yields to the card it would otherwise hide. */
  position: sticky; top: var(--sp2); z-index: 1;
  left: auto; right: calc(-1 * var(--lane-pull, 0px));
  width: var(--card-w, 14rem);
  max-height: 40vh; overflow-y: auto;
  pointer-events: auto;
  display: flex; flex-direction: column; gap: var(--sp2);
}
.cmt-general[hidden] { display: none; }
/* GENERAL-SLOT-CAP: the cap is a flex COLUMN, so its children default to flex-shrink:1
   and would COMPRESS to fit 40vh (each card squished, its .comment-card overflow:hidden
   clipping the body) instead of overflowing into the scroll. Pin them to natural height so
   the cap scrolls, never squishes — the whole point of the overflow-y:auto. */
.cmt-general > * { flex-shrink: 0; }
/* the reused inline cards are position:absolute in the rail — re-flow them in the slot */
.cmt-general .comment-card { position: static; }
/* MINIMIZED lane → the author-avatar disc, the SAME initials/hue recipe as the collapsed
   word marker (.comment-avatar.comment-marker) but STATIC in the slot's flow — a distinct
   class so it never pollutes the word-rail's .comment-marker set. */
.cmt-general-avatar {
  width: 22px; height: 22px; border-radius: 50%; padding: 0; border: none;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media); background: oklch(0.56 0.072 var(--cmt-hue, 250));
  cursor: pointer; user-select: none; pointer-events: auto;
}
.cmt-general-avatar:hover, .cmt-general-avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.transcript[data-margin-minimized] .cmt-general { width: auto; align-items: flex-end; }
/* DRY (Jesse 2026-06-29): the inline card REUSES the dock's renderOne row (sin #5), so its
   body must read at the SAME size as the Notes-tab dock — NO font-size override here. The
   prior `--fs-chip` shrink drifted the inline body to 10px while the dock body is --fs-base
   (14px); dropping it re-unifies them (cardHeight recalibrated for the larger line below). */
/* The head stays ONE row in the narrow inline card — the author truncates so avatar ·
   author · time · INTERNAL never wrap to a 2nd row (a wrapped head adds an unpredicted
   ~23px and breaks the analytic cardHeight → cards graze). The wide dock head is untouched. */
.comment-card .cmt-head { flex-wrap: nowrap; }
.comment-card .cmt-author { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the root READ surface — a full-width transparent button that opens the dock thread */
.comment-card-open {
  display: block; width: 100%; text-align: left;
  padding: var(--sp2); border: 0; background: none;
  font: inherit; color: inherit; cursor: pointer;
}
.comment-card-tc {
  display: inline-block; margin-top: var(--sp1);
  font-size: var(--fs-chip); color: var(--ink-deco);
}
/* the show/hide-replies toggle LINE — the SHARED affordance (CM-LANE, sin #5) used by
   BOTH the margin card AND the Notes dock: a quiet full-width row with a rotating caret.
   The pretext.cool expand/collapse feel informs the caret (a quiet reveal, not an
   accordion slam). In the margin card a hairline + inset divides it from the root. */
/* The replies-toggle now lives IN the footer row (Jesse 2026-06-29) — an inline quiet
   button on the left, NOT a full-width line. The caret stays the pretext-style reveal. */
.cmt-replies-toggle {
  display: inline-flex; align-items: center; gap: var(--sp1);
  padding: 0; border: 0; background: none;
  font: inherit; font-size: var(--fs-chip); color: var(--ink-deco); cursor: pointer;
}
.cmt-replies-toggle::before {
  content: "\25B8"; display: inline-block; font-size: 0.85em;   /* ▸ */
  transition: transform 120ms ease;
}
.cmt-replies-toggle[aria-expanded="true"]::before { transform: rotate(90deg); }
.cmt-replies-toggle:hover { color: var(--ink); }
/* the inline card's footer row (replies-toggle + reply/resolve) — inset to the card's sp2
   gutter so the disclosure caret clears the rounded border (Jesse 2026-06-29: it bumped the
   edge). Matches the open-button + replies padding, so the whole card reads on one margin. */
.comment-card .cmt-actions { padding: 0 var(--sp2); }
/* in the margin card the expanded reply rows read compact (the lane is tight) — no dock
   indent; the dock keeps its own .cmt-replies indent rule (left-border thread cue) above. */
.comment-card .cmt-replies {
  margin-left: 0; padding: 0 var(--sp2) var(--sp2); border-left: 0;
}
/* R14 §8.2 thread-open — the opened suggestion's panel: a neutral card naming the
   proposed ops, docked left of the dot rail and placed at the anchor's Y by the
   shared entryLocator (no second positioner). NEUTRAL/non-final feel this slice —
   the width, hue, and the accept/reject controls are BANKED to feel-check-queue.md. */
.suggestion-thread {
  position: absolute; top: 0; right: 0.75rem;
  width: 14rem; pointer-events: auto;
  padding: var(--sp3); border: 1px solid var(--line);
  border-radius: 6px; background: var(--bg-deep);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
  font-size: var(--fs-chip); color: var(--ink);
  z-index: 2;
}
.suggestion-thread-ops { margin: 0; padding-left: 1.1rem; }
.suggestion-thread-ops li { margin: 0; }
/* the × close affordance — header position only; hue/skin BANKED to feel-check-queue */
.suggestion-thread-close {
  position: absolute; top: var(--sp1); right: var(--sp2);
  border: 0; background: none; cursor: pointer;
  color: var(--ink-faint); font-size: 1rem; line-height: 1; padding: 2px 4px;
}

/* BS-4 BELONG — the in-flow comment bubble LAYER (commentBubbles.ts). A paper-
   relative overlay the band math paints positioned bubbles into. The layer is INERT
   so the word column underneath stays clickable (the gutter's pointer-events rule);
   each bubble opts back in. The bubbles seat in REAL reserved tail space (layout.ts
   bands) BELOW the words they comment on, never over them — so this overlay perturbs
   nothing in the word flow (Probe B 0-delta holds). STRUCTURAL skin only this slice:
   the body content (Shift 2), final hue / density / click-target / seat voice are all
   a Jesse feel-gate (BANKED to feel-check-queue.md), never tuned blind here. */
.comment-bubble-layer { position: absolute; inset: 0; pointer-events: none; }
.comment-bubble {
  position: absolute;            /* left/top/width/height ride in inline from the band rects */
  box-sizing: border-box;
  pointer-events: auto; cursor: pointer;
  text-align: left; overflow: hidden;
  display: flex; align-items: flex-start; gap: var(--sp2);   /* disc | body — the dock row idiom, compacted */
  padding: var(--sp1) var(--sp2);
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--bg-deep); color: var(--ink); font: inherit;
}
.comment-bubble:hover, .comment-bubble:focus-visible { border-color: var(--ink-deco); }
/* the avatar disc — the dock's --cmt-hue law, shrunk to the in-flow scale (the disc
   warms the bubble; final size/feel is banked to feel-check-queue). */
.comment-bubble-avatar {
  flex: 0 0 auto;
  width: 16px; height: 16px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 8px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media);
  background: oklch(0.56 0.072 var(--cmt-hue, 250));
  user-select: none;
}
/* the body snippet — the warm --ink read surface the bubble shrinkwrapped to; clips
   to the bubble's height (overflow:hidden on .comment-bubble) when the band is tight. */
.comment-bubble-body { flex: 1 1 auto; min-width: 0; color: var(--ink); }
/* the three seats read distinct without a second lookup (the class carries the band
   math's chosen placement — drop it and they look alike). A QUIET structural tier
   only; the final per-seat voice is feel-banked. */
.comment-bubble.seat-inline { border-left: 2px solid var(--ink-deco); }
.comment-bubble.seat-full-section { border-left-width: 3px; }
/* tight widths: the verbs matter, the annotations go first */
/* MOBILE-2-STRUCT (b) — at phone width the meta readout drops its nowrap so a long
   "section · N words · 0:03.2" line BREAKS instead of forcing the wrapped row wider
   than the screen (the clipped-both-edges bar in Jesse's shot). Pairs with the
   the retired verb bar once used; desktop keeps the single nowrap meta line. */
@media (max-width: 560px) { .sel-meta { white-space: normal; } }
/* the thumb handles share the bottom-right corner — step over the docked bar */

.keys {
  margin-top: auto;
  padding: var(--sp4);
  display: flex; flex-wrap: wrap; gap: var(--sp2) var(--sp4);
  font-size: var(--fs-chip); color: var(--ink-faint);
}
.keys kbd { margin: 0 2px 0 0; }
/* ------------------------------------------------ strip */
.stripbar {
  border-top: 1px solid var(--line);
  background: var(--bg-deep);
  padding: var(--sp2) clamp(10px, 2vw, 24px);
}
@media (max-width: 640px) { .striplabels span:first-child { display: none; } }
/* touch-action none: a horizontal scrub must never fight page scroll — the
   strip is not a scroll surface (transport-strip §2, same pin as .divider) */
#strip { width: 100%; height: 36px; display: block; cursor: crosshair; border-radius: 3px; touch-action: none; }
.striplabels {
  display: flex; justify-content: space-between;
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--ink-faint); margin-top: 3px;
  font-family: var(--f-ui);
}
.striplabels b { font-weight: 600; }
/* LOOK-2: the provenance colour key (.lg-ord/.lg-reo/.lg-dup) was retired with
   the default provenance paint — the legend follows the neutral paint (empty)
   until a provenance overlay reinstates the colours. */
/* D-1: the legend + zoom readout are the ROOM's chrome — they name marks that
   only exist with the room open, so they show only in mode 3 (the panel's own
   visibility). The right span keeps the resting "kept of" info in every mode. */
#striplegend, #stripzoom { display: none; }
.stripbar[data-tmode="3"] #striplegend,
.stripbar[data-tmode="3"] #stripzoom { display: inline; }
.stripbar[data-tmode="3"] #strip { display: none; }
#stripzoom { color: var(--ink-faint); }

/* T15 transport states — every number here is a structure placeholder for
   Jesse's pass (digest riders). The panel precedes the canvas in the footer,
   so opening it grows the footer UPWARD; the bar row persists beneath. */
.timelinepanel {
  position: relative;   /* anchors the gap-Target pause popover (slice-5 remainder) */
  border-bottom: 1px solid var(--line);
  padding: var(--sp3) 0 var(--sp2); margin-bottom: var(--sp1);
  min-height: 96px;
  display: flex; flex-direction: column; justify-content: center; gap: var(--sp2);
  color: var(--ink-faint); font-family: var(--f-ui); font-size: var(--fs-ui);
}
.tl-rangebar {
  min-height: 32px;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp3);
  padding: 0 2px;
}
.tl-range-state {
  min-width: 0; display: flex; align-items: baseline; gap: var(--sp2);
  font-size: var(--fs-label); letter-spacing: 0.08em; text-transform: uppercase;
}
.tl-range-state > span { color: var(--ink-faint); }
.tl-range-state output {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink); letter-spacing: 0.025em; text-transform: none;
  font-variant-numeric: tabular-nums;
}
.tl-range-actions { display: flex; align-items: center; gap: 5px; }
.tl-range-actions button {
  min-height: 30px; display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 9px; border: 1px solid var(--line); border-radius: 6px;
  background: transparent; color: var(--ink-faint);
  font: 500 var(--fs-control)/1 var(--f-ui); cursor: pointer;
}
.tl-range-actions button:hover:not(:disabled) {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--line));
  color: var(--ink); background: var(--accent-soft);
}
.tl-range-actions button[data-set="true"] {
  border-color: color-mix(in oklab, var(--accent) 52%, var(--line));
  color: var(--ink);
}
.tl-range-actions button:disabled { opacity: 0.38; cursor: default; }
.tl-range-actions kbd {
  color: var(--ink-faint); font: 600 10px/1 var(--f-mono);
  border: 1px solid var(--line); border-radius: 3px; padding: 2px 3px;
}
.tl-range-actions output {
  min-width: 42px; color: var(--ink); font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) {
  .tl-rangebar { align-items: stretch; flex-direction: column; gap: var(--sp2); }
  .tl-range-actions { display: grid; grid-template-columns: 1fr 1fr auto; }
  .tl-range-actions button { justify-content: center; }
}
/* One contextual surface for concrete timeline objects. It stays compact enough
   for precision work, but names both the object (Pause/Cut) and the available verb.
   The notch follows the actual clicked x even when the surface clamps at an edge. */
.tl-actionpop {
  position: absolute; z-index: 12;
  display: flex; align-items: center; gap: 2px; padding: 3px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 7px;
  box-shadow: 0 8px 24px -10px rgb(0 0 0 / 0.62);
}
.tl-actionpop[data-side="above"] { transform: translateY(calc(-100% - 8px)); }
.tl-actionpop[data-side="below"] { transform: translateY(8px); }
.tl-actionpop::before {
  content: ""; position: absolute; left: var(--tl-action-anchor-x); width: 7px; height: 7px;
  margin-left: -4px; background: var(--surface); transform: rotate(45deg);
}
.tl-actionpop[data-side="above"]::before {
  bottom: -5px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.tl-actionpop[data-side="below"]::before {
  top: -5px; border-left: 1px solid var(--line); border-top: 1px solid var(--line);
}
.tl-actionpop[hidden] { display: none; }
.tl-actionpop-context {
  padding: 0 7px; color: var(--ink-faint);
  font: 600 var(--fs-note)/1 var(--f-ui); letter-spacing: 0.08em; text-transform: uppercase;
}
.tl-actionpop-verb {
  min-height: 30px; white-space: nowrap; padding: 4px 9px;
  border: none; border-radius: 4px;
  font: 500 var(--fs-control)/1 var(--f-ui);
  color: var(--ink-faint); background: none; cursor: pointer;
}
.tl-actionpop-verb:hover { color: var(--ink); background: var(--wash); }
.tl-actionpop-verb[aria-pressed="true"] {
  color: var(--accent); background: var(--accent-soft);
}
/* the single-verb popovers (Join, Restore) read as the primary act they are */
.tl-razorpop-verb:hover,
.tl-rustpop-verb:hover { color: var(--accent); background: var(--accent-soft); }
.timelinepanel[hidden] { display: none; }
/* D-1: the timecode ruler above the room. Slim; height is a structure placeholder
   for Jesse's pass. touch-action none so a scrub never fights page scroll (same pin
   as the room + strip). Sits flush above the room canvas (the panel's gap is the
   only seam) so the ticks read as belonging to the strip below. */
.timelineruler { width: 100%; height: 20px; display: block; cursor: crosshair; border-radius: 3px; touch-action: none; }
/* R13 slice 2 → D-9: the timeline canvas. Fills the panel's width. 116px — the
   clip CONTAINER anatomy (caption row → word cells → speaker underline → in-clip
   waveform) needs vertical room; 64px packed it into a ticker-tape ribbon.
   touch-action none so a tap-to-seek never fights page scroll (same pin as strip). */
.timelinecanvas { width: 100%; height: 116px; display: block; cursor: crosshair; border-radius: 3px; touch-action: none; }
/* Z-c: the double-bubble navigator — a slim whole-cut overview beneath the room.
   Its height is a structure placeholder for Jesse's pass; touch-action none so a
   drag never fights page scroll (same pin as the room + strip). The cursor is set
   inline by navigator.ts (grab / ew-resize / pointer) per the grabbed bubble part. */
.timelinenav { width: 100%; height: 22px; display: block; border-radius: 3px; touch-action: none; }
/* D-1: the room's gesture micro-legend footer — a quiet reminder of the editor's
   verbs, riding the transcript chip idiom's register. Look-lane placeholder for
   Jesse's pass (exact copy + weight). */
.tl-gesturelegend {
  font-size: var(--fs-label); letter-spacing: 0.045em;
  color: var(--ink-faint); text-align: center;
  font-family: var(--f-ui);
}
/* LOOK-1 item B: Alt (Option) held over the strip arms the razor — a scissors
   cursor (Premiere-style) so cut-mode is legible before the click. The SVG is
   inline (no asset fetch); crosshair is the fallback for no-SVG-cursor engines.
   The class is toggled by render.ts (refreshCursor) only between gestures. */
.timelinecanvas.razor-arm {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='6' cy='6' r='3'/%3E%3Ccircle cx='6' cy='18' r='3'/%3E%3Cline x1='20' y1='4' x2='8.12' y2='15.88'/%3E%3Cline x1='14.47' y1='14.48' x2='20' y2='20'/%3E%3Cline x1='8.12' y1='8.12' x2='12' y2='12'/%3E%3C/svg%3E") 8 11, crosshair;
}
/* D-6 grip language: the cursor names the verb before the grab, mirroring the press
   priority (a band MOVE beats a trim). A clip edge under proximity reads col-resize
   (trim by word); the movable selection band reads grab (relocate it). Both toggled
   by render.ts (refreshCursor) only between gestures, so a live drag keeps its cursor. */
.timelinecanvas.grip-arm { cursor: col-resize; }
.timelinecanvas.range-edge-arm { cursor: ew-resize; }
.timelinecanvas.band-grab { cursor: grab; }
#timelinetoggle {
  background: none; border: none; padding: 0; margin-left: var(--sp3);
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: var(--ink-faint); cursor: pointer;
}
#timelinetoggle:hover { color: var(--ink); }
/* the chevron flips while the room is open — same door, now an exit */
.stripbar[data-tmode="3"] #timelinetoggle .chev { display: inline-block; transform: rotate(180deg); }


/* Chapter dots' hover bubble (Jesse 2026-07-16 — the podcast-player grammar):
   a small floating title chip above the bar. strip.ts owns position + text
   (fixed coords from the canvas rect); pointer-inert so the bar keeps every
   event. Ink/размер are look-lane placeholders. */
.strip-chaptip {
  position: fixed; z-index: 40; pointer-events: none;
  transform: translate(-50%, -100%);
  background: var(--surface); color: var(--ink);
  border: 1px solid color-mix(in oklab, var(--ink) 16%, transparent);
  border-radius: 6px; padding: 3px 10px;
  font-size: 12px; letter-spacing: 0.01em; white-space: nowrap;
  box-shadow: 0 4px 14px rgb(0 0 0 / 0.35);
}
/* ------------------------------------------------ floaters */
/* follow pill — region-owned: absolute within .transcript, on the edge the
   playhead left by (data-edge set by view.syncFollowPill) */
.pill {
  position: absolute; left: 50%; transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--accent);
  color: var(--accent); border-radius: 99px; padding: 6px 14px;
  font-size: var(--fs-ui); z-index: 9;
  white-space: nowrap;
}
.pill .dir { font-family: var(--f-tc); }
.pill[data-edge="top"] { top: var(--sp3); }
.pill[data-edge="bottom"], .pill:not([data-edge]) { bottom: var(--sp3); }
/* COMMENT-OVERFLOW-INDICATORS — the "+N comments" edge pills ride the COMMENT LANE (right),
   not the transcript centre the follow pill takes, so the two never overlap. Same
   accent-outlined pill token, no new colour; a quiet accent-tint hover echoes the
   avatar disc (style.css avatar). It's a button, so it shows a pointer + compacts. */
.cmt-overflow {
  left: auto; right: var(--sp2); transform: none;
  cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px;
  /* COMMENT-PILL-OVERLAP-NANO — the pill rides the reserved comment-lane strip (the
     rightmost --margin-w px render() publishes). Cap its box to that strip minus its own
     right inset on BOTH sides and clip, so its LEFT edge can never cross laneLeft
     (containerRight − margin-w) into the reading column and overlap a word — the same
     disjoint-column invariant the margin cards obey (DA-2), made structural for the pill.
     In the wide EXPANDED lane the cap dwarfs the pill (inert); it only bites in the narrow
     avatar lane, where the full pill outgrew the 58px strip and spilled ~20px over the text. */
  box-sizing: border-box;
  max-width: calc(var(--margin-w, 320px) - 2 * var(--sp2));
  overflow: hidden;
}
.cmt-overflow[hidden] { display: none; }   /* inline-flex would defeat the hidden attr otherwise */
.cmt-overflow:hover { background: color-mix(in oklab, var(--accent) 12%, var(--surface)); }
.cmt-overflow .cmt-of-n { font-variant-numeric: tabular-nums; }
/* In the MINIMIZED avatar lane (~58px) the full pill (arrow + comment glyph + count) is
   wider than the strip. Drop the redundant comment glyph — the arrow names the direction,
   the count the amount, and the pill's seat in the comment lane + its aria-label already
   say "comments" — and tighten the padding so "⌃ +N" fits the narrow strip without clipping. */
.transcript[data-margin-minimized] .cmt-overflow { gap: 3px; padding: 4px 6px; }
.transcript[data-margin-minimized] .cmt-overflow .icn { display: none; }
/* TRANSCRIPT-ZONE-1 — the pill lives in .transcript-body and the verb bar now docks
   in the .transcript-footer band BELOW it, so the body (and its pill) already end
   above the retired verb bar. The old bottom-lift step-over is obsolete:
   the reserved footer clears the pill structurally, no magic offset to drift. */
.toast {
  position: fixed; bottom: 86px; left: var(--transMid, 40vw);
  transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; padding: 8px 14px; font-size: var(--fs-ui); z-index: 10;
  max-width: 380px;
}

/* ---------- context-menu popover (menu.ts) ---------- */
/* The floating popover every context menu rides. It body-mounts through the
   overlay layer at position:fixed (the caller sets
   left/top); this rule is the SURFACE the widget never carried — an OPAQUE
   --surface card in a --line hairline, seated above every shell layer (z 90 >
   the mobile rail's 50) so it can never read THROUGH to the nav beneath it (the
   "switcher lands on top of the sidebar" sighting was a naked, transparent menu).
   Token-only; the ✓ current mark, the footer register + the separator are the F0
   states. */
/* Dialogs (confirm / text / picker) — the look-lane pass the widget deferred
   (dialog.ts set only `position:fixed` inline, so an un-inset fixed box collapsed
   to the top-left over the logo). The backdrop is a fixed scrim that flex-centers
   the panel; the panel is a --surface card in the same material as .ctx-menu. All
   three dialogs share these classes, so one block dresses them. z-index 200 sits
   above the ctx-menu (90) — a dialog is the top modal. */
.dialog-backdrop {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp5); background: var(--scrim);
}
.dialog-panel {
  width: min(30rem, 100%);
  max-height: calc(100dvh - 2 * var(--sp5)); overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--sp3);
  padding: var(--sp5);
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 24px 64px -24px rgba(0, 0, 0, 0.7);
  color: var(--ink);
}
.dialog-panel[data-danger="true"] { border-color: color-mix(in oklab, oklch(0.55 0.15 25) 55%, var(--line)); }
.picker-panel { width: min(38rem, 100%); gap: var(--sp2); overflow: hidden; }
.picker-description { max-width: 58ch; margin-bottom: var(--sp2); }
.picker-search {
  width: 100%; min-height: 42px; padding: var(--sp2) var(--sp3);
  color: var(--ink); caret-color: var(--accent); font: var(--fs-control)/1.3 var(--f-ui);
  background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
}
.picker-search::placeholder { color: var(--ink-faint); }
.picker-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.picker-list {
  min-height: 8rem; max-height: min(25rem, 52dvh); overflow: auto;
  margin-top: var(--sp2); border-block: 1px solid var(--line);
  scrollbar-color: var(--line-strong, var(--line)) transparent;
}
.picker-group { display: flex; flex-direction: column; padding-block: var(--sp2); }
.picker-group + .picker-group { border-top: 1px solid var(--line); }
.picker-group-heading {
  display: flex; align-items: center; gap: var(--sp2); min-height: 32px; padding: 0 var(--sp2) var(--sp1);
  color: var(--ink); font: 650 var(--fs-control)/1.25 var(--f-ui);
}
.picker-group-copy { display: flex; min-width: 0; flex-direction: column; gap: 1px; }
.picker-group-copy strong { overflow: hidden; color: var(--ink); font: inherit; text-overflow: ellipsis; white-space: nowrap; }
.picker-group-copy small { overflow: hidden; color: var(--ink-faint); font: 400 var(--fs-note)/1.2 var(--f-ui); text-overflow: ellipsis; white-space: nowrap; }
.picker-project-mark, .picker-option-mark {
  position: relative; flex: 0 0 auto; width: 22px; height: 22px;
  color: var(--ink-dim);
}
.picker-project-mark {
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--wash);
}
.picker-project-mark::before {
  content: ""; position: absolute; left: 7px; top: 7px; width: 3px; height: 3px;
  border-radius: 1px; background: currentColor;
  box-shadow: 6px 0 currentColor, 0 6px currentColor, 6px 6px currentColor;
}
.picker-option {
  width: 100%; min-height: 46px; display: flex; align-items: center; gap: var(--sp2);
  padding: 7px var(--sp2); color: var(--ink); text-align: left; cursor: pointer;
  background: transparent; border: 0; border-radius: 6px;
}
.picker-option:hover { background: var(--wash); }
.picker-option:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; background: var(--wash); }
.picker-option[aria-selected="true"] { background: color-mix(in oklab, var(--accent) 11%, transparent); }
.picker-option-mark[data-kind="project"]::before {
  content: "↳"; position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--ink-faint); font: 500 17px/1 var(--f-ui);
}
.picker-option-mark[data-kind="folder"]::before {
  content: ""; position: absolute; left: 2px; right: 2px; bottom: 3px; height: 12px;
  border: 1px solid currentColor; border-radius: 3px;
}
.picker-option-mark[data-kind="folder"]::after {
  content: ""; position: absolute; left: 4px; top: 4px; width: 8px; height: 4px;
  border: 1px solid currentColor; border-bottom: 0; border-radius: 3px 3px 0 0;
}
.picker-option-copy { display: flex; flex: 1 1 auto; min-width: 0; flex-direction: column; gap: 2px; }
.picker-option-copy strong {
  overflow: hidden; color: var(--ink); font: 550 var(--fs-control)/1.25 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.picker-option-copy small {
  overflow: hidden; color: var(--ink-faint); font: var(--fs-note)/1.25 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.picker-option-check { flex: 0 0 auto; color: var(--accent); opacity: 0; font: 650 var(--fs-control)/1 var(--f-ui); }
.picker-option[aria-selected="true"] .picker-option-check { opacity: 1; }
.picker-empty { margin: 0; padding: var(--sp5) var(--sp3); color: var(--ink-faint); text-align: center; font: var(--fs-control)/1.4 var(--f-ui); }
.picker-panel .dialog-actions { margin-top: var(--sp2); }
.picker-confirm { min-width: 7rem; }
.location-picker-panel {
  width: min(42rem, 100%); height: min(39rem, calc(100dvh - 2 * var(--sp5)));
  gap: var(--sp2); overflow: hidden;
}
.location-subject {
  min-height: 48px; display: flex; align-items: center; gap: var(--sp3);
  padding: var(--sp2) var(--sp3); background: var(--wash); border-radius: 7px;
}
.location-subject img {
  width: 64px; aspect-ratio: 16 / 9; flex: 0 0 auto; object-fit: cover;
  background: var(--bg); border: 1px solid var(--line); border-radius: 4px;
}
.location-subject-mark {
  position: relative; width: 30px; height: 36px; flex: 0 0 auto;
  border: 1px solid var(--line-strong, var(--line)); border-radius: 4px;
}
.location-subject-mark::after {
  content: ""; position: absolute; right: -1px; top: -1px; width: 9px; height: 9px;
  background: var(--wash); border-left: 1px solid var(--line-strong, var(--line)); border-bottom: 1px solid var(--line-strong, var(--line));
}
.location-subject strong {
  min-width: 0; overflow: hidden; color: var(--ink); font: 600 var(--fs-control)/1.3 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.location-description { margin-bottom: var(--sp1); }
.location-search { flex: 0 0 auto; }
.location-trail {
  min-height: 36px; display: flex; align-items: center; gap: var(--sp1);
  overflow-x: auto; padding-inline: 2px; white-space: nowrap; scrollbar-width: none;
}
.location-trail::-webkit-scrollbar { display: none; }
.location-crumb {
  min-height: 30px; padding: 4px 7px; color: var(--ink-dim); cursor: pointer;
  background: transparent; border: 0; border-radius: 5px; font: 550 var(--fs-note)/1.2 var(--f-ui);
}
.location-crumb:hover, .location-crumb:focus-visible { color: var(--ink); background: var(--wash); }
.location-crumb[aria-current="location"] { color: var(--ink); }
.location-trail-sep { color: var(--ink-faint); font: var(--fs-control)/1 var(--f-ui); }
.location-list {
  flex: 1 1 auto; min-height: 0; max-height: none; margin-top: 0;
  padding-block: var(--sp1); border-block: 1px solid var(--line);
}
.location-row {
  width: 100%; min-height: 52px; display: flex; align-items: center; gap: var(--sp3);
  padding: var(--sp2); color: var(--ink); text-align: left; cursor: pointer;
  background: transparent; border: 0; border-radius: 6px;
}
.location-row:hover { background: var(--wash); }
.location-row:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; background: var(--wash); }
.location-row-mark { position: relative; width: 28px; height: 28px; flex: 0 0 auto; color: var(--ink-dim); }
.location-row-mark[data-kind="project"] {
  border: 1px solid var(--line); border-radius: 6px; background: var(--wash);
}
.location-row-mark[data-kind="project"]::before {
  content: ""; position: absolute; left: 8px; top: 8px; width: 3px; height: 3px;
  border-radius: 1px; background: currentColor;
  box-shadow: 7px 0 currentColor, 0 7px currentColor, 7px 7px currentColor;
}
.location-row-mark[data-kind="folder"]::before {
  content: ""; position: absolute; left: 2px; right: 2px; bottom: 3px; height: 17px;
  border: 1px solid currentColor; border-radius: 4px;
}
.location-row-mark[data-kind="folder"]::after {
  content: ""; position: absolute; left: 5px; top: 3px; width: 10px; height: 5px;
  border: 1px solid currentColor; border-bottom: 0; border-radius: 3px 3px 0 0;
}
.location-row-copy { display: flex; flex: 1 1 auto; min-width: 0; flex-direction: column; gap: 2px; }
.location-row-copy strong {
  overflow: hidden; color: var(--ink); font: 600 var(--fs-control)/1.3 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.location-row-copy small {
  overflow: hidden; color: var(--ink-faint); font: var(--fs-note)/1.25 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.location-row-arrow { flex: 0 0 auto; color: var(--ink-faint); font: 20px/1 var(--f-ui); }
.location-empty { align-self: center; margin-block: auto; }
.location-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; }
.location-destination-summary {
  min-width: 0; margin: 0; overflow: hidden; color: var(--ink-faint);
  font: var(--fs-note)/1.3 var(--f-ui); text-overflow: ellipsis; white-space: nowrap;
}
.access-dialog {
  width: min(42rem, calc(100vw - 2rem)); max-height: calc(100dvh - 2rem);
  gap: 0; padding: 0; overflow: hidden;
}
.access-dialog-header {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp3);
  padding: var(--sp5) var(--sp5) var(--sp2);
}
.access-dialog-heading { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp2); min-width: 0; }
.access-dialog-mode {
  padding: 3px 8px; color: var(--ink-dim); background: var(--wash); border: 1px solid var(--line);
  border-radius: 999px; font: 600 var(--fs-note)/1.2 var(--f-ui); white-space: nowrap;
}
.access-dialog-close {
  width: 36px; height: 36px; flex: 0 0 auto; display: grid; place-items: center;
  margin: -8px -8px 0 0; padding: 0; color: var(--ink-dim); background: transparent;
  border: 0; border-radius: 6px; font: 400 24px/1 var(--f-ui); cursor: pointer;
}
.access-dialog-close:hover { color: var(--ink); background: var(--wash); }
.access-dialog-close:focus-visible, .access-dialog-row-action:focus-visible,
.access-dialog-invite-actions button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.access-dialog-description, .access-dialog-empty {
  margin: 0; color: var(--ink-dim); font: var(--fs-note)/1.5 var(--f-ui);
}
.access-dialog-description { max-width: 62ch; padding: 0 var(--sp5) var(--sp4); }
.access-dialog-search { padding: 0 var(--sp5) var(--sp4); }
.access-dialog-search-label {
  display: flex; flex-direction: column; gap: var(--sp1);
  color: var(--ink-dim); font: 600 var(--fs-note)/1.35 var(--f-ui);
}
.access-dialog-search-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp2); }
.access-dialog-search-controls input {
  min-width: 0; min-height: 44px; padding: var(--sp2) var(--sp3);
  color: var(--ink); caret-color: var(--accent); font: var(--fs-control)/1.3 var(--f-ui);
  background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
}
.access-dialog-search-controls input::placeholder { color: var(--ink-faint); }
.access-dialog-search-controls input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.access-dialog-search-controls button {
  min-height: 44px; padding-inline: var(--sp4); border: 1px solid var(--accent); border-radius: 7px;
  color: var(--bg); background: var(--accent); font: 650 var(--fs-control)/1 var(--f-ui); cursor: pointer;
}
.access-dialog-search-controls button:disabled { opacity: .45; cursor: default; }
.access-dialog-feedback { margin: calc(-1 * var(--sp2)) var(--sp5) var(--sp3); color: var(--ink-dim); font: var(--fs-note)/1.4 var(--f-ui); }
.access-dialog-feedback[data-tone="success"] { color: var(--accent); }
.access-dialog-feedback[data-tone="error"] { color: var(--danger, #e88b76); }
.access-dialog-body { flex: 1 1 auto; min-height: 0; max-height: min(27rem, 52dvh); overflow-y: auto; border-block: 1px solid var(--line); }
.access-dialog-section { padding: var(--sp3) var(--sp5); }
.access-dialog-section + .access-dialog-section { border-top: 1px solid var(--line); }
.access-dialog-section h3 {
  margin: 0; padding-block: var(--sp1) var(--sp2); color: var(--ink-faint);
  font: 650 var(--fs-note)/1.25 var(--f-ui); letter-spacing: .035em; text-transform: uppercase;
}
.access-dialog-person {
  display: grid; grid-template-columns: 36px minmax(0, 1fr) auto auto;
  align-items: center; gap: var(--sp3); min-height: 60px; padding-block: var(--sp2);
}
.access-dialog-person + .access-dialog-person { border-top: 1px solid color-mix(in oklab, var(--line) 74%, transparent); }
.access-dialog-avatar {
  width: 36px; height: 36px; display: grid; place-items: center; color: var(--ink-dim);
  background: var(--wash); border: 1px solid var(--line); border-radius: 50%;
  font: 650 var(--fs-note)/1 var(--f-ui); letter-spacing: .02em;
}
.access-dialog-avatar.is-pending { border-style: dashed; color: var(--accent); }
.access-dialog-identity { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.access-dialog-identity strong { overflow: hidden; color: var(--ink); font: 600 var(--fs-control)/1.25 var(--f-ui); text-overflow: ellipsis; white-space: nowrap; }
.access-dialog-identity small { color: var(--ink-faint); font: var(--fs-note)/1.35 var(--f-ui); overflow-wrap: anywhere; }
.access-dialog-provenance { color: var(--ink-faint); font: var(--fs-note)/1.25 var(--f-ui); white-space: nowrap; }
.access-dialog-row-action, .access-dialog-invite-actions button {
  min-height: 34px; padding-inline: var(--sp3); color: var(--ink); background: transparent;
  border: 1px solid var(--line); border-radius: 6px; font: 600 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.access-dialog-row-action:hover, .access-dialog-invite-actions button:hover { background: var(--wash); border-color: var(--ink-faint); }
.access-dialog-row-action.is-remove { color: var(--ink-dim); }
.access-dialog-row-action:disabled, .access-dialog-invite-actions button:disabled { opacity: .45; cursor: wait; }
.access-dialog-invite-actions { display: flex; gap: var(--sp1); }
.access-dialog-empty { padding-block: var(--sp3); }
.access-dialog > .dialog-actions { flex: 0 0 auto; margin: 0; padding: var(--sp3) var(--sp5) var(--sp4); }
@media (max-width: 560px) {
  .dialog-backdrop:has(.access-dialog) { align-items: flex-end; padding: 0; }
  .access-dialog { align-self: end; width: 100%; height: 88dvh; max-height: 88dvh; border-radius: 14px 14px 0 0; }
  .access-dialog-header { padding: var(--sp4) var(--sp4) var(--sp2); }
  .access-dialog-description { padding: 0 var(--sp4) var(--sp3); }
  .access-dialog-search { padding: 0 var(--sp4) var(--sp3); }
  .access-dialog-feedback { margin-inline: var(--sp4); }
  .access-dialog-section { padding-inline: var(--sp4); }
  .access-dialog-person { grid-template-columns: 36px minmax(0, 1fr) auto; gap: var(--sp2); }
  .access-dialog-provenance { grid-column: 2; grid-row: 2; margin-top: -8px; }
  .access-dialog-row-action, .access-dialog-invite-actions { grid-column: 3; grid-row: 1 / span 2; }
  .access-dialog-invite-actions { flex-direction: column; }
  .access-dialog-row-action, .access-dialog-invite-actions button { min-height: 44px; }
  .access-dialog > .dialog-actions { padding: var(--sp3) var(--sp4) max(var(--sp3), env(safe-area-inset-bottom)); }
  .access-dialog > .dialog-actions button { min-height: 46px; }
  .dialog-backdrop:has(.picker-panel) { align-items: flex-end; padding: 0; }
  .picker-panel { width: 100%; max-height: 82dvh; padding: var(--sp4); border-radius: 14px 14px 0 0; }
  .picker-search, .picker-option { min-height: 48px; }
  .picker-list { max-height: 48dvh; }
  .picker-panel .dialog-actions button { min-height: 46px; }
  .dialog-backdrop:has(.location-picker-panel) { align-items: flex-end; padding: 0; }
  .location-picker-panel { width: 100%; height: min(86dvh, 42rem); max-height: 86dvh; padding: var(--sp4); border-radius: 14px 14px 0 0; }
  .location-row { min-height: 52px; }
  .location-actions { grid-template-columns: 1fr 1fr; }
  .location-destination-summary { grid-column: 1 / -1; white-space: normal; }
  .location-actions button { min-height: 46px; }
}
.dialog-title { margin: 0; font-family: var(--f-ui); font-size: 18px; font-weight: 600; color: var(--ink); }
.dialog-body { margin: 0; font-family: var(--f-ui); font-size: var(--fs-control); line-height: 1.45; color: var(--ink-dim); }
.dialog-cascade { margin: 0; padding-left: var(--sp4); display: flex; flex-direction: column; gap: var(--sp1); }
.dialog-cascade li { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.dialog-field { display: flex; flex-direction: column; gap: var(--sp1); font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.dialog-input {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink);
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  padding: var(--sp2) var(--sp3);
}
.dialog-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.dialog-textarea { resize: vertical; min-block-size: 5.5rem; line-height: 1.45; }
.dialog-field-hint { color: var(--ink-faint); font-size: var(--fs-note); line-height: 1.35; }
.dialog-field-error { color: var(--rust); font-size: var(--fs-note); line-height: 1.35; }
.dialog-input[aria-invalid="true"] { border-color: var(--rust); }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--sp2); margin-top: var(--sp2); }
.dialog-actions button {
  font-family: var(--f-ui); font-size: var(--fs-control); cursor: pointer;
  padding: var(--sp2) var(--sp4); border-radius: 6px; border: 1px solid var(--line);
}
.dialog-cancel { background: none; color: var(--ink-dim); }
.dialog-cancel:hover { background: var(--bg); color: var(--ink); }
.dialog-confirm { background: var(--accent); border-color: var(--accent); color: var(--bg); font-weight: 600; }
.dialog-confirm:hover { background: color-mix(in oklab, var(--accent) 86%, var(--bg)); border-color: color-mix(in oklab, var(--accent) 86%, var(--bg)); }
.dialog-confirm:disabled { opacity: 0.5; cursor: not-allowed; }
.dialog-confirm.dialog-danger { background: oklch(0.55 0.15 25); border-color: oklch(0.55 0.15 25); color: oklch(0.98 0.01 25); }

.ctx-menu {
  min-width: 208px; max-width: 320px; padding: var(--sp1);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 8px 28px -12px rgba(0, 0, 0, 0.55);
  z-index: 90; display: flex; flex-direction: column; gap: 1px;
}
.ctx-menuitem {
  display: flex; align-items: center; gap: var(--sp2);
  width: 100%; text-align: left; cursor: pointer;
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink);
  background: none; border: none; border-radius: 5px;
  padding: 6px 10px 6px 26px;          /* left pad reserves the ✓ gutter so labels align */
}
.ctx-menuitem:hover, .ctx-menuitem:focus-visible { background: var(--wash); }
.ctx-swatches {
  display: flex; align-items: center; gap: var(--sp2);
  padding: 7px 10px;
}
.ctx-swatches-label {
  flex: 0 0 auto;
  color: var(--ink-dim); font-family: var(--f-ui); font-size: var(--fs-note);
}
.ctx-swatches-options {
  display: flex; align-items: center; justify-content: flex-end; gap: 7px;
  margin-inline-start: auto;
}
.ctx-swatch {
  inline-size: 20px; block-size: 20px; flex: 0 0 20px;
  padding: 0; border: 2px solid transparent; border-radius: 50%;
  background: var(--ctx-swatch); cursor: pointer;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ink) 18%, transparent);
}
.ctx-swatch:hover,
.ctx-swatch:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
.ctx-swatch[aria-checked="true"] {
  border-color: var(--surface);
  box-shadow:
    0 0 0 2px var(--ink),
    inset 0 0 0 1px color-mix(in oklab, var(--ink) 18%, transparent);
}
/* the ✓ current mark — only a CHECKED checkable row paints it; an unchecked one
   keeps the gutter empty, so every label stays on the same left edge. */
.ctx-menuitem[data-checked="true"] { position: relative; }
.ctx-menuitem[data-checked="true"]::before {
  content: "✓"; position: absolute; left: 9px; color: var(--accent); font-size: var(--fs-note);
}
/* the footer register (+ New workspace…) — a quieter create verb, set off the
   list by the separator above it. */
.ctx-menuitem[data-footer="true"] { color: var(--ink-dim); }
.ctx-sep { height: 1px; margin: var(--sp1) 6px; background: var(--line); }

/* Pointer-positioned menus are correct on desktop, where the invocation point
   carries context. On a phone that coordinate is usually a cramped shelf button
   or overflow affordance, so the same menu becomes a bottom sheet: one renderer,
   one action registry, a context-appropriate placement. Physical inset overrides
   intentionally beat menu.ts's inline pointer coordinates at this breakpoint. */
@media (max-width: 40rem) {
  .ctx-menu {
    left: var(--sp2) !important;
    right: var(--sp2);
    top: auto !important;
    bottom: max(var(--sp2), env(safe-area-inset-bottom));
    min-width: 0;
    max-width: none;
    max-height: min(70dvh, 30rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--sp2);
    border-radius: 12px;
  }
  .ctx-menuitem {
    min-block-size: 2.75rem;
    padding-block: var(--sp2);
  }
}

/* ---------- the background-job dock (jobDock.ts) ---------- */
/* JOB-PROGRESS-DOCK-1. One persistent corner surface for every async job the app
   runs — an MP4 render today, an ingest next, auto-chapters after that. It is
   body-level and rides mountOverlayLayer, so it keeps painting after a navigation
   takes the launching surface away; that outliving is the whole reason a dock
   exists instead of a spinner inside the panel that started the work.

   Bottom-RIGHT, deliberately clear of `.toast` (bottom-centre on --transMid): the
   toast is a sentence that leaves, the dock is work that persists, and the two must
   never fight for the same corner. z 80 seats it under the ctx-menu (90) and the
   dialogs (200) — a job row must never cover a modal the user is answering. */
.jobdock {
  position: fixed;
  right: var(--sp3);
  bottom: var(--sp3);
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: var(--sp2);
  width: min(22rem, calc(100vw - 2 * var(--sp3)));
}
.jobdock[hidden] { display: none; }   /* flex would defeat the hidden attr otherwise */

.jobrow {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  padding: var(--sp2) var(--sp2) var(--sp2) var(--sp3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: var(--f-ui);
}
.jobrow-main { flex: 1 1 auto; min-width: 0; }
.jobrow-label {
  font-size: var(--fs-ui);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.jobrow-detail { font-size: var(--fs-note); color: var(--ink-dim); margin-top: 1px; }
/* The state colour lives on the DETAIL line, not the label: the job's name doesn't
   change when it finishes, its state does. */
.jobrow[data-phase="done"] .jobrow-detail { color: var(--ready); }
.jobrow[data-phase="failed"] .jobrow-detail { color: var(--rust); }

/* The bar only exists while a job is working. Determinate when the tenant hands us a
   real fraction; otherwise a travelling sliver that says "working" without claiming a
   number — an honest indeterminate beats a 99% that never moves. */
.jobrow-track {
  margin-top: var(--sp1);
  height: 3px;
  border-radius: 2px;
  background: var(--wash-soft);
  overflow: hidden;
}
.jobrow-fill { height: 100%; background: var(--working); border-radius: 2px; }
.jobrow-track[data-indeterminate] .jobrow-fill {
  width: 40%;
  animation: jobdock-slide 1.4s ease-in-out infinite;
}
@keyframes jobdock-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
/* No travelling sliver on the reduced tier — a steady half-bar still reads as "working"
   without the loop. The ATTRIBUTE tier is the one motion key (viewOptions mirrors the OS
   query into it); a second @media fork here would drift from the user's own toggle. */
:root[data-motion="reduced"] .jobrow-track[data-indeterminate] .jobrow-fill {
  animation: none; width: 50%;
}

/* The completion action — the row's one primary verb. Sized as a real target so the
   auto-fired download is genuinely re-clickable, not a hairline link. */
.jobrow-action {
  flex: 0 0 auto;
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--accent-soft);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 6px var(--sp2);
  cursor: pointer;
}
.jobrow-action:hover { background: var(--accent-soft-hot); }
.jobrow-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.jobrow-x {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  font-size: 15px; line-height: 1;
  color: var(--ink-faint);
  background: none; border: none; border-radius: 4px;
  cursor: pointer;
}
.jobrow-x:hover { color: var(--ink); background: var(--wash); }
.jobrow-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (max-width: 40rem) {
  /* On a phone the dock spans the width above the safe area — the same posture the
     ctx-menu takes at this breakpoint. */
  .jobdock {
    left: var(--sp2);
    right: var(--sp2);
    bottom: max(var(--sp2), env(safe-area-inset-bottom));
    width: auto;
  }
  .jobrow-action { min-block-size: 2.75rem; }
}
/* ---------- shell pages (A1 s3c) ---------- */
/* While a shell page shows, anything that isn't the page container OR a
   body-level overlay leaves the flow (icndefs, the editor chrome). :not(#route-page)
   carries id specificity, so this outranks every sibling's own display rule.
   :not([data-overlay]) is the one seam every transient overlay (dialogs, the
   context menu, the quota panel, the toast) rides so it PIERCES the curtain —
   an overlay fired FROM a shell page (a browse Rename, a card menu, a rename
   toast, a quota refusal) must paint, not sit dead at 0×0. mountOverlayLayer
   stamps the marker (overlayLayer.ts / DLG-CURTAIN-1); a static overlay carries
   it inline (#toast). (Slice B retired the FLAG-A "boot" cold-load seed: the
   editor chrome no longer ships inline in index.html — showEditor injects
   EDITOR_CHROME into #route-page on the editor route — so there is no inline
   chrome to hold out of the first paint, and a cold load paints an empty
   #route-page until renderRoute resolves the route.) */
body[data-route="page"] > :not(#route-page):not([data-overlay]), body[data-route="landing"] > :not(#route-page):not([data-overlay]) { display: none; }
/* Editor and browse routes intentionally lock body scrolling and give it to
   their internal panes. Public/ceremony pages are documents: on a short phone
   their column must be allowed to grow past 100dvh and scroll, or the footer and
   recovery doors are clipped with no route to reach them. */
body[data-route="page"], body[data-route="landing"] {
  height: auto;
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
  overflow-y: auto;
}
.route-page {
  grid-row: 1 / -1;            /* the body grid's only visible child fills it */
  justify-self: center; align-self: start;
  padding: 22vh var(--sp4) var(--sp4);
  max-width: 460px;
}
/* Every public account door needs an explicit border-box measure. A bare
   max-width leaves the grid item's intrinsic width in charge; on a phone that
   made signup/invite wider than the viewport and then clipped the unreachable
   right edge. The shared wordmark is the stable family marker. */
body[data-route="page"]:has(.auth-brand) .route-page {
  width: min(100%, 420px);
  box-sizing: border-box;
}
.route-page h1 {
  font-family: var(--f-body); font-weight: 500; font-size: 26px;
  margin: 0 0 var(--sp2);
}
.route-page p { color: var(--ink-dim); margin: 0 0 var(--sp4); }
.route-page a { color: var(--accent); text-decoration: none; }
.route-page a:hover { text-decoration: underline; }

/* The signed-out door family — /signin, /signup, ceremony (ONBOARDING-UX-1 Stage 3,
   §THE DESIGN SPEC Surfaces 2–4). One coherent centered column: the cream Spectral
   ¶Proof wordmark crowns it (the SAME brand note as the app header — one voice),
   then the doors as full-width --surface-in---line buttons that light --accent ONLY
   on hover/focus. NO new color vocabulary; the one accent fill (--primary-action)
   stays reserved for the render command, so no auth door is PRIMARY. */
.route-page .auth-brand {
  font-family: var(--f-brand); font-weight: 600; font-size: var(--fs-brand);
  letter-spacing: 0.01em; color: var(--ink);
  margin: 0 0 var(--sp5);
}
.route-page .auth-brand-mark { color: var(--ink); }

/* Shared door skin: the passkey button (.signin-passkey), the create-account link
   (.signin-create-link, a real <a href="/signup"> re-skinned OFF the accent-anchor
   default), and the signup submit (.signup-go) all read as one full-width door. */
.route-page .signin-passkey,
.route-page .signin-dev-login,
.route-page .signin-create-link,
.route-page .signin-social,
.route-page .signin-email-go,
.route-page .signin-code-go,
.route-page .signup-go,
.route-page .signup-code-retry {
  display: block; width: 100%; box-sizing: border-box; text-align: center;
  font: inherit; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 5px; min-height: 46px; padding: 10px 12px; cursor: pointer;
}
.route-page .signin-passkey:hover, .route-page .signin-passkey:focus-visible,
.route-page .signin-dev-login:hover, .route-page .signin-dev-login:focus-visible,
.route-page .signin-create-link:hover, .route-page .signin-create-link:focus-visible,
.route-page .signin-social:hover, .route-page .signin-social:focus-visible,
.route-page .signin-email-go:hover, .route-page .signin-email-go:focus-visible,
.route-page .signin-code-go:hover, .route-page .signin-code-go:focus-visible,
.route-page .signup-go:hover, .route-page .signup-go:focus-visible,
.route-page .signup-code-retry:hover, .route-page .signup-code-retry:focus-visible {
  border-color: var(--accent); color: var(--accent); text-decoration: none;
}
.route-page .auth-intro { margin: 0 0 var(--sp5); color: var(--ink-dim); }
/* The friendly social primary (AUTH-FRIENDLY-1) LEADS the door list when the
   advertise seam is live; the wrapper is empty (no box, no gap) until then, so with
   no creds the door column is byte-identical to before. Each door stacks with the
   same rhythm as the co-equal doors below it. */
.route-page .signin-social-doors:not(:empty) { margin: 0 0 var(--sp5); display: flex; flex-direction: column; gap: var(--sp2); }
.route-page .signin-social-google {
  background: var(--ink); color: var(--bg-deep); border-color: var(--ink);
}
.route-page .signin-social-google:hover,
.route-page .signin-social-google:focus-visible {
  background: var(--accent); color: var(--bg-deep); border-color: var(--accent);
}
/* The click-wrap consent line under the social doors (SOCIAL-MINT-GATE rider): a
   social mint carries no tosAccepted body, so this notice IS the on-screen
   acceptance the server's bootstrap stamp records. Mounts only beside a live door. */
.route-page .signin-social-terms {
  margin: calc(-1 * var(--sp1)) 0 0;
  color: var(--ink-faint); font-size: 0.9em; line-height: 1.4; text-align: left;
}

/* /signin — TWO co-equal doors above the fold (ENTRY-DOOR-1, do not regress): the
   familiar email-code sign-in, then the "Create an account" invitation → /signup.
   A friendly social primary (Continue with Google) leads here once the
   provider-advertise seam is live (AUTH-FRIENDLY-1); absent until then (sin #11).
   Stacked as a labeled door list — no hairline between co-equal doors. */
.route-page .signin-email-door { margin: 0; }
.signin-email-form, .signin-code-form {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp3);
}
.auth-field-label {
  display: flex; flex-direction: column; gap: var(--sp1);
  color: var(--ink-dim); font-size: 0.86rem; letter-spacing: 0.01em;
}
.signin-email, .signin-code {
  width: 100%; min-width: 0; box-sizing: border-box;
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 5px; min-height: 48px;
  padding: 10px 12px;
}
.signin-email:focus, .signin-code:focus {
  border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent);
}
.signin-code {
  font-family: var(--f-mono); font-size: 1.2rem; letter-spacing: 0.22em;
  font-variant-numeric: tabular-nums;
}
.route-page .signin-code-lead { margin: 0 0 var(--sp1); color: var(--ink-dim); line-height: 1.45; }
.signin-code-lead strong { color: var(--ink); font-weight: 500; }
.route-page .signin-email-change {
  align-self: center; border: 0; background: transparent; color: var(--ink-dim);
  font: inherit; font-size: 0.88rem; padding: var(--sp1); cursor: pointer;
}
.route-page .signin-code-note {
  min-height: 1.25em; margin: calc(-1 * var(--sp1)) 0 0;
  color: var(--ink-dim); font-size: 0.88rem; line-height: 1.4;
}
.route-page .signin-code-resend {
  align-self: center; border: 0; background: transparent; color: var(--ink-dim);
  font: inherit; font-size: 0.88rem; padding: var(--sp1); cursor: pointer;
}
.route-page .signin-code-resend:hover,
.route-page .signin-code-resend:focus-visible { color: var(--accent); }
.route-page .signin-code-resend:disabled { color: var(--ink-faint); cursor: default; }
.route-page .signin-email-change:hover,
.route-page .signin-email-change:focus-visible { color: var(--accent); }
/* SIGNIN-IDENTIFIER-FIRST-1: the "More ways to sign in ···" disclosure is gone.
   Its two children are now plain stacked doors in the same rhythm as the email
   door above them. */
.route-page .signin-returning {
  margin: var(--sp4) 0 0; display: flex; flex-direction: column; gap: var(--sp2);
}
.route-page .signin-returning .signin-help { margin: 0 0 var(--sp2); }
.route-page .signin-create { margin-top: var(--sp5); padding-top: var(--sp4); border-top: 1px solid var(--line); }
.route-page .signin-create .signin-help { margin: 0 0 var(--sp2); }
/* The dev/seed token-paste ceremony stays demoted below the fold behind a hairline,
   only rendered behind ?register=token — it can never be mistaken for a public door. */
.route-page .signin-register {
  margin-top: var(--sp5); padding-top: var(--sp5);
  border-top: 1px solid var(--line);
}
.route-page .signin-register .signin-help { color: var(--ink-faint); margin: 0 0 var(--sp3); }
.signin-form { display: flex; gap: var(--sp2); align-items: center; }
.signin-token {
  flex: 1; min-width: 0; font: inherit; color: inherit;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; padding: 6px 10px;
}
.signin-token::placeholder { color: var(--ink-faint); }
.signin-token:focus { border-color: var(--ink-dim); }
.route-page .signin-status { margin: var(--sp4) 0 0; min-height: 1em; }
.route-page .signin-status[data-tone="error"] { color: var(--rust); }
.route-page .signin-status[data-tone="busy"] { color: var(--ink-dim); }

@media (max-width: 560px) {
  body[data-route="page"]:has(.auth-brand) .route-page {
    padding: max(56px, 10vh) var(--sp4) var(--sp5);
  }
  .route-page .signin-email-go,
  .route-page .signin-code-go,
  .route-page .signin-passkey,
  .route-page .signin-dev-login,
  .route-page .signin-social,
  .route-page .signup-go,
  .route-page .signup-code-retry { min-height: 50px; }
}

/* /signup — the PUBLIC self-serve door (PUB-SIGNUP-1b). One stacked form (name +
   email + create) filling the door column, reusing the shared .signin-status line.
   The full-width fields + door-skinned submit read as one family with /signin. */
.signup-form { display: flex; flex-direction: column; gap: var(--sp3); align-items: stretch; }
.signup-name, .signup-email {
  font: inherit; color: inherit; min-width: 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 5px; min-height: 48px; padding: 10px 12px;
}
.signup-name::placeholder, .signup-email::placeholder { color: var(--ink-faint); }
.signup-name:focus, .signup-email:focus {
  border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent);
}
/* The shared pre-flight helper marks the exact field that needs attention.
   Keep the treatment in the existing rust error register (not the accent
   interaction register), and let it override the ordinary focus ring. */
.signin-email[aria-invalid="true"],
.signin-code[aria-invalid="true"],
.signup-name[aria-invalid="true"],
.signup-email[aria-invalid="true"] {
  border-color: var(--rust);
  box-shadow: 0 0 0 1px var(--rust);
}
.route-page .signup-go { margin-top: var(--sp1); }
.route-page .signup-code-recovery {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp3);
}
.route-page .signup-code-recovery h2 {
  margin: 0; color: var(--ink); font: 600 1.1rem/1.3 var(--f-sans);
}
.route-page .signup-code-recovery-line {
  margin: 0; color: var(--ink-dim); line-height: 1.5;
}
.route-page .signup-code-fallback { margin-top: var(--sp1); }

/* The terms acceptance row (TOS-ACCEPTANCE-SEAM-1 door (a)) — a quiet checkbox + line
   under the fields, above the submit. The checkbox aligns to the first text line so a
   wrapped sentence hangs cleanly under it. */
/* The terms-acceptance row — shared by /signup and the invite consent landing
   (TOS-ACCEPTANCE-SEAM-1 doors (a) + (c)); one home for the ToS-row treatment. */
.signup-tos, .invite-tos {
  display: flex; align-items: flex-start; gap: var(--sp1);
  color: var(--ink-faint); font-size: 0.9em; line-height: 1.4;
  cursor: pointer; text-align: left;
}
.signup-tos-box, .invite-tos-box { margin-top: 2px; flex: none; accent-color: var(--accent); cursor: pointer; }
.signup-tos-box[aria-invalid="true"],
.invite-tos-box[aria-invalid="true"] {
  outline: 2px solid var(--rust);
  outline-offset: 2px;
}
.signup-tos-text, .invite-tos-text { min-width: 0; }
/* The invite consent landing renders in renderPanel's centred .route-page column;
   the ToS row and its refusal line ride that same flow. --rust is the app's
   failure register (there is no --danger token — see dock.css). */
.invite-tos { margin: var(--sp3) 0 0; }
.invite-tos-error:not(:empty) { margin: var(--sp2) 0 0; color: var(--rust); font-size: 0.9em; }

/* The auth cross-link footer shared by /signin ↔ /signup — a quiet line pointing a
   stranger at the other door. The link wears the page's accent-anchor treatment
   (.route-page a above). */
.route-page .auth-alt { margin: var(--sp5) 0 0; color: var(--ink-faint); font-size: 0.9em; }

/* The FRONT PORCH — unauthed `/` (ENTRY-DOOR-2). One screen-and-a-half landing
   in the door family's vocabulary (--bg-deep stage, cream Spectral wordmark,
   --surface-in---line CTA that lights --accent on interaction only) so porch →
   door reads as one house. body[data-route="landing"] widens the page column
   past the 460px door width — the proof capture needs room to argue. NO new
   color vocabulary; the copy Jesse owes is stamped data-placeholder-copy in the
   markup, not styled differently (a placeholder must look exactly like the real
   thing, or the structure was never proven). */
body[data-route="landing"] { background: var(--bg-deep); }
body[data-route="landing"] .route-page {
  max-width: 960px; width: 100%; box-sizing: border-box;
  padding: var(--sp5) var(--sp5) var(--sp7);
}
.landing-nav {
  display: flex; align-items: baseline; justify-content: space-between;
  margin: 0 0 12vh;
}
/* the wordmark reuses the door family's .auth-brand; kill its door margin here */
.landing-nav .auth-brand { margin: 0; }
/* the quiet returning-user door: ink-dim at rest, accent on interaction —
   deliberately NOT the accent-anchor default (.route-page a), so the loud CTA
   below stays the page's one leading action. */
.route-page .landing-signin { color: var(--ink-dim); }
.route-page .landing-signin:hover, .route-page .landing-signin:focus-visible {
  color: var(--accent); text-decoration: none;
}
.landing-hero { max-width: 640px; margin: 0 0 var(--sp7); }
.landing-claim {
  font-family: var(--f-brand); font-weight: 500;
  font-size: clamp(30px, 4.5vw, 42px); line-height: 1.15;
  margin: 0 0 var(--sp3);
}
.route-page .landing-sub {
  color: var(--ink-dim); font-size: 17px; line-height: 1.5;
  max-width: 34em; margin: 0 0 var(--sp5);
}
/* the one loud door — the shared door skin (surface-in-line, accent on
   interaction only; --primary-action stays reserved for the render command),
   sized up to carry the hero. */
.route-page .landing-cta {
  display: inline-block; font: inherit; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 5px; padding: 11px 22px;
}
.route-page .landing-cta:hover, .route-page .landing-cta:focus-visible {
  border-color: var(--accent); color: var(--accent); text-decoration: none;
}
/* the proof slot: the interface shown plainly — a lined frame on the deep
   stage, no glow, no mockup chrome. The <img> is the swappable asset. */
.landing-proof { margin: 0 0 var(--sp7); }
.landing-shot {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg);
}
.route-page .landing-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp3);
  color: var(--ink-faint); font-size: var(--fs-note);
  border-top: 1px solid var(--line); padding-top: var(--sp4);
}
.route-page .landing-legal { display: flex; align-items: center; gap: var(--sp4); }
.route-page .landing-legal a { color: var(--ink-faint); }
.route-page .landing-legal a:hover,
.route-page .landing-legal a:focus-visible { color: var(--accent); text-decoration: none; }
@media (max-width: 560px) {
  body[data-route="landing"] .route-page { padding-inline: var(--sp4); }
  .landing-nav { margin-bottom: 9vh; }
  .route-page .landing-cta {
    display: block; width: 100%; box-sizing: border-box; text-align: center;
    min-height: 50px; padding: 12px 16px;
  }
  /* Quiet does not mean tiny. These remain typographically secondary while
     meeting a phone-sized hit target for recovery and legal actions. */
  .route-page .signin-code-resend,
  .route-page .signin-email-change,
  .route-page .landing-legal a {
    min-height: 44px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* /invitations/:id/accept — the invite landing (RFC invite-land, Shift A). The page
   is a full member of the signed-out door family: every async state retains the
   wordmark and a quiet "Team invitation" context line. Its ONE call-to-action
   (accept → your workspace, or sign-in-to-accept) is the surface's single primary
   verb, so it wears the --primary-action fill like the browse New create verb
   (one accent, one command). Reasons with no forward action
   (expired/revoked/wrong-account) render as a heading + line only — no dead button. */
.route-page .invite-door { width: 100%; }
.route-page .invite-door .auth-brand { margin-bottom: var(--sp5); }
.route-page .invite-kicker {
  margin: 0 0 var(--sp2);
  color: var(--ink-faint);
  font: 600 var(--fs-note)/1.2 var(--f-ui);
  letter-spacing: 0.11em;
  text-transform: uppercase;
}
.route-page .invite-line { line-height: 1.5; }
.invite-action {
  display: block;
  width: 100%;
  min-height: 46px;
  font: inherit; font-weight: 600;
  color: var(--bg-deep);
  background: var(--primary-action);
  border: 1px solid transparent; border-radius: 5px;
  padding: 6px 14px; cursor: pointer;
}
.invite-action:hover {
  background: oklch(from var(--primary-action) calc(l + var(--hover-delta)) c h);
}
.invite-action:active {
  background: oklch(from var(--primary-action) calc(l - var(--hover-delta)) c h);
}
.route-page .invite-existing {
  margin: var(--sp3) 0 0;
  color: var(--ink-dim);
  font-size: 0.9rem;
  text-align: center;
}
.route-page .invite-existing a { color: var(--ink); }
.route-page .invite-code-form { margin-top: var(--sp4); }
.route-page .invite-code-status {
  min-height: 1.3em;
  margin: var(--sp2) 0 0;
  color: var(--accent);
  font-size: 0.88rem;
}
@media (max-width: 560px) {
  .invite-action { min-height: 50px; }
}
/* ------------------------------------------------ touch affordances (P5)
   One coarse-pointer tier, ADDITIVE sizing/visibility only — never layout,
   never display, never a forked UI per input. Gesture GRAMMAR (long-press
   selection, drag disambiguation, pencil) is R15's surface; this tier only
   guarantees that what already exists can be SEEN and HIT by a finger.
   `pointer: coarse` is the one key — no hover:none second axis (two queries
   = two rule sets that drift; same one-key ruling as the motion tier). */

/* tap responsiveness everywhere: interactive chrome opts out of double-tap
   zoom (scroll + pinch stay native — G11). NOT .divider/.scrubval: those are
   pointer-captured drag surfaces and keep their stronger touch-action: none. */
button, .word, .sil, .speaker { touch-action: manipulation; }

@media (pointer: coarse) {
  /* a finger can't hover: the hover-revealed block controls rest quietly
     visible. The block:hover reveal is now true-hover-gated (§W1.6 P1), so on a
     coarse pointer this 0.55 rest is the whole story — no sticky tap-reveal */
  .grip, .secmenu, .seckill { opacity: 0.55; }

  /* the seam can't be discovered by hover: the divider hairline rests visible */
  .divider::after { background: var(--line); }

  /* 8px grab zone → 24px tap zone — HIT AREA only, via a transparent ::before
     bleed; the divider's box (and the -4px math centering it on the seam)
     never moves, so placement rules stay untouched */
  .divider::before { content: ""; position: absolute; }
  .divider[data-axis="x"]::before { top: 0; bottom: 0; left: -8px; right: -8px; }
  .divider[data-axis="y"]::before { left: 0; right: 0; top: -8px; bottom: -8px; }

  /* the chip's one action gets a real tap pad — it grows INTO the chip body's
     seek zone, and that's the right trade on touch: the action wins, words
     still seek. Visual glyph size unchanged. The chip box is 25px tall (it sits
     IN the text line like a word), so the 44px AAA floor is geometrically
     impossible here without breaking the inline rhythm — this is the WCAG 2.5.5
     "inline target" exception; 24px is the 2.5.8-AA spacing floor, the honest
     ceiling for an in-line chip. (Same exception holds for the divider above:
     its hit zone is deliberately 24px, not 44px — a 44px seam grab would bleed
     22px into each flanking panel and swallow clicks on adjacent content.) */
  .sil-x { padding: 0 6px; min-width: 24px; }

  /* the section-move grip (⠿) is the block's PRIMARY drag handle but rests only
     16px wide — under a finger that's a miss. A transparent ::before bleeds the
     HIT AREA to the 44px WCAG 2.5.5 floor (16 + 14 each side) without moving the
     visible dot cluster (::after) or the grip's own box; it lives in the left
     gutter so the right edge bleeds only ~2px toward content. Full block height
     already clears 44px on any real paragraph. 44px is the convergent target of
     WCAG 2.5.5 (AAA), Apple HIG, and Material — a primary drag affordance earns
     the AAA floor (touch-input law #7; review-enforced, jsdom can't measure it). */
  .grip::before {
    content: ""; position: absolute;
    top: 0; bottom: 0; left: -14px; right: -14px;
  }

  /* the section-header ☰ MENU and STRIKE (×) buttons rest only ~22px wide —
     a near-miss under a finger. A transparent ::before bleeds the HIT AREA
     HORIZONTALLY without moving the button box (the flex header never reflows —
     same box-untouched discipline as .grip / .divider). The bleed is
     horizontal-ONLY by design: the paragraph's first line sits directly under
     the 20px header, so a downward halo would steal taps from the first word —
     the same adjacent-content exception that caps .sil-x / .divider at the AA
     floor instead of the gutter grip's 44px AAA. 6px each side stays well inside
     the gap to its neighbour, so neither halo reaches one; the controls are
     already AA via the WCAG 2.5.8 spacing rule (well separated), and this widens
     the actual tap pad. */
  .secmenu, .seckill { position: relative; }
  .secmenu::before, .seckill::before {
    content: ""; position: absolute;
    top: 0; bottom: 0; left: -6px; right: -6px;
  }

  /* the browse/project-row ⋯ kebab (.row-actions) rests opacity:0 +
     pointer-events:none and is revealed ONLY by :hover / :focus-within / a
     tab-driven :focus-visible — a finger issues none of those, so the SAME
     menu reachable by right-click/hover on desktop is UNREACHABLE on touch.
     Under a coarse pointer it rests fully shown (RFC W1.4 P0). Covers the two
     hidden-rest forms: the asset card (list + its .browse-grid descendant) and
     the home project-row (.project-item). The .project-header and .project-tile
     forms already rest visible, and the base .row-actions never hides — so they
     need no fallback here. */
  .browse-card > .row-actions,
  .project-item > .row-actions { opacity: 1; pointer-events: auto; }

  /* MOBILE-2b — the STANDALONE chrome buttons: the selection verb bar (Strike/
     Cut/Copy…), the dock's panel tabs (Tools/Notes), and the cut-tab bar's tab +
     "+ New cut". Each rests ~28–33px — a near-miss under a finger. Unlike the
     inline-in-text .sil-x (capped at the 24px AA exception), these sit in their
     OWN bars with room below, so they earn the full 44px WCAG 2.5.5 floor on
     their own box. min-height only: additive, the box never moves on a fine
     pointer and no ::before halo is needed (the button itself grows). */
  .dock-tabs button,
  .canvastabs .tab,
  .canvastabs .newcut { min-height: 44px; }
  .browse-new-project { min-height: 44px; }
  .tl-range-actions button { min-height: 44px; }
  .tl-actionpop { gap: 3px; padding: 4px; }
  .tl-actionpop-verb { min-height: 44px; padding-inline: 13px; }

  /* The transcript selection handle is intentionally a 26px inline mark so it
     does not disturb line rhythm. On touch, a transparent halo raises its real
     target to 44px without changing the placement box; once open, the detached
     action tray has room for full 44px controls. */
  .selpop-icon { position: relative; }
  .selpop-icon::before {
    content: ""; position: absolute; inset: -9px;
  }
  .selpop-verbs button { min-height: 44px; padding-inline: 13px; }

  /* the per-cut delete × is a tiny glyph in the tab slot; like the section ×
     it earns its tap PAD from a transparent ::before halo, not a min-width that
     would reflow the tab bar. It sits at the slot's end with the bar gap after
     it, so a symmetric ±16px bleed reaches no neighbour; min-height gives the
     44px-tall box the halo then covers top-to-bottom. */
  .canvastabs .tab-close { min-height: 44px; position: relative; }
  .canvastabs .tab-close::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: -16px; right: -16px;
  }

  /* MOBILE-2c — the EDITOR HEADER chrome pills: the mode toggle (edit/watch), the
     transcript-recall ¶ peek, the Share entry, and the account avatar. They rest
     21–32px — near-misses under a finger. Like the MOBILE-2b bar buttons they sit
     in the header with room, so they earn the full 44px WCAG 2.5.5 floor on their
     own box: min-height only for the text pills (additive, the box never moves on
     a fine pointer, no forked layout — a button centres its label natively so no
     alignment rule is needed). The avatar is a 26px disc; it grows symmetrically
     to a 44px disc (width+height, still the base 50% radius) rather than a
     min-height that would ovalise the circle. The header stays flex-wrap, so the
     taller pills reflow to a new row instead of forcing horizontal overflow at
     phone width. */
  .modepick button,
  .sharehandle { min-height: 44px; }
  .avatar { width: 44px; height: 44px; }

  /* MOBILE-2d — the tools/notes handles must NOT float over the scrolling
     transcript on a phone. The fixed cluster rests in the bottom-right thumb
     zone (l.1712), so any block header's × (delete, top-right of the block)
     that scrolls under it is transiently occluded — elementFromPoint at the
     ×'s centre returned the overlay, not the ×, and the delete was unreachable
     (the bug Jesse filed). At the phone/sheet tier (rail summoned) the cluster
     DOCKS into a reserved bottom band: a real strip the transcript reserves via
     padding-bottom, the same "a docked row never floats over text" discipline
     as the retired verb bar established. The scroll area then ends ABOVE the bar, so no block ×
     renders beneath it at ANY scroll offset — a reserve, not a re-pin. Desktop
     (fine pointer) keeps the floating overlay untouched. The :has() twin keeps
     the dock winning over the selection-panel lift (l.2439) when a range is
     live, and the pills shed their floating drop-shadow inside the bar (the
     bar's own border/lift carries the elevation now). */
  :root { --handlebarH: 56px; }
  body[data-tier-rail="summoned"] .handles {
    left: 0; right: 0; bottom: 0;
    justify-content: flex-end; align-items: center; gap: var(--sp2);
    min-height: var(--handlebarH);
    padding: var(--sp2) var(--sp3) max(var(--sp2), env(safe-area-inset-bottom));
    background: var(--surface); border-top: 1px solid var(--line);
    box-shadow: 0 -6px 20px oklch(0 0 0 / 0.28);
  }
  body[data-tier-rail="summoned"] .handles button { box-shadow: none; }
  body[data-tier-rail="summoned"] .transcript {
    padding-bottom: calc(var(--handlebarH) + env(safe-area-inset-bottom));
  }
}
/* B2.e I3-B — the empty-scope drag-drop zone. The empty-state box becomes a
   full-bleed drop target (position context for the overlay); the overlay is a
   dashed, accent-washed sheet shown only while a drag is over it, labelled so
   the affordance reads. Token-only, no hard-coded color (the contrast swaps
   carry through --accent/--line). The wiring (drop → same ingest path) lives in
   browse.ts; this is purely the look-lane feedback. */
.browse-state.dropzone { position: relative; }
.drop-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border: 2px dashed var(--accent);
  border-radius: 8px;
  background: var(--accent-wash);
  color: var(--accent);
  font-weight: 600;
  pointer-events: none;   /* the sheet must not eat the drop it announces */
}
/* the [hidden] attribute is the source of truth (JS toggles it) — this rule must
   out-specify `.drop-overlay { display: grid }` so the UA hidden rule isn't lost. */
.drop-overlay[hidden] { display: none; }

/* B2.i — the determinate upload bar on an 'ingesting' pending card. A thin,
   token-washed track that fills with --accent as bytes land (the byte-PUT drives
   `value` via setCardProgress); with no value attribute the UA renders its own
   indeterminate sweep, which we leave alone (the `Uploading…` label carries the
   state). Token-only, cross-engine reset so the fill reads on WebKit + Gecko. */
.card-progress {
  display: block;
  width: 100%;
  height: 4px;
  margin: 4px 0;
  border: none;
  border-radius: 2px;
  background: var(--line);
  appearance: none;
  -webkit-appearance: none;
}
.card-progress::-webkit-progress-bar { background: var(--line); border-radius: 2px; }
.card-progress::-webkit-progress-value { background: var(--working); border-radius: 2px; }
.card-progress::-moz-progress-bar { background: var(--working); border-radius: 2px; }

/* B2.i slice-2 — the 'processing' card's INDETERMINATE affordance. Once the bytes
   are in the provider's hands there's no client ETA, so the determinate bar gives
   way to a leading spun glyph (::before) beside the status copy (Processing… /
   Transcribing…) — read: "working, no %". Token-only color. The reduced-motion
   floor swaps the spin for a static accent dot (a11y, never an animation). */
.card-spinner {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0;
  font-size: var(--fs-note);
  color: var(--ink-dim);
}
.card-spinner::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--working-soft);
  border-top-color: var(--working);
  animation: card-spin 0.8s linear infinite;
}
@keyframes card-spin { to { transform: rotate(360deg); } }
/* a11y floor: the attribute tier (main.ts resolves pref-or-OS into it) swaps the
   spin for a static accent dot — never an animation. Same selector the rest of
   the reduced-motion rules use (no @media fork, per uiprefs). */
:root[data-motion="reduced"] .card-spinner::before { animation: none; border-color: var(--working); }

/* B2.i slice-3 — the pending card's blurred LOCAL preview thumbnail. The picked
   File rides an objectURL into a muted, never-played <video> whose first frame is
   the static poster; CSS blurs it + lays a token-tinted wash over it so the card
   reads "this specific video, working" instead of a bare text tile. The box
   reserves a FIXED 16/9 slot from byte zero so the later handoff to the real
   storyboard thumbnail (creds-gated, a follow-up slice) moves zero pixels — the
   "no layout shift" acceptance made concrete. Token-only; if blur is unsupported
   the frame stays a dimmed poster (the wash), never a raw bright frame fighting
   the card chrome. The blur radius + wash opacity are look-lane (tunable). */
.card-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 4px;
  background: var(--line);   /* a settled placeholder behind a not-yet-decoded frame */
}
.card-thumb-media {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(8px);
  transform: scale(1.08);    /* bleed past the blur's transparent edge halo */
}
.card-thumb::after {         /* the token-tinted working wash that keeps copy legible over the frame */
  content: "";
  position: absolute;
  inset: 0;
  background: var(--working-wash);
}
/* the READY-side poster (B2.i slice-3b) is the real storyboard frame, not a
   working-state placeholder — show it sharp: drop the blur/scale and the tint
   wash the pending local-preview carried. Same 16/9 box ⇒ zero-shift handoff. */
.card-thumb.ready .card-thumb-media {
  filter: none;
  transform: none;
}
.card-thumb.ready::after {
  display: none;
}

/* browse-chrome 3 — the no-thumbnail placeholder. A settled asset that never minted
   a poster keeps the SAME reserved 16/9 .card-thumb box every sibling tile has (the
   gallery row stays even), painted as a quiet --line-tinted panel with a faint
   centered frame glyph. Terminal/neutral, NOT a working state — drop the
   working-wash overlay like .ready does. Restrained by design (this is not the loud
   audio waveform). The mark dims to a hint via opacity; --ink is the only color. */
.card-thumb.blank {
  display: grid;
  place-items: center;
}
.card-thumb.blank::after {
  display: none;
}
.card-thumb-blankmark {
  width: 26%;
  max-width: 40px;
  color: var(--ink);
  opacity: 0.16;
}
.card-thumb-blankmark svg {
  width: 100%;
  height: auto;
  display: block;
}

/* AK-3 — the AUDIO-ONLY poster. A ready asset with no video frame (MUX skips the
   storyboard on an audio ingest, and the playback-id thumbnail the route mints would
   404) fills the SAME reserved 16/9 box with the canonical waveform (browse.ts
   AUDIO_WAVE_D) on a FLAT darkroom ground, instead of a broken <img> or a bare box.
   The ground is the deepest surface token (--bg-deep) so the amber waveform + glow
   pops — a flat paint, NOT a gradient blend (the sheet bans painted surface gradients;
   design-audit.test pins that). Zero-shift with the video poster it stands in for. The
   working-state wash (::after) is suppressed — this is a settled poster, not a
   processing tint. Static (no motion ⇒ no reduced-motion fork); the svg is aria-hidden
   (the card name carries identity). Glow = the token at a soft drop-shadow. Token-only. */
.card-thumb.audio {
  background: var(--bg-deep);
}
.card-thumb.audio::after { display: none; }
.card-thumb.audio .audio-wave { display: block; width: 100%; height: 100%; }
.card-thumb.audio .audio-wave path {
  fill: none;
  stroke: var(--audio-wave);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 3px var(--audio-wave));
}

/* B2.i slice-4 — the terminal 'errored' card's failure fold. The working states
   (bar/spinner) speak --working (the semantic status amber); failure speaks
   --rust (the palette's negative/"stricken" role — there is NO dedicated
   --error/--danger token yet; this is the sane semantic default, flagged for the
   look-lane). The three statuses now each own a hue. A quiet rust-washed region
   reads as a STOPPED dead-end, not a working state. Token-only, no hard-coded
   color; static (no motion, so no reduced-motion fork needed). The glyph is a
   decorative rust disc (the copy + role=alert carry the meaning). */
.card-error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 4px 0;
  padding: 6px 8px;
  border-radius: 4px;
  background: var(--rust-wash);
  font-size: var(--fs-note);
}
.card-error-glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--rust);
  color: var(--bg-deep);             /* dark mark on the rust disc — reads cleanly */
  font-weight: 700;
  line-height: 1;
}
.card-error-glyph::before { content: "!"; }
.card-error .card-state { color: var(--rust); font-weight: 600; }
/* the reason wraps to its own line under the headline+glyph (the flex row) so it
   reads as the explanatory subline, with Retry trailing as the recovery control */
.card-error-reason { flex: 1 1 100%; color: var(--ink-dim); }
.card-error .card-retry { flex: none; }   /* the standard button treatment (style above); Retry sits inline */

/* brief §3 — the SEMANTIC STATUS DOT. Every settled card's state label (and the
   detail pane's) leads with a small disc whose hue MEANS the status: green=ready ·
   amber=working (ingesting/processing/Transcribing…) · rust=errored. The copy
   carries the meaning; the dot is decorative reinforcement that lets the status
   read at a glance down a list — so no contrast gate. The hue is profile-constant
   (browse.ts stamps a state-* kind class; status never swaps under the cool
   library). Token-only, no hex; an unkinded label gets the neutral --ink-faint
   default. Dot size/gap are look-lane placeholders. */
.card-state::before, .preview-state::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  vertical-align: middle;
  background: var(--ink-faint);
}
.state-ready::before   { background: var(--ready); }
.state-working::before { background: var(--working); }
.state-errored::before { background: var(--rust); }

/* The BROWSE RAIL (browse.ts browseRail) — the persistent left nav (§4): the
   library's wayfinding as cool-neutral chrome that RECEDES (brief §3). The rail
   sits in a flex shell beside the content `main`; when the rail is present the
   route-page column drops its narrow signin measure and stretches full-bleed
   (:has — the shell is on EVERY browse state). Role discipline: the chrome is
   --bg-deep/--line/--ink-dim (recede); teal (--accent) appears ONLY on the
   active nav item / active project (aria-current) — the lone interaction note;
   disabled entries sit at --ink-deco WITH their reason in the title. Token-only,
   no hex. The active-project marker dot is a uniform restrained neutral (no
   per-project color data yet — rainbow borders are an .impeccable reject). */
/* MOBILE-1 — cap the shell's grid COLUMN to the viewport. body is a grid whose single
   implicit column is `auto`, so it grows to the content's min-content — on a narrow
   phone the browse list's meta tracks push that past the viewport and the whole page
   pans (the ◆ Jesse-from-his-phone gap). minmax(0, 1fr) pins the column to the available
   width (0 minimum defeats the min-content blowout), so the shell never exceeds the
   viewport and inner content that's genuinely too wide is contained, not a page pan.
   Scoped to the shell routes (browse + the MANAGE surfaces) via :has — the editor route
   (body:has(.top)) keeps its own grid untouched. Byte-inert on desktop (1fr == the wide
   viewport there), load-bearing only where the content would otherwise overflow. */
body:has(.browse-shell) { grid-template-columns: minmax(0, 1fr); }
#route-page:has(.browse-shell) {
  max-width: none;
  justify-self: stretch;
  align-self: stretch;
  padding: 0;
  min-width: 0;   /* let the capped grid column shrink the item below its content */
  /* C1 — pin the 1fr grid row's automatic minimum to 0 so a tall shell can't
     grow the track past the viewport (the default min-height:auto is what let
     the rail run off-screen and clip under body's overflow:hidden). */
  min-height: 0;
}
/* App-Shell Slice B / B3 LAYOUT PRE-STAGE — the editor's full-bleed escape.
   Today the editor lives INLINE in index.html and `body` IS its grid
   (display:grid; grid-template-rows:auto 1fr auto — header.top / main / footer
   .stripbar are body-grid children). The B3 mount switch will instead inject
   EDITOR_CHROME into #route-page (mirroring showBrowse/showManage) so the editor
   becomes a mounted route like the others and the data-route=boot band-aid can
   retire. When it does, #route-page must RE-HOST that body grid or the editor
   collapses into the 460px .route-page measure — the named #1 B3 risk.
   This rule pre-stages exactly that re-host: it mirrors the body editor grid
   (the auto/1fr/auto rows + overflow so `main` scrolls INSIDE, never the body)
   and the browse-shell full-bleed escape above. It is BYTE-INERT today — `.top`
   is the editor header class, an editor-exclusive marker that only ever lives as
   a body child (index.html:40), so `#route-page:has(.top)` matches NOTHING until
   B3 moves the chrome inside. Landing + pinning it now turns B3's biggest layout
   risk into a proven, reviewed quantity so the flip is a small, verifiable diff. */
#route-page:has(.top) {
  display: grid;
  grid-template-rows: auto 1fr auto;
  /* MOBILE-NANO-0711 — cap the editor grid's COLUMN to the viewport, mirroring the
     MOBILE-1 browse-shell fix (`body:has(.browse-shell)` above). The implicit single
     column is `auto`, which sizes to the header's MAX-content: on a narrow phone the
     .top row's fixed controls (mode · Share · avatar) push that track past the
     viewport, so .top inflates to ~427px and the overflow:hidden below clips the
     right-pinned .avatar off-screen — silently unreachable, no h-scroll (proven live
     @380: .top right=427, .avatar x=391 past the 380 edge). minmax(0,1fr) pins the
     column to the available width (0 minimum defeats the min-content blowout), so .top
     fits 100% and the avatar's margin-left:auto lands at the real right edge. Byte-inert
     on desktop (1fr == the wide viewport there), load-bearing only at phone width. */
  grid-template-columns: minmax(0, 1fr);
  max-width: none;
  justify-self: stretch;
  align-self: stretch;
  padding: 0;
  min-height: 0;
  /* mirror body's overflow:hidden — the editor's `main` owns its own scroll, the
     route container clips so a tall transcript can't grow the body past 100dvh. */
  overflow: hidden;
}
.browse-shell {
  display: flex;
  /* C2 — the shell is a COLUMN: the shared app header spans the top, the rail +
     content row fills the rest. (Was a bare row; the header lands above it.) */
  flex-direction: column;
  align-items: stretch;
  /* C1 — the shell is viewport-BOUNDED, not growable: height (not min-height)
     pins it to the 1fr grid row, so over-tall content scrolls INSIDE the rail
     and main regions rather than overflowing the body (which clips silently,
     overflow:hidden). min-height:0 lets the flex children shrink and own scroll. */
  height: 100%;
  min-height: 0;
}
/* C1 — the rail + content row beneath the header. flex:1 takes the leftover
   column height; min-height:0 lets its scroll regions (rail, main) shrink and
   own their overflow rather than growing the column past the viewport. */
.browse-body {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  min-height: 0;
}

/* The shared APP HEADER (appHeader.ts) — the shell's top bar: wordmark · account.
   Cool-recede chrome (a --bg-deep surface under a --line hairline,
   same temperature as the rail); the cream serif wordmark is the lone brand note,
   teal stays for interaction. flex:0 0 auto so it never scrolls or shrinks.
   Token-only — no hex. */
.app-header {
  display: flex;
  align-items: center;
  gap: var(--sp3);
  flex: 0 0 auto;
  padding: var(--sp2) var(--sp3);
  background: var(--bg-deep);
  border-bottom: 1px solid var(--line);
}
/* the descendant selector out-specifies `.route-page a` (which would otherwise
   paint this in-page anchor with --accent) so the wordmark stays the CREAM brand
   note — teal is reserved for interaction, never the brand (§3 temperature split). */
.app-header .app-header-brand {
  flex: 0 0 auto;
  font-family: var(--f-brand); font-weight: 600; font-size: var(--fs-brand);
  color: var(--ink); text-decoration: none; letter-spacing: 0.01em;
}
/* the ¶ pilcrow mark prefixing the wordmark — decorative (aria-hidden in markup),
   inherits the Spectral wordmark face/weight/size; a hair of breathing before the P */
.brand-mark { margin-right: 0.06em; }
/* ── the header SEARCH surface (headerSearch.ts, SEARCH-1c) ──────────────────
   The centre slot: a flexible field that takes the slack between the wordmark and
   the right cluster, capped so it never sprawls across a wide monitor. It is the
   POSITIONING CONTEXT for its own results popover — the pop is absolutely placed
   against this box, so it scrolls and moves with the field natively (G11: no
   reposition loop, no scroll listener).
   Token-only; teal is the interaction note (focus ring), the field itself recedes. */
.app-header-search {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 30rem;
  margin-inline: var(--sp2);
}
.app-header-search-input {
  width: 100%;
  padding: var(--sp1) var(--sp2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink);
  font-family: var(--f-ui); font-size: var(--fs-control);
}
.app-header-search-input::placeholder { color: var(--ink-dim); }
/* the focus ring is channel E and is never suppressed (law 8 — the outline-none ban):
   the accent border is an ADDITION to the platform ring, not a replacement for it. */
.app-header-search-input:focus-visible {
  border-color: var(--accent);
}
/* the results popover — a floating card under the field, the same panel chrome the
   other in-shell popovers wear. Capped in height so a full result set scrolls inside
   the pop rather than growing the header. */
.app-header-search-pop {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0; right: 0;
  max-height: min(60vh, 30rem);
  overflow-y: auto;
  padding: var(--sp1);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 8px 28px -12px rgba(0, 0, 0, 0.55);   /* the .ctx-menu popover shadow idiom */
}
.app-header-search-pop[hidden] { display: none; }
/* the status line — Searching… / no matches / couldn’t search. Quiet by design: it
   is never an option, so it never carries the selected treatment. */
.app-header-search-note {
  padding: var(--sp2);
  color: var(--ink-dim);
  font-family: var(--f-ui); font-size: var(--fs-note);
}
.app-header-search-retry {
  padding: 0;
  background: none;
  border: 0;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
/* one result — poster · (name + breadcrumb) / excerpt / meta. The active row (arrowed
   onto or hovered) lifts on --wash, the same active-row note the menus use. */
.app-header-search-hit {
  display: flex;
  gap: var(--sp2);
  align-items: flex-start;
  padding: var(--sp1) var(--sp2);
  border-radius: 5px;
  cursor: pointer;
}
.app-header-search-hit[aria-selected='true'] { background: var(--wash); }
.app-header-search-hit .card-thumb {
  flex: 0 0 auto;
  width: 64px;
  border-radius: 4px;
  overflow: hidden;
}
.app-header-search-hit-body { min-width: 0; flex: 1 1 auto; }
.app-header-search-hit-head {
  display: flex;
  gap: var(--sp2);
  align-items: baseline;
  justify-content: space-between;
}
.app-header-search-hit-name {
  color: var(--ink);
  font-family: var(--f-ui); font-size: var(--fs-control);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-header-search-hit-crumb {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--sp1);
  color: var(--ink-dim);
  font-family: var(--f-ui); font-size: var(--fs-note);
}
.app-header-search-hit-chip {
  width: 8px; height: 8px; border-radius: 50%;
  border: 1px solid var(--line);
}
/* the excerpt — two lines at most, so a long snippet can't push a result set off
   the screen; the highlighted terms are the reason the row is here, so they carry
   the accent while the surrounding speech recedes. */
.app-header-search-hit-snippet {
  margin-top: 2px;
  color: var(--ink-dim);
  font-family: var(--f-ui); font-size: var(--fs-note);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.app-header-search-mark {
  background: none;
  color: var(--accent);
  font-weight: 600;
}
/* a name/cut-lane row has no excerpt — this says why it matched instead. */
.app-header-search-hit-lane {
  margin-top: 2px;
  color: var(--ink-dim);
  font-family: var(--f-ui); font-size: var(--fs-note);
  font-style: italic;
}
.app-header-search-hit-meta {
  margin-top: 2px;
  display: flex; gap: var(--sp2);
  color: var(--ink-dim);
  font-family: var(--f-ui); font-size: var(--fs-note);
}

/* the New shortcut — a STANDARD button (inherits the base button skin; never the
   accent-fill PRIMARY, P3(b)). margin-left:auto leads the right cluster; the header
   `gap` spaces it from the account. */
.app-header-new { flex: 0 0 auto; margin-left: auto; }
/* the account anchor — the avatar circle. Pinned to the far right; disabled-with-
   reason on an unwired host, with no fabricated identity. */
.app-header-account {
  flex: 0 0 auto;
  margin-left: auto;
  width: 24px; height: 24px;
  padding: 0;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-dim);
  font: var(--fs-chip) var(--f-ui);
  cursor: default;
  overflow: hidden;                 /* clip the identity disc to the circle */
}
.app-header-account:not(:disabled) { cursor: pointer; }
/* the identity face inside the account circle — the shared chip fills the anchor so
   the disc IS the button face (its hue/photo overrides the neutral --surface). At this
   size the initials read at --fs-chip. */
.app-header-account .identity-chip { display: block; width: 100%; height: 100%; }
.app-header-account .identity-disc { font-size: var(--fs-chip); }

/* the shared identity disc (identityChip.ts) — the ONE face recipe: initials on a
   stable per-person hue (the --cmt-hue law: theme-fixed chroma/lightness, only the hue
   rotates), or the avatar photo. Fills its host; every identity surface (header chip,
   /account avatar) paints through it, so a person reads the same disc everywhere. */
.identity-disc {
  width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--f-ui); font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media); background: oklch(0.56 0.072 var(--cmt-hue, 250));
  user-select: none;
}
/* the neutral pre-identity glyph — an honest silhouette shown until the chip's read
   lands (or on a miss); recede ink on the plain anchor surface, never a fabricated
   face. Sits on the disc with NO hue background yet (the disc paints hue only once a
   real identity replaces it). */
.identity-disc:has(.identity-glyph) { background: var(--surface); }
.identity-glyph { width: 100%; height: 100%; display: block; color: var(--ink-faint); }

/* Account menu — a compact identity/organization switcher anchored to the avatar.
   It is deliberately one lifted sheet, not a miniature settings page: identity first,
   memberships in the middle, two durable account actions at the foot. */
.account-menu {
  position: fixed;
  z-index: 80;
  width: min(304px, calc(100vw - 24px));
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in oklab, var(--bg-deep) 82%, var(--surface));
  box-shadow: 0 18px 48px oklch(0.08 0.006 255 / 0.58), 0 2px 8px oklch(0.08 0.006 255 / 0.34);
  color: var(--ink);
  font-family: var(--f-ui);
  transform-origin: top right;
  animation: account-menu-in 120ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes account-menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.985); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
:root[data-motion="reduced"] .account-menu { animation: none; }
.account-menu-identity {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  align-items: center;
  gap: var(--sp3);
  padding: var(--sp4);
  background: var(--wash-soft);
  border-bottom: 1px solid var(--line);
}
.account-menu-face { width: 44px; height: 44px; font-size: var(--fs-control); }
.account-menu-identity-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.account-menu-name,
.account-menu-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-menu-name { font-size: var(--fs-control); font-weight: 600; line-height: 1.25; }
.account-menu-email { color: var(--ink-faint); font-size: var(--fs-note); line-height: 1.35; }
.account-menu-section { padding: var(--sp2); }
.account-menu-label {
  padding: var(--sp2) var(--sp2) var(--sp1);
  color: var(--ink-faint);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.account-menu-orgs { display: flex; flex-direction: column; gap: 2px; }
.account-menu-row {
  width: 100%;
  min-height: 38px;
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp2);
  padding: 7px var(--sp2);
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  text-align: left;
}
.account-menu-row:hover,
.account-menu-row:focus-visible { background: var(--wash); color: var(--ink); }
.account-menu-row svg { width: 19px; height: 19px; color: var(--ink-faint); }
.account-menu-org[aria-checked="true"] { color: var(--ink); background: var(--wash-soft); }
.account-menu-org-badge {
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px;
  background: color-mix(in oklab, var(--accent) 16%, var(--surface));
  color: var(--accent);
  font-size: var(--fs-chip); font-weight: 600;
}
.account-menu-org-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-menu-check { color: var(--accent); font-size: var(--fs-control); padding-right: 2px; }
.account-menu-loading {
  display: block;
  padding: var(--sp2);
  color: var(--ink-faint);
  font-size: var(--fs-note);
}
.account-menu-error { color: var(--rust); }
.account-menu-actions {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp2);
  border-top: 1px solid var(--line);
}
.account-menu-signout { color: var(--rust); }
.account-menu-signout svg { color: currentColor; }

@media (max-width: 30rem) {
  .account-menu { left: 12px !important; right: 12px; width: auto; }
}
/* MOBILE-1 — the header hamburger that summons the off-canvas rail drawer. HIDDEN on
   desktop (the in-flow rail + its collapse toggle serve there); it lights up only under
   the 48rem breakpoint where the rail goes off-canvas. Prepended into the shared header
   (appShellFrame), so it leads the bar left of the wordmark — the universal mobile-nav
   seat. Quiet recede chrome at rest, the lone teal note on hover/focus (interaction,
   never wayfinding — it borrows no active-item role). Token-only, no icon font. */
.app-header-menu {
  display: none;   /* desktop: the rail is in-flow — no drawer, no summon control */
  flex: 0 0 auto;
  align-items: center; justify-content: center;
  width: 30px; height: 30px;
  padding: 0; border: none; border-radius: 6px;
  background: none; color: var(--ink-dim); cursor: pointer;
}
.app-header-menu:hover, .app-header-menu:focus-visible { color: var(--accent); background: var(--wash); }
/* the three-bar glyph, drawn in CSS (a bar + two pseudo-bars) — decorative; the button
   carries the accessible name. currentColor tracks the button ink (recede → teal). */
.app-header-menu-bars { position: relative; }
.app-header-menu-bars,
.app-header-menu-bars::before,
.app-header-menu-bars::after {
  display: block; width: 16px; height: 2px; border-radius: 1px;
  background: currentColor; content: "";
}
.app-header-menu-bars::before { position: absolute; top: -5px; }
.app-header-menu-bars::after { position: absolute; top: 5px; }

/* MOBILE-1 — the drawer scrim. Only real while the drawer is open ([hidden] keeps it
   out of the a11y tree + display:none at rest, flipped by appShellFrame). Fixed full-
   viewport, the app's standard --scrim dim under the sliding rail; a tap anywhere on it
   dismisses (wired in appShellFrame). z-index below the rail(50), above the content. */
.browse-scrim {
  position: fixed; inset: 0; z-index: 40;
  background: var(--scrim);
}
.browse-scrim[hidden] { display: none; }

.browse-rail {
  flex: 0 0 var(--rail-w, 208px);
  display: flex;
  flex-direction: column;
  gap: var(--sp1);
  padding: var(--sp3) var(--sp2);
  background: var(--bg-deep);
  border-right: 1px solid var(--line);
  font-family: var(--f-ui);
  /* C1 — the rail owns its OWN vertical scroll (a long project list never
     clips); overflow-x stays hidden so labels still clip cleanly on collapse. */
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  /* §4 Slice-B: the collapse animates the basis; the reduced-motion floor below
     snaps it. cubic mirrors the layout transition vocabulary already in use. */
  transition: flex-basis 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rail-workspace {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  min-height: 40px;
  padding: var(--sp1) var(--sp1) var(--sp1) var(--sp2);
  margin-bottom: var(--sp3);
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
}
.rail-ws-chip {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp2);
}
.rail-ws-avatar {
  flex: 0 0 auto;
  width: 18px; height: 18px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink-dim);
  font-size: var(--fs-chip); font-weight: 600;
}
.rail-ws-name {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-note); color: var(--ink-dim);
}
.rail-ws-chip--pending .rail-ws-avatar,
.rail-ws-chip--pending .rail-ws-name {
  min-width: 18px;
  min-height: 12px;
  border-radius: 4px;
  background: var(--wash);
}
.rail-nav { display: flex; flex-direction: column; gap: 1px; margin-bottom: var(--sp3); }
.rail-settings-label {
  padding: var(--sp3) var(--sp2) var(--sp1);
  color: var(--ink-deco);
  font-size: var(--fs-chip);
  letter-spacing: .055em;
  text-transform: uppercase;
}
.rail-settings-label:first-child { padding-top: 0; }
.rail-nav-item {
  position: relative;   /* anchors the active inset accent bar (::before) */
  display: flex; align-items: center; gap: var(--sp2);
  padding: var(--sp1) var(--sp2);
  border-radius: 6px;
  font-size: var(--fs-note); color: var(--ink-dim);
  text-decoration: none; cursor: pointer;
}
.rail-nav-icon {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
}
.rail-nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.rail-nav-item:hover { background: var(--wash); }
.rail-nav-item[aria-current="page"] {
  background: var(--accent-wash);
  color: var(--accent);
}
.rail-workspaces {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 var(--sp3);
}
.rail-workspaces[hidden] { display: none; }
.rail-workspaces-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--sp2) var(--sp1);
}
.rail-workspaces-label {
  color: var(--ink-deco);
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  letter-spacing: .04em;
  text-transform: uppercase;
}
.rail-workspaces-add {
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.rail-workspaces-add:hover, .rail-workspaces-add:focus-visible {
  background: var(--wash);
  color: var(--accent);
}
.rail-workspace-item {
  position: relative;
  width: 100%;
  overflow: hidden;
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  align-items: center;
  gap: var(--sp2);
  padding: var(--sp1) var(--sp2);
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  text-align: left;
}
.rail-workspace-item:hover { background: var(--wash); }
.rail-workspace-item[aria-current="true"] { background: var(--accent-wash); color: var(--ink); }
.rail-workspace-glyph {
  width: 18px;
  height: 18px;
  display: grid;
  grid-template-columns: repeat(2, 4px);
  grid-template-rows: repeat(2, 4px);
  place-content: center;
  gap: 2px;
  border-radius: 5px;
  background: var(--wash);
  color: var(--ink-deco);
}
.rail-workspace-glyph i {
  width: 4px;
  height: 4px;
  border: 1px solid currentColor;
  border-radius: 1px;
}
.rail-workspace-item[aria-current="true"] .rail-workspace-glyph {
  background: color-mix(in oklab, var(--accent) 14%, transparent);
  color: var(--accent);
}
.rail-workspace-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* (The rail's PROJECTS list + its section head / dot / name / count / load-state note were
   RETIRED 2026-07-20 — projects live on the home + the /projects index; the sidebar list was
   redundant. Only the workspace-switcher chrome + the nav items remain.) */
/* ACTIVE marker = a separate inset accent bar in the gutter, NOT a left-border on the row
   (a single-side border fights the 6px rounded corners). A short centered pill reads as a
   clean document-outline tick; the row fill (--accent-wash) carries the rest. Absolutely-
   positioned so it adds no layout width. Teal reserved for the ACTIVE item only (§3). */
.rail-nav-item[aria-current="page"]::before {
  content: ""; position: absolute;
  left: 2px; top: 50%; transform: translateY(-50%);
  width: 2px; height: 1.05em;
  border-radius: 1px;
  background: var(--accent);
}

/* RAIL ROLE-DISCIPLINE armor — the rail's anchors (Home + each project) are
   chrome, but the broad `.route-page a` rule (style.css ~1367: color:--accent +
   hover:underline) out-specifies their bare rules at RUNTIME, leaking interaction-
   teal onto a RESTING link and underlining it on hover — a cascade the source-level
   recede pins above can't see. Container-scope reclaims it so the rail stays recede
   --ink-dim, teal reserved for the ACTIVE item only (§3, the same armor the wordmark
   carries). `a.rail-nav-item` targets the Home anchor so the disabled `<span>` nav
   items keep their dimmer --ink-deco. */
.browse-rail a.rail-nav-item { color: var(--ink-dim); text-decoration: none; }
.browse-rail a.rail-nav-item:hover { text-decoration: none; }
.browse-rail a.rail-nav-item[aria-current="page"] { color: var(--accent); }

.rail-back {
  margin-top: auto;
  color: var(--ink-deco);
}

/* The panel control is a recognizable sidebar diagram rather than an ambiguous
   chevron. Its 30px target is easy to acquire; the arrow within the diagram
   reverses with state, and the label remains explicit for assistive technology. */
.rail-toggle {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--ink-deco);
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease, border-color 140ms ease,
    transform 100ms ease;
}
.rail-toggle svg {
  width: 18px;
  height: 18px;
  transition: transform 180ms cubic-bezier(.22, 1, .36, 1);
}
.rail-toggle:hover, .rail-toggle:focus-visible {
  color: var(--ink);
  background: var(--wash);
  border-color: color-mix(in oklab, var(--line) 55%, var(--ink-dim));
}
.rail-toggle:active { transform: scale(.94); }

/* Collapsed = a compact tool rail. Work destinations remain recognizable and usable;
   their visible labels and the workspace list fold away while aria-label preserves
   explicit names. Settings mode stays folded because its destinations have no compact
   icon vocabulary and the avatar remains the honest doorway back into administration. */
.browse-shell--rail-collapsed .browse-rail { flex-basis: var(--rail-w-collapsed, 46px); }
.browse-shell--rail-collapsed .rail-ws-name,
.browse-shell--rail-collapsed .rail-nav--settings,
.browse-shell--rail-collapsed .rail-workspaces,
.browse-shell--rail-collapsed .rail-back,
.browse-shell--rail-collapsed .rail-ws-chip { display: none; }
.browse-shell--rail-collapsed .rail-nav--work {
  display: flex;
  align-items: center;
  gap: 3px;
}
.browse-shell--rail-collapsed .rail-nav--work .rail-nav-item {
  width: 34px;
  height: 34px;
  justify-content: center;
  padding: 0;
}
.browse-shell--rail-collapsed .rail-nav--work .rail-nav-label { display: none; }
.browse-shell--rail-collapsed .rail-workspace {
  min-height: 40px;
  padding: var(--sp1);
  justify-content: center;
}
.browse-shell--rail-collapsed .rail-toggle svg { transform: scaleX(-1); }

/* Reduced-motion floor — the width change SNAPS, same posture as the summoned
   sheets above (a width animation is exactly the vestibular trigger the tier
   kills). The pref/OS tier owns the attribute; this rail honors it. */
:root[data-motion="reduced"] .browse-rail { transition: none; }

/* ─────────────────────────────────────────── MOBILE-1 — the off-canvas rail drawer
   Below the 48rem breakpoint the fixed-column rail can't share the width with the
   content (a 375px phone would get a shrunk desktop shell that pans horizontally —
   ◆ Jesse, from his phone). So the rail leaves the flow and becomes a summoned DRAWER:
   the hamburger lights up, the rail slides in over a scrim, the content grid runs the
   full single-column width. Everything is analytic CSS keyed off the .browse-shell
   --drawer-open flag appShellFrame toggles — zero geometry reads. */
@media (max-width: 48rem) {
  /* The hamburger appears and the two persistent header actions graduate from dense
     desktop controls to 44px touch targets. Their glyphs/faces keep their own visual
     size; the larger box is acquisition room, not louder chrome. */
  .app-header-menu { display: inline-flex; width: 44px; height: 44px; }
  .app-header-account { width: 44px; height: 44px; }
  /* the rail goes OFF-CANVAS: fixed to the left edge, slid fully out of view, summoned
     in by --drawer-open. Its own width (the desktop flex-basis is inert on a fixed box);
     a hair under the viewport so the scrim always shows a dismiss margin on the right. */
  .browse-rail {
    position: fixed; left: 0; top: 0; bottom: 0; z-index: 50;
    width: min(82vw, 300px);
    flex-basis: auto;
    transform: translateX(-100%);
    transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
    box-shadow: 2px 0 18px oklch(0.1 0.01 255 / 0.35);
  }
  .browse-shell--drawer-open .browse-rail { transform: translateX(0); }
  /* the desktop collapse toggle is meaningless here (the drawer + hamburger replace it) */
  .rail-toggle { display: none; }

  /* a desktop "collapsed" pref must never empty the mobile drawer — inside the
     breakpoint the drawer is always its full self (collapse is a desktop width concern).
     Restore the sections the .browse-shell--rail-collapsed rule folds away, and undo the
     thin-strip workspace layout, so a user who narrows from a collapsed desktop rail
     still gets a complete, usable drawer. */
  .browse-shell--rail-collapsed .browse-rail { width: min(82vw, 300px); flex-basis: auto; }
  .browse-shell--rail-collapsed .rail-nav,
  .browse-shell--rail-collapsed .rail-back { display: flex; }
  .browse-shell--rail-collapsed .rail-ws-chip { display: flex; }
  .browse-shell--rail-collapsed .rail-workspaces:not([hidden]) { display: flex; }
  .browse-shell--rail-collapsed .rail-nav--work .rail-nav-label { display: inline; }
  .browse-shell--rail-collapsed .rail-nav--work .rail-nav-item {
    width: auto;
    height: auto;
    justify-content: flex-start;
    padding: var(--sp1) var(--sp2);
  }
  .browse-shell--rail-collapsed .rail-workspace { justify-content: flex-start; }

  /* Drawer destinations are touched, not moused. Preserve the desktop density while
     giving every navigation/workspace action the platform-standard 44px hit area. */
  .rail-nav-item,
  .rail-workspace-item { min-height: 44px; }
  .rail-workspaces-add { min-width: 44px; min-height: 44px; }
}

/* Reduced-motion floor for the drawer slide — the summon SNAPS (a transform-in from
   off-screen is the same vestibular trigger the rail-width animation is; honor the same
   attribute tier, no @media prefers-reduced-motion fork — uiprefs invariant). */
:root[data-motion="reduced"] .browse-shell--drawer-open .browse-rail { transition: none; }
:root[data-motion="reduced"] .rail-toggle,
:root[data-motion="reduced"] .rail-toggle svg { transition: none; }

/* C1 — main owns its OWN vertical scroll independent of the rail; min-height:0
   lets it shrink below its content inside the bounded shell so the card list
   scrolls HERE, never the body. This default carries the loading / empty / error
   states (and the home), which render one box straight into main and want main's
   own padding + scroll. The populated scoped view opts OUT via .browse-canvas. */
.browse-main { flex: 1 1 auto; min-width: 0; min-height: 0; overflow-y: auto; padding: var(--sp4); }

/* BC-1 controls-above-objects (the pin). The populated scoped view turns `main`
   into a column: a non-scrolling control BAND over a single scrolling object
   CANVAS — Spooky's FileBrowser arbitration (its one right call), so the toolbar,
   breadcrumb, and project header stay reachable while only the cards scroll under
   them. Pure CSS flex (band = flex-shrink:0; scroll = flex:1 + min-height:0 +
   overflow) — ZERO getBoundingClientRect; the analytic-layout invariant binds the
   browse chrome too. main sheds its own padding/scroll here and hands them to the
   two children, so the band's top/side gutter and the scroll's gutter stay honest
   (the column-flex sidesteps position:sticky fighting a padded container). */
.browse-main.browse-canvas { overflow: hidden; padding: 0; display: flex; flex-direction: column; }
/* BC-2: the pinned band reads as a HEADER over the scroll — a quiet --line hairline
   (token-only, matching the v2 mockup + Spooky's `border-b`) seats it above the
   objects. The band carries its OWN sp4 bottom padding now (was 0), so content
   breathes above the rule; the last child's trailing margin is collapsed so the
   gap below the toolbar/header doesn't DOUBLE with the scroll's padding-top — the
   wsp_-scope no-projectHeader double-gap the BC-1 pass left owed (toolbar's
   margin-bottom + scroll padding-top both fired). One rhythm: band content → sp4 →
   hairline → sp4 → first card, with or without a projectHeader. */
.browse-canvas > .browse-band { flex-shrink: 0; padding: var(--sp4); border-bottom: 1px solid var(--line); }
.browse-canvas > .browse-band > :last-child { margin-bottom: 0; }
.browse-canvas > .browse-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--sp4); }

/* ---------------------------------------------- the passkey enrollment offer
   PASSKEY-ENROLL-PROMPT-1. A quiet corner card, deliberately NOT a modal and NOT
   a toast: a modal would put a credential ceremony in front of work the user just
   navigated to, and a toast would vanish before anyone read it. Bottom-right, one
   --surface card in a --line hairline, seated above the shell's chrome but below
   the overlay layer (menus/popovers must still open over it). Same calm register
   as the guest-continue banner — the only accent is the primary verb. */
.passkey-offer {
  position: fixed; z-index: 60; right: var(--sp4); bottom: var(--sp4);
  width: min(340px, calc(100vw - 2 * var(--sp4)));
  display: flex; flex-direction: column; gap: var(--sp2);
  padding: var(--sp3); background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--lift-1);
}
.passkey-offer-title { margin: 0; font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink); }
.passkey-offer-line { margin: 0; font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.passkey-offer-actions { display: flex; align-items: center; gap: var(--sp2); margin-top: var(--sp1); }
.passkey-offer-add, .passkey-offer-later, .passkey-offer-done {
  font-family: var(--f-ui); font-size: var(--fs-chip); cursor: pointer;
  background: none; border: 1px solid var(--line); border-radius: 4px; padding: 3px 10px;
}
/* the ask carries the weight; declining stays a whisper, because it must cost nothing */
.passkey-offer-add, .passkey-offer-done { color: var(--ink); }
.passkey-offer-later { color: var(--ink-faint); border-color: transparent; }
.passkey-offer-add:hover:not(:disabled), .passkey-offer-add:focus-visible:not(:disabled),
.passkey-offer-done:hover, .passkey-offer-done:focus-visible,
.passkey-offer-later:hover:not(:disabled), .passkey-offer-later:focus-visible:not(:disabled) {
  border-color: var(--accent); color: var(--accent);
}
.passkey-offer-add:disabled, .passkey-offer-later:disabled { opacity: 0.55; cursor: progress; }
.passkey-offer-note { margin: 0; font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.passkey-offer-note:empty { display: none; }
.passkey-offer-note[data-tone="error"] { color: var(--rust); }
/* The browse toolbar (browse.ts renderToolbar) — the View's controls in a row:
   Appearance (the layout toggles) + "Sorted by" (the sort select). A quiet chrome
   strip that recedes (cool-neutral, brief §3); the controls speak --accent on
   their active/focus states like the rest of the library surface. Token-only. */
.browse-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp2);
  margin-bottom: var(--sp4);
}
/* The LEFT facet cluster (mockup, toolbar far-left) — "what am I looking at":
   All-assets + Archived grouped as ONE flex child, so the bar's space-between reads
   [facets | tools] (three loose siblings floated the Archived chip dead-center). */
.browse-facets { display: flex; align-items: center; gap: var(--sp2); }
/* The facet chips — recede chrome: cool bordered filter pills, TWINS (the Archived
   chip had no rule at all — a raw UA button — half of why the strip read random).
   aria-pressed = the teal-soft active fill, the same state language the layout
   segments speak (honest voice: pressed IS "you're looking at it"). */
.browse-scope,
.browse-archived {
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 4px 9px;
  cursor: pointer;
}
.browse-scope:hover:not(:disabled),
.browse-archived:hover { background: var(--wash); color: var(--ink); }
.browse-scope[aria-pressed="true"],
.browse-archived[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--ink);
  border-color: color-mix(in oklab, var(--accent) 35%, var(--line));
}
.browse-scope:focus-visible,
.browse-archived:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-scope:disabled { cursor: not-allowed; }
/* "New" action (mockup, toolbar far-right) — the CREATE call-to-action. It now
   wears the codified --primary-action role (DECISIONS #27(1)), the mockup's amber
   CTA fill — NOT the cool --accent (wayfinding) and NOT status --working (the
   --accent overload sin). Ink is --bg-deep on the light fill (contrast-pinned);
   hover/active ride the shared --hover-delta (channel A's one knob), exactly like
   button.primary. New is now LIVE (it opens the name dialog → mints a project,
   265811e); the :disabled recede below stays only as a defensive cue for any
   future disabled posture (the shared --set-aside-opacity dim, never a dead grey). */
.browse-new {
  font: inherit;
  font-size: var(--fs-chip);
  font-weight: 600;
  color: var(--bg-deep);
  background: var(--primary-action);
  border: 1px solid transparent;
  border-radius: 5px;
  padding: 4px 10px;
}
.browse-new:not(:disabled):hover {
  background: oklch(from var(--primary-action) calc(l + var(--hover-delta)) c h);
}
.browse-new:not(:disabled):active {
  background: oklch(from var(--primary-action) calc(l - var(--hover-delta)) c h);
}
/* the create verb earns the same 2px --accent focus ring its toolbar cluster-mates
   carry (.browse-sort, .browse-search-input) — it was the lone keyboard-reachable
   browse control without one. OUTSET (offset:1px) like those siblings, NOT the inset
   .layout-toggle needs: .browse-new has no overflow:hidden parent to clip the ring. */
.browse-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-new:disabled { cursor: not-allowed; opacity: var(--set-aside-opacity); }
.browse-folder-new {
  min-height: 30px;
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--ink);
  background: transparent;
  font: 500 var(--fs-chip)/1.2 var(--f-ui);
}
.browse-folder-new:hover { background: var(--wash); border-color: var(--ink-dim); }
.browse-folder-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* the view controls travel as ONE cluster (mockup) — layout switch · Fields · Sort,
   side by side on the right, not split far-left/far-right (the old margin-left:auto) */
.browse-tools {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  inline-size: 100%;
}
.browse-tools .browse-search { margin-inline-end: auto; }
/* the layout toggles read as one segmented control — a joined group, the mockup's
   view-mode switch. The GROUP owns the border + radius; the buttons inside shed
   their own box and share a hairline divider, so the trio is one object, not three
   loose chips. Token-only — teal-soft fills the active segment (cool profile). */
.layout-group {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 5px;
  overflow: hidden;
}
.layout-toggle { display: inline-flex; align-items: center; gap: 5px; }
.layout-toggle .icn { inline-size: 14px; block-size: 14px; flex: none; }
/* Scoped to .layout-group (NOT .browse-toolbar) so BOTH instances get the segmented
   chrome AND the active state — the home lens switch (.browse-home--populated) and the
   in-project View toolbar band, matching the focus-ring rule below. Previously the
   active-state background was .browse-toolbar-only, so on the home page the pressed
   segment rendered transparent — indistinguishable from its siblings (which mode am I in?). */
.layout-group .layout-toggle {
  font-size: var(--fs-chip);
  padding: 4px 10px;
  text-transform: capitalize;
  border: 0;
  border-right: 1px solid var(--line);
  border-radius: 0;
  color: var(--ink-dim);                 /* inactive segments RECEDE so the active one leads */
}
.layout-group .layout-toggle:last-child { border-right: 0; }
.layout-group .layout-toggle:hover { background: var(--surface); color: var(--ink); }
/* the active segment is unmistakable: accent-tinted fill + accent label + a touch of
   weight — the "you are here" of the segmented control, on-theme (accent is the note). */
.layout-group .layout-toggle[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}
/* the toggles are plain tab stops (each a focusable <button>), so a keyboard user
   ROVES onto them — they earn the same visible focus ring their siblings already
   carry (.browse-sort, .browse-search-input, .canvastabs .tab). UNSCOPED like
   .layout-group itself, so BOTH instances get it: the home lens switch (.browse-
   home--populated) and the in-project View toolbar band. The ring is INSET
   (outline-offset:-2px) because .layout-group's `overflow:hidden` would clip an
   outset one — the .canvastabs .tab-in-a-bar precedent. Token-only, 2px --accent. */
.layout-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.browse-sort {
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink-dim);   /* recede with its chip siblings — the value is chrome, not content */
  background: none;
  border: 1px solid var(--line);
  border-radius: 5px;      /* the toolbar chips' shared radius — one object family */
  padding: 4px 8px;
  cursor: pointer;
}
.browse-lifecycle {
  min-height: 30px;
  padding: 4px 28px 4px 9px;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink-dim);
  font: 500 var(--fs-chip)/1.2 var(--f-ui);
}
.browse-lifecycle:hover { color: var(--ink); border-color: var(--line-strong); }
.browse-lifecycle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-display-option {
  white-space: nowrap;
  color: var(--ink-faint);
}
.browse-sort:hover { border-color: var(--ink-dim); }
.browse-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* "Search" — the toolbar text facet (mockup, cluster-left). A quiet bordered field
   matching the .browse-sort chrome (same hairline, radius, fs-chip, teal focus ring);
   the form is layout-only so the input carries the box. Token-only, the cool profile. */
.browse-search { display: inline-flex; align-items: center; }
.browse-search-input {
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 4px 8px;
}
.browse-search-input::placeholder { color: var(--ink-dim); }
.browse-search-input:hover { border-color: var(--ink-dim); }
.browse-search-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* "Fields" — the metadata projection control (brief §5), the third member of the
   toolbar trio. It rides INSIDE the `.browse-tools` cluster now (no longer pinned to
   the far edge by margin-left:auto) — Appearance · Fields · Sorted by sit together.
   A native <details> popover: the summary is a quiet chrome chip, the panel a
   token-framed checkbox column. Token-only — teal interaction (the cool profile
   re-values --accent for the library surface), cool-neutral chrome ground, no hex. */
.browse-fields { position: relative; }
.browse-fields > summary {
  font-size: var(--fs-chip);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  list-style: none;            /* drop the default disclosure triangle */
  user-select: none;
}
.browse-fields > summary::-webkit-details-marker { display: none; }
.browse-fields > summary:hover { border-color: var(--ink-dim); }
.browse-fields > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-fields[open] > summary { border-color: var(--accent); color: var(--ink); }
.browse-fields-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  right: 0;
  min-width: 9rem;
  display: flex;
  flex-direction: column;
  gap: var(--sp1);
  padding: var(--sp2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: 0 6px 20px oklch(0 0 0 / 0.45);   /* the menu/popover shadow idiom */
}
.browse-field-opt {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  font-size: var(--fs-chip);
  color: var(--ink);
}
/* the visibility checkbox + label take the row's slack so the ↑/↓ reorder buttons
   hug the right edge of the popover. */
.browse-field-label {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  flex: 1;
  cursor: pointer;
}
.browse-field-label input { accent-color: var(--accent); cursor: pointer; }
/* the per-row reorder affordance — a quiet ghost icon button mirroring the toolbar's
   restraint (no fill, a wash only on hover), dimmed + inert at the ends of the run. */
.browse-field-move {
  border: 0;
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
  padding: 0 var(--sp1);
  line-height: 1;
  font-size: var(--fs-chip);
  border-radius: 4px;
}
.browse-field-move:hover:not(:disabled) { color: var(--ink); background: var(--wash); }
.browse-field-move:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-field-move:disabled { opacity: 0.3; cursor: default; }
/* Rename ✎ / Retire × on a custom mf_ field row — the schema-edit verbs, sharing the
   saved-views inline-edit idiom (recede → teal on hover/focus). Quiet ghost buttons that
   hug the row's right edge beside the ↑/↓ reorder controls. */
.browse-field-rename,
.browse-field-retire {
  flex: 0 0 auto;
  border: 0;
  background: none;
  color: var(--ink-dim);
  cursor: pointer;
  padding: 0 var(--sp1);
  line-height: 1;
  font-size: var(--fs-chip);
  border-radius: 4px;
}
.browse-field-rename:hover:not(:disabled),
.browse-field-retire:hover:not(:disabled) { color: var(--accent); background: var(--wash); }
.browse-field-rename:focus-visible,
.browse-field-retire:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-field-retire:disabled { opacity: 0.5; cursor: default; }
/* the inline edit form that replaces the row on rename — the input claims the row. */
.browse-field-rename-form { display: flex; flex-wrap: wrap; gap: var(--sp1); flex: 1; }
.browse-field-rename-name {
  flex: 1;
  min-width: 6rem;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
}
.browse-field-rename-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-field-rename-name:disabled { opacity: 0.5; cursor: default; }
.browse-field-rename-err { flex: 1 0 100%; font-size: var(--fs-chip); color: var(--rust); }

/* "＋ Add field" — the define form at the popover foot, fenced off the field rows by a
   hairline so it reads as the catalog-authoring affordance, not another toggle. The name
   input takes the slack, the type select hugs beside it, the button drops to its own row. */
.browse-field-add {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp1);
  margin-top: var(--sp1);
  padding-top: var(--sp2);
  border-top: 1px solid var(--line);
}
.browse-field-add-name {
  flex: 1;
  min-width: 5rem;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
}
.browse-field-add-name:focus-visible,
.browse-field-add-type:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-field-add-type {
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1);
}
.browse-field-add-btn {
  flex: 1 0 100%;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--accent);
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
  cursor: pointer;
}
.browse-field-add-btn:hover:not(:disabled) { background: var(--wash); }
.browse-field-add-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-field-add-btn:disabled { opacity: 0.5; cursor: default; }
.browse-field-add-err { flex: 1 0 100%; font-size: var(--fs-chip); color: var(--rust); }
.browse-field-manage {
  margin-top: var(--sp1); padding: var(--sp2); border: 0; border-top: 1px solid var(--line);
  background: none; color: var(--accent); text-align: left; font: var(--fs-chip)/1.2 var(--f-ui); cursor: pointer;
}
.browse-field-manage:hover { background: var(--wash); }

/* "Views" — the saved-view popover (browse.ts renderToolbar, SAVED VIEWS slice-3).
   Same details/summary + panel chrome as .browse-fields (one popover idiom, sin #34):
   recede-by-default summary, teal --accent on focus/open; the list entries are quiet
   wayfinding buttons (teal on hover/focus — re-applying a view IS navigation); the
   Save form mirrors the Add-field foot. Role tokens only — no hardcoded hex. */
.browse-views { position: relative; }
.browse-views > summary {
  font-size: var(--fs-chip);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.browse-views > summary::-webkit-details-marker { display: none; }
.browse-views > summary:hover { border-color: var(--ink-dim); }
.browse-views > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views[open] > summary { border-color: var(--accent); color: var(--ink); }
.browse-views-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  right: 0;
  min-width: 10rem;
  display: flex;
  flex-direction: column;
  gap: var(--sp1);
  padding: var(--sp2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: 0 6px 20px oklch(0 0 0 / 0.45);
}
.browse-views-empty { font-size: var(--fs-chip); color: var(--ink-dim); margin: 0; }
.browse-views-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
/* a saved-view entry = a re-apply button + a trailing delete affordance. */
.browse-views-item { display: flex; align-items: stretch; gap: 2px; }
.browse-views-apply {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
  cursor: pointer;
}
.browse-views-apply:hover { color: var(--accent); background: var(--wash); }
.browse-views-apply:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* the prune affordance — recedes (cool-neutral) until hover/focus reveals it. */
.browse-views-remove {
  flex: 0 0 auto;
  font: inherit;
  font-size: var(--fs-chip);
  line-height: 1;
  color: var(--ink-dim);
  background: none;
  border: 0;
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
  cursor: pointer;
}
.browse-views-remove:hover:not(:disabled) { color: var(--accent); background: var(--wash); }
.browse-views-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views-remove:disabled { opacity: 0.5; cursor: default; }
/* the inline-rename pencil shares the remove idiom (recede → teal on hover/focus). */
.browse-views-rename,
.browse-views-update {
  flex: 0 0 auto;
  font: inherit;
  font-size: var(--fs-chip);
  line-height: 1;
  color: var(--ink-dim);
  background: none;
  border: 0;
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
  cursor: pointer;
}
.browse-views-rename:hover:not(:disabled),
.browse-views-update:hover:not(:disabled) { color: var(--accent); background: var(--wash); }
.browse-views-rename:focus-visible,
.browse-views-update:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views-update:disabled { opacity: 0.5; cursor: default; }
/* the inline edit form that replaces the row on rename — the input claims the row. */
.browse-views-rename-form { display: flex; flex-wrap: wrap; gap: var(--sp1); flex: 1; }
.browse-views-rename-name {
  flex: 1;
  min-width: 6rem;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
}
.browse-views-rename-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views-rename-name:disabled { opacity: 0.5; cursor: default; }
.browse-views-rename-err { flex: 1 0 100%; font-size: var(--fs-chip); color: var(--rust); }
/* the save-current-view form at the popover foot — the Add-field foot's idiom. */
.browse-views-save {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp1);
  margin-top: var(--sp1);
  padding-top: var(--sp2);
  border-top: 1px solid var(--line);
}
.browse-views-save-name {
  flex: 1;
  min-width: 6rem;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--ink);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
}
.browse-views-save-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views-save-btn {
  flex: 1 0 100%;
  font: inherit;
  font-size: var(--fs-chip);
  color: var(--accent);
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: var(--sp1) var(--sp2);
  cursor: pointer;
}
.browse-views-save-btn:hover:not(:disabled) { background: var(--wash); }
.browse-views-save-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.browse-views-save-btn:disabled { opacity: 0.5; cursor: default; }
.browse-views-save-err { flex: 1 0 100%; font-size: var(--fs-chip); color: var(--rust); }

/* Slice-2 grouped-list lane divider (browse.ts laneHeader). A quiet uppercase
   micro-label marking each group bucket — the same idiom as the panel/strip
   headings (uppercase + tracking carries the smallness). Token-only; the full
   look (sticky lane, count badge) is the deferred look lane's. `grid-column:1/-1`
   makes it span the row when the grid skin lands; inert in today's unstyled list. */
.browse-lane {
  list-style: none;
  grid-column: 1 / -1;
  margin: var(--sp4) 0 var(--sp1);
  padding-top: var(--sp2);
  border-top: 1px solid var(--line);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: var(--fs-label);
  color: var(--ink-faint);
}
.browse-lane:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
/* Slice-3c — the bucket's TOTAL count (all pages), a dim parenthetical pill trailing
   the lane label ("ARCHIVED (12)"). Cool-neutral recede (chrome, not content): it's
   wayfinding metadata, so it stays quieter than the label and never competes with a
   teal interaction or warm content name. The parens are the CSS's so the DOM text is
   the raw number (one source: the server's count). */
.browse-lane-count {
  margin-left: var(--sp1);
  color: var(--ink-faint);
  opacity: 0.75;
  letter-spacing: 0;
}
.browse-lane-count::before { content: '('; }
.browse-lane-count::after { content: ')'; }

/* LIST-AS-TABLE (browse.ts list layout) — the brief §5 headline surface: the
   `list` layout is a dense metadata TABLE, not a stack of cards. A column header
   names each field; every settled row aligns into the SAME columns via the shared
   --list-cols track set (computed in browse.ts from the active Fields projection,
   so a hidden field drops its column in lock-step). Role tokens only (§3): the cool
   chrome RECEDES — faint uppercase header labels, --line row rules, dim meta inks —
   while the warm cream serif NAME leads as content and teal stays the lone
   interaction note (the openable hover/focus). The Name cell includes a compact
   media thumbnail; grid uses the same image language at gallery scale. */
.browse-list[data-layout="list"] {
  list-style: none; margin: 0; padding: 0;
  /* One parent grid resolves max-content ONCE across the header and every row.
     The former sibling grids reused the same template string but independently
     measured their contents, so Project/Duration/Status visibly wandered. */
  display: grid; grid-template-columns: var(--list-cols); column-gap: var(--sp3);
}
.browse-list[data-layout="list"] .browse-card,
.browse-list[data-layout="list"] .browse-colhead {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  /* the column rhythm rides the --sp3 step (was --sp4): the mockup's meta columns
     sit in a tighter cadence than the airy 16px spread read as. One token down
     pulls the name→meta gap onto the mockup's denser table rhythm. */
  column-gap: inherit;
  padding: var(--sp2);
  border-bottom: 1px solid var(--line);
  border-left: 3px solid transparent;   /* the project-color rail's gutter; header/colorless rows keep it clear */
}
/* the column-header row — quiet uppercase micro-labels, the same register as the
   group-lane divider; chrome recedes in --ink-faint with a firmer head rule. */
.browse-list[data-layout="list"] .browse-colhead {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-family: var(--f-ui);
  font-size: var(--fs-label);
  color: var(--ink-faint);
  border-bottom-color: var(--ink-deco);
}
.browse-list[data-layout="list"] .browse-colhead > *,
.browse-list[data-layout="list"] .browse-card > * { justify-self: start; text-align: left; }
/* the asset NAME is the content hero: warm serif, full ink, truncating (never
   wrapping) so the row rhythm holds down a long list. */
.browse-list[data-layout="list"] .card-name {
  font-family: var(--f-body);
  font-size: var(--fs-base);
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.browse-list[data-layout="list"] .asset-name-cell {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp2);
  min-width: 0;
  width: 100%;
}
.asset-version-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp1);
  min-height: 1.75rem;
  padding: 0 var(--sp2);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-dim);
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}
.asset-version-toggle:hover {
  border-color: var(--line-strong);
  background: var(--wash);
  color: var(--ink);
}
.asset-version-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.asset-version-icon { font-size: 0.9em; }
.asset-version-chevron {
  transition: transform 120ms ease;
  transform-origin: center;
}
.asset-version-toggle[aria-expanded="true"] .asset-version-chevron { transform: rotate(180deg); }
:root[data-motion="reduced"] .asset-version-chevron { transition: none; }
.browse-grid .asset-version-toggle {
  position: absolute;
  z-index: 1;
  top: var(--sp3);
  left: var(--sp3);
  border-color: color-mix(in oklab, var(--ink) 22%, transparent);
  background: color-mix(in oklab, var(--canvas) 82%, transparent);
  color: var(--ink);
  backdrop-filter: blur(10px);
}
.version-stack-drawer {
  min-width: 0;
  margin-top: var(--sp2);
  padding: var(--sp3);
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
  color: var(--ink);
}
.version-stack-drawer[hidden] { display: none; }
.version-stack-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp3);
  padding-bottom: var(--sp2);
  color: var(--ink);
  font: 600 var(--fs-ui)/1.3 var(--f-ui);
}
.version-stack-head > span {
  color: var(--ink-faint);
  font-size: var(--fs-note);
  font-weight: 400;
}
.version-stack-list {
  display: grid;
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  border-block: 1px solid var(--line);
}
.version-stack-member {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp3);
  width: 100%;
  min-height: 44px;
  padding: var(--sp2);
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.version-stack-list li + li .version-stack-member { border-top: 1px solid var(--line); }
.version-stack-member:hover { background: var(--wash); }
.version-stack-member:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.version-stack-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 1.75rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--ink-dim);
  font: 600 var(--fs-chip)/1 var(--f-ui);
  font-variant-numeric: tabular-nums;
}
.version-stack-copy {
  display: grid;
  min-width: 0;
  gap: 2px;
}
.version-stack-copy strong {
  overflow: hidden;
  color: var(--ink);
  font: 500 var(--fs-ui)/1.25 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.version-stack-copy span,
.version-stack-open,
.version-stack-state {
  color: var(--ink-faint);
  font: var(--fs-note)/1.3 var(--f-ui);
}
.version-stack-open { color: var(--accent); }
.version-stack-state { margin: 0; padding: var(--sp3); }
.version-stack-error { color: var(--rust); }
.browse-list[data-layout="list"] .version-stack-drawer {
  grid-column: 1 / -1;
  justify-self: stretch;
  width: 100%;
  margin: var(--sp1) 0;
}
.browse-grid .version-stack-drawer { margin: var(--sp1) var(--sp2) var(--sp2); }
.asset-version-toggle--recent {
  position: absolute;
  z-index: 2;
  /* Anchor to the upload ROW, not 50% of the whole .recent-item: that wrapper
     grows when history expands, and percentage centering would drag this trigger
     down into its own drawer. */
  top: 1.9rem;
  right: calc(44px + var(--sp2));
  transform: translateY(-50%);
}
.recent-item:has(.asset-version-toggle--recent) .recent-link {
  padding-right: calc(9rem + var(--sp4));
}
.recent-item > .version-stack-drawer {
  margin: 0 var(--sp3) var(--sp3);
}
@media (max-width: 48rem) {
  .asset-version-toggle { min-height: 44px; }
  .asset-version-toggle--recent {
    position: static;
    transform: none;
    margin: var(--sp1) calc(44px + var(--sp2)) var(--sp2) var(--sp3);
  }
  .recent-item:has(.asset-version-toggle--recent) .recent-link {
    padding-right: calc(44px + var(--sp2));
  }
  .version-stack-head { align-items: flex-start; flex-direction: column; gap: 2px; }
  .version-stack-open { display: none; }
}
.browse-list[data-layout="list"] .asset-name-cell .card-thumb {
  width: 5rem;
  aspect-ratio: 16 / 9;
  border-radius: 3px;
}
.browse-list[data-layout="list"] .pending-status {
  grid-column: 2 / -1;
  justify-self: stretch;
  display: flex;
  align-items: center;
  gap: var(--sp3);
  min-width: 0;
  color: var(--ink-dim);
  font-family: var(--f-ui);
  font-size: var(--fs-note);
}
.browse-list[data-layout="list"] .pending-status .card-progress {
  flex: 1 1 12rem;
  width: min(18rem, 100%);
  max-width: 18rem;
  margin: 0;
}
.browse-list[data-layout="list"] .pending-status .card-spinner,
.browse-list[data-layout="list"] .pending-status .card-error { margin: 0; }
.browse-list[data-layout="list"] .pending-status .card-error {
  flex-wrap: nowrap;
  width: min(100%, 36rem);
}
.browse-list[data-layout="list"] .pending-status .card-error-reason {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* the meta columns recede to chrome ink + the UI face. */
.browse-list[data-layout="list"] .card-project,
.browse-list[data-layout="list"] .card-dur,
.browse-list[data-layout="list"] .card-state {
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink-dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.browse-list[data-layout="list"] .card-dur { font-variant-numeric: tabular-nums; }
.browse-list[data-layout="list"] .card-mf-select { justify-self: stretch; }
.card-mf-link { color: var(--accent); text-decoration: none; display: block; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-mf-link:hover { text-decoration: underline; }
/* CUSTOM mf_ FIELDS — the look-lane skin for user-defined columns (8f0360e landed
   the cells UNSTYLED, so a custom column read at full base size and broke the row
   rhythm). A custom field is the user's OWN authored datum, NOT system chrome: it
   joins the table recede (cool UI face, note size, truncating) so the column aligns
   into the same rhythm as the built-in meta, but reads at full --ink — one ink-step
   UP from the --ink-dim built-ins — so a column the user deliberately added carries
   a touch more presence as their data, WITHOUT borrowing teal (--accent stays the
   lone interaction note, §3 theme discipline). tabular-nums because custom fields
   skew numeric (fps, frame rate, day #). Token-only; Jesse owns the final feel. */
.browse-list[data-layout="list"] .card-mf {
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The project-color rail is RETIRED on ASSET rows (Jesse 2026-07-20): a project's
   identity color belongs on the project DISPLAY (its circle/tile), not echoed onto every
   asset inside it — that read as noise. Assets keep the transparent gutter (line ~506) so
   the row rhythm holds; a genuine per-ASSET color (none by default) is a separate,
   low-priority idea. The rail stays on the project rows/tiles (.project-item/.project-tile). */
/* the openable row earns a teal-adjacent hover wash (the focus ring is the shared
   .openable:focus-visible below). */
.browse-list[data-layout="list"] .browse-card.openable:hover { background: var(--wash-soft); }
/* Selection is a working SET, not a hover accident. One click and Shift/Cmd-click
   therefore earn a persistent field + complete inset frame in both projections.
   The frame is deliberately uniform (never a side stripe) so it cannot be confused
   with project identity or lifecycle state. */
.browse-card.selected {
  background: color-mix(in oklab, var(--wash) 76%, var(--accent) 6%);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 72%, var(--line));
}
.browse-card.selected:hover {
  background: color-mix(in oklab, var(--wash) 84%, var(--accent) 8%);
}
.asset-selection-mark {
  display: none;
  position: absolute;
  inset-block-start: var(--sp2);
  inset-inline-start: var(--sp2);
  inline-size: 1.25rem;
  block-size: 1.25rem;
  place-items: center;
  border: 1px solid color-mix(in oklab, var(--accent) 78%, var(--line));
  border-radius: 4px;
  background: var(--accent);
  color: var(--bg);
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  font-weight: 700;
  line-height: 1;
  pointer-events: none;
  z-index: 2;
}
.browse-card.selected .asset-selection-mark { display: grid; }
.browse-list[data-layout="list"] .asset-selection-mark {
  inset-block-start: 50%;
  transform: translateY(-50%);
}
/* an archived row keeps its dim + the named "Archived" chip, but in the grid the
   chip's ::after must not claim a metadata column — span it full-width on its own
   line below the row so the columns stay aligned. */
.browse-list[data-layout="list"] .browse-card.archived::after {
  grid-column: 1 / -1;
  justify-self: start;
  margin-left: 0;
}

/* GRID GALLERY (browse.ts grid layout, Slice-3) — the brief §5 card grid: the SAME
   server rows the list TABLE renders, only RE-SHAPED into a thumbnail gallery (sin
   #34 — no re-query, no client predicate; the layout decides shape, never data). An
   auto-fill track lays as many cards per row as the content measure allows and
   reflows responsively; `cardSize` (sm|md|lg, stamped data-card-size) tunes the min
   card width so the same view goes denser/larger without a new layout. Role tokens
   only (§3): the cool --surface tile recedes as chrome, the warm serif name leads as
   content, the project-color rail carries identity, teal stays the lone interaction
   note (the shared .openable hover/focus below). The grouped-lane band already spans
   the row (.browse-lane carries grid-column:1/-1) so buckets read across the gallery. */
.browse-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--grid-card-min, 15rem), 1fr));
  gap: var(--sp4);
  align-items: start;
}
.browse-grid[data-card-size="sm"] { --grid-card-min: 11rem; }
.browse-grid[data-card-size="md"] { --grid-card-min: 15rem; }
.browse-grid[data-card-size="lg"] { --grid-card-min: 20rem; }
/* a gallery card is a STACKED tile — the 16/9 poster on top (the shared .card-thumb
   box), the warm serif name beneath, then the cool meta run. The project-color rail
   wraps the tile's left edge in the SAME --card-accent the list row carries, so a
   project reads the same one mode over. */
.browse-grid .browse-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp2);
  padding: var(--sp2);
  border: 1px solid var(--line);   /* uniform hairline — the project-color left rail is RETIRED on asset tiles (Jesse 2026-07-20; color lives on the project display) */
  border-radius: 8px;
  background: var(--surface);
  min-width: 0;
}
.browse-grid .browse-card.openable:hover { background: var(--wash-soft); }
.browse-grid .card-name {
  font-family: var(--f-body);   /* warm serif = content/name */
  font-size: var(--fs-base);
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* the card's meta run wraps under the name (cool UI face, dim ink) — the SAME Fields
   projection the list cells lay, only stacked instead of column-aligned. */
.browse-grid .card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp1) var(--sp2);
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink-dim);
  min-width: 0;
}
.browse-grid .card-meta .card-mf { color: var(--ink); font-variant-numeric: tabular-nums; }
.card-mf-select {
  display: inline-flex; align-items: center; width: max-content; min-height: 1.5rem;
  padding: 0 .55rem; border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink-dim); background: color-mix(in oklab, var(--surface) 78%, transparent);
  font-size: var(--fs-note); white-space: nowrap;
}
.card-mf-select[data-color="amber"] { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line)); }
.card-mf-select[data-color="rust"] { color: var(--rust); border-color: color-mix(in oklab, var(--rust) 45%, var(--line)); }
.card-mf-select[data-color="green"] { color: var(--ready); border-color: color-mix(in oklab, var(--ready) 45%, var(--line)); }
.card-mf-picker {
  max-width: 12rem; border: 0; background: transparent; color: inherit;
  font: inherit; cursor: pointer;
}
.card-mf-picker:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 999px; }
.card-mf-picker:disabled { cursor: wait; opacity: .65; }

/* Slice-3 — the detail-arm master–detail surface (browse.ts detailLayout). The
   master list keeps its `.browse-list` skin; this only lays the optional preview
   pane beside it (a two-column split that collapses to one when the pane is absent
   or the viewport is narrow). FUNCTIONAL structure only — the preview's full voice
   (player surface, field rows, chrome) is the deferred look lane's. Token-only. */
.browse-detail { display: grid; gap: var(--sp5); align-items: start; }
.browse-detail:has(.browse-preview) { grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); }
.browse-preview {
  position: sticky;
  top: var(--sp4);
  display: flex;
  flex-direction: column;
  gap: var(--sp2);
  padding: var(--sp4);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
}
.browse-preview.empty { color: var(--ink-faint); }
/* the pane's asset name is CONTENT, not chrome — the SAME warm serif (--f-body) at
   the content size (--fs-base) the list/grid .card-name carry, so one name reads as
   one voice across all three skins (sin #34). It's the largest instance of the name
   (the pane heading), so it must NOT recede to the cool --f-ui/--fs-ui chrome face. */
.preview-name { margin: 0; font-family: var(--f-body); font-size: var(--fs-base); color: var(--ink); }
.preview-meta { display: flex; flex-wrap: wrap; gap: var(--sp2); font-size: var(--fs-note); color: var(--ink-dim); }
/* the same custom-field signature in the detail pane: the user's authored datum
   reads a step UP (--ink) from the --ink-dim sibling notes, tabular figures, no
   teal. The pane wraps, so no truncation here. (mirrors .card-mf above). */
.preview-meta .preview-mf { color: var(--ink); font-variant-numeric: tabular-nums; }

@media (max-width: 48rem) {
  /* narrow: stack the pane under the list, drop the sticky follow */
  .browse-detail:has(.browse-preview) { grid-template-columns: 1fr; }
  .browse-preview { position: static; }

  /* A phone is not a compressed desktop asset table. The prior subgrid kept all
     five desktop headings while the row contents overflowed beneath one another,
     producing the mashed "NAMEPROJECT" label and metadata with no reliable track.
     Recompose the same row into a compact record: identity owns the first read,
     while state + the primary custom field (Status by default) remain available.
     Project is redundant inside a project and Duration yields at this width; the
     Fields control still governs which surviving custom fields are rendered. */
  .browse-list[data-layout="list"] {
    display: block;
  }
  .browse-list[data-layout="list"] .browse-colhead {
    display: none;
  }
  .browse-list[data-layout="list"] .browse-card {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--sp2);
    row-gap: var(--sp1);
  }
  .browse-list[data-layout="list"] .asset-name-cell {
    grid-column: 1 / -1;
    grid-template-columns: 4rem minmax(8rem, 1fr);
  }
  .browse-list[data-layout="list"] .asset-name-cell .card-thumb { width: 4rem; }
  .browse-list[data-layout="list"] .card-project,
  .browse-list[data-layout="list"] .card-dur {
    display: none;
  }
  .browse-list[data-layout="list"] .card-state {
    grid-column: 1;
    grid-row: 2;
    padding-inline-start: calc(4rem + var(--sp2));
  }
  .browse-list[data-layout="list"] .card-mf[data-field="mf_status"] {
    grid-column: 2;
    grid-row: 2;
  }
  .browse-list[data-layout="list"] .pending-status { gap: var(--sp2); }
}

/* a READY card opens its canvas on click/Enter (browse.ts, LANE-1 slice 3). The
   functional affordance only — pointer + a keyboard-focus ring; the card's full
   look (rail, hover, grid skin) is the deferred look lane's. Token-only outline. */
.browse-card.openable { cursor: pointer; }
.browse-card.openable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The selected-assets shelf makes the file-manager grammar self-explanatory:
   one click primes an asset, Open is explicit for people who dislike double-click,
   and Actions rides the SAME selection-aware menu provider as right-click/⋯.
   A compact centered shelf avoids covering the rail and scales down to a phone-width
   bottom bar with safe-area breathing room. */
.asset-selection-shelf {
  position: fixed;
  inset-inline-start: 50%;
  inset-block-end: max(var(--sp4), env(safe-area-inset-bottom));
  z-index: 40;
  transform: translateX(-50%);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp3);
  inline-size: min(44rem, calc(100vw - 2 * var(--sp5)));
  min-block-size: 3.5rem;
  padding: var(--sp2);
  border: 1px solid color-mix(in oklab, var(--accent) 44%, var(--line));
  border-radius: 8px;
  background: color-mix(in oklab, var(--surface) 94%, var(--bg));
  box-shadow: 0 12px 32px color-mix(in oklab, var(--bg) 72%, transparent);
}
.asset-selection-shelf[hidden] { display: none; }
.asset-selection-clear,
.asset-selection-all,
.asset-selection-more,
.asset-selection-share,
.asset-selection-open {
  min-block-size: 2.5rem;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: transparent;
  color: var(--ink);
  font: 600 var(--fs-ui) / 1 var(--f-ui);
  cursor: pointer;
}
.asset-selection-clear {
  inline-size: 2.5rem;
  color: var(--ink-dim);
  font-size: var(--fs-base);
}
.asset-selection-copy {
  display: grid;
  gap: 2px;
  min-inline-size: 0;
}
.asset-selection-summary,
.asset-selection-hint {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-selection-summary {
  color: var(--ink);
  font: 600 var(--fs-ui) / 1.3 var(--f-ui);
}
.asset-selection-hint {
  color: var(--ink-faint);
  font: var(--fs-note) / 1.3 var(--f-ui);
}
.asset-selection-actions { display: flex; gap: var(--sp2); }
.asset-selection-more,
.asset-selection-all,
.asset-selection-share,
.asset-selection-open { padding-inline: var(--sp3); }
.asset-selection-open {
  border-color: color-mix(in oklab, var(--accent) 68%, var(--line));
  background: var(--accent);
  color: var(--bg);
}
.asset-selection-clear:hover,
.asset-selection-all:hover,
.asset-selection-more:hover,
.asset-selection-share:hover { background: var(--wash); }
.asset-selection-open:hover {
  background: oklch(from var(--accent) calc(l + var(--hover-delta)) c h);
}
.asset-selection-clear:focus-visible,
.asset-selection-all:focus-visible,
.asset-selection-more:focus-visible,
.asset-selection-share:focus-visible,
.asset-selection-open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 40rem) {
  .asset-selection-shelf {
    inset-block-end: max(var(--sp2), env(safe-area-inset-bottom));
    inline-size: calc(100vw - 2 * var(--sp2));
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--sp2);
  }
  .asset-selection-clear,
  .asset-selection-all,
  .asset-selection-more,
  .asset-selection-share,
  .asset-selection-open {
    min-block-size: 2.75rem;
  }
  .asset-selection-hint,
  .asset-selection-all,
  .asset-selection-more { display: none; }
  /* One selected asset gets the direct Open action. A batch cannot open, so
     spend that same scarce mobile slot on the shared Actions menu instead of
     leaving the user with a selection they can only dismiss. */
  .asset-selection-shelf.is-multi .asset-selection-more:not([hidden]) {
    display: block;
  }
}

/* the scoped-project header (W2 lane-2) — FUNCTIONAL structure only: a row with a
   color swatch that consumes --project-accent (the recolor swatch must show the
   color it represents) and the project title. The full voice (typography, chrome,
   the recolor swatch grid) is the deferred look lane's, like the cards above. */
/* the scope-climb breadcrumb (brief §7): the ancestor trail above the list. §3
   theme discipline — cool-neutral chrome RECEDES: the separator + the current leaf
   read in the dim/faint inks, while the one climbable crumb (Home) is the lone
   interaction note in --accent (teal). No hardcoded hex — all token-borne. */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  min-inline-size: 0;
  overflow-x: auto;
  scrollbar-width: none;
  white-space: nowrap;
  font-family: var(--f-ui);
  font-size: var(--fs-base);
  line-height: 1.4;
}
.breadcrumb::-webkit-scrollbar { display: none; }
.crumb-sep { color: var(--ink-faint); user-select: none; }
.crumb-current { color: var(--ink); font-weight: 500; }
/* The hierarchy band stays typographically flat and quiet. Populated scopes move
   project utilities into the toolbar; an empty scope may keep their compact group
   in the optional trailing track because it has no toolbar of its own. */
.project-header.browse-orient {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp4);
  inline-size: 100%;
  margin-bottom: var(--sp3);
}
.browse-orient .project-title {
  font: inherit;
  font-weight: 500;
  color: inherit;
  line-height: inherit;
}
.project-identity-copy,
.project-identity {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.project-header .project-description,
.project-list .project-description,
.project-grid .project-description,
.preview-description {
  color: var(--ink-faint);
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  font-weight: 400;
  line-height: 1.35;
}
.project-header .project-description {
  max-inline-size: min(34rem, 40vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.crumb-link {
  color: var(--ink-dim);
  text-decoration: none;
  cursor: pointer;
}
.crumb-link:hover { color: var(--ink); }

.project-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* NO base row spacing here: the `.browse-orient` variant owns the complete
     breadcrumb-and-controls band's --sp3 rhythm (design-audit pins this). */
}
.project-scope-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--sp1);
  justify-self: end;
}
.project-swatch {
  inline-size: 0.85rem;
  block-size: 0.85rem;
  border-radius: 50%;
  flex: none;
  background: var(--project-accent, var(--project-slate));
}
.project-swatch.no-accent { background: var(--project-slate); }
.project-title { font-weight: 600; }

/* Project people are internal access, not the external Share action. The compact
   labelled control lives beside the project identity so access is discoverable
   without turning the breadcrumb into another toolbar. */
.project-people {
  display: inline-flex;
  align-items: center;
  gap: var(--sp1);
  min-block-size: 2rem;
  padding: 0 var(--sp2);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font: 600 var(--fs-chip)/1 var(--f-ui);
  cursor: pointer;
}
.project-people-label { display: none; }
.project-people svg { inline-size: 1rem; block-size: 1rem; }
.project-people-summary {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-variant-numeric: tabular-nums;
}
.project-people-summary[hidden] { display: none; }
.project-people-avatars { display: inline-flex; align-items: center; }
.project-people-avatar {
  display: inline-grid;
  place-items: center;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  border: 1px solid var(--bg);
  border-radius: 50%;
  background: color-mix(in oklab, var(--accent) 18%, var(--surface));
  color: var(--ink);
  font: 650 0.48rem/1 var(--f-ui);
  letter-spacing: -0.02em;
}
.project-people-avatar + .project-people-avatar { margin-inline-start: -0.35rem; }
.project-people-count { color: var(--ink); }
.project-people:hover { color: var(--ink); border-color: var(--line-strong); background: var(--wash); }
.project-people:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.project-people[data-loading="true"] { cursor: progress; }
.project-people[data-loading="true"] .project-people-icon { animation: project-people-pulse 900ms ease-in-out infinite alternate; }
@keyframes project-people-pulse { to { opacity: .35; } }
:root[data-motion="reduced"] .project-people[data-loading="true"] .project-people-icon {
  animation: none;
  opacity: .55;
}
.project-people[data-access-mode="restricted"]::after {
  content: "Restricted";
  color: var(--ink-faint);
  font-weight: 500;
}
@media (max-width: 560px) {
  .browse-orient { gap: var(--sp2); }
  .breadcrumb { padding-block: var(--sp1); font-size: 1rem; }
  .browse-toolbar { align-items: stretch; flex-direction: column; }
  .browse-facets {
    max-inline-size: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .browse-facets::-webkit-scrollbar { display: none; }
  /* Mobile keeps the complete project toolset visible. The previous horizontal
     scroller technically retained Upload/New folder/sort/layout, but presented no
     scroll cue and left every control after Views outside the viewport. Reflow the
     SAME controls instead: search gets its own line, metadata/filter controls pack
     naturally, and List/Grid remains the stable trailing edge. No mobile-only menu
     or duplicate action model. */
  .browse-tools {
    max-inline-size: 100%;
    flex-wrap: wrap;
    align-items: center;
    overflow: visible;
    row-gap: var(--sp2);
  }
  .browse-tools > * { flex: none; }
  .browse-tools .browse-search {
    flex: 1 0 100%;
    margin-inline-end: 0;
    min-inline-size: 0;
  }
  .browse-tools .browse-search-input {
    inline-size: 100%;
    min-block-size: 2.75rem;
  }
  .browse-tools .browse-fields > summary,
  .browse-tools .browse-views > summary,
  .browse-tools .browse-sort,
  .browse-tools .browse-lifecycle,
  .browse-tools .browse-folder-new,
  .browse-tools .browse-new,
  .browse-tools .browse-scope,
  .browse-tools .layout-toggle {
    min-block-size: 2.75rem;
  }
  .browse-tools .browse-fields > summary,
  .browse-tools .browse-views > summary {
    display: flex;
    align-items: center;
  }
  .browse-tools .layout-group { margin-inline-start: auto; }
  .project-people { min-inline-size: 2.75rem; min-block-size: 2.75rem; justify-content: center; padding: 0 var(--sp1); }
  .project-people-label,
  .project-people[data-access-mode="restricted"]::after { display: none; }
  .project-people:has(.project-people-summary:not([hidden])) .project-people-icon { display: none; }
  .project-people-avatar { inline-size: 1.35rem; block-size: 1.35rem; }
  .project-people-avatar:nth-child(n + 2) { display: none; }
}

/* the ARCHIVED cue (look-lane) — both the scoped header (self-archived, STAYS in
   view) and a still-shown card carry `.archived` from the render (browse.ts
   170/233) and the optimistic Archive verb (cardmenu.ts), yet the class was
   visually SILENT: only the transient toast cued it. Two reads: (1) a uniform dim
   so the set-aside item recedes without disappearing, and (2) a quiet token-only
   "Archived" chip via ::after so the state is named, not just inferred from the
   fade. Pure look — no DOM/JS change. FUNCTIONAL voice only (Jesse owns the exact
   styling), same discipline as the recolor swatch / R28 rip; static, no motion. */
.browse-card.archived,
.project-header.archived,
/* a home project row optimistically archived from its context menu (cardmenu.ts):
   the row dims in place until the next navigation drops it (the home lists active
   projects). Same set-aside recede as a card/header — the chip placement on the
   --list-cols grid row is a look-lane follow-on (#18). */
.project-item.archived,
.project-tile.archived { opacity: var(--set-aside-opacity); }
.browse-card.archived::after,
.project-header.archived::after {
  content: "Archived";
  flex: none;                 /* in the flex header it's a non-growing trailing item */
  align-self: center;
  padding: 1px var(--sp1);
  border-radius: 3px;
  /* the trio's grey status, made visible: a grey-washed pill ringed in the
     --archived role (not the generic --wash) so "grey = archived" SHOWS and the
     role is genuinely consumed — revert the token and the pill loses its grey. */
  background: var(--archived-wash);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--archived) 45%, transparent);
  color: var(--ink-dim);
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* the header is the flex row above — its `gap` already separates the chip; the
   card is block flow, so the chip needs its own inline separation. */
.browse-card.archived::after { display: inline-block; margin-left: var(--sp2); vertical-align: middle; }

/* the per-row / header ⋯ Actions affordance (A2 slice-3 DISCOVERABILITY): the W2/W3
   Rename/Recolor/Archive/Move verbs were reachable by right-click ONLY (invisible) —
   this is the file-manager kebab that opens the SAME menu. FUNCTIONAL voice + a11y
   only; Jesse owns the final feel. At rest it recedes to chrome ink; hover/focus
   lights the lone interaction note (--accent teal), never a second color (§3). */
.row-actions {
  flex: none;
  border: none;
  background: none;
  cursor: pointer;
  padding: 0 var(--sp1);
  border-radius: 4px;
  color: var(--ink-faint);
  font-size: var(--fs-base);
  line-height: 1;
}
.row-actions:hover { color: var(--accent); background: var(--accent-soft); }
.row-actions:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; color: var(--accent); }
/* IN A CARD: absolutely placed so it claims NO grid track — the --list-cols columns
   and the colhead stay column-aligned (an in-flow child would add a stray cell). It
   reveals on row hover / focus so it never sits over the last column's text at rest. */
.browse-card { position: relative; }
.browse-card > .row-actions {
  position: absolute;
  top: 50%;
  right: var(--sp2);
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
}
.browse-card:hover > .row-actions,
.browse-card:focus-within > .row-actions,
.browse-card > .row-actions:focus-visible { opacity: 1; pointer-events: auto; }
/* IN THE GRID gallery the card is tall — pin the kebab to the top-right corner. */
.browse-grid .browse-card > .row-actions { top: var(--sp2); right: var(--sp2); transform: none; }
/* IN THE SCOPE HEADER it belongs to the explicit trailing control group, never the
   breadcrumb reading stream. Keep it large enough to be a calm touch target. */
.project-scope-actions > .row-actions {
  display: inline-grid;
  place-items: center;
  inline-size: 2rem;
  block-size: 2rem;
  padding: 0;
}
@media (max-width: 560px) {
  .project-scope-actions > .row-actions { inline-size: 2.75rem; block-size: 2.75rem; }
}
/* ON A HOME PROJECT ROW (list .project-item / detail master) — the ROW-PRIMITIVE pilot.
   The li is now a `.row` and the kebab lives in its `.row-trail` slot (styles/row.css),
   which drives the opacity hover-reveal. A grid-table row can't let that trailing slot
   claim a --list-cols track (it would knock the rows out of alignment with the colhead),
   so — exactly like the asset card — pin `.row-trail` ABSOLUTELY over the trailing edge
   instead of the primitive's in-flow margin-left:auto. The li carries the data-ctx='project'
   host the kebab forwards into the project menu. */
.project-item { position: relative; }
.project-item > .row-trail {
  position: absolute;
  top: 50%;
  right: var(--sp2);
  transform: translateY(-50%);
}
/* pointer-events gating (the primitive reveals with opacity only): under a hover pointer
   the invisible-at-rest slot must not eat a click over the last cell's text, so it stays
   non-interactive until the row is hovered/focused — matching the opacity reveal. On a
   coarse/no-hover pointer the primitive keeps the slot VISIBLE, and pointer-events default
   to auto here, so the kebab stays tappable (touch can't hover-reveal). */
@media (hover: hover) {
  .project-item > .row-trail { pointer-events: none; }
  .project-item:hover > .row-trail,
  .project-item:focus-within > .row-trail { pointer-events: auto; }
}
/* ON A GRID TILE the tile is tall — pin the kebab to the top-right corner (matching
   the asset grid card), always shown (a stacked tile has no last-column text to clear). */
.project-tile { position: relative; }
.project-tile > .row-actions {
  position: absolute; top: var(--sp2); right: var(--sp2);
  /* TILE-KEBAB-LIFT (Jesse 2026-07-20): the has-poster tile lifts its full-tile link to
     z-index:1 (text above the art, ~line 1399) — without a HIGHER index here the link ate
     the kebab's clicks on every poster tile (the tell: right-click still worked, it rides
     the row socket). z-index:2 restores the click on poster and poster-less tiles alike. */
  z-index: 2;
  /* …and make it READ on any backdrop (poster frames + color bands both defeated the bare
     --ink-faint glyph): the ink-on-media glyph on a quiet scrim disc — the same two tokens
     the poster tile's name legibility already rides (one on-media language). */
  color: var(--ink-on-media);
  background: var(--scrim);
  border-radius: 6px;
  padding: 2px var(--sp1);
}
.project-tile > .row-actions:hover,
.project-tile > .row-actions:focus-visible {
  color: var(--accent);
  background: var(--scrim);
}

/* Recent uploads use the same trailing-action placement as project rows. Previously the
   bare kebab sat after the full-width row button, which made it wrap onto a stray second
   line on phones. It is an overlay affordance, not another row of content. */
.recent-item { position: relative; }
.recent-item > .row-actions {
  position: absolute;
  top: 50%;
  right: var(--sp2);
  transform: translateY(-50%);
}
@media (hover: hover) {
  .recent-item > .row-actions { opacity: 0; pointer-events: none; transition: opacity 0.12s ease; }
  .recent-item:hover > .row-actions,
  .recent-item:focus-within > .row-actions { opacity: 1; pointer-events: auto; }
}

/* ───────────────────────────────────────────────────────────────────────────
   The HOME / scope-state shell (browser-shell brief §1/§6 — the nav-shell's
   landing surface). browse.ts renders EVERY messaging state (no-scope home,
   empty-project scope, loading, true-empty welcome, couldn't-load) through ONE
   `.browse-state` box: an `.state-title` heading, a `.state-line` subline, an
   optional `.state-cta` (New project), and — on the populated home — a
   `.project-list` and a `.recent-list` of row links into each scope. These were
   structurally correct but VISUALLY SILENT (raw unstyled HTML stacked at the
   top-left) — the "doesn't feel like a real app / no evidence of real flow" gap
   Jesse filed the brief over. This is the FUNCTIONAL-voice look-lane pass: §3
   token discipline (cool-neutral chrome RECEDES, warm serif names content, teal
   --accent is the lone interaction note), no hardcoded hex. Jesse owns the exact
   visual taste (#18 feel-check); this just makes the states read deliberate.
   NB: the home's "New project" is a real action but NOT the app's single PRIMARY
   verb — that register (`button.primary`, accent fill) is reserved for the
   editor's render command. The brief's "amber = primary action" wants a
   dedicated primary token the palette doesn't carry yet → flagged for the
   look-lane, not invented here; the CTA stays the standard bordered register. */
/* HOME-FLASH (Jesse: "two incorrect frames … worse than NO feedback") — the
   delay-before-show gate the boot surface + the scoped skeleton already ride, done
   in CSS: the home's loading state stays INVISIBLE for its first ~150ms, so a fast
   settle paints the real home with ZERO intermediate frames; a genuinely slow load
   fades the feedback in (still honest). settle() drops aria-busy → this rule stops
   matching → the final copy is visible instantly regardless of timing. */
.browse-state[data-state="no-scope"][aria-busy="true"] {
  opacity: 0;
  animation: state-delay-reveal 0.18s ease 0.15s forwards;
}
@keyframes state-delay-reveal { to { opacity: 1; } }

.browse-state {
  max-width: 52ch;            /* a readable measure — copy + lists don't sprawl edge-to-edge */
  margin: var(--sp6) auto 0;
  padding: 0 var(--sp4);
  display: flex;
  flex-direction: column;
  gap: var(--sp3);
}
/* `display:flex` is author CSS and therefore outranks the browser's built-in
   `[hidden] { display:none }`. Loading gates use the hidden attribute as their
   source of truth, so pin it explicitly or a logically hidden fast-load state
   still flashes on screen. */
.browse-state[hidden] { display: none; }
/* The home (`state-no-scope`) is a real library surface, so it is left-anchored and
   fills the content measure (shell-bounded by C1 — CONTAINED, never sprawls) for
   ALL its states: loading, populated, empty-welcome, couldn't-load. Keying the
   geometry on `state-no-scope` (not only the populated class browse.ts adds once
   content paints) kills the loading→populated JUMP (Jesse: the "thin centered page
   then snap to the real layout" microflash) — the box never repositions, only its
   copy + list swap in place. The 52ch dead-centered message box stays for the
   SCOPED states (empty project, no-results) — this rule is home-only. */
.browse-state.browse-home--populated,
.browse-state.state-no-scope {
  max-width: none;
  margin: var(--sp4) 0 0;
}
/* EMPTY-SCOPE = a proper DROP-ZONE CARD (Jesse 2026-07-20 — Frame/Spooky parity). The old
   52ch left-stacked "Nothing here yet" column read weird; this fills the content area as a
   dashed, centered upload target: cloud glyph → title → line → the actions in a ROW. The
   armDropZone .drag-over state lights the border + a soft wash so a drag reads as "drop
   here" (the .drop-overlay copy rides on top). position:relative anchors that overlay. */
.browse-state.state-empty-scope {
  max-width: none;
  margin: var(--sp4) 0 0;
  min-height: 340px;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--sp2);
  padding: var(--sp6);
  border: 2px dashed var(--line);
  border-radius: 12px;
  position: relative;
}
.browse-state.state-empty-scope.drag-over {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.empty-glyph { color: var(--ink-faint); line-height: 0; }
.empty-glyph svg { width: 44px; height: 44px; display: block; }
/* the actions ride a ROW (not the stacked left-anchored .state-cta default) */
.empty-actions { display: flex; gap: var(--sp2); margin-top: var(--sp2); }
.empty-actions .state-cta { align-self: auto; margin-top: 0; }
.state-title {
  margin: 0;
  font-family: var(--f-body);   /* warm serif = content/names */
  font-size: var(--fs-clock);
  font-weight: 600;
  color: var(--ink);
}
.state-line {
  margin: 0;
  font-family: var(--f-ui);
  font-size: var(--fs-control);
  color: var(--ink-dim);
}
/* a home SECTION sub-heading (Recent uploads · Recent projects) — the warm serif of
   .state-title one step down in size, with breathing room above so a second strip reads
   as its OWN labeled section, not a continuation of the table above (Jesse 2026-07-20). */
.browse-section-head {
  margin: var(--sp5) 0 var(--sp2);
  font-family: var(--f-body);
  font-size: var(--fs-ui);
  font-weight: 600;
  color: var(--ink);
}
/* Home's two display lenses travel as one restrained toolbar. Layout stays left;
   the recency question sits beside it instead of masquerading as a global setting.
   The label is explicit because Created and Activity answer genuinely different jobs. */
.home-project-controls {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  inline-size: 100%;
}
.home-project-controls > .layout-group { margin-inline-start: auto; }
.project-search {
  flex: 1 1 18rem;
  max-inline-size: 28rem;
}
.project-search .browse-search-input { inline-size: 100%; }
.project-filter-empty {
  margin: var(--sp4) 0 0;
  color: var(--ink-dim);
  font: 500 var(--fs-control)/1.4 var(--f-ui);
}
.home-recency {
  display: inline-flex;
  align-items: center;
  gap: var(--sp2);
  color: var(--ink-dim);
  font: 500 var(--fs-chip)/1.2 var(--f-ui);
}
.home-recency .browse-sort:disabled {
  cursor: progress;
  opacity: var(--set-aside-opacity);
}
@media (max-width: 640px) {
  .home-project-controls {
    align-items: stretch;
    flex-wrap: wrap;
  }
  .project-search {
    flex-basis: 100%;
    max-inline-size: none;
  }
}
.state-cta { align-self: flex-start; margin-top: var(--sp2); }
.state-cta:hover { border-color: var(--accent); color: var(--accent); }
/* The home "New project" hero CTA wears the SAME codified --primary-action role
   as the toolbar New (.browse-new, DECISIONS #27(1)) — the create verb is amber
   wherever it appears, retiring the bordered/wayfinding-accent register for THIS
   primary verb. The OTHER state CTAs (Upload videos, Clear, secondary New folder/
   Cancel) keep the standard skin — only the create verb is the primary action, so
   no two amber verbs collide. Color ROLE only: the CTA keeps its own state sizing;
   hover/active ride channel A's one --hover-delta knob, exactly like .browse-new.
   The :hover/:active here out-specify .state-cta:hover (0,3,0 > 0,2,0). */
.state-cta.primary-action {
  color: var(--bg-deep);
  background: var(--primary-action);
  border-color: transparent;
  font-weight: 600;
}
.state-cta.primary-action:hover {
  color: var(--bg-deep);
  border-color: transparent;
  background: oklch(from var(--primary-action) calc(l + var(--hover-delta)) c h);
}
.state-cta.primary-action:active {
  background: oklch(from var(--primary-action) calc(l - var(--hover-delta)) c h);
}
/* The home HEAD row keeps identity and creation in one compact reading group.
   The action sits beside the title rather than drifting to the far viewport edge,
   so empty and populated homes use the same stable toolbar geography. */
.browse-home-head {
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  gap: var(--sp4);
}
.browse-home-titles { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* First run is the empty library, not a tour. Give its single explanatory line a
   readable measure and optically center the outcome-specific action against the
   two-line title block. */
.browse-home--first-run .browse-home-head { align-items: center; }
.browse-home--first-run .state-line { max-width: 58ch; line-height: 1.5; }
/* The clear "+ New project" button — the codified amber create verb (same --primary-action
   role + --hover-delta knob as .state-cta.primary-action / .browse-new), sized as a real
   top-bar action. flex:none so it never shrinks against a long title. */
.browse-new-project {
  flex: none;
  align-self: flex-start;
  font: inherit;
  font-size: var(--fs-chip);
  font-weight: 600;
  color: var(--bg-deep);
  background: var(--primary-action);
  border: 1px solid transparent;
  border-radius: 5px;
  padding: 7px 14px;
  cursor: pointer;
  white-space: nowrap;
}
.browse-new-project:hover { background: oklch(from var(--primary-action) calc(l + var(--hover-delta)) c h); }
.browse-new-project:active { background: oklch(from var(--primary-action) calc(l - var(--hover-delta)) c h); }
.browse-new-project:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 560px) {
  .browse-home-head {
    align-items: flex-start;
    flex-direction: column;
    gap: var(--sp3);
  }
}
/* R-REC-3 — the couldn't-load message-box's "Try again": a quiet inline text-button
   (no chrome at rest, --accent on hover/focus), the SAME recede-ink grammar the rail
   note uses (.rail-retry), shared by the home AND Recents couldn't-load states. Left-
   anchored on its own line under the state copy, above the New-project CTA. */
.state-retry {
  align-self: flex-start; margin-top: var(--sp1);
  background: none; border: none; padding: 0;
  font: inherit; color: var(--accent); cursor: pointer;
}
.state-retry:hover, .state-retry:focus-visible { text-decoration: underline; }

/* Mutations confirm from a quiet, reusable corner toast rather than inserting
   celebratory copy into the object list. The action remains easy to reach without
   displacing rows or stealing the page's visual hierarchy. */
.browse-toast {
  position: fixed;
  inset-inline-end: max(var(--sp4), env(safe-area-inset-right));
  inset-block-end: max(var(--sp4), env(safe-area-inset-bottom));
  z-index: 120;
  display: inline-flex;
  align-items: center;
  gap: var(--sp3);
  max-inline-size: min(24rem, calc(100vw - 2 * var(--sp4)));
  padding: 8px 10px 8px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: color-mix(in oklab, var(--surface) 94%, var(--bg-deep));
  color: var(--ink);
  box-shadow: 0 12px 36px -18px rgb(0 0 0 / .72);
  font: 500 var(--fs-chip)/1.3 var(--f-ui);
}
.browse-toast[hidden] { display: none; }
.browse-toast .toast-action {
  min-block-size: 28px;
  padding-inline: 8px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
}
.browse-toast .toast-action:hover { background: var(--accent-soft); }
.browse-toast .toast-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The two home lists — the org's projects and its recent uploads — as clean
   file-browser row lists: no bullets, hairline-separated rows. */
.project-list,
.recent-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}
.project-item,
.recent-item { border-bottom: 1px solid var(--line); }

/* the project-color rail (the 0009 facet): a 3px left stripe in --card-accent on the
   PROJECT rows — a project's identity color lives on its own display. The gutter is
   always reserved (transparent default) so colored and colorless rows align down the
   edge; .no-accent keeps it clear, never a faked hue. RETIRED on .recent-item (Jesse
   2026-07-20): recent uploads are ASSETS, and a project's color shouldn't echo onto the
   files inside it — same ruling as the in-project asset rows/tiles above. */
.project-item { border-left: 3px solid var(--card-accent, var(--project-slate)); }
.project-item.no-accent { border-left-color: var(--project-slate); }

/* Each row is a full-width button that must read as a LIST ROW, not a boxed
   button — override the base bordered treatment (border/padding from the base
   `button` rule). The name carries warm serif (content); hover lifts the
   themed --wash; focus shows the same --accent ring the openable cards use. */
.project-link,
.recent-link {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  border-radius: 4px;
  padding: var(--sp2) var(--sp3);
  background: none;
  font-family: var(--f-body);
  font-size: var(--fs-base);
  color: var(--ink);
}
.project-link:hover,
.recent-link:hover { background: var(--wash); border-color: transparent; }
.project-link:focus-visible,
.recent-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* a recent upload is a level quieter than a project (a project is a place you
   go; an upload is a thing inside one) — its name reads in the dim ink. */
.recent-link { color: var(--ink-dim); }

/* The home strip's "see all" bridge into /recents — a quiet text link at the foot
   of the strip. Reads as an action (themed accent ink), not chrome; the arrow nudges
   on hover so it feels like a forward move into the dedicated surface. Left-aligned
   under the rows, padded to echo the row inset. */
.recent-seeall {
  display: inline-flex;
  align-items: center;
  /* the home box is a flex COLUMN (stretches its children) — pin to content width so
     this reads as a compact link, never a full-width clickable bar. */
  align-self: flex-start;
  gap: var(--sp1);
  margin-top: var(--sp2);
  padding: var(--sp1) var(--sp3);
  font-family: var(--f-body);
  font-size: var(--fs-ui);
  color: var(--accent);
  text-decoration: none;
  border-radius: 4px;
}
.recent-seeall:hover { background: var(--wash); }
.recent-seeall:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.recents-tools {
  display: flex;
  align-items: center;
  margin-block: var(--sp3);
}
.recents-search {
  inline-size: min(24rem, 100%);
}
.recents-search .browse-search-input {
  inline-size: 100%;
}
.recents-filter-empty {
  margin: var(--sp4) 0;
  padding: var(--sp3);
  border-block: 1px solid var(--line);
  color: var(--ink-dim);
  font: var(--fs-ui) / 1.5 var(--f-ui);
}
.recent-seeall-arrow { transition: transform 120ms ease; }
.recent-seeall:hover .recent-seeall-arrow { transform: translateX(2px); }
/* the arrow nudge snaps under the reduced-motion attribute tier (the stylesheet's ONLY
   motion key — never a @media prefers-reduced-motion fork, uiprefs invariant). */
:root[data-motion="reduced"] .recent-seeall-arrow { transition: none; }

/* BOTH home strips — the org's projects AND its recent uploads — ride the SAME
   list-as-table machinery as the in-project asset table (Slice-C view-engine): a
   `.browse-colhead` column header + each row flattened to grid CELLS under the shared
   --list-cols template (browse.ts computes each from its kind's ColumnDescriptor set),
   so the entry-point home reads as ONE library surface — the two strips never visually
   fork (the old feed-vs-table split is gone; sin #34: one renderer, never two). The
   colhead is a sibling presentation row; the per-row grid lives on the row button (the
   rail + accent gutter stay on the `-item` wrapper). Each list carries its OWN
   --list-cols template (projects: Assets·Updated; uploads: Duration·Uploaded), so its
   columns align down the page. */
.project-list .browse-colhead,
.project-list .project-link,
.recent-list .browse-colhead,
.recent-list .recent-link {
  display: grid;
  grid-template-columns: var(--list-cols);
  align-items: center;
  gap: var(--sp3);
}
/* the colhead reserves the SAME 3px rail gutter the rows carry + matches the row
   button's padding, so its column 1 starts exactly under the row names. Quiet
   uppercase micro-labels — the cool chrome recedes, the same register as the
   in-project table head. */
.project-list .browse-colhead,
.recent-list .browse-colhead {
  border-left: 3px solid transparent;
  padding: var(--sp2) var(--sp3);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-family: var(--f-ui);
  font-size: var(--fs-label);
  color: var(--ink-faint);
  border-bottom: 1px solid var(--ink-deco);
}
/* the NAME is the content hero (warm serif, truncating); its COLOR is inherited from
   the row button — full --ink for a project (a place you go), the quieter --ink-dim for
   an upload (a thing inside one) — so the hierarchy distinction survives the shared
   cadence. The meta cells recede to the cool chrome register (dim ink, UI face, tabular
   figures) — the same content/chrome split the in-project table draws. */
.project-list .project-name,
.recent-list .recent-name {
  font-family: var(--f-body);
  font-size: var(--fs-base);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.project-list .project-identity,
.recent-list .recent-identity {
  display: grid;
  align-items: center;
  gap: var(--sp2);
  min-width: 0;
  overflow: hidden;
}
.project-list .project-identity {
  grid-template-columns: 3rem minmax(0, 1fr);
}
.recent-list .recent-identity {
  grid-template-columns: 5rem minmax(0, 1fr);
}
.recent-list .recent-identity .card-thumb {
  inline-size: 5rem;
  aspect-ratio: 16 / 9;
  border-radius: 3px;
}
.project-list .project-description {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.project-list .project-count,
.project-list .project-created,
.project-list .project-updated,
.recent-list .recent-dur,
.recent-list .recent-uploaded {
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;   /* counts/ages align cleanly down the column */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* the active-sort mark (#39): the column the rows are ORDERED by lifts out of the quiet
   chrome register — full ink + a direction caret — so the order the rows arrive in is the
   order the header advertises. The caret is decoration (aria-hidden); the cell's `title`
   carries the read-aloud signal. Sits inside the list AND recent colheads (one indicator
   vocabulary), keyed by `.is-sorted` so a non-active column stays plain. */
.browse-colhead .is-sorted {
  color: var(--ink);
}
.browse-colhead .colhead-sort {
  margin-left: 0.3em;
  font-size: 0.85em;
  opacity: 0.8;
}
/* a click-to-sort header (#39): the sortable Name + Created cells are real <button>s, so
   strip the UA chrome and inherit the colhead's type register — they read as the SAME
   header labels, just pressable. The cursor + a hover/focus ink-lift advertise the
   affordance; the active (`.is-sorted`) one already carries full ink + the caret. Keyboard
   focus rides the app's global :focus-visible ring (a real button is reachable by Tab — the
   whole point of not faking it with a div). Inline-flex so the caret rides beside the
   truncating label without a second layout context. */
.browse-colhead button.colhead-sortable {
  display: inline-flex;
  align-items: baseline;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
.browse-colhead button.colhead-sortable:hover {
  color: var(--ink);
}

/* The Recents recency lanes (Slice-1) ride the SAME .browse-lane divider the browse
   gallery uses (sin #34: one lane renderer, never a fork). But here the lane is a plain
   list row, NOT a grid cell — so .browse-lane's `grid-column: 1 / -1` is inert and its
   label would hang flush at the ul edge, left of the row names. Re-inset it under the
   names: match the row button's left padding PLUS the 3px accent-rail gutter the
   `.recent-item` wrappers carry, so the lane label and the upload names share one left
   edge down the surface. */
.recent-list .browse-lane {
  padding-left: calc(var(--sp3) + 3px);
}

/* The home's project GRID arm (`?layout=grid`) gives durable containers more visual
   identity than a table row without making them impersonate 16:9 assets. The cover is
   square (album/campaign language); the title and metadata remain live text in a calm
   footer. Posterless projects get a stable id-derived motif colored by --card-accent,
   while a real poster replaces only the cover plane. */
.project-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--sp3);
}
.project-tile {
  min-width: 0;
  aspect-ratio: 1;
}
/* One square object, not cover + database-row chin. The art is full-bleed; live title
   and metadata sit over its calm lower exposure behind a shared scrim. */
.project-grid .project-link {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0;
  block-size: 100%;
  padding: 0 0 var(--sp3);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg);
  overflow: hidden;            /* clip the hero band's color to the rounded frame */
}
.project-grid .project-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  inset: 32% 0 0;
  background: var(--bg);
  mask-image: linear-gradient(to bottom, transparent, black 72%);
}
.project-grid .project-link:hover { border-color: color-mix(in oklab, var(--ink) 24%, var(--line)); }
.project-grid .project-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* Darkroom light, not software-gradient wallpaper: the project's chosen color controls
   the room while Proof amber warms a deterministic ray. Project-id geometry variables
   move the source and angle so repeated palette choices still have individual covers. */
.project-cover {
  display: block;
  overflow: hidden;
  isolation: isolate;
  background: color-mix(in oklab, var(--card-accent, var(--project-slate)) 34%, var(--bg));
}
.project-grid .project-tile-hero {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
}
.project-list .project-list-thumb {
  position: relative;
  inline-size: 3rem;
  aspect-ratio: 1;
  border: 1px solid var(--line);
  border-radius: 5px;
}
.project-cover::before,
.project-cover::after {
  content: "";
  position: absolute;
  pointer-events: none;
  transform-origin: 28% 24%;
}
.project-cover::before {
  inline-size: 128%;
  block-size: 112%;
  inset: -38% auto auto -38%;
  clip-path: polygon(42% 0, 61% 0, 100% 100%, 8% 100%);
  background: color-mix(in oklab, var(--card-accent, var(--project-slate)) 44%, var(--primary-action));
  filter: blur(8px);
  opacity: 0.34;
  translate: var(--cover-ray-x, 0) var(--cover-ray-y, 0);
  transform: rotate(var(--cover-ray-angle, -12deg));
}
.project-cover::after {
  inline-size: 140%;
  block-size: 64%;
  inset: auto -28% -28% auto;
  border-radius: 50%;
  background: color-mix(in oklab, var(--card-accent, var(--project-slate)) 70%, var(--accent));
  filter: blur(28px);
  opacity: 0.18;
  transform: rotate(-8deg);
}
[data-cover-variant="1"] .project-cover::before {
  inset: -34% -38% auto auto;
  transform: rotate(26deg);
}
[data-cover-variant="1"] .project-cover::after {
  inset: auto auto -26% -34%;
  transform: rotate(14deg);
}
[data-cover-variant="2"] .project-cover::before {
  inline-size: 148%;
  block-size: 104%;
  inset: -16% auto auto -62%;
  transform: rotate(-28deg);
}
[data-cover-variant="2"] .project-cover::after {
  inset: -22% -42% auto auto;
  transform: rotate(28deg);
}
[data-cover-variant="3"] .project-cover::before {
  inline-size: 118%;
  block-size: 142%;
  inset: -62% auto auto -8%;
  transform: rotate(62deg);
}
[data-cover-variant="3"] .project-cover::after {
  inset: auto -42% -32% auto;
  transform: rotate(-18deg);
}
[data-cover-variant="4"] .project-cover::before {
  inset: -44% -28% auto auto;
  transform: rotate(38deg);
}
[data-cover-variant="4"] .project-cover::after {
  inset: auto auto -32% -22%;
  transform: rotate(-24deg);
}
[data-cover-variant="5"] .project-cover::before {
  inline-size: 154%;
  block-size: 108%;
  inset: -30% auto auto -56%;
  transform: rotate(8deg);
}
[data-cover-variant="5"] .project-cover::after {
  inset: -18% -46% auto auto;
  transform: rotate(22deg);
}
.project-item.no-accent .project-cover,
.project-tile.no-accent .project-cover {
  background: color-mix(in oklab, var(--project-slate) 68%, var(--bg));
}
/* Live copy overlays the exposure rather than becoming part of generated art. */
.project-grid .project-name {
  position: relative;
  z-index: 2;
  font-family: var(--f-body);
  font-size: var(--fs-clock);
  line-height: 1.12;
  color: var(--ink-on-media);
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.project-grid .project-identity {
  position: relative;
  z-index: 2;
  gap: 3px;
  margin: 0 var(--sp3);
}
.project-grid .project-identity .project-name { margin: 0; }
.project-grid .project-description {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  color: color-mix(in oklab, var(--ink-on-media) 76%, transparent);
}
/* the SAME meta cells the list arm renders — count · updated — here a quiet stacked run
   beneath the name (the cool chrome register: dim ink, UI face, tabular figures). */
.project-grid .project-tile-meta {
  position: relative;
  z-index: 2;
  display: flex;
  gap: var(--sp2);
  margin: var(--sp1) var(--sp3) 0;
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: color-mix(in oklab, var(--ink-on-media) 68%, transparent);
  font-variant-numeric: tabular-nums;
}

/* A real representative image occupies the same cover slot as generated art. Keep it
   crisp: the project name no longer sits on the image, so blur and a heavy scrim would
   only hide useful recognition detail. */
:is(.project-grid, .project-list, .project-preview-hero) .project-tile-poster {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;            /* clip the scaled blur bleed to the tile box */
  pointer-events: none;        /* the tile's button owns the clicks, not the art */
  border-radius: inherit;
}
:is(.project-grid, .project-list, .project-preview-hero) .project-tile-poster-media {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: none;
  transform: none;
}
.has-poster .project-cover::before,
.has-poster .project-cover::after { display: none; }

/* Folders are quiet structure inside the existing browser, not a permanent third
   navigation column. Their section precedes direct assets and collapses away when
   empty. List rows stay compact; Grid gives the folder a little more silhouette. */
.folder-section:has(ul:empty) { display: none; }
.folder-list,
.folder-grid { list-style: none; margin: 0 0 var(--sp3); padding: 0; }
.folder-list { border-block: 1px solid var(--line); }
.folder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--sp2);
}
.folder-item {
  position: relative;
  display: flex;
  align-items: stretch;
  min-width: 0;
  color: var(--ink);
}
.folder-list .folder-item + .folder-item { border-top: 1px solid var(--line); }
.folder-grid .folder-item {
  min-height: 5.5rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
}
.folder-item.drop-target {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 2px var(--accent);
}
.browse-card.dragging { opacity: var(--set-aside-opacity); }
.browse-card.version-drop-target {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  background: color-mix(in oklab, var(--accent) 9%, var(--surface));
}
.browse-card.version-drop-target::after {
  content: "Stack as new version";
  position: absolute;
  inset: auto var(--sp2) var(--sp2) auto;
  z-index: 4;
  padding: .32rem .5rem;
  border: 1px solid color-mix(in oklab, var(--accent) 55%, var(--line));
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: var(--fs-chip);
  font-weight: 650;
  line-height: 1;
  box-shadow: var(--lift-1);
  pointer-events: none;
}
.folder-open {
  display: flex;
  align-items: center;
  gap: var(--sp2);
  flex: 1;
  min-width: 0;
  min-height: 46px;
  padding: var(--sp2) calc(44px + var(--sp2)) var(--sp2) var(--sp3);
  border: 0;
  color: inherit;
  background: transparent;
  text-align: left;
}
.folder-open:hover { background: var(--wash); }
.folder-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.folder-glyph { inline-size: 1.35rem; block-size: 1.35rem; flex: none; color: var(--working); }
.folder-grid .folder-glyph { inline-size: 2rem; block-size: 2rem; }
.folder-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.folder-actions {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-end: var(--sp1);
  z-index: 4;
  display: grid;
  place-items: center;
  inline-size: 40px;
  block-size: 40px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  transform: translateY(-50%);
}
.folder-actions:hover,
.folder-actions:focus-visible { color: var(--accent); background: var(--accent-soft); }
.folder-empty { padding: clamp(2.5rem, 10vh, 6rem) var(--sp4); text-align: center; }
.folder-empty-title { margin: 0; font: 600 var(--fs-lg)/1.2 var(--f-body); color: var(--ink); }
.folder-empty-line { margin: var(--sp2) 0 0; color: var(--ink-dim); }

/* The home's project DETAIL arm — a master–detail surface mirroring the in-project
   .browse-detail layout, but over PROJECTS: the SAME .project-list table as the master
   column (sin #34 — one renderer) beside a sticky preview pane projecting the lead
   project. The pane's full voice (asset thumbs, field rows) is the look lane's; this is
   the functional structure. Token-only — no magic px past the shared shape, no hex. */
.project-detail { display: grid; gap: var(--sp5); align-items: start; }
.project-detail:has(.project-preview) { grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); }
.project-preview {
  position: sticky;
  top: var(--sp4);
  display: flex;
  flex-direction: column;
  gap: var(--sp2);
  padding: var(--sp4);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;            /* clip the hero band's color to the rounded frame */
}
.project-preview.empty { color: var(--ink-faint); }
/* the project's identity HERO band — full-bleed to the pane edge, painted from the SAME
   --card-accent the grid tile + list rail read (a project has no poster, so its color IS
   the hero). A colorless project (.no-accent) keeps the quiet --wash-soft face — never a
   faked hue (the rail's honest voice, identical to the grid hero one arm over). */
.project-preview-hero {
  block-size: 4rem;
  margin: calc(-1 * var(--sp4)) calc(-1 * var(--sp4)) 0;
  background: var(--card-accent, var(--wash-soft));
  position: relative;          /* anchor the absolute .project-tile-poster the hero now carries */
  overflow: hidden;            /* clip the poster's scaled blur bleed to the hero band */
}
.project-preview.no-accent .project-preview-hero { background: color-mix(in oklab, var(--project-slate) 55%, var(--bg)); }
/* Selected projects carry the same quiet wash in List/Grid and in the dormant Detail
   master. The project's accent rail remains the identity signal; selection only changes
   the field behind it, so a multi-range reads clearly without becoming a neon block. */
.project-item.selected,
.project-tile.selected { background: var(--wash); }
@media (max-width: 48rem) {
  .project-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .project-grid .project-name { font-size: var(--fs-base); }
  .folder-grid { grid-template-columns: 1fr; }
  .folder-open { min-height: 52px; }
  .folder-actions { inline-size: 44px; block-size: 44px; }
  .browse-folder-new { min-height: 44px; }
  /* A phone is not a narrow desktop table. Collapse the two home tables into compact
     two-line records: the identity gets the full first read, useful recency survives,
     and low-priority creation metadata yields. The persistent action remains in the
     trailing thumb target instead of wrapping onto its own line. */
  .project-list .browse-colhead,
  .recent-list .browse-colhead { display: none; }

  .project-list .project-link {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "name updated"
      "workspace count";
    row-gap: var(--sp1);
    padding-right: calc(44px + var(--sp2));
  }
  .project-list .project-identity { grid-area: name; }
  .project-list .project-workspace {
    grid-area: workspace;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--f-ui);
    font-size: var(--fs-note);
    color: var(--ink-dim);
  }
  .project-list .project-count { grid-area: count; }
  .project-list .project-created { display: none; }
  .project-list .project-updated { grid-area: updated; }
  .project-list .project-updated::before { content: "Updated "; color: var(--ink-faint); }

  .recent-list .recent-link {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "name name"
      "duration uploaded";
    row-gap: var(--sp1);
    padding-right: calc(44px + var(--sp2));
  }
  .recent-list .recent-identity {
    grid-area: name;
    grid-template-columns: 4rem minmax(0, 1fr);
  }
  .recent-list .recent-identity .card-thumb { inline-size: 4rem; }
  .recent-list .recent-dur { grid-area: duration; }
  .recent-list .recent-uploaded { grid-area: uploaded; }
  .recent-list .recent-uploaded::before { content: "Uploaded "; color: var(--ink-faint); }

  .row-actions { min-width: 44px; min-height: 44px; }

  /* narrow: stack the pane under the master list, drop the sticky follow (the
     .browse-detail precedent) */
  .project-detail:has(.project-preview) { grid-template-columns: 1fr; }
  .project-preview { position: static; }
}

@media (max-width: 24rem) {
  .project-grid { grid-template-columns: 1fr; }
}

/* §6 partial-failure notice (populated dimension): one home section loaded but
   the other's read broke. A quiet rust-register inline alert — failure speaks
   --rust across this app (no dedicated --error token yet; flagged for the
   look-lane). The loaded content stays put; this just keeps a swallowed read
   failure from reading as a clean, complete home. Token-only, static. */
.home-partial-notice {
  margin: var(--sp2) 0 0;
  padding: 6px 8px;
  border-radius: 4px;
  background: var(--rust-wash);
  font-family: var(--f-ui);
  font-size: var(--fs-note);
  color: var(--ink-dim);
}

/* the home's LOADING state (browse.ts sets aria-busy on the box, "Loading your
   home…") earns a quiet working cue: the title leads with a small spun glyph in
   the semantic --working amber. Reduced-motion swaps it for a static --working
   dot — the same a11y floor the card spinner uses (no @media fork; the
   data-motion attribute tier owns it). */
.browse-state[aria-busy="true"] .state-title::before {
  content: "";
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-right: var(--sp2);
  border-radius: 50%;
  border: 2px solid var(--working-soft);
  border-top-color: var(--working);
  animation: card-spin 0.8s linear infinite;
}
:root[data-motion="reduced"] .browse-state[aria-busy="true"] .state-title::before { animation: none; border-color: var(--working); }
/* ───────────────────────────────────────────── the MANAGE surface (§2.5 schema #1)
   The org Members roster — the MANAGE archetype's first surface, rendered into the
   designed shell page (the signin/projects/404 treatment) and reached from the rail.
   A subject-record panel: a title, one section, the roster rows + role chips. Cool
   chrome that RECEDES (the meta reads dim); --accent stays the lone interaction note
   (the back link only). The role chip is a neutral bordered token — NOT teal, NOT a
   faked hue — so a role reads as information, never an interaction cue. */
.manage-panel { width: 100%; }
.manage-title {
  font-family: var(--f-body); font-weight: 500; font-size: 26px;
  margin: 0 0 var(--sp4);
}
.manage-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp3); margin: 0 0 var(--sp3);
  padding-bottom: var(--sp2); border-bottom: 1px solid var(--line);
}
.manage-section-title {
  font-family: var(--f-ui); font-weight: 600; font-size: var(--fs-note);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-dim);
  margin: 0;
}
.manage-status { color: var(--ink-dim); font-size: var(--fs-note); margin: var(--sp2) 0 0; }

/* Organization automations. The surface is intentionally a short recipe
   builder—not a provider event dump. Each saved rule reads left-to-right as
   destination → human event → controls, and collapses cleanly on mobile. */
.automation-intro {
  max-width: 48rem; margin: 0 0 var(--sp4);
  color: var(--ink-dim); font: var(--fs-body)/1.5 var(--f-ui);
}
.automation-new,
.automation-primary,
.automation-secondary,
.automation-remove,
.automation-secret-copy {
  min-height: 38px; padding: 7px 11px; border: 1px solid var(--line);
  border-radius: 5px; background: none; color: var(--ink-dim);
  font: 500 var(--fs-note)/1.2 var(--f-ui); cursor: pointer;
}
.automation-new, .automation-primary {
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 48%, var(--line));
}
.automation-new:hover, .automation-primary:hover, .automation-secondary:hover,
.automation-secret-copy:hover { border-color: var(--ink-dim); color: var(--ink); }
.automation-remove:hover, .automation-remove[data-armed="true"] {
  color: var(--rust); border-color: color-mix(in oklab, var(--rust) 58%, var(--line));
}
.automation-new:focus-visible, .automation-primary:focus-visible,
.automation-secondary:focus-visible, .automation-remove:focus-visible,
.automation-secret-copy:focus-visible, .automation-input:focus-visible,
.automation-select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.automation-new:disabled, .automation-primary:disabled, .automation-secondary:disabled,
.automation-remove:disabled, .automation-secret-copy:disabled { opacity: .55; cursor: progress; }
.automation-composer {
  display: grid; grid-template-columns: minmax(12rem, .75fr) minmax(16rem, 1.25fr);
  gap: var(--sp3); margin: 0 0 var(--sp4); padding: var(--sp4);
  border: 1px solid var(--line); border-radius: 7px;
  background: color-mix(in oklab, var(--surface) 84%, var(--bg));
}
.automation-composer[hidden] { display: none; }
.automation-composer-head { grid-column: 1 / -1; }
.automation-composer-title {
  margin: 0 0 var(--sp1); color: var(--ink);
  font: 600 var(--fs-control)/1.25 var(--f-ui);
}
.automation-composer-note {
  max-width: 42rem; margin: 0; color: var(--ink-faint);
  font: var(--fs-note)/1.45 var(--f-ui);
}
.automation-input, .automation-select {
  min-width: 0; min-height: 42px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--bg); color: var(--ink); font: var(--fs-ui)/1.2 var(--f-ui);
}
.automation-select, .automation-trigger-hint, .automation-error,
.automation-composer-actions { grid-column: 1 / -1; }
.automation-trigger-hint {
  margin: calc(var(--sp2) * -1) 0 0; color: var(--ink-faint);
  font: var(--fs-note)/1.4 var(--f-ui);
}
.automation-error { margin: 0; color: var(--rust); font: var(--fs-note)/1.4 var(--f-ui); }
.automation-error[hidden] { display: none; }
.automation-composer-actions { display: flex; justify-content: flex-end; gap: var(--sp2); }
.automation-secret {
  position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--sp1) var(--sp3); margin: 0 0 var(--sp4); padding: var(--sp3);
  border: 1px solid color-mix(in oklab, var(--accent) 45%, var(--line));
  border-radius: 7px; background: color-mix(in oklab, var(--accent) 7%, var(--surface));
}
.automation-secret[hidden] { display: none; }
.automation-secret-title { font: 600 var(--fs-ui)/1.3 var(--f-ui); }
.automation-secret-note {
  grid-column: 1; margin: 0; color: var(--ink-dim); font: var(--fs-note)/1.4 var(--f-ui);
}
.automation-secret-value {
  grid-column: 1; overflow: hidden; padding: 8px 10px; border: 1px solid var(--line);
  border-radius: 4px; background: var(--bg); color: var(--ink);
  font: var(--fs-note)/1.3 var(--f-tc); text-overflow: ellipsis; white-space: nowrap;
}
.automation-secret-copy { grid-column: 2; grid-row: 1 / span 3; align-self: center; }
.automation-list { border-top: 1px solid var(--line); }
.automation-row {
  display: grid; grid-template-columns: minmax(13rem, 1fr) minmax(14rem, 1.25fr) auto;
  align-items: center; gap: var(--sp4); min-height: 76px;
  padding: var(--sp3) 0; border-bottom: 1px solid var(--line);
}
.automation-row[data-enabled="false"] .automation-identity,
.automation-row[data-enabled="false"] .automation-event { opacity: .48; }
.automation-identity, .automation-event { min-width: 0; display: grid; gap: 3px; }
.automation-name, .automation-event-name {
  overflow: hidden; color: var(--ink); font: 600 var(--fs-note)/1.3 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.automation-destination, .automation-event-detail {
  overflow: hidden; color: var(--ink-faint); font: var(--fs-note)/1.35 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.automation-health {
  color: var(--ink-faint); font: var(--fs-note)/1.35 var(--f-ui);
}
.automation-health::before {
  display: inline-block; width: 6px; height: 6px; margin-right: 7px;
  border-radius: 50%; background: currentColor; content: "";
  vertical-align: .08em;
}
.automation-health-delivered { color: var(--success, var(--accent)); }
.automation-health-failed { color: var(--rust); }
.automation-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp2); }
.automation-enabled {
  display: inline-flex; align-items: center; gap: var(--sp1); color: var(--ink-dim);
  font: var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.automation-enabled input { width: 17px; height: 17px; accent-color: var(--accent); }
.automation-empty {
  margin: 0; padding: var(--sp5) 0; color: var(--ink-dim);
  font: var(--fs-body)/1.5 var(--f-ui);
}
@media (max-width: 760px) {
  .automation-composer { grid-template-columns: 1fr; padding: var(--sp3); }
  .automation-input { grid-column: 1; }
  .automation-secret { grid-template-columns: 1fr; }
  .automation-secret-copy { grid-column: 1; grid-row: auto; justify-self: start; }
  .automation-row { grid-template-columns: 1fr; gap: var(--sp2); padding: var(--sp4) 0; }
  .automation-actions { justify-content: flex-start; flex-wrap: wrap; }
  .automation-destination, .automation-event-detail { white-space: normal; }
}

/* Organization activity. This is a readable accountability trail, not a raw
   database console: person → human event → time on one calm, scanning line. */
.activity-modes {
  display: flex; align-items: center; gap: var(--sp1);
  margin: 0 0 var(--sp4); border-bottom: 1px solid var(--line);
}
.activity-mode {
  position: relative; min-height: 40px; padding: 7px var(--sp3);
  border: 0; background: none; color: var(--ink-faint);
  font: 600 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.activity-mode::after {
  position: absolute; inset: auto var(--sp2) -1px; height: 2px;
  background: transparent; content: "";
}
.activity-mode[aria-selected="true"] { color: var(--ink); }
.activity-mode[aria-selected="true"]::after { background: var(--accent); }
.activity-mode:hover { color: var(--ink-dim); }
.activity-mode:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.audit-intro {
  max-width: 46rem; margin: calc(var(--sp4) * -0.5) 0 var(--sp4);
  color: var(--ink-dim); font: var(--fs-body)/1.5 var(--f-ui);
}
.audit-controls { display: flex; align-items: center; gap: var(--sp2); margin-left: auto; }
.audit-search {
  min-height: 38px; width: min(15rem, 28vw); padding: 6px 10px;
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink); font: var(--fs-note)/1.2 var(--f-ui);
}
.audit-search::placeholder { color: var(--ink-faint); }
.audit-filter, .audit-period {
  min-height: 38px; max-width: 15rem; padding: 6px 30px 6px 10px;
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink-dim); font: var(--fs-note)/1.2 var(--f-ui);
}
.audit-export {
  min-height: 38px; padding: 6px 10px; border: 1px solid var(--line);
  border-radius: 5px; background: none; color: var(--accent);
  font: 500 var(--fs-note)/1.2 var(--f-ui); cursor: pointer; white-space: nowrap;
}
.audit-export:hover { border-color: var(--ink-dim); color: var(--ink); }
.audit-search:focus-visible, .audit-filter:focus-visible, .audit-period:focus-visible,
.audit-export:focus-visible, .audit-load-more:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.audit-search:disabled, .audit-filter:disabled, .audit-period:disabled,
.audit-export:disabled { opacity: .6; cursor: progress; }
.audit-list { list-style: none; padding: 0; margin: 0; }
.audit-day {
  padding: var(--sp4) 0 var(--sp1); color: var(--ink-faint);
  font: 600 var(--fs-note)/1.2 var(--f-ui); letter-spacing: .04em; text-transform: uppercase;
}
.audit-day:first-child { padding-top: 0; }
.audit-row {
  display: grid; grid-template-columns: 36px minmax(0, 1fr) auto;
  align-items: center; gap: var(--sp3); min-height: 64px;
  padding: var(--sp2) 0; border-bottom: 1px solid var(--line);
}
.audit-avatar {
  display: grid; place-items: center; width: 34px; height: 34px;
  border: 1px solid var(--line); border-radius: 50%;
  background: color-mix(in oklab, var(--surface) 78%, var(--bg));
  color: var(--ink-dim); font: 600 var(--fs-note)/1 var(--f-ui);
}
.audit-body { min-width: 0; display: flex; align-items: baseline; gap: var(--sp2); }
.audit-actor {
  flex: none; color: var(--ink); font: 600 var(--fs-note)/1.35 var(--f-ui);
}
.audit-event {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink-dim); font: var(--fs-note)/1.35 var(--f-ui);
}
.audit-time {
  color: var(--ink-faint); font: var(--fs-note)/1.3 var(--f-ui); white-space: nowrap;
}
.product-activity-tail { display: flex; align-items: center; gap: var(--sp3); }
.product-activity-open {
  border: 0; padding: 4px 0; background: none; color: var(--accent);
  font: 500 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.product-activity-open:hover { color: var(--ink); }
.product-activity-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.audit-load-more {
  display: block; min-height: 40px; margin: var(--sp4) auto 0; padding: 7px 12px;
  border: 1px solid var(--line); border-radius: 4px; background: none;
  color: var(--accent); font: 500 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.audit-load-more:hover { border-color: var(--ink-dim); color: var(--ink); }
.audit-load-more:disabled { opacity: .55; cursor: progress; }

@media (max-width: 640px) {
  .audit-intro { margin-bottom: var(--sp3); }
  .audit-row { grid-template-columns: 34px minmax(0, 1fr); gap: var(--sp2); padding: var(--sp3) 0; }
  .audit-body { display: grid; gap: 2px; }
  .audit-event { white-space: normal; }
  .audit-time { grid-column: 2; }
  .product-activity-tail { grid-column: 2; justify-content: space-between; }
  .manage-section-head:has(.audit-controls) { align-items: center; }
  .audit-controls { width: 100%; margin-left: 0; flex-wrap: wrap; }
  .audit-search { width: 100%; }
  .audit-filter, .audit-period { min-width: 0; max-width: none; flex: 1 1 10rem; }
}

/* Organization field catalog. The browse Fields popover chooses visible columns;
   this settings surface owns the schema itself. It keeps Manage's quiet register
   but uses a real column rhythm so a large catalog remains scannable. */
.field-catalog-intro { margin: 0 0 var(--sp4); color: var(--ink-dim); max-width: 48rem; }
.field-catalog-section-title { margin: var(--sp5) 0 var(--sp1); font: 600 var(--fs-ui)/1.2 var(--f-ui); }
.field-catalog-section-note { margin: 0 0 var(--sp3); max-width: 52rem; color: var(--ink-faint); font: var(--fs-note)/1.45 var(--f-ui); }
.field-new,
.field-editor-submit,
.field-editor-cancel,
.field-option-add,
.field-catalog-edit,
.field-catalog-retire {
  font: 500 var(--fs-note)/1 var(--f-ui); color: var(--ink-dim); background: none;
  border: 1px solid var(--line); border-radius: 4px; padding: 7px 10px; cursor: pointer;
}
.field-new { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 50%, var(--line)); }
.field-new:hover, .field-editor-submit:hover, .field-option-add:hover, .field-catalog-edit:hover { color: var(--ink); border-color: var(--ink-dim); }
.field-catalog-retire:hover, .field-catalog-retire[data-armed="true"] { color: var(--rust); border-color: color-mix(in oklab, var(--rust) 55%, var(--line)); }
.field-editor-submit:disabled, .field-catalog-retire:disabled { opacity: .55; cursor: progress; }
.field-editor-host:empty { display: none; }
.field-editor {
  margin: 0 0 var(--sp4); padding: var(--sp4); border: 1px solid var(--line);
  border-radius: 6px; background: color-mix(in oklab, var(--surface) 82%, var(--bg));
  display: grid; grid-template-columns: minmax(12rem, 1fr) minmax(9rem, .45fr); gap: var(--sp3);
}
.field-editor-head { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; }
.field-editor-title { margin: 0; font: 600 var(--fs-control)/1.2 var(--f-ui); }
.field-editor-close { border: 0; background: none; color: var(--ink-faint); font-size: 22px; cursor: pointer; }
.field-editor-name, .field-editor-type, .field-option-name, .field-option-color {
  min-width: 0; color: var(--ink); background: var(--bg); border: 1px solid var(--line);
  border-radius: 4px; padding: 9px 10px; font: var(--fs-ui)/1.2 var(--f-ui);
}
.field-editor-name:focus, .field-editor-type:focus, .field-option-name:focus, .field-option-color:focus { border-color: var(--accent); outline: 1px solid var(--accent); }
.field-option-editor, .field-editor-note, .field-editor-error, .field-editor-actions { grid-column: 1 / -1; }
.field-option-list { display: flex; flex-direction: column; gap: var(--sp2); }
.field-option-row { display: grid; grid-template-columns: auto minmax(10rem, 1fr) 8rem repeat(3, 2rem); gap: var(--sp2); align-items: center; }
.field-option-grip { color: var(--ink-faint); letter-spacing: -0.25em; }
.field-option-move, .field-option-remove { width: 2rem; height: 2rem; border: 0; border-radius: 4px; background: none; color: var(--ink-faint); cursor: pointer; }
.field-option-move:hover:not(:disabled), .field-option-remove:hover { color: var(--ink); background: var(--wash); }
.field-option-move:disabled { opacity: .25; cursor: default; }
.field-option-add { margin-top: var(--sp2); }
.field-editor-note { margin: 0; color: var(--ink-faint); font-size: var(--fs-note); }
.field-editor-error { margin: 0; color: var(--rust); font-size: var(--fs-note); }
.field-editor-actions { display: flex; justify-content: flex-end; gap: var(--sp2); }
.field-editor-submit { color: var(--accent); }
.field-catalog { list-style: none; padding: 0; margin: 0; }
.field-catalog-row {
  display: grid; grid-template-columns: minmax(14rem, 1.5fr) minmax(7rem, .6fr) minmax(7rem, .6fr) minmax(6rem, .5fr) auto;
  gap: var(--sp3); align-items: center; min-height: 58px; padding: var(--sp2) 0; border-bottom: 1px solid var(--line);
}
.field-catalog-identity { display: flex; align-items: center; gap: var(--sp3); min-width: 0; }
.field-type-icon { flex: none; display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 5px; border: 1px solid var(--line); color: var(--ink-dim); font: 600 var(--fs-note)/1 var(--f-ui); }
.field-type-icon--link { color: var(--accent); }
.field-type-icon--system { color: var(--accent); background: color-mix(in oklab, var(--accent) 8%, transparent); }
.field-catalog-names { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.field-catalog-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 var(--fs-ui)/1.2 var(--f-ui); }
.field-catalog-key { color: var(--ink-faint); font: var(--fs-label)/1.2 var(--f-tc); }
.field-catalog-type, .field-catalog-scope, .field-catalog-options { color: var(--ink-dim); font: var(--fs-note)/1.2 var(--f-ui); }
.field-catalog-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: var(--sp2); }
.field-visibility { display: inline-flex; align-items: center; gap: var(--sp1); color: var(--ink-dim); font: var(--fs-note)/1 var(--f-ui); cursor: pointer; }
.field-visibility input { accent-color: var(--accent); }
.field-catalog-row[data-visible="false"] .field-catalog-identity,
.field-catalog-row[data-visible="false"] .field-catalog-type,
.field-catalog-row[data-visible="false"] .field-catalog-scope,
.field-catalog-row[data-visible="false"] .field-catalog-options { opacity: .48; }
.field-default-badge { border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; color: var(--ink-faint); font: 600 var(--fs-label)/1 var(--f-ui); text-transform: uppercase; letter-spacing: .06em; }
.field-catalog-empty { padding: var(--sp5) 0; color: var(--ink-dim); }

@media (max-width: 720px) {
  .field-editor { grid-template-columns: 1fr; padding: var(--sp3); }
  .field-editor-name, .field-editor-type { grid-column: 1; }
  .field-option-row { grid-template-columns: minmax(0, 1fr) 7rem repeat(3, 2rem); }
  .field-option-grip { display: none; }
  .field-catalog-row { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp2); padding: var(--sp3) 0; }
  .field-catalog-type, .field-catalog-scope, .field-catalog-options { grid-column: 1; padding-left: 42px; }
  .field-catalog-actions { grid-column: 2; grid-row: 1 / span 4; align-self: center; flex-direction: column; }
}
.manage-member-toolbar {
  display: grid; grid-template-columns: minmax(12rem, 1fr) auto auto auto auto auto;
  align-items: center; gap: var(--sp2); margin-bottom: var(--sp2);
}
.manage-member-search, .manage-member-filter, .manage-member-sort, .manage-member-workspace {
  min-width: 0; min-height: 40px; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink); font: var(--fs-note)/1.2 var(--f-ui);
}
.manage-member-search { padding: 7px 10px; }
.manage-member-filter, .manage-member-sort, .manage-member-workspace { padding: 6px 28px 6px 9px; color: var(--ink-dim); }
.manage-member-search:focus, .manage-member-filter:focus, .manage-member-sort:focus, .manage-member-workspace:focus {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.manage-member-export {
  min-height: 40px; padding: 6px 11px; border: 1px solid var(--line); border-radius: 5px;
  background: none; color: var(--accent); font: 500 var(--fs-note)/1.2 var(--f-ui);
  cursor: pointer; white-space: nowrap;
}
.manage-member-export:hover { border-color: var(--ink-dim); color: var(--ink); }
.manage-member-export:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.manage-member-export:disabled { opacity: .5; color: var(--ink-faint); cursor: not-allowed; }
.manage-member-result-count {
  min-width: 4rem; color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); text-align: right;
}
.manage-member-select-visible {
  display: inline-flex; align-items: center; gap: var(--sp1); min-height: 40px;
  padding: 0 var(--sp2); color: var(--ink-dim); font: var(--fs-note)/1.2 var(--f-ui);
  white-space: nowrap; cursor: pointer;
}
.manage-member-select-visible[hidden] { display: none; }
.manage-member-select-visible-input {
  width: 18px; height: 18px; margin: 0; accent-color: var(--accent); cursor: pointer;
}
.manage-member-select-visible-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.manage-member-select-visible-input:disabled { opacity: .45; cursor: not-allowed; }
.manage-member-bulk {
  display: flex; align-items: center; gap: var(--sp2); min-height: 52px;
  padding: var(--sp2); margin-bottom: var(--sp2); border: 1px solid var(--line);
  background: color-mix(in oklab, var(--surface) 72%, var(--bg));
}
.manage-member-bulk[hidden] { display: none; }
.manage-member-bulk-count {
  min-width: 6rem; color: var(--ink); font: 600 var(--fs-note)/1.2 var(--f-ui);
}
.manage-member-bulk-add, .manage-member-bulk-remove, .manage-member-bulk-clear {
  min-height: 40px; padding: 6px 11px; border: 1px solid var(--line); border-radius: 4px;
  background: none; color: var(--ink-dim); font: 500 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.manage-member-bulk-add { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 50%, var(--line)); }
.manage-member-bulk-remove { color: var(--rust); }
.manage-member-bulk-add:disabled, .manage-member-bulk-remove:disabled,
.manage-member-bulk-clear:disabled { opacity: .5; cursor: progress; }
.manage-member-bulk-note {
  margin-left: auto; color: var(--ink-dim); font: var(--fs-note)/1.3 var(--f-ui);
}
.manage-member-bulk-note[data-tone="ok"] { color: var(--accent); }
.manage-member-bulk-note[data-tone="error"] { color: var(--rust); }
.manage-members { list-style: none; margin: 0; padding: 0; }
.manage-member {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sp3); padding: var(--sp3) 0;
  border-bottom: 1px solid var(--line);
}
.manage-member[hidden] { display: none; }
.manage-member-identity {
  display: flex; align-items: center; gap: var(--sp3); min-width: 0;
}
.manage-member-select {
  flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--accent); cursor: pointer;
}
.manage-member:last-child { border-bottom: none; }
.manage-member-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.manage-member-name {
  font-family: var(--f-body); color: var(--ink); font-size: var(--fs-control);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.manage-member-email {
  font-family: var(--f-ui); color: var(--ink-faint); font-size: var(--fs-note);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.manage-member-joined {
  color: var(--ink-faint); font: var(--fs-note)/1.25 var(--f-ui);
}
.manage-member-access-button {
  min-height: 32px; padding: 4px 9px; border: 1px solid var(--line); border-radius: 4px;
  background: none; color: var(--ink-dim); font: 500 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.manage-member-access-button:hover, .manage-member-access-button[aria-expanded="true"] {
  color: var(--accent); border-color: color-mix(in oklab, var(--accent) 45%, var(--line));
}
.manage-member-access-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.manage-member-access {
  flex: 1 1 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--sp1) var(--sp3); margin: 0 0 var(--sp1) 30px; padding: var(--sp3);
  border: 1px solid var(--line); background: color-mix(in oklab, var(--surface) 58%, var(--bg));
}
.manage-member-access[hidden] { display: none; }
.manage-member-access-title { color: var(--ink); font: 600 var(--fs-body)/1.25 var(--f-ui); }
.manage-member-access-hint {
  grid-column: 1; color: var(--ink-faint); font: var(--fs-note)/1.3 var(--f-ui);
}
.manage-member-access-list {
  grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--sp1); margin-top: var(--sp1); color: var(--ink-dim); font: var(--fs-note)/1.3 var(--f-ui);
}
.manage-member-workspace-row {
  display: flex; align-items: center; gap: var(--sp2); min-height: 48px; padding: var(--sp2);
  border: 1px solid var(--line); cursor: pointer;
}
.manage-member-workspace-row:hover { background: var(--wash); }
.manage-member-workspace-checkbox {
  flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--accent);
}
.manage-member-workspace-identity { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.manage-member-workspace-identity strong {
  overflow: hidden; color: var(--ink); font: 600 var(--fs-note)/1.2 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.manage-member-workspace-identity small { color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); }
.manage-member-access-note {
  grid-column: 1 / -1; min-height: 1.3em; color: var(--ink-dim); font: var(--fs-note)/1.3 var(--f-ui);
}
.manage-member-access-note:empty { display: none; }
.manage-member-access-note[data-tone="ok"] { color: var(--accent); }
.manage-member-access-note[data-tone="error"] { color: var(--rust); }
.manage-role {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-dim);
  border: 1px solid var(--line); border-radius: 99px; padding: 2px 8px;
}
/* The owner-editable role control: the change-role select + its inline write
   note. The select wears the same quiet bordered register as the read-only chip
   (it is the same datum, now mutable) — never teal, which stays the lone
   interaction note (§3). The note speaks only after a write: rust on refusal
   (the app's failure register, no dedicated --error token yet), dim on success,
   and stays out of the layout's height when empty. Token-only. */
.manage-role-edit { flex: none; display: inline-flex; align-items: center; gap: var(--sp2); }
.manage-role-select {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-dim);
  background: none; border: 1px solid var(--line); border-radius: 99px;
  padding: 2px 8px; cursor: pointer;
}
.manage-role-select:disabled { opacity: 0.55; cursor: progress; }
.manage-role-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.manage-role-note:empty { display: none; }
.manage-role-note[data-tone="error"] { color: var(--rust); }
/* The right cluster of a row: the role control + (when manageable) the Remove
   control, held together against the name on the left. */
.manage-member-actions { flex: none; display: inline-flex; align-items: center; gap: var(--sp3); }
/* The owner-only Remove control. A quiet text button — destructive, so it warms to
   --rust on hover/focus rather than wearing a fill at rest (no dedicated --error
   token; --rust is the app's failure register). Its inline note speaks only on a
   refused delete, and stays out of the row's height when empty. Token-only. */
.manage-remove { display: inline-flex; align-items: center; gap: var(--sp2); }
.manage-remove-btn {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-faint);
  background: none; border: none; padding: 2px 4px; cursor: pointer;
}
.manage-remove-btn:hover, .manage-remove-btn:focus-visible { color: var(--rust); }
.manage-remove-btn:disabled { opacity: 0.55; cursor: progress; }
.manage-remove-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.manage-remove-note:empty { display: none; }
.manage-remove-note[data-tone="error"] { color: var(--rust); }
@media (max-width: 680px) {
  .manage-member-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .manage-member-export { grid-column: 2; grid-row: 2; }
  .manage-member-select-visible { grid-column: 1; padding-left: 0; }
  .manage-member-result-count { grid-column: 2; text-align: right; }
  .manage-member-bulk { flex-wrap: wrap; }
  .manage-member-bulk-count { flex: 1 1 100%; }
  .manage-member-workspace { flex: 1 1 100%; }
  .manage-member-bulk-add, .manage-member-bulk-remove, .manage-member-bulk-clear { flex: 1 1 auto; }
  .manage-member-bulk-note { flex: 1 1 100%; margin-left: 0; }
  .manage-member { align-items: flex-start; flex-wrap: wrap; }
  .manage-member-identity { flex: 1 1 100%; }
  .manage-member-id { flex: 1 1 auto; }
  .manage-member-actions { flex: 1 1 100%; flex-wrap: wrap; justify-content: flex-end; }
  .manage-member-access { margin-left: 0; grid-template-columns: 1fr; }
  .manage-member-access-list { grid-template-columns: 1fr; }
}
.manage-invite {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.manage-invite[disabled] { opacity: 0.55; cursor: not-allowed; }
/* The owner invite reveal-form: a quiet inline row (email · role · send · note) in
   the same bordered register as the rest of the surface — no teal (§3). Hidden until
   the Invite button toggles it; the [hidden] attribute does the hiding. Token-only. */
.manage-invite-form {
  display: flex; align-items: center; gap: var(--sp2);
  margin: 0 0 var(--sp3); flex-wrap: wrap;
}
.manage-invite-form[hidden] { display: none; }
.manage-invite-email {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 8px; min-width: 16rem;
}
.manage-invite-role {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-dim);
  background: none; border: 1px solid var(--line); border-radius: 99px;
  padding: 2px 8px; cursor: pointer;
}
.manage-invite-send {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.manage-invite-send:disabled { opacity: 0.55; cursor: progress; }
.manage-invite-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.manage-invite-note:empty { display: none; }
.manage-invite-note[data-tone="error"] { color: var(--rust); }
/* The pending-invite list — one row per recorded invite (email · role · status ·
   delivery fallback).
   SEEDED from the server's pending roster (the listInvitations read) on load and grown
   by the invites made this session: one coherent list. A failed seed read speaks the
   note below, never a fabricated empty list. */
.manage-invite-toolbar {
  display: grid; grid-template-columns: minmax(12rem, 1fr) auto auto auto;
  align-items: center; gap: var(--sp2); margin-bottom: var(--sp1);
}
.manage-invite-toolbar[hidden] { display: none; }
.manage-invite-search, .manage-invite-filter {
  min-width: 0; min-height: 40px; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink); font: var(--fs-note)/1.2 var(--f-ui);
}
.manage-invite-search { padding: 7px 10px; }
.manage-invite-filter { padding: 6px 28px 6px 9px; color: var(--ink-dim); }
.manage-invite-export {
  min-height: 40px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 5px;
  background: none; color: var(--accent); font: 500 var(--fs-note)/1.2 var(--f-ui);
  cursor: pointer; white-space: nowrap;
}
.manage-invite-export:hover { border-color: var(--ink-dim); color: var(--ink); }
.manage-invite-export:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.manage-invite-search:focus, .manage-invite-filter:focus {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.manage-invite-count {
  min-width: 4rem; color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); text-align: right;
}
.manage-invites { list-style: none; margin: 0 0 var(--sp3); padding: 0; }
.manage-invite-row {
  display: flex; align-items: center; gap: var(--sp3); padding: var(--sp2) 0;
  border-bottom: 1px solid var(--line);
}
.manage-invite-row[hidden] { display: none; }
.manage-invite-row:last-child { border-bottom: none; }
.manage-invite-addr {
  font-family: var(--f-body); color: var(--ink); font-size: var(--fs-control);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  margin-right: auto;
}
.manage-invite-rolechip {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-dim);
  border: 1px solid var(--line); border-radius: 99px; padding: 2px 8px;
}
.manage-invite-status {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-note);
  color: var(--ink-faint);
}
.manage-invite-actions {
  flex: none; display: inline-flex; align-items: center; gap: var(--sp2);
}
.manage-invite-copy, .manage-invite-resend, .manage-invite-revoke {
  min-height: 32px; padding: 4px 8px; border: 1px solid var(--line); border-radius: 4px;
  background: none; color: var(--ink-dim); font: 500 var(--fs-note)/1 var(--f-ui); cursor: pointer;
}
.manage-invite-copy { color: var(--accent); }
.manage-invite-revoke { border-color: transparent; color: var(--ink-faint); }
.manage-invite-revoke:hover { color: var(--rust); }
.manage-invite-copy:disabled, .manage-invite-resend:disabled, .manage-invite-revoke:disabled {
  opacity: .5; cursor: progress;
}
.manage-invite-ctl-note { color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); }
.manage-invite-ctl-note:empty { display: none; }
.manage-invite-ctl-note[data-tone="ok"] { color: var(--accent); }
.manage-invite-ctl-note[data-tone="error"] { color: var(--rust); }
/* The pending-list status line: the honest empty-state ("none yet") or a load-failure
   note. Hidden when empty so a populated list shows no chrome. */
.manage-invites-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-faint);
  margin: 0 0 var(--sp3);
}
.manage-invites-note:empty { display: none; }
.manage-invites-note[data-tone="error"] { color: var(--rust); }
@media (max-width: 680px) {
  .manage-invite-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .manage-invite-search { grid-column: 1 / -1; }
  .manage-invite-count { grid-column: 1 / -1; min-width: 0; text-align: left; }
  .manage-invite-row { align-items: flex-start; flex-wrap: wrap; }
  .manage-invite-addr { flex: 1 1 calc(100% - 5rem); }
  .manage-invite-actions { flex: 1 1 100%; flex-wrap: wrap; }
}

/* MANAGE schema #2 — the org settings. Same bordered, no-teal register as the rest
   of the surface (§3). The owner form stacks labeled fields; the non-owner display
   shows the same label/value pairs read-only. Token-only. */
/* MANAGE schema #4 — the signed-in user's account (the SAME Manage primitive)
   rides the org-settings chrome: the EDITABLE name form reuses the org-settings
   field/input/save/note rules, the read-only email row the value-cell rule — ONE
   layout, never a forked surface (sin #34). All fold into the org-settings rules
   below rather than copying them. */
.org-settings-form, .org-settings-display, .account-display, .account-form {
  display: flex; flex-direction: column; gap: var(--sp3);
  margin: var(--sp3) 0 0; max-width: 28rem;
}
.org-settings-field { display: flex; flex-direction: column; gap: 4px; }
.org-settings-label {
  font-family: var(--f-ui); font-size: var(--fs-chip); color: var(--ink-dim);
  letter-spacing: 0.04em; text-transform: uppercase;
}
.org-settings-name, .org-settings-workspace, .account-name, .account-avatar-url {
  font-family: var(--f-body); font-size: var(--fs-control); color: var(--ink);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 8px;
}
.org-settings-row { display: flex; flex-direction: column; gap: 4px; }
.org-settings-name-val, .org-settings-workspace-val,
.account-email-val {
  font-family: var(--f-body); font-size: var(--fs-control); color: var(--ink);
}
.org-settings-save, .account-save {
  align-self: flex-start; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.org-settings-save:disabled, .account-save:disabled { opacity: 0.55; cursor: progress; }
.org-settings-note, .account-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.org-settings-note:empty, .account-note:empty { display: none; }
.org-settings-note[data-tone="error"], .account-note[data-tone="error"] { color: var(--rust); }
.account-note[data-tone="ok"] { color: var(--ink-dim); }

/* the avatar control (FACE-AVATAR-1) — a profile photo atop the identity: the
   preview disc (the SAME initials/hue face recipe as .cmt-avatar, scaled up) beside
   a URL field + Save/Remove. The disc holds an <img> when an avatar is set, the
   initials fallback otherwise. Token-only, the same bordered register as the form. */
.account-avatar { display: flex; gap: var(--sp3); align-items: flex-start; margin: var(--sp3) 0 0; }
.account-avatar-preview {
  flex: 0 0 auto; width: 56px; height: 56px; border-radius: 50%; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--f-ui); font-size: 18px; font-weight: 600; line-height: 1; letter-spacing: 0.02em;
  color: var(--ink-on-media); background: oklch(0.56 0.072 var(--cmt-hue, 250));
  user-select: none;
}
.identity-disc-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.account-avatar-controls { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 auto; }
.account-avatar-row { display: flex; gap: var(--sp2); align-items: center; }
.account-avatar-save, .account-avatar-remove {
  align-self: flex-start; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.account-avatar-remove { color: var(--rust); }
.account-avatar-save:disabled, .account-avatar-remove:disabled { opacity: 0.55; cursor: progress; }
.account-avatar-note { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.account-avatar-note:empty { display: none; }
.account-avatar-note[data-tone="error"] { color: var(--rust); }
/* Sign out — the account panel's foot control (its natural home; was the retired /projects
   stub's only affordance). A quiet bordered button that speaks --rust on hover (a session-
   ending verb, not a destructive-data one), separated from the sections above. */
.account-signout {
  align-self: flex-start; margin-top: var(--sp5);
  font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 5px 14px; color: var(--ink-dim); cursor: pointer;
}
.account-signout:hover { color: var(--rust); border-color: color-mix(in oklab, var(--rust) 45%, var(--line)); }
.account-signout:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.org-settings-reason {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-faint);
  margin: var(--sp2) 0 0;
}
.workspace-settings-add, .workspace-settings-save, .workspace-settings-people-button {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.workspace-settings-add { margin-left: auto; }
.workspace-settings-add:disabled { opacity: .55; cursor: progress; }
.workspace-settings-hint {
  margin: var(--sp2) 0 var(--sp3); max-width: 42rem;
  color: var(--ink-dim); font-family: var(--f-ui); font-size: var(--fs-note);
}
.workspace-settings-toolbar {
  display: grid; grid-template-columns: minmax(12rem, 1fr) auto auto auto;
  align-items: center; gap: var(--sp2); max-width: 54rem; margin-bottom: var(--sp2);
}
.workspace-settings-search, .workspace-settings-sort {
  min-width: 0; min-height: 40px; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink); font: var(--fs-note)/1.2 var(--f-ui);
}
.workspace-settings-search { padding: 7px 10px; }
.workspace-settings-sort { padding: 6px 28px 6px 9px; color: var(--ink-dim); }
.workspace-settings-export {
  min-height: 40px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 5px;
  background: none; color: var(--accent); font: 500 var(--fs-note)/1.2 var(--f-ui);
  cursor: pointer; white-space: nowrap;
}
.workspace-settings-export:hover { border-color: var(--ink-dim); color: var(--ink); }
.workspace-settings-export:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.workspace-settings-export:disabled { opacity: .55; cursor: default; }
.workspace-settings-search:focus, .workspace-settings-sort:focus {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.workspace-settings-result-count {
  min-width: 6rem; color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); text-align: right;
}
.workspace-settings-list { display: flex; flex-direction: column; max-width: 54rem; border-top: 1px solid var(--line); }
.workspace-settings-row {
  border-bottom: 1px solid var(--line); color: var(--ink);
}
.workspace-settings-row[hidden] { display: none; }
.workspace-settings-summary {
  display: grid; grid-template-columns: minmax(12rem, 1fr) minmax(18rem, auto) auto;
  align-items: center; gap: var(--sp3); min-height: 64px; padding: var(--sp2);
  font-family: var(--f-ui); list-style: none;
}
details > .workspace-settings-summary { cursor: pointer; }
.workspace-settings-summary::-webkit-details-marker { display: none; }
.workspace-settings-summary:hover { background: var(--wash); }
.workspace-settings-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.workspace-settings-identity { display: flex; align-items: center; gap: var(--sp2); min-width: 0; }
.workspace-settings-mark {
  display: grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px;
  border: 1px solid var(--line); border-radius: 5px; background: var(--surface);
  color: var(--ink-dim); font: 600 var(--fs-note)/1 var(--f-ui);
}
.workspace-settings-summary-name {
  overflow: hidden; color: var(--ink); font: 600 var(--fs-body)/1.25 var(--f-ui);
  text-overflow: ellipsis; white-space: nowrap;
}
.workspace-settings-summary-stats { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp3); }
.workspace-settings-summary-stat, .workspace-settings-summary-created {
  color: var(--ink-faint); font: var(--fs-note)/1.25 var(--f-ui); white-space: nowrap;
}
.workspace-settings-summary-access {
  color: var(--ink-dim); font: var(--fs-note)/1.25 var(--f-ui); white-space: nowrap;
}
.workspace-settings-manage-label {
  color: var(--accent); font: 600 var(--fs-note)/1.2 var(--f-ui); white-space: nowrap;
}
details[open] .workspace-settings-manage-label::after { content: " ↑"; }
details:not([open]) .workspace-settings-manage-label::after { content: " ↓"; }
.workspace-settings-detail {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp3);
  padding: var(--sp2) var(--sp2) var(--sp3) 48px; background: color-mix(in oklab, var(--surface) 42%, transparent);
}
.workspace-settings-fields {
  display: grid; grid-template-columns: minmax(10rem, 1fr) minmax(12rem, auto);
  align-items: center; gap: var(--sp3); min-width: 0;
}
.workspace-settings-actions {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--sp2); min-width: 0;
}
.workspace-settings-note, .workspace-people-note {
  margin: 0; color: var(--ink-faint); font: var(--fs-note)/1.3 var(--f-ui);
}
.workspace-settings-note:empty, .workspace-people-note:empty { display: none; }
.workspace-settings-note[data-tone="ok"], .workspace-people-note[data-tone="ok"] { color: var(--accent); }
.workspace-settings-note[data-tone="error"], .workspace-people-note[data-tone="error"] { color: var(--rust); }
.workspace-settings-people-button { justify-self: end; min-height: 44px; }
.workspace-settings-people {
  grid-column: 1 / -1; border-top: 1px solid var(--line); padding-top: var(--sp2);
  color: var(--ink-dim); font-family: var(--f-ui); font-size: var(--fs-note);
}
.workspace-people-toolbar {
  display: grid; grid-template-columns: minmax(12rem, 1fr) auto auto;
  align-items: center; gap: var(--sp2); margin-bottom: var(--sp1);
}
.workspace-people-search, .workspace-people-filter {
  min-width: 0; min-height: 40px; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink); font: var(--fs-note)/1.2 var(--f-ui);
}
.workspace-people-search { padding: 7px 10px; }
.workspace-people-filter { padding: 6px 28px 6px 9px; color: var(--ink-dim); }
.workspace-people-search:focus, .workspace-people-filter:focus {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.workspace-people-count {
  min-width: 4rem; color: var(--ink-faint); font: var(--fs-note)/1.2 var(--f-ui); text-align: right;
}
.workspace-people-note { margin-bottom: var(--sp1); }
.workspace-person { display: flex; align-items: center; gap: var(--sp2); min-height: 44px; padding: 5px 2px; cursor: pointer; }
.workspace-person[hidden] { display: none; }
.workspace-person + .workspace-person { border-top: 1px solid color-mix(in oklab, var(--line) 60%, transparent); }
.workspace-person:has(input:disabled) { cursor: default; }
.workspace-person-checkbox { width: 18px; height: 18px; accent-color: var(--accent); }
.workspace-person-identity { display: flex; flex-direction: column; min-width: 0; }
.workspace-person-identity strong { color: var(--ink); font: 600 var(--fs-body)/1.25 var(--f-ui); }
.workspace-person-identity small { color: var(--ink-faint); font: var(--fs-note)/1.35 var(--f-ui); overflow-wrap: anywhere; }
.workspace-settings-name {
  min-width: 0; padding: 4px 8px; border: 1px solid transparent; border-radius: 4px;
  background: transparent; color: var(--ink); font-family: var(--f-body); font-size: var(--fs-control);
}
.workspace-settings-access {
  min-width: 0; padding: 4px 8px; border: 1px solid var(--line); border-radius: 4px;
  background: var(--surface); color: var(--ink-dim); font-family: var(--f-ui); font-size: var(--fs-note);
}
.workspace-settings-name:not(:disabled):focus { border-color: var(--line); }
.workspace-settings-save:disabled { opacity: .55; cursor: progress; }
.workspace-settings-empty {
  max-width: 54rem; margin: 0; padding: var(--sp4) var(--sp2);
  color: var(--ink-dim); font: var(--fs-note)/1.4 var(--f-ui);
}
@media (max-width: 680px) {
  .workspace-settings-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .workspace-settings-search { grid-column: 1 / -1; }
  .workspace-settings-result-count { grid-column: 1 / -1; min-width: 0; text-align: left; }
  .workspace-settings-summary { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp1) var(--sp2); }
  .workspace-settings-summary-stats {
    grid-column: 1 / -1; justify-content: flex-start; flex-wrap: wrap; padding-left: 40px;
  }
  .workspace-settings-manage-label { grid-column: 2; grid-row: 1; }
  .workspace-settings-detail { grid-template-columns: 1fr; padding-left: var(--sp2); }
  .workspace-settings-fields { grid-template-columns: 1fr; }
  .workspace-settings-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
  .workspace-settings-save, .workspace-settings-people-button { flex: 1 1 50%; min-height: 44px; }
  .workspace-settings-people { grid-column: 1; }
  .workspace-people-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .workspace-people-count { grid-column: 1 / -1; min-width: 0; text-align: left; }
}

/* "Your organizations" rows (FACE-F1, accountPanel.ts) — the old row mashed three
   adjacent spans with no spacing into "Proof DevOwnerCurrent". Now each row reads
   as THREE legible things: the name (--ink body), the role (dimmed, a middot set
   off), and the state/affordance pushed to the right — a --wash "Current" tag on
   the active row, an --accent "Switch" hint on the rest. The switchable row is an
   obvious button (bordered, cursor, an --accent edge on hover/focus); the current
   row is inert. Token-only, no new color vocabulary. */
.account-organizations-hint {
  max-width: 36rem; margin: var(--sp1) 0 0;
  color: var(--ink-dim); font: var(--fs-note)/1.45 var(--f-ui);
}
.account-organizations { display: flex; flex-direction: column; gap: var(--sp1); margin: var(--sp3) 0 0; max-width: 28rem; }
.account-organization {
  display: flex; align-items: baseline; gap: var(--sp2);
  width: 100%; text-align: left; cursor: pointer;
  background: none; border: 1px solid var(--line); border-radius: 6px;
  padding: var(--sp2) var(--sp3);
}
.account-organization:hover:not(:disabled), .account-organization:focus-visible:not(:disabled) { border-color: var(--accent); }
.account-organization:disabled { cursor: default; }
.account-organization-name { font-family: var(--f-body); font-size: var(--fs-control); color: var(--ink); }
.account-organization-role { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.account-organization-role::before { content: "·"; margin-right: var(--sp2); color: var(--ink-faint); }
.account-organization-current, .account-organization-switch {
  margin-left: auto; font-family: var(--f-ui); font-size: var(--fs-chip); letter-spacing: 0.04em;
}
.account-organization-current {
  padding: 2px 8px; border-radius: 99px; background: var(--wash);
  color: var(--ink-dim); text-transform: uppercase;
}
.account-organization-switch { color: var(--accent); }

/* "Passkeys" rows (AUTH-RECOVERY-1, accountPanel.ts) — the organizations-row grammar
   verbatim: name in --ink, the added-date dimmed behind a middot, the one verb
   pushed right (--rust Remove, the avatar-remove precedent). The add CTA + note
   wear the avatar controls' exact clothes. Token-only, zero new color. */
.account-passkeys-hint { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); margin: var(--sp2) 0 0; max-width: 28rem; }
.account-passkeys { display: flex; flex-direction: column; gap: var(--sp1); margin: var(--sp3) 0 0; max-width: 28rem; }
.account-passkey {
  display: flex; align-items: baseline; gap: var(--sp2);
  border: 1px solid var(--line); border-radius: 6px;
  padding: var(--sp2) var(--sp3);
}
.account-passkey-name { font-family: var(--f-body); font-size: var(--fs-control); color: var(--ink); }
.account-passkey-when { font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.account-passkey-when::before { content: "·"; margin-right: var(--sp2); color: var(--ink-faint); }
.account-passkey-remove {
  margin-left: auto; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--rust); cursor: pointer;
}
.account-passkey-add {
  margin-top: var(--sp3); align-self: flex-start; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.account-passkey-add:hover:not(:disabled), .account-passkey-add:focus-visible:not(:disabled),
.account-passkey-remove:hover, .account-passkey-remove:focus-visible { border-color: var(--accent); }
/* the ARMED state (share-revoke's confirming grammar): the rest color is already
   --rust, so arming escalates by weight + a rust border — unmissable, still quiet */
.account-passkey-remove.confirming, .account-passkey-remove.confirming:hover,
.account-passkey-remove.confirming:focus-visible { font-weight: 600; border-color: var(--rust); }
.account-passkey-add:disabled { opacity: 0.55; cursor: progress; }
.account-passkey-note { display: block; margin-top: var(--sp2); font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim); }
.account-passkey-note:empty { display: none; }
.account-passkey-note[data-tone="error"] { color: var(--rust); }

/* MANAGE schema #3 — the org's plan & billing (the SAME Manage primitive). Token-
   only, no teal (§3): the plan headline + a metered-dimension roster + the gated
   "Manage plan" CTA. An at-cap metered row speaks --rust (the semantic-status
   over-limit read); the disabled CTA wears the set-aside opacity (an honest "not
   yet", never a dead grey). */
.billing-plan { margin: var(--sp3) 0; }
.billing-plan-name {
  font-family: var(--f-body); font-size: var(--fs-clock); color: var(--ink);
}
.billing-dimensions {
  list-style: none; margin: 0; padding: 0; max-width: 28rem;
}
.billing-dimension {
  display: flex; align-items: baseline; gap: var(--sp2);
  padding: var(--sp2) 0; border-bottom: 1px solid var(--line);
}
.billing-dimension-label {
  flex: 1; font-family: var(--f-body); font-size: var(--fs-control); color: var(--ink);
}
.billing-dimension-usage {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
.billing-dimension-tag {
  font-family: var(--f-ui); font-size: var(--fs-chip); color: var(--ink-faint);
  letter-spacing: 0.04em; text-transform: uppercase;
}
.billing-dimension[data-state="over"] .billing-dimension-usage { color: var(--rust); }
.billing-manage {
  align-self: flex-start; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer; margin-top: var(--sp3);
}
.billing-manage:hover:not(:disabled) { border-color: var(--ink-dim); color: var(--ink); }
.billing-manage:disabled { opacity: var(--set-aside-opacity); cursor: not-allowed; }
.billing-manage-reason {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-faint);
  margin: var(--sp2) 0 0;
}
.billing-manage-reason:empty { display: none; }
.billing-manage-reason[data-tone="error"] { color: var(--rust); }

/* FACE-F3 — the org-settings API-keys section (apiKeys.ts). Same quiet
   bordered register as the invite form (the section IS a manage schema
   sibling). The show-once secret strip is the one louder element — a bordered
   card so "copy now or never" reads as a moment, not a row; a revoked key
   greys but stays listed (history, not a hole). Token-only, no teal (§3). */
.api-keys-new {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.api-keys-form {
  display: flex; align-items: center; gap: var(--sp2);
  margin: 0 0 var(--sp3); flex-wrap: wrap;
}
.api-keys-form[hidden] { display: none; }
.api-keys-name {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 8px; min-width: 20rem;
}
.api-keys-create {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.api-keys-create:disabled { opacity: 0.55; cursor: progress; }
.api-keys-note {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
}
.api-keys-note:empty { display: none; }
.api-keys-note[data-tone="error"] { color: var(--rust); }
.api-keys-secret {
  border: 1px solid var(--line); border-radius: 6px;
  padding: var(--sp3); margin: 0 0 var(--sp3);
}
.api-keys-secret-line {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink);
  margin: 0 0 var(--sp2);
}
.api-keys-secret-row {
  display: flex; align-items: center; gap: var(--sp2); padding: 2px 0;
  min-width: 0;
}
.api-keys-secret-label {
  flex: none; width: 4.5rem; font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-faint);
}
.api-keys-secret-value {
  font-size: var(--fs-note); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  user-select: all;
}
.api-keys-copy-id, .api-keys-copy-secret, .api-keys-copy-blob {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 2px 8px; color: var(--ink-dim); cursor: pointer;
}
.api-keys-list { list-style: none; margin: 0; padding: 0; }
.api-key {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp3); padding: var(--sp3) 0; border-bottom: 1px solid var(--line);
}
.api-key:last-child { border-bottom: none; }
.api-key.revoked > :not(.api-key-revoked-tag) { opacity: 0.5; }
.api-key-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.api-key-name {
  font-family: var(--f-body); color: var(--ink); font-size: var(--fs-control);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.api-key-keyid {
  font-size: var(--fs-note); color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.api-key-dates {
  font-family: var(--f-ui); font-size: var(--fs-note); color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
.api-key-revoke {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  background: none; border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; color: var(--ink-dim); cursor: pointer;
}
.api-key-revoke:disabled { opacity: 0.55; cursor: progress; }
.api-key-revoked-tag {
  flex: none; font-family: var(--f-ui); font-size: var(--fs-chip);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-faint);
  border: 1px solid var(--line); border-radius: 99px; padding: 2px 8px;
}

/* BILL-1d — the quota-refusal panel (quotaPanel.ts). The 402's designed
   surface: scrim + a small surface card carrying the product-voice line, the
   used/allowance read (label · tabular numbers · a meter bar), and the two
   exits. The meter speaks --rust at/over the cap — the billing roster's own
   over-limit vocabulary — and --accent below it (a mid-cycle refusal on a
   windowed dimension can sit under the cap by the time it's seen). */
.quota-backdrop {
  position: fixed; inset: 0; z-index: 80;
  background: var(--scrim);
  display: grid; place-items: center;
}
/* The quota backdrop pierces the shell-page curtain via the shared data-overlay
   seam (mountOverlayLayer stamps it; the curtain excludes [data-overlay] — see
   :2755 / overlayLayer.ts), so its own display:grid above just applies. The old
   bespoke per-class exception here was retired when DLG-CURTAIN-1 generalised the
   fix to every overlay (dialogs, the context menu, the toast had the same trap). */
.quota-panel {
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  padding: var(--sp4) var(--sp4) var(--sp3);
  width: min(26rem, calc(100vw - 2 * var(--sp4)));
  box-shadow: 0 12px 40px rgb(0 0 0 / 0.45);
}
.quota-title {
  font-family: var(--f-body); font-size: var(--fs-clock); font-weight: 500;
  color: var(--ink); margin: 0 0 var(--sp2);
}
.quota-line {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink-dim);
  margin: 0 0 var(--sp3); line-height: 1.45;
}
.quota-usage { margin: 0 0 var(--sp3); }
.quota-usage-row {
  display: flex; align-items: baseline; gap: var(--sp2);
  margin-bottom: var(--sp1);
}
.quota-usage-label {
  flex: 1; font-family: var(--f-ui); font-size: var(--fs-chip); color: var(--ink-faint);
  letter-spacing: 0.04em; text-transform: uppercase;
}
.quota-usage-numbers {
  font-family: var(--f-ui); font-size: var(--fs-control); color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.quota-usage[data-state="over"] .quota-usage-numbers { color: var(--rust); }
.quota-meter {
  height: 4px; border-radius: 2px; background: var(--wash-soft); overflow: hidden;
}
.quota-meter-fill {
  height: 100%; border-radius: 2px; background: var(--accent);
}
.quota-usage[data-state="over"] .quota-meter-fill { background: var(--rust); }
.quota-actions {
  display: flex; justify-content: flex-end; gap: var(--sp2); margin-top: var(--sp3);
}
.quota-dismiss, .quota-plans {
  font-family: var(--f-ui); font-size: var(--fs-chip);
  border-radius: 4px; padding: 4px 12px; cursor: pointer;
}
.quota-dismiss {
  background: none; border: 1px solid var(--line); color: var(--ink-dim);
}
.quota-dismiss:hover { border-color: var(--ink-dim); color: var(--ink); }
.quota-plans {
  background: var(--primary-action); border: 1px solid var(--primary-action);
  color: var(--bg-deep); font-weight: 600;
}
.quota-plans:hover { background: oklch(from var(--primary-action) calc(l + var(--hover-delta)) c h); }
/* Shared asset viewer — one quiet darkroom for every file that does not belong
   in the transcript editor. Preview engines vary; the surrounding grammar does
   not. The source stays visually primary and metadata remains a compact footer. */
body[data-route="asset-viewer"] {
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
}
body[data-route="asset-viewer"] > :not(#route-page):not([data-overlay]) { display: none; }
body[data-route="asset-viewer"] .route-page {
  grid-row: 1 / -1;
  align-self: stretch;
  justify-self: stretch;
  width: 100%;
  max-width: none;
  min-width: 0;
  min-height: 0;
  padding: 0;
}
.asset-viewer {
  height: 100vh;
  height: 100dvh;
  min-width: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  background: var(--bg-deep);
  color: var(--ink);
}
.asset-viewer-header {
  min-width: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp4);
  min-height: 76px;
  padding: var(--sp3) var(--sp5);
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}
.asset-viewer-back,
.asset-viewer-project {
  color: var(--ink-dim);
  background: transparent;
  border-color: transparent;
}
.asset-viewer-back:hover,
.asset-viewer-project:hover { color: var(--ink); background: var(--wash); }
.asset-viewer-identity { min-width: 0; }
.asset-viewer-trail {
  display: flex;
  align-items: center;
  gap: var(--sp1);
  color: var(--ink-faint);
  font: 500 0.75rem/1.2 var(--f-ui);
}
.asset-viewer-project {
  min-height: 28px;
  padding: 0 var(--sp1);
  font: inherit;
}
.asset-viewer-identity h1 {
  overflow: hidden;
  margin: var(--sp1) 0 0;
  color: var(--ink);
  font: 500 clamp(1rem, 2vw, 1.2rem)/1.2 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-viewer-actions { display: flex; align-items: center; }
.asset-viewer-download {
  min-height: 42px;
  display: inline-flex;
  align-items: center;
  padding: 0 var(--sp4);
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--ink);
  font: 600 0.9rem/1 var(--f-ui);
  text-decoration: none;
}
.asset-viewer-download:hover { background: var(--wash); border-color: var(--ink-deco); }
.asset-viewer-body {
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
}
.asset-viewer-content {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
}
.asset-viewer-stage {
  min-width: 0;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: clamp(var(--sp4), 4vw, var(--sp8));
  overflow: hidden;
  background: var(--bg-deep);
}
.asset-viewer-text {
  width: min(100%, 980px);
  max-height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: clamp(var(--sp4), 4vw, var(--sp7));
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--bg);
  color: var(--ink);
  font: 400 0.88rem/1.65 var(--f-mono);
  overflow-wrap: anywhere;
  tab-size: 2;
  white-space: pre-wrap;
}
.asset-viewer-text-note {
  width: min(100%, 980px);
  box-sizing: border-box;
  margin: 0 0 var(--sp2);
  color: var(--ink-dim);
  font: 500 0.78rem/1.4 var(--f-ui);
}
.asset-viewer-archive {
  width: min(100%, 920px);
  max-height: 100%;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--bg);
  box-shadow: 0 18px 56px rgb(0 0 0 / 0.22);
}
.asset-viewer-archive-head {
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp4);
  padding: var(--sp4);
  border-bottom: 1px solid var(--line);
}
.asset-viewer-archive-head > div {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp3);
}
.asset-viewer-archive-head .asset-viewer-filemark {
  width: 48px;
  flex: 0 0 48px;
}
.asset-viewer-archive-head h2 {
  overflow: hidden;
  margin: 0;
  color: var(--ink);
  font: 600 0.95rem/1.25 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-viewer-archive-head p {
  margin: var(--sp1) 0 0;
  color: var(--ink-faint);
  font: 400 0.75rem/1.25 var(--f-ui);
}
.asset-viewer-archive-badge {
  flex: 0 0 auto;
  padding: var(--sp1) var(--sp2);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-dim);
  font: 600 0.68rem/1.3 var(--f-ui);
}
.asset-viewer-archive-list {
  min-height: 0;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  list-style: none;
}
.asset-viewer-archive-list li {
  min-width: 0;
  display: grid;
  grid-template-columns: 54px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp3);
  min-height: 48px;
  padding: var(--sp2) var(--sp4);
  border-bottom: 1px solid var(--line);
}
.asset-viewer-archive-list li:last-child { border-bottom: 0; }
.asset-viewer-archive-kind {
  color: var(--ink-faint);
  font: 600 0.64rem/1 var(--f-ui);
  letter-spacing: 0.07em;
  text-transform: uppercase;
}
.asset-viewer-archive-path {
  min-width: 0;
  overflow: hidden;
  color: var(--ink-dim);
  font: 450 0.82rem/1.3 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-viewer-archive-size {
  color: var(--ink-faint);
  font: 400 0.75rem/1 var(--f-mono);
  white-space: nowrap;
}
.asset-viewer-archive-empty {
  display: block !important;
  padding-block: var(--sp6) !important;
  color: var(--ink-faint);
  font: 400 0.82rem/1.4 var(--f-ui);
  text-align: center;
}
.asset-viewer-archive-note {
  margin: 0;
  padding: var(--sp3) var(--sp4);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font: 400 0.72rem/1.35 var(--f-ui);
}
.asset-viewer-discussion {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  border-left: 1px solid var(--line);
  background: var(--bg);
}
.asset-viewer-discussion-head {
  min-height: 54px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp3);
  padding: var(--sp3) var(--sp4);
  border-bottom: 1px solid var(--line);
}
.asset-viewer-discussion-head h2 {
  margin: 0;
  font: 600 0.9rem/1.2 var(--f-ui);
}
.asset-viewer-discussion-head span {
  color: var(--ink-faint);
  font: 500 0.75rem/1 var(--f-ui);
}
.asset-viewer-comment-feed {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp2) var(--sp4);
}
.asset-viewer-comment {
  padding: var(--sp3) 0;
  border-bottom: 1px solid var(--line);
}
.asset-viewer-comment.is-resolved { opacity: 0.58; }
.asset-viewer-comment-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp2);
  color: var(--ink);
  font: 500 0.78rem/1.25 var(--f-ui);
}
.asset-viewer-comment-head time {
  flex: 0 0 auto;
  color: var(--ink-faint);
  font-weight: 400;
}
.asset-viewer-comment p {
  margin: var(--sp2) 0 0;
  color: var(--ink-dim);
  font: 400 0.86rem/1.5 var(--f-ui);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.asset-viewer-comment-empty {
  margin: var(--sp4) 0;
  color: var(--ink-faint);
  font: 400 0.82rem/1.45 var(--f-ui);
}
.asset-viewer-comment-form {
  display: grid;
  gap: var(--sp2);
  padding: var(--sp3) var(--sp4) var(--sp4);
  border-top: 1px solid var(--line);
}
.asset-viewer-comment-form label {
  color: var(--ink-dim);
  font: 600 0.75rem/1.2 var(--f-ui);
}
.asset-viewer-comment-form textarea {
  width: 100%;
  min-height: 76px;
  resize: vertical;
}
.asset-viewer-comment-form-actions {
  min-height: 36px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp2);
}
.asset-viewer-comment-form-actions button { min-height: 36px; }
.asset-viewer-comment-status {
  color: var(--ink-faint);
  font: 400 0.72rem/1.2 var(--f-ui);
}
.asset-viewer-image {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  box-shadow: 0 18px 56px rgb(0 0 0 / 0.32);
}
.asset-viewer-pdf {
  width: min(100%, 1180px);
  height: 100%;
  min-height: 360px;
  border: 1px solid var(--line);
  background: var(--surface);
}
.asset-viewer-audio,
.asset-viewer-empty {
  width: min(100%, 560px);
  display: grid;
  justify-items: center;
  gap: var(--sp4);
  text-align: center;
}
.asset-viewer-audio audio { width: min(100%, 480px); }
.asset-viewer-filemark {
  width: 84px;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--lift-1);
  color: var(--ink-dim);
  font: 600 0.9rem/1 var(--f-tc);
  letter-spacing: 0.08em;
}
.asset-viewer-empty h1,
.asset-viewer-empty h2 { margin: 0; font: 500 1.25rem/1.25 var(--f-ui); }
.asset-viewer-empty p { max-width: 38ch; margin: calc(-1 * var(--sp2)) 0 0; color: var(--ink-dim); }
.asset-viewer-meta {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--sp6);
  margin: 0;
  padding: var(--sp3) var(--sp5);
  border-top: 1px solid var(--line);
  background: var(--bg);
}
.asset-viewer-meta-item {
  min-width: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp2);
  align-items: baseline;
}
.asset-viewer-meta dt {
  color: var(--ink-faint);
  font: 600 0.68rem/1 var(--f-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.asset-viewer-meta dd {
  overflow: hidden;
  margin: 0;
  color: var(--ink-dim);
  font: 400 0.8rem/1.2 var(--f-ui);
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asset-viewer-loading,
.asset-viewer-error { place-items: center; grid-template-rows: 1fr; padding: var(--sp5); }
.asset-viewer-loading-line { color: var(--ink-dim); font: 500 0.9rem/1.4 var(--f-ui); }

@media (max-width: 640px) {
  .asset-viewer-header {
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--sp2);
    min-height: 68px;
    padding: var(--sp2) var(--sp3);
  }
  .asset-viewer-back { min-width: 44px; min-height: 44px; padding-inline: var(--sp2); }
  .asset-viewer-trail { display: none; }
  .asset-viewer-identity h1 { margin: 0; }
  .asset-viewer-actions { grid-column: 1 / -1; }
  .asset-viewer-download {
    width: 100%;
    min-height: 44px;
    justify-content: center;
  }
  .asset-viewer-stage { padding: var(--sp3); }
  .asset-viewer {
    height: auto;
    min-height: 100dvh;
  }
  .asset-viewer-body { min-height: auto; }
  .asset-viewer-content {
    min-height: auto;
    grid-template-columns: 1fr;
  }
  .asset-viewer-stage { min-height: min(58dvh, 540px); }
  .asset-viewer-archive-head { align-items: flex-start; }
  .asset-viewer-archive-list li {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--sp2);
    min-height: 52px;
    padding-inline: var(--sp3);
  }
  .asset-viewer-archive-kind { display: none; }
  .asset-viewer-archive-path { white-space: normal; overflow-wrap: anywhere; }
  .asset-viewer-discussion {
    min-height: 420px;
    border-top: 1px solid var(--line);
    border-left: 0;
  }
  .asset-viewer-comment-form textarea { min-height: 88px; }
  .asset-viewer-comment-form-actions button { min-height: 44px; }
  .asset-viewer-meta {
    gap: var(--sp3);
    padding: var(--sp3);
    overflow-x: auto;
  }
  .asset-viewer-meta-item { flex: 0 0 auto; }
}
/* Local-only semantic token workbench. The tool itself uses the same roles it edits. */
.theme-lab { position: fixed; right: 16px; bottom: 16px; z-index: 10000; font-family: var(--f-ui); }
.theme-lab-toggle {
  display: flex; align-items: center; gap: var(--sp2); margin-left: auto; min-height: 40px;
  padding: 0 var(--sp3); border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink); box-shadow: 0 8px 30px rgb(0 0 0 / 0.28);
}
.theme-lab-toggle:hover, .theme-lab-toggle[aria-expanded="true"] { border-color: var(--accent); }
.theme-lab-mark { display: flex; gap: 2px; align-items: center; }
.theme-lab-mark i { width: 6px; height: 14px; border-radius: 3px; background: var(--accent); }
.theme-lab-mark i:nth-child(2) { height: 20px; background: var(--primary-action); }
.theme-lab-mark i:nth-child(3) { height: 10px; background: var(--rust); }
.theme-lab-panel {
  position: absolute; right: 0; bottom: 50px; width: min(380px, calc(100vw - 24px));
  max-height: min(720px, calc(100vh - 86px)); overflow: hidden; border: 1px solid var(--line);
  border-radius: 10px; background: var(--bg-deep); color: var(--ink);
  box-shadow: 0 18px 54px rgb(0 0 0 / 0.42);
}
.theme-lab-head { display: flex; align-items: center; justify-content: space-between; padding: var(--sp4); border-bottom: 1px solid var(--line); }
.theme-lab-head div, .theme-lab-copy { display: grid; gap: 2px; }
.theme-lab-head strong { font-size: var(--fs-control); }
.theme-lab-head small, .theme-lab-copy small { color: var(--ink-faint); font-size: var(--fs-chip); }
.theme-lab-close { width: 32px; height: 32px; padding: 0; border: 0; color: var(--ink-dim); font-size: 22px; line-height: 1; }
.theme-lab-close:hover { color: var(--ink); }
.theme-lab-presets { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; padding: var(--sp3) var(--sp4); border-bottom: 1px solid var(--line); }
.theme-lab-presets button { min-height: 32px; color: var(--ink-dim); }
.theme-lab-presets button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.theme-lab-preset-note { margin: 0; padding: 0 var(--sp4) var(--sp3); border-bottom: 1px solid var(--line); color: var(--ink-faint); font-size: var(--fs-chip); line-height: 1.35; }
.theme-lab-list { max-height: calc(min(720px, 100vh - 86px) - 228px); overflow: auto; padding: var(--sp2) var(--sp4); }
.theme-lab-row { display: grid; grid-template-columns: minmax(0, 1fr) 152px; gap: var(--sp3); align-items: center; padding: var(--sp2) 0; }
.theme-lab-copy strong { font-size: var(--fs-note); font-weight: 600; }
.theme-lab-controls { display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: var(--sp2); align-items: center; }
.theme-lab-color { width: 32px; height: 32px; padding: 2px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); cursor: pointer; }
.theme-lab-value { width: 100%; height: 32px; padding: 0 var(--sp2); border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font: 11px/1 var(--f-tc); }
.theme-lab-value[aria-invalid="true"] { border-color: var(--rust); }
.theme-lab-actions { display: flex; align-items: center; gap: var(--sp2); padding: var(--sp3) var(--sp4); border-top: 1px solid var(--line); }
.theme-lab-actions button { min-height: 32px; padding: 0 var(--sp3); }
.theme-lab-actions button:last-of-type { border-color: var(--accent); color: var(--accent); }
.theme-lab-status { margin-left: auto; color: var(--ink-faint); font-size: var(--fs-chip); }

@media (max-width: 560px) {
  .theme-lab { right: 10px; bottom: 10px; }
  .theme-lab-panel { position: fixed; inset: auto 8px 58px; width: auto; max-height: calc(100dvh - 74px); }
  .theme-lab-list { max-height: calc(100dvh - 302px); }
  .theme-lab-row { grid-template-columns: 1fr; gap: var(--sp2); padding: var(--sp3) 0; }
  .theme-lab-controls { grid-template-columns: 40px minmax(0, 1fr); }
  .theme-lab-color, .theme-lab-value { height: 40px; }
}
