/* RAIWeb — themed from the snapshot's `theme` block: main.js writes
   the server-resolved token palette onto these custom properties on
   every theme change, so the values below are only the fallback for
   servers that predate the block. The fallbacks are transcribed from
   RAITheme.default (RAITheme.swift), not eyeballed — keep them in
   sync with that file, never invent hues here. */

:root {
  --bg: #0e1116;
  --panel: #171c24;
  --panel-2: #1f2530;
  --border: #2c333d;
  --text: #f1f3f5;
  --dim: #848c9a;
  --accent: #5cf09d;
  --accent-fg: #06190d;
  --danger: #fb7185;
  --warn: #fcd34d;
  --info: #7dd3fc;
  --ok: #57ab5a;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* Ask for a DESIGN, the Mac's RAIFontFamily model: ui-sans-serif /
     system-ui resolve to the platform's own face everywhere. The old
     Apple-only stack fell through to Arial off-Mac — Raina saw SF Pro
     while everyone else got Arial at small sizes. No webfont: payload,
     licence, and it would contradict ask-for-a-design. */
  /* Platform-first on purpose (RAIFontFamily: ask for the platform's
     design). On Linux the generics resolve to DejaVu Sans, ~14% wider
     than the Mac faces — that is absorbed by sizing text budgets
     (e.g. the 62ch description gate) against DejaVu as the binding
     face, NOT by front-loading a Linux face, which would override the
     platform on any machine that happens to have it installed.
     Re-measure in a real browser before changing this line — fc-match
     alone is not the render step; browsers stop at the first name
     that resolves. */
  font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* The host-chrome atmosphere behind the tree — the layer that never
   crosses the wire in the tree itself. Per the Neuroglass design spec
   (Raina Docs/neuroglass-design-spec.md): faint accent glow upper-
   right at 8%, soft surface wash mid-left. Spec hexes replaced with
   the wire-fed vars so a server theme swap repaints the atmosphere.

   TWO SELECTORS, ONE LAYER (Sonar's find, 2026-08-20): the class is
   set by applyTheme, which only runs when a STAGE app snapshot
   arrives — so the home shell, where nothing is running, painted flat
   #0e1116 and the default theme never introduced itself to exactly
   the person who needed introducing. The html[data-rai-chrome] arm is
   stamped by the server's pre-paint insert (HTTPServer, same script
   that stamps data-rai-theme), so the landing screen carries the
   atmosphere before any app runs. applyTheme keeps the class for live
   swaps; the stamp covers first paint. */
html[data-rai-chrome="neuroglass"] body,
body.rai-atmospheric {
  background-color: var(--bg);
  background-image:
    radial-gradient(60% 50% at 100% 0%,
      color-mix(in srgb, var(--accent) 8%, transparent), transparent 70%),
    radial-gradient(70% 60% at 20% 45%,
      color-mix(in srgb, var(--panel-2) 55%, transparent), transparent 75%);
  background-attachment: fixed;
}

/* ============================================================
   STACKING ORDER — pick a number by looking it up, not by
   measuring a collision.

   Twice this codebase has had a pinned element render under the
   content it was pinned above: the docks over the library shelf,
   and #topbar under everything. Both were found by Raina noticing,
   and both were fixed by someone choosing a number under pressure.
   This table exists so the next number is a lookup.

     0 –  19   CONTENT    scrolls with the page. Timeline furniture,
                          in-place editor affordances, drag states.
      20 – 39  CHROME     pinned to the frame. #topbar = 20.
      40 – 49  POPOVER    opens FROM chrome and must cover it.
                          #theme-pop, .rai-dropdown-menu = 40.
      50 – 59  SCRIM      full-screen dim. .help-scrim = 50,
                          .picker-scrim = 55.
      60 – 99  SYSTEM     persistent, outranks the app. #auth-banner = 60.
     1000 +    TRANSIENT  toasts, context menus. Always last.

   FIVE RULES, each one paid for:

   1. The default is `auto`. A number is a CLAIM that this must cover
      something specific. If you cannot name what, you do not need one.

   2. Take the BOTTOM of your tier. Only go higher to order against a
      sibling you can name (that is the only reason 55 sits above 50).

   3. A sticky or fixed element with NO z-index does NOT stay on top.
      Paint order falls back to DOM order, and chrome is early in the
      document, so everything later wins. This is the exact bug that
      put content over #topbar — measured at 19 of 23 probe points on a
      1440x420 window, and invisible at 1440x900 because a taller
      window scrolls less. Pinning something is only half the job.

   4. z-index does nothing on `position: static`. Below the phone
      breakpoint #topbar is static and its 20 is inert — correctly, as
      it scrolls away and cannot collide. Do not "fix" that.

   5. Two elements that never share a PAGE need no ordering — but say
      so where the number lives, because merging pages reintroduces the
      collision silently. Live example: .rai-completion is a popup at
      10, below chrome, which would be wrong if it ever met #topbar.
      It cannot today — #topbar is on index.html and tools.html only,
      and .rai-completion is on code.html. IF RAI CODE EVER GAINS THE
      SHELL TOPBAR, .rai-completion MUST MOVE ABOVE 20 IN THE SAME
      COMMIT.

   Census and method, including what was NOT tested:
   Sonar's stacking-order audit, design tree @ 95aa1a5da
   ============================================================ */

#topbar {
  display: flex;
  align-items: center;
  gap: 8px;
  /* FULL-BLEED BAR, MEASURED CONTENTS. The background and border stay
     edge-to-edge -- that is correct, it is page-wide chrome -- but the
     wordmark and the controls start at the same x as MY STUDIO below
     them. This was the one element outside the container.

     `100%` and NOT `100vw` on purpose: percentage padding resolves
     against the containing block's width, which is what `margin: 0 auto`
     on a capped child also divides. 100vw includes the scrollbar, so a
     vw-based version would sit half a scrollbar off the content it is
     supposed to line up with -- a misalignment that only appears when
     the page is long enough to scroll, i.e. never in a screenshot.

     max() rather than a media query: below the measure both sides
     collapse to the same --space-lg the containers use, so they agree at
     EVERY width instead of at the widths someone remembered to test. */
  padding: 10px max(var(--space-lg), calc((100% - var(--measure)) / 2 + var(--space-lg)));
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  /* Sticky with z-index:auto does NOT stay on top — paint order falls
     back to DOM order, and everything is later than the topbar, so
     scrolling content painted straight over the pinned header (Raina's
     demo-morning screenshots; 19 of 23 probe points covered at
     1440x420). 20 sits above ordinary content and below the layers
     that must beat the topbar — dropdowns/menus at 40/60, modals at
     1000 (enumerated, Sonar 2026-08-20). Same class as the dock
     sticky fix near the grid rules; general stacking-order pass is a
     post-demo item. */
  z-index: 20;
}

#wordmark {
  font-weight: 700;
  letter-spacing: 0.5px;
  margin-right: 6px;
}

/* The open project — the workspace's navigationTitle. Neutral on
   purpose: accent marks the NEXT ACTION, never current state, and
   this chip is state — it was the largest accented area on the
   first screen (Sonar §5c). */
#project-name {
  /* IT IS A BUTTON NOW AND MUST NOT LOOK LOUDER FOR IT. Accent marks the
     NEXT ACTION, never current state, and this chip is still state
     (Sonar §5c) — so the resting appearance is unchanged to the pixel
     and only hover/focus disclose that it presses. */
  font: inherit;
  color: inherit;
  cursor: pointer;
  font-weight: 600;
  padding: 2px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel-2, transparent);
  max-width: 28ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* REVEALED, NEVER ASSERTED (Sonar §9). A control that is also your
   ORIENTATION must not look like it will move you: its resting
   appearance is a promise about WHERE YOU ARE, and an affordance is a
   promise about what happens if you act — which belongs at the moment of
   intent, not at rest. So the disclosure mark has ZERO width until
   hover or focus, and the pill's resting geometry is unchanged. */
#project-name::after {
  content: "\25BE";
  display: inline-block;
  overflow: hidden;
  max-width: 0;
  opacity: 0;
  vertical-align: baseline;
  transition: max-width 120ms ease, opacity 120ms ease, margin-left 120ms ease;
}
#project-name:hover::after,
#project-name:focus-visible::after {
  max-width: 1.2em;
  margin-left: 0.35em;
  opacity: 0.7;
}
#project-name:hover { border-color: var(--dim); }
#project-name:focus-visible {
  outline: 2px solid var(--accent, currentColor);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  #project-name::after { transition: none; }
}

#project-name:empty {
  display: none;
}

.dim {
  color: var(--dim);
}

/* The everyone-door into RAI Code — a touch of accent so a first-time
   visitor spots it, without shouting over the project pill. */
#vibe-code {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

/* The Tools nav link. It is an <a> so it middle-clicks, opens in a new
   tab and copies like an address — but in the bar it must read as a
   sibling of the buttons beside it, so it borrows the button's exact
   chrome. A nav item sitting a pixel off its neighbours reads as
   broken before anyone can say why. */
/* One font for everything in the bar. `button` never set a font, so
   the controls were rendering in the UA default (Arial 13.33px) while
   the wordmark and project pill beside them used the site face — the
   bar was already mismatched, it just took adding a sibling to make it
   measurable. Scoped to #topbar deliberately: this normalises one row,
   not every button on the site. */
#topbar button,
#tools-link {
  font: inherit;
  /* Pinned, not `normal`: these labels carry symbol glyphs (✦, ⌘, ⚯)
     that fall back to a font with taller metrics, and with an auto
     line-height each control ends up as tall as whichever glyph it
     happens to contain. That is why the bar drifted by a few pixels
     per control rather than uniformly. */
  line-height: 1.2;
  display: inline-flex;
  align-items: center;
}

#tools-link {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  line-height: normal;
}

#tools-link:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}

#status.error {
  color: var(--danger);
}

#topbar input[type="text"] {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 5px 9px;
}

#server-url {
  width: 210px;
}

#apps {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 10px 0 0;
  flex-wrap: wrap;
}
/* #apps sets display:flex, which would defeat the hidden attribute —
   the dock lesson, restated here on purpose. */
#apps[hidden] { display: none; }

main {
  padding: 18px;
  max-width: 1100px;
  margin: 0 auto;
}

/* ...BUT 1100px IS A READING MEASURE, and an app that asked for a wide
   stage did not ask for prose. `#root` is a <main>, so this generic rule
   was holding the TIMELINE at 1100 inside a stage grown to 1740 — the
   lanes stopped at 1068 and never moved again while the stage kept
   going. Measured 2026-08-25 by walking the ancestor chain at 2560:

     .rai-tp-lanes  1068   max-width none
     .rai-tp-canvas 1068   max-width none
     #root (main)   1100   max-width 1100px   <- the cap
     #stage         1740   max-width none
     #workspace     2100   max-width 2100px   (--measure, deliberate)

   Worth naming what this is NOT: `timeline-studio.rai` passes
   `:width 960`, and that number pins `.rai-tp-stage` — the VIDEO monitor,
   which is aspect-locked and correct to cap. It never governed the lanes.
   Reading the 960 as the cause sends the fix into her script, and the
   script is not where the bug is.

   Scoped to the wide classes so prose apps keep their measure — the same
   unbundling as the docks, one level down: asking for a wide stage should
   not also spend your Bin, and it should not be silently overruled by the
   reading width either.

   ⚠ THE PREDICATE IS A PROXY, and that is recorded here on purpose
   (Sonar, design DRI, 2026-08-25). The true rule is A READING MEASURE
   BELONGS ON PROSE, NOT ON EVERY <main>. A chart, a canvas, a thumbnail
   grid all want width and none of them necessarily declares wide.
   `declaredWide` is simply the only signal available today and it is a
   serviceable stand-in. When content can declare its own KIND, this
   predicate should change — and whoever changes it should not have to
   rediscover that it was a proxy rather than the rule. */
#workspace.wide-stage #root,
#workspace.stage-wide #root { max-width: none; }

/* WHAT THIS DID NOT FIX, recorded with its BAND so nobody closes it by
   testing at the wrong width. The transport row's natural width is ~1064px
   (twelve controls plus gaps) and it has no width of its own, so it clips
   wherever the stage is narrower than that. Measured on this build:

     viewport   stage   transport / natural   clipped
       1440      1312      1272 / 1272          0     (docks rail here)
       1445      1317      1277 / 1277          0
       1446      1050      1010 / 1064         54     <- the whole residue
       1500      1104      1064 / 1064          0
       1920      1524      1484 / 1484          0

   It is a NARROW BAND from the first docks-shown width up to about 1500 —
   NOT "below 1440", which is what I first reported and had to retract. The
   band sits at the threshold BY CONSTRUCTION: that is the width where the
   docks appear and take 346px out of the stage. So it MOVES when the
   threshold moves — populate the trailing dock and the threshold becomes
   1760 and the band goes with it. Anyone testing at a literal 1440 sees
   zero clipping in both arms and concludes the row is fine.

   The row still needs grouping (Sonar's, in progress); it is one populated
   dock away from clipping again wherever it sits. */

button {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  /* 6px vertical = the field's chrome — adjacent controls in a row
     share one height (the publish row's ragged boxes). */
  padding: 6px 12px;
  cursor: pointer;
  /* A control's label is its identity — "Publish to EyeTell" on
     three lines reads as three controls. Tight rows wrap WHOLE
     controls (see .rai-row), never the words inside one. */
  white-space: nowrap;
}

button:hover:not(:disabled) {
  border-color: var(--accent);
}

button:disabled {
  opacity: 0.45;
  cursor: default;
}

/* --- rendered tree --- */

/* ── THE SPACING SCALE ──────────────────────────────────────────────
   The positions the language names. `RAITokens.swift` defines them and
   this host has always COMPILED that file — it simply never read it, and
   wrote `gap: 8px` for all six instead. Measured 2026-08-22: 364 sites
   authored 'xs and got double the spacing they asked for; 119 authored
   'md and got two thirds.

   The language names positions; the design chooses numbers. These are
   the DESKTOP numbers, on the 4pt grid the Mac and the radius scale
   already sit on.

   MOBILE is the same positions with three deltas — lg, xl, 2xl — because
   a fingertip needs room BETWEEN GROUPS, not between a label and its
   value. The small end is deliberately untouched. Selected below by
   `pointer: coarse`, which this stylesheet already tested for.        */
/* ── THE RADIUS SCALE ───────────────────────────────────────────────
   The positions RAIRadiusToken names, with Raina's own Neuroglass
   metrics (confirmed by Sonar, 2026-08-23, who owns the numbers). Named
   --radius-* to match --space-*: one naming convention for the token
   scales, not two.

   `circle` is deliberately NOT a value here. It is 50% PLUS
   aspect-ratio:1, because the Mac documents circle as clipping to a true
   Circle() independent of the node's aspect — 50% alone on a non-square
   node gives an ELLIPSE, which would be a silent divergence of exactly
   the kind this package removes. It is applied as a class, not a var. */
:root {
  --radius-none: 0px;
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   12px;
  --radius-pill: 999px;
}

.rai-radius-circle {
  border-radius: 50%;
  aspect-ratio: 1;
}

:root {
  --space-none: 0px;
  --space-xs:   4px;
  --space-sm:   8px;
  --space-md:   12px;
  --space-lg:   16px;
  --space-xl:   24px;
  --space-2xl:  32px;
  /* A populated side dock's width. A TOKEN because JS has to answer
     "would the stage still clear its minimum if I showed the docks?",
     and that question cannot be measured off a COLLAPSED dock — it
     reads 32px, the rail, and the answer comes back yes forever. One
     source of truth beats a constant in main.js drifting from this. */
  --dock-w:     300px;

  /* THE MEASURE — the one number every page-level container centres on.
     It was written 1400px in THREE places (#studio, #workspace, and
     nowhere for #topbar, which is how they came to disagree). A constant
     repeated per-container is a constant that drifts per-container: at
     1920 the topbar's left edge was 0 and the content's was 260, and the
     eye reads two left edges on one page before it can name what is
     wrong. Chad's inspection, relayed 2026-08-24: "these views don't feel
     aligned."

     RESPONSIVE, NOT FIXED. Held at 1400 it left 45% of a 2560 screen
     unused while the timeline showed the same number of seconds it showed
     at 1440 -- and horizontal space on a workbench IS how much film a
     person sees at once. 2100 is the ceiling because unbounded full-bleed
     is worse than the frozen width was; 36px keeps 18px of ground each
     side at every width, which is the rule #workspace.stage-wide already
     stated for its own case.

     SAFE FOR READING SURFACES, and this is the check that decided it:
     the reading measure is NOT this cap. It is enforced INSIDE the
     container at 864px (:2334) and 900px (:2553), so widening the
     container does not lengthen a single line of text. */
  --measure: min(calc(100vw - 36px), 2100px);
}

/* ── A CLIP HANDS THE GESTURE BACK UNTIL THE HOLD COMPLETES ──────────
   The JS half (beginDrag) stops capturing the pointer on touch; this is
   what lets the browser actually scroll in the meantime. `pan-x` says the
   one thing the lanes can do with a horizontal swipe, so a finger moving
   across the timeline moves the VIEW — which is what Raina was trying to
   do when she instead moved her clips.

   `-webkit-touch-callout` and `user-select` are not tidying: a 350ms hold
   on iOS raises the system selection callout, which would fire at exactly
   the moment the drag arms and steal it back. */
@media (pointer: coarse) {
  .rai-tp-clip {
    /* `manipulation`, NOT `pan-x` (Tiki, reviewing PR #142). pan-x permits
       horizontal panning and NOTHING ELSE, so it would have taken two
       gestures while handing back one: `.rai-tp-scroll` is
       overflow-y: hidden and this page is ordinary flow taller than a
       phone, so VERTICAL scrolling is the document's — a finger landing on
       a clip and swiping down would have done nothing. And the page has no
       user-scalable=no, so PINCH-ZOOM works here today and pan-x would have
       removed it over the clips, on the surface she called "so tight".
       That is the same "no gesture is left that can do X" defect this
       branch exists to fix, rotated 90 degrees.
       The JS is what prevents the drag; the CSS only needs to stop
       CLAIMING the gesture, not narrow it to one axis. */
    touch-action: manipulation;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }

  /* The hold needs to be VISIBLE or it is a secret. `arming` is the
     press being noticed; `armed-flash` is the moment the drag takes, so a
     person learns the gesture by feeling it work rather than by being
     told. Both are suppressed under prefers-reduced-motion, where the
     border alone still reports the state. */
  .rai-tp-clip.arming {
    outline: 2px solid color-mix(in srgb, var(--accent, #5cf09d) 55%, transparent);
    outline-offset: -2px;
  }

  .rai-tp-clip.armed-flash {
    outline: 3px solid var(--accent, #5cf09d);
    outline-offset: -3px;
    animation: rai-tp-armed 220ms ease-out;
  }

  @keyframes rai-tp-armed {
    from { transform: scale(1); }
    50%  { transform: scale(1.04); }
    to   { transform: scale(1); }
  }

  @media (prefers-reduced-motion: reduce) {
    .rai-tp-clip.armed-flash { animation: none; }
  }
}

@media (pointer: coarse) {
  :root {
    --space-lg:  20px;
    --space-xl:  32px;
    --space-2xl: 48px;
  }
}

.rai-column {
  display: flex;
  flex-direction: column;
  gap: var(--rai-gap, var(--space-sm));
}

.rai-column[data-align="center"] { align-items: center; }
.rai-column[data-align="trailing"] { align-items: flex-end; }

/* (toast …) — the page-corner stack. Levels ride the same semantic
   colors the rest of the chrome uses. */
#rai-toasts {
  position: fixed;
  bottom: 16px;
  right: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 1000;
  pointer-events: none;
}
.rai-toast {
  padding: 9px 14px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent, #4a9eff);
  border-radius: 8px;
  background: var(--panel, var(--bg));
  color: var(--text);
  font-size: 13px;
  max-width: 40ch;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  transition: opacity 0.4s, transform 0.4s;
}
.rai-toast[data-level="success"] { border-left-color: #3fb96f; }
.rai-toast[data-level="warning"] { border-left-color: #d9a13b; }
.rai-toast[data-level="error"] { border-left-color: var(--danger, #d9534f); }
.rai-toast-out { opacity: 0; transform: translateY(6px); }

/* A shared link whose ?app= doesn't resolve says so on the Stage —
   durably, where the status line's few seconds can't (Sonar #9). */
.rai-dead-link-note {
  margin: 24px auto;
  max-width: 52ch;
  padding: 14px 18px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--warn, #d9a13b);
  border-radius: 10px;
  background: var(--panel, var(--bg));
  font-size: 14px;
  line-height: 1.5;
}
.rai-dead-link-note strong { display: block; margin-bottom: 4px; }
/* Only toasts carrying a control take pointer events — a passive
   toast must never eat a click meant for the page beneath it. */
.rai-toast.has-action {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.rai-toast-undo {
  border: none;
  background: transparent;
  color: var(--accent, #4a9eff);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
}
.rai-toast-undo:hover { background: var(--border); }
.rai-toast-undo:focus-visible {
  outline: 2px solid var(--accent, #4a9eff);
  outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) {
  .rai-toast { transition: none; }
}

/* Depth stack: one grid cell, every child layered into it — DOM
   order is the paint order (later children on top). The alignment
   pair maps the Mac's RAIZStackAlignment tokens. */
.rai-zstack { display: grid; }
.rai-zstack > * { grid-area: 1 / 1; }
.rai-zstack[data-align="center"] { justify-items: center; align-items: center; }
.rai-zstack[data-align="top"] { justify-items: center; align-items: start; }
.rai-zstack[data-align="bottom"] { justify-items: center; align-items: end; }
.rai-zstack[data-align="leading"] { justify-items: start; align-items: center; }
.rai-zstack[data-align="trailing"] { justify-items: end; align-items: center; }
.rai-zstack[data-align="top-leading"] { justify-items: start; align-items: start; }
.rai-zstack[data-align="top-trailing"] { justify-items: end; align-items: start; }
.rai-zstack[data-align="bottom-leading"] { justify-items: start; align-items: end; }
.rai-zstack[data-align="bottom-trailing"] { justify-items: end; align-items: end; }

.rai-row {
  display: flex;
  flex-direction: row;
  gap: var(--rai-gap, var(--space-sm));
  align-items: center;
  /* Panel-width rows: when the row can't hold everything, whole
     items drop to the next line instead of crushing each other into
     slivers (the Screening Room publish row at dock widths). */
  flex-wrap: wrap;
}

.rai-row[data-align="top"] { align-items: flex-start; }
.rai-row[data-align="bottom"] { align-items: flex-end; }

/* A row of structural columns (a common app shape) wrapped at intrinsic
   width, so cards kept whatever width their content implied — 607 next
   to 468 next to 732 — and the leftover column encoded which source
   column a card was declared in, meaning nothing to the user. A 440px
   basis with flex-grow gives both behaviours from one rule: at a narrow
   Stage two columns can't fit (2×440+gap > ~732) so each takes a full
   line and grow expands it to the full width; at a wide Stage (~1060+)
   they sit side by side, which the enclosing-view design depends on.
   This does NOT reorder the steps — two source columns render as all of
   A then all of B (1,2,3,6 then 4,5,7); fixing the numbering is a
   separate change. Controls (buttons, fields) keep wrapping as whole
   items — this targets column children only. (Sonar, punch-list 4a; 440
   is a judgement to eyeball at wide Stage, not a measurement.) */
.rai-row > .rai-column { flex: 1 1 440px; }

.rai-scroll {
  overflow: auto;
  max-height: 70vh;
}

/* A SCROLLER INSIDE A SCROLLER IS WORSE THAN EITHER, so the modal wins.
   `.rai-modal-body` is already capped and already scrolls. A script that
   also declares `(scroll :axis 'vertical …)` then produces two nested
   regions of nearly the same height — measured 2026-08-26 with 40
   projects: modal body 731px, inner scroller 630px, BOTH scrolling. The
   wheel does the wrong thing at the boundary and the outer scroll exists
   only to reveal a strip of chrome.

   WHY THE SCRIPT IS RIGHT TO DECLARE IT ANYWAY, and why this is a CSS
   answer rather than an argument with the author: on the MAC the same
   wrapper is load-bearing. `lazy-grid` is pure layout with no scroller,
   the Mac's card has a fixed height, and thirty projects are squashed
   inside it — Raina: "when i do see them, i have no scroll." (Waikiki's
   find; the durable fix is the macOS renderer's missing ScrollView and
   is theirs.) The web has never needed it: a card here grows and the
   page scrolls.

   ONE SCRIPT, TWO RENDERERS, AND THE CHROME OWNS SCROLLING ON THIS ONE.
   So the web honours the intent — "this content is scrollable" — and
   declines the mechanism where a container above already provides it. */
.rai-modal-body .rai-scroll {
  max-height: none;
  overflow: visible;
}

.rai-scroll[data-axis="horizontal"] {
  max-height: none;
  overflow-x: auto;
  overflow-y: hidden;
}

/* Inside a DECLARED horizontal scroll, the row is a console: sections
   run left-to-right and the scrollbar is the overflow story — that is
   what the author asked for by writing (scroll :axis 'horizontal …).
   The panel-width wrap default above is for rows that have no other
   escape; here wrapping stacked Raina's mixing-board sections into a
   vertical sliver column and the scroll had nothing to do. Scoped to
   horizontal scrolls only. */
.rai-scroll[data-axis="horizontal"] .rai-row {
  flex-wrap: nowrap;
}
.rai-scroll[data-axis="horizontal"] .rai-row > * {
  flex-shrink: 0;
  /* INTRINSIC width, not the panel default. Cards fill their parent by
     default — correct in a vertical stack, catastrophic in a nowrap
     row: four sections each resolved to the STAGE's width, the console
     measured 5228px with 2 of 12 knobs visible at every viewport, and
     the "designed too wide" read was really this rule missing (Sonar
     measured the monster; the design was ~1100px all along). */
  width: max-content;
  max-width: none;
  flex-basis: auto;
}

/* ⚠ AND `width: max-content` ABOVE CANNOT REACH A DECLARED WIDTH, which
   is why the console was still clipping itself at 1440 (Sonar, measured
   in the screenshot: AIR dB overlapping THRSH, MAKEUP overlapping MIX %).
   `knob-col` declares `:width 60`, the renderer writes that as an INLINE
   style, and an inline `width` beats a stylesheet `width` every time. So
   the column stayed 60px, the labels that are wider than 60px overflowed
   it, and they landed on top of the next section — inside a scroller
   whose entire declared purpose is that nothing gets clipped.

   `min-width` is the property that outranks an inline `width`, which is
   what makes this a fix rather than a second opinion. The author's number
   still governs the knob's column; it just stops being a cap the label
   cannot be held by.

   THE GENERAL RULE, because this will recur: INSIDE A DECLARED
   HORIZONTAL SCROLL, A DECLARED WIDTH IS A PREFERENCE, NOT A CLAMP.
   Overflow is the one thing the author has already said they will
   tolerate — that is what writing (scroll :axis 'horizontal …) means —
   so content that does not fit must push the scroller wider rather than
   land on its neighbour. */
.rai-scroll[data-axis="horizontal"] .rai-column,
.rai-scroll[data-axis="horizontal"] .rai-card,
.rai-scroll[data-axis="horizontal"] .rai-row {
  min-width: max-content;
}

/* Glass surface per the spec: surface @ ~55% over the atmosphere,
   real backdrop blur, hairline highlight border, top inner highlight,
   soft large-radius shadow. The blur only reads as glass because the
   atmosphere puts color and edge behind it. */
.rai-card {
  background: color-mix(in srgb, var(--panel) 55%, transparent);
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 12px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  /* :gap is honoured here the same way .rai-column honours it. The
     fallback IS the previous constant, so a card that says nothing about
     spacing does not move — the "no-loss" sites stay where they are, which
     is the whole reason the default is written out rather than dropped. */
  gap: var(--rai-gap, 8px);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, 0.25),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* (card :emphasis 'filled|'outlined|'ghost) — Apple's three, and the
   treatments are TRANSCRIBED FROM WHAT APPLE DRAWS rather than invented,
   so the word means the same thing on both platforms. Measured from
   RAINodeView.swift (fill/border) and RAIChromeStyle.raiCardShadow:

     filled    fill surface -> surfaceAlt gradient · border muted 0.28
               · shadow black 0.25, radius 8, y 3
     outlined  fill accent 0.10 -> accent 0.04 · border accent 0.65
               · NO shadow  (Apple's own comment: "the selected look —
                 accent border + soft accent-tinted fill so the active
                 row reads as active")
     ghost     fill clear · border muted 0.28 · no shadow

   'outlined being the SELECTION treatment is the part worth knowing:
   122 authored sites use it, so 122 places were trying to show a
   selected/active card and the web drew every one of them as an
   ordinary card.

   The web's radii, spacing and blur are its own and are not touched —
   parity is at the PRIMITIVE, and a design difference inside a theme is
   allowed. What must match is what the WORD means.

   TOKEN NAMES ARE NOT SHARED, and my first version of these rules was
   silently dead because of it. I wrote var(--surface), var(--surface-alt)
   and var(--muted) — the RAI token names — and none of the three is a CSS
   variable here. An undefined var makes the whole declaration invalid, so
   the gradients never painted and border-color fell back to currentColor:
   the cards came out with near-white borders and no fill, and NOTHING
   reported an error. The translation already exists and is the one place
   that owns it, RAI_COLOR_VAR in render.js:541 —
     surface -> --panel · surface-alt -> --panel-2 · muted -> --dim
   Use those. Caught by measuring computed style rather than reading the
   rules back. */

.rai-card[data-emphasis="filled"] {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--panel) 70%, transparent),
    color-mix(in srgb, var(--panel-2) 70%, transparent)
  );
  border-color: color-mix(in srgb, var(--dim) 28%, transparent);
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.25),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* SONAR'S WATCH-LINES, captured on a controlled pair 2026-08-23 and put
   here rather than in a doc because this is where someone debugging it
   will be standing:

   1. FLAT + TINTED CAN READ AS DISABLED RATHER THAN SELECTED where
      nothing else on the screen is raised. It does not on the screens
      captured (halo: the 0.65 accent border carries the whole signal
      because the fill is nearly invisible on white; neuroglass: border
      plus a faint mint fill). If anyone reports "the active card looks
      greyed out", THIS RULE IS WHERE TO LOOK FIRST — the shadow removal
      is deliberate and matches what Apple draws, so the fix is not
      simply putting the shadow back.

   2. ANY SCREEN SHOWING TWO OR MORE OUTLINED CARDS AT ONCE is worth a
      look during testing. 'outlined is the SELECTED treatment; several
      at once makes "selected" decoration, and everything-selected reads
      as nothing-selected. That is a script being wrong about the word
      rather than a renderer bug — this rule is just what makes it
      visible for the first time. */

.rai-card[data-emphasis="outlined"] {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--accent) 10%, transparent),
    color-mix(in srgb, var(--accent) 4%, transparent)
  );
  border-color: color-mix(in srgb, var(--accent) 65%, transparent);
  box-shadow: none;
}

.rai-card[data-emphasis="ghost"] {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-color: color-mix(in srgb, var(--dim) 28%, transparent);
  box-shadow: none;
}

/* :tint — AFTER the emphasis rules on purpose. On the Mac a tint WINS over the
   emphasis-derived fill (RAINodeView.swift:3524), and these selectors carry the
   same specificity, so source order is what makes tint win here too. Moving
   this block above them silently restores the old behaviour for every
   non-regular card.

   THE PROPERTY EXISTED AND NOTHING READ IT. render.js has written --tint since
   it was added; `grep -rn -- "--tint" Web/` returned ONE line, the write, and
   no CSS rule and no JS ever consumed it. So :tint did nothing on web for ANY
   value — not a token-resolution gap, no rendering at all. Sonar found that
   after first reporting the narrower version, and after I had already shipped a
   token resolver that resolved correctly into a property nobody read: green,
   tested, and visually identical to before.

   Two stops at 92% and 72%, the Mac's own numbers, so a tinted card still reads
   as a card rather than a flat block and adjacent tinted cards shade into a
   vertical strip. */
.rai-card[data-tinted="1"] {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--tint) 92%, transparent),
    color-mix(in srgb, var(--tint) 72%, transparent)
  );
}


.rai-card-title,
.rai-section-title {
  font-weight: 600;
  color: var(--dim);
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: 0.6px;
}

.rai-section {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Spec toolbar chrome: surfaceAlt-to-surface wash with a bottom
   hairline that fades toward the edges. */
.rai-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px 8px;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--panel-2) 85%, transparent),
    color-mix(in srgb, var(--panel) 65%, transparent));
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(to right,
    color-mix(in srgb, var(--dim) 5%, transparent),
    color-mix(in srgb, var(--dim) 35%, transparent),
    color-mix(in srgb, var(--dim) 5%, transparent)) 1;
}

.rai-toolbar-title {
  font-weight: 600;
  margin-right: auto;
}

.rai-split {
  display: flex;
  gap: 12px;
}

.rai-split[data-direction="vertical"] {
  flex-direction: column;
}

.rai-split > * {
  flex: 1;
  min-width: 0;
}

.rai-mono,
.rai-markdown {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font: 12px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
  white-space: pre-wrap;
  margin: 0;
}

.rai-button[data-variant="primary"] {
  background: var(--accent);
  border-color: var(--accent);
  /* accent-fg, because "white on accent" fails on light accents —
     the default theme's mint needs its dark ink. */
  color: var(--accent-fg, #fff);
  font-weight: 600;
}

/* Interactive-state colors ride the wire too (theme.states →
   --rai-<token>--<state>); fallbacks are the spec's hover/pressed. */
.rai-button[data-variant="primary"]:hover:not(:disabled) {
  background: var(--rai-accent--hover, #7af5b3);
  border-color: var(--rai-accent--hover, #7af5b3);
}

.rai-button[data-variant="primary"]:active:not(:disabled) {
  background: var(--rai-accent--pressed, #3dd684);
  border-color: var(--rai-accent--pressed, #3dd684);
}

.rai-button[data-variant="destructive"] {
  background: var(--danger);
  border-color: var(--danger);
  /* Same rule as primary's, same reason: the default theme's danger
     token is a LIGHT red (#fb7185) — white on it measured 2.69:1,
     the only AA failure in the set. Dark ink takes it to 6.77:1.
     A colour chosen for one role used in another (Sonar). */
  color: var(--accent-fg, #16171b);
  font-weight: 600;
}

/* :preview — hover-to-ENLARGE. Apple's `previewFlag`, default false.
   A FIRST PASS BY ORCA; THE NUMBERS ARE SONAR'S TO TUNE — what is
   load-bearing here is that the keyword does something at all, not that
   1.6 is the right multiplier.

   It scales in place rather than reflowing: a grid of thumbnails must not
   jump when the pointer crosses one, which is the failure mode a naive
   width change produces. z-index lifts it above its neighbours so the
   enlarged tile is not clipped by the next cell.

   Composes with :video-mode 'on-hover — one hover can both enlarge and
   play, because they are separate affordances on the same element and
   Apple treats them as separate keywords. */
.rai-media-preview {
  transition: transform 120ms ease-out;
  transform-origin: center;
}

.rai-media-preview:hover {
  transform: scale(1.6);
  position: relative;
  z-index: 5;
}

@media (prefers-reduced-motion: reduce) {
  /* The enlargement still happens — it is the affordance, not decoration.
     Only the animation is dropped. */
  .rai-media-preview { transition: none; }
}

/* 'success — an affirmative ACTION (confirm, approve, mark done). One of
   Apple's five, and until 2026-08-23 the web accepted it and styled
   nothing, so a script asking for success rendered a plain secondary
   button: the word was in the language and invisible on this platform.

   It uses --ok, the same token the success BADGE and BANNER already use,
   so the three agree about what success looks like rather than each
   inventing a green. Dark ink for the same reason primary and destructive
   carry it — --ok is a mid-light green and white on it does not clear AA.

   FIRST PASS BY ORCA; THE PIXELS ARE SONAR'S TO TUNE — the same footing
   'selected landed on. What is load-bearing here is that the word renders
   DISTINCTLY at all; the exact green is design's call.

   Note for whoever tunes it: the only 'success in the corpus
   (spark-count.rai:129) is a toggled-ON state reaching for affirmative
   colour, which is what 'selected was added to serve. Styling success as
   an action may make that one site the wrong word rather than a wrong
   colour — a script question, not a CSS one. */
.rai-button[data-variant="success"] {
  background: var(--ok);
  border-color: var(--ok);
  color: var(--accent-fg, #16171b);
  font-weight: 600;
}

/* 'selected — a toggled-ON state (segmented controls, filters,
   toggles), NOT an action. Affirmative without the action fill:
   accent ink and border on a light accent-tinted ground, so a row of
   them can sit on one screen without impersonating "do this next"
   (Sonar's Deep Dive measurement: nine simultaneous 'primary, three
   of them a segmented control's on state wearing the action color —
   and the only 'success in 405 buttons was the same reach). First
   pass by Orca; the pixels are Sonar's to tune. */
.rai-button[data-variant="selected"] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: var(--accent);
  /* Plain --text, NOT accent-mixed ink (Sonar, measured): every mix
     toward accent dims the label against its own tinted ground
     (72% mix = 8.42:1 vs neighbours' 13.82:1 — the chosen item was
     the hardest to read; plain text = 10.42:1). The accent lives in
     the border, which at 7.95:1 in a row of invisible borders
     (secondary's is 1.21:1) is an unambiguous signal on its own.
     Ground tint + accent border + ink — and weight 400, NOT 600
     (Sonar, measured): bold reflows the row — "curious" grows
     5.17px when selected, so tapping between adjacent options
     slides the others under your cursor, jitter guaranteed in the
     one control whose whole interaction is tapping between
     neighbours. The border's 7.95:1-vs-1.21:1 edge is a LUMINANCE
     ratio, so it survives any colour-vision deficiency without
     weight as backup. Two lessons, one shape: every channel past
     the third stops helping and starts taking something. */
  color: var(--text);
  font-weight: 400;
}
.rai-button[data-variant="selected"]:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 24%, transparent);
}

.rai-badge {
  border-radius: 999px;
  padding: 1px 9px;
  font-size: 11px;
  border: 1px solid var(--border);
}

.rai-badge[data-level="success"] { color: var(--ok); border-color: var(--ok); }
.rai-badge[data-level="warning"] { color: var(--warn); border-color: var(--warn); }
.rai-badge[data-level="error"] { color: var(--danger); border-color: var(--danger); }

.rai-banner {
  border-radius: 8px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  background: var(--panel-2);
}

.rai-banner[data-level="warning"] { border-color: var(--warn); }
.rai-banner[data-level="error"] { border-color: var(--danger); }
.rai-banner[data-level="success"] { border-color: var(--ok); }

.rai-field {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 6px 9px;
  width: 100%;
}

.rai-field-labeled {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1;
  min-width: 0;
  font-size: 11px;
  color: var(--dim);
}

/* A context-menu host inside a tree row must own the whole row —
   otherwise right-clicks in the row's empty stretch hit dead space. */
.rai-tree-row > .rai-menu-host {
  flex: 1;
  min-width: 0;
}
.rai-menu-host > .rai-row {
  width: 100%;
}

/* Inside a dense tree row (the bin's inline rename) a 100%-width
   field blows the row out of its panel — share the row instead. */
.rai-tree-row .rai-field {
  width: auto;
  flex: 1 1 60px;
  min-width: 40px;
  padding: 2px 6px;
}

.rai-toggle,
.rai-slider {
  display: flex;
  align-items: center;
  gap: 8px;
}

.rai-list {
  display: flex;
  flex-direction: column;
}

.rai-list > * {
  padding: 6px 4px;
  border-bottom: 1px solid var(--border);
}

.rai-grid {
  display: grid;
  /* minmax(0,1fr), not 1fr: grid items' implicit min-width is their
     CONTENT width, so one long unbreakable label (an asset id) blows
     every column past the container. Zero the floor and let cells
     truncate instead of the grid escaping the panel. */
  grid-template-columns: repeat(var(--columns, 2), minmax(0, 1fr));
  gap: 8px;
}
.rai-grid > * { min-width: 0; }
/* Media and labels inside grid cells size to the CELL, never the
   other way around. */
.rai-grid .rai-media { width: 100%; object-fit: cover; }
.rai-grid .rai-mono {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* In a grid cell the mono is a LABEL, not content — the chip
     chrome (fill, border, padding) outweighed the thumbnail it
     captions (Sonar, assets-panel-cards.md). */
  background: none;
  border: none;
  padding: 2px 0;
}
/* A cell-width row never wraps: the label shrinks and truncates so
   its row-mates (the open-view control) keep the same line, flush to
   the cell's right edge — wrapping is what turned two-row asset
   cards into three. */
.rai-grid .rai-row {
  flex-wrap: nowrap;
}
.rai-grid .rai-row .rai-mono {
  flex: 1 1 auto;
  min-width: 0;
  /* NO rtl leading-ellipsis trick here, deliberately: probed, and
     bidi reorders digit-led names even when they fit (002.png
     rendered as png.002). The backstop direction stays end-clip;
     instead the elision budget (20ch vs a ~39ch column) keeps the
     backstop from ever firing, and the card's hover title carries
     the full name when everything else has failed (Sonar's option
     1, chosen on the probe's evidence). */
}

.rai-image {
  max-width: 100%;
  border-radius: 8px;
}

.rai-tabs {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* SONAR'S CONDITION, measured 2026-08-24 on a controlled pair, recorded
   here rather than in a doc because this is where the fix goes when it is
   needed: THE STRIP HAS NO OVERFLOW STRATEGY. At 390 its content is 323px
   inside a 294px box — nowrap, overflow visible, already 29px over.
   Nothing is clipped WITH TODAY'S LABEL SET (all four fit, every tab
   clears 44px), so this is a condition and not a defect yet.

   IT BITES on the first strip with five labels, or one long one — a
   localisation, a longer project name. THE FIX IS THEN `overflow-x: auto`
   ON THIS RULE: scroll, not wrap, because a wrapped tab bar reads as two
   rows of controls rather than one.

   NOT caused by the :title fix. The strip was always nowrap; it was
   INVISIBLE because every label was the same 104px "<view-node>" and
   there was no content to look at. Fixing the content is what made the
   layout question real — the second-order surprise landed in the
   container rather than in the content. */

.rai-tab-bar {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--border);
}

.rai-tab-button {
  border: none;
  background: none;
  border-radius: 6px 6px 0 0;
  padding: 6px 14px;
  color: var(--dim);
}

.rai-tab-button[data-selected="true"] {
  color: var(--text);
  background: var(--panel-2);
}

/* Disclosures are the section primitive — box them so a stack of
   sections (the Assets panel) reads as distinct groups, not one
   run-on list. */
.rai-disclosure {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: color-mix(in srgb, var(--panel-2, #1a1e26) 55%, transparent);
  padding: 8px 10px;
}

.rai-disclosure + .rai-disclosure {
  margin-top: 8px;
}

.rai-disclosure summary {
  cursor: pointer;
  color: var(--text);
  font-size: 13px;
  font-weight: 650;
  letter-spacing: 0.02em;
  user-select: none;
}

.rai-disclosure[open] > summary {
  margin-bottom: 8px;
}

.rai-disclosure-count {
  margin-left: 6px;
  font-size: 11px;
  font-weight: 400;
  color: var(--dim);
}

.rai-spacer-flex {
  flex: 1;
}

.rai-tappable {
  cursor: pointer;
}

.rai-live-placeholder {
  color: var(--accent);
  opacity: 0.7;
  font-size: 12px;
}

.rai-placeholder {
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.rai-placeholder-chip {
  align-self: flex-start;
  font: 11px ui-monospace, monospace;
  color: var(--dim);
  background: var(--bg);
  border-radius: 4px;
  padding: 1px 6px;
}

/* A node the server could not fill says why, in place. Readable rather
   than shouty: this is a sentence a person acts on, not an alarm. */
.rai-placeholder-error {
  margin: 0;
  max-width: 60ch;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

/* An inserted cell: a disclosure row over its body. No close — the page
   that inserted it owns its runtime; the viewer only folds it. */
.rai-subnode {
  width: 100%;
  max-width: 100%;
  min-width: 0;
}
.rai-subnode-head {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  font: 11px ui-monospace, Menlo, monospace;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  margin: 6px 0 8px;
  user-select: none;
}
.rai-subnode-head::-webkit-details-marker { display: none; }
.rai-subnode-tri { display: inline-block; transition: transform 120ms ease; }
.rai-subnode[open] > .rai-subnode-head .rai-subnode-tri { transform: rotate(90deg); }
.rai-subnode-body {
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
}

/* ── THE MIXING DESK AS A SHEET, REACHED FROM A CLIP ─────────────────
   Raina, 2026-10-04: "make the mixing channel a pop-over that is accessed
   from the context menu so it's clear it's for clips."

   The element never moves — it stays exactly where the host spliced it, so
   the cell keeps its runtime and its live meters. CLOSED it draws nothing
   at all (its summary is hidden and a closed <details> has no body), so it
   costs no page height and leaves no orphan row where it used to sit.
   OPEN, it lifts off the page as a bottom sheet.

   Bottom, not centre: it is operated with a thumb while the timeline above
   it stays visible, which is the point of mixing one clip. 85dvh so the
   lanes are never fully covered — you can still see which clip you are
   mixing, and that was Sonar's whole finding about this surface. */
.rai-subnode-pop:not([open]) > .rai-subnode-head { display: none; }
.rai-subnode-pop[open] {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1100;
  max-height: 85dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 12px 16px;
  box-sizing: border-box;
  background: var(--panel, #171c24);
  border-top: 1px solid var(--line, #2c3442);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.55);
}
/* The summary becomes the sheet's title bar and its only way out. It is
   still the native <details> toggle, so dismissing needs no new handler
   and no state of its own — which is what keeps this a disclosure rather
   than a modal with a close button (Raina, 2026-08-21: "subnodes do not
   have a close button they have a disclose"). */
.rai-subnode-pop[open] > .rai-subnode-head {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 0 -12px 8px;
  padding: 12px;
  min-height: 44px;
  background: var(--panel, #171c24);
  border-bottom: 1px solid var(--line, #2c3442);
}
.rai-subnode-pop[open] > .rai-subnode-head .rai-subnode-tri { transform: rotate(90deg); }
.rai-subnode-pop[open] > .rai-subnode-head::after {
  content: "Done";
  margin-left: auto;
  color: var(--accent, #5cf09d);
  text-transform: none;
  letter-spacing: 0;
  font-size: 13px;
}

.rai-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  display: inline-block;
  animation: rai-spin 0.8s linear infinite;
}

@keyframes rai-spin {
  to { transform: rotate(360deg); }
}

hr {
  border: none;
  border-top: 1px solid var(--border);
  width: 100%;
  margin: 2px 0;
}

.rai-heading {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 1px;
}

.rai-caption {
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.5px;
}

.rai-body {
  font-size: 13px;
  line-height: 1.55;
}

.rai-title {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.5px;
}

.rai-display {
  font: 13px ui-monospace, "SF Mono", Menlo, monospace;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 8px;
  color: var(--accent);
}

.rai-sparkline {
  width: 100%;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.rai-knob:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 8px;
}

.rai-knob {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 10px;
  color: var(--dim);
  touch-action: none; /* the drag IS the interaction — no scroll steal */
  cursor: ns-resize;
  user-select: none;
}

.rai-knob-track {
  fill: none;
  stroke: color-mix(in srgb, var(--dim) 30%, transparent);
  stroke-width: 4;
  stroke-linecap: round;
}

.rai-knob-fill {
  fill: none;
  stroke: var(--accent);
  stroke-width: 4;
  stroke-linecap: round;
}

.rai-knob-body {
  fill: color-mix(in srgb, black 55%, transparent);
  stroke: color-mix(in srgb, white 18%, transparent);
  stroke-width: 1;
}

.rai-knob-tick {
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
}

/* (file-drop …) — drop target; hot while a drag hovers it */
.rai-file-drop { border-radius: 12px; }
.rai-file-drop-hot { outline: 2px dashed var(--accent); outline-offset: 4px; }

/* (drag-target …) — in-app payload landing area. NO idle chrome
   (bin.rai's rule: the zone is invisible until a drag hovers); the
   hot state mirrors file-drop's dashed accent plus a faint tint so a
   large invisible zone reads as one lit region, not just an edge. */
.rai-drag-target { border-radius: 12px; }
.rai-drag-target.rai-drop-hot {
  outline: 2px dashed var(--accent);
  outline-offset: 4px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* (media-view …) — served image asset */
.rai-media { border-radius: 10px; border: 1px solid var(--border); display: block; max-width: 100%; }

/* (chart …) — inline SVG plot */
.rai-chart { width: 100%; background: var(--surface-alt, #171c24); border: 1px solid var(--border); border-radius: 10px; padding: 6px; box-sizing: border-box; }
.rai-hidden { display: none; }

#app-picker {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 5px 9px;
  max-width: 210px;
}

.rai-sprite-canvas {
  border-radius: 10px;
  border: 1px solid var(--border);
  max-width: 100%;
}

.rai-dropdown {
  position: relative;
  display: inline-block;
}

.rai-dropdown > summary {
  list-style: none;
  cursor: pointer;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  user-select: none;
}

.rai-dropdown > summary::after {
  content: " ▾";
  color: var(--dim);
}

.rai-dropdown-menu {
  position: absolute;
  z-index: 40;
  min-width: 100%;
  margin-top: 4px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel, var(--bg));
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

.rai-dropdown-row {
  display: block;
  padding: 6px 10px;
  border-radius: 5px;
  cursor: pointer;
  white-space: nowrap;
}

.rai-dropdown-row:hover {
  background: color-mix(in srgb, var(--dim) 12%, transparent);
}

.rai-dropdown-current {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.rai-dropdown-current::after {
  content: " ✓";
  color: var(--accent);
}

.rai-button-icon {
  margin-right: 4px;
}

.rai-button:empty,
.rai-button .rai-button-icon:only-child {
  margin-right: 0;
}

/* Icon-only anchor buttons (:open / :download with no label) stay
   compact — a full button next to an asset caption is shouting. */
.rai-button.rai-icon-only {
  padding: 1px 6px;
  line-height: 1.2;
}

.rai-color-swatch.rai-tappable:hover {
  transform: scale(1.12);
}

.rai-timeline-player {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.rai-tp-stage {
  position: relative;
  width: 100%;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
  /* The monitor takes what is LEFT after the timeline is seated, not
     the other way round (Sonar's vertical half of #75): aspect-locked,
     the stage grew +148px when the docks collapsed, and on any window
     under ~890px the lanes fell entirely below the fold — a black
     monitor showing 00:00:00 held 724px while the timeline needed a
     scrollbar to exist. Clamp: never taller than the viewport minus
     the room the transport, lanes and release block need (~470px),
     never so short the preview is useless (240px), never taller than
     its own declared size (540px). The frames object-fit: contain, so
     a clamped stage letterboxes instead of cropping. */
  max-height: clamp(240px, calc(100dvh - 470px), 540px);
}

.rai-tp-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 120ms linear;
}

.rai-tp-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  font-size: 13px;
}

.rai-tp-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  /* The row must never be wider than its box. Unbounded, its 544px of
     content reached <body> through a chain of nowrap ancestors and set
     the layout width for the whole product on a phone. min-width: 0
     lets it be narrowed; overflow-x makes it CONTAIN what it cannot
     hold (#75): before it, the overflow escaped — a sideways document
     scroll at 1100, and at 1440 the last control group rendered
     outside the stage card into the gutter beside BIN, unboxed, where
     it read as BIN's. The worst case must be a strip you scroll,
     never a layout you lose. */
  min-width: 0;
  overflow-x: auto;
}

.rai-tp-view {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The overflow control exists only where the row cannot hold the view
   cluster; on a wide screen the cluster is already visible and a "⋯"
   next to it would be a button that reveals what you can see. */
.rai-tp-more {
  display: none;
  flex: 0 0 auto;
}

@media (max-width: 640px) {
  .rai-tp-more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Transport and undo stay; zoom/fit/snap are one tap away. The player
     already opens fit-to-window (pps = 0), so a phone arrives at the
     scale it would have reached for anyway. */
  .rai-tp-view {
    display: none;
  }

  .rai-tp-view.open {
    display: flex;
    flex-wrap: wrap;
  }

  .rai-tp-bar {
    flex-wrap: wrap;
    row-gap: 8px;
  }

  /* Collapsed, the row must be ONE row — the whole point of moving the
     view cluster out was to avoid a two-row transport on the surface with
     the least vertical room. A flex item's default `min-width: auto` is its
     intrinsic width, so anything here that cannot give forces the wrap.
     (The range input was the worst offender and is gone — Raina, 2026-10-29:
     "we do not need the slider since they can grab the cursor".) */
  .rai-tp-clock {
    min-width: 0;
  }

}

.rai-tp-play {
  width: 36px;
  height: 28px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg);
  cursor: pointer;
}

/* The slider used to sit here with `flex: 1`, and that one declaration was
   what held the transport row open — everything after it was pushed right by
   it, not by any rule of its own. With it gone (Raina, 2026-10-29: "we do not
   need the slider since they can grab the cursor") the clock takes that job
   with `margin-right: auto`, so the row keeps the arrangement it had instead
   of bunching against the play button. */
.rai-tp-clock {
  font: 12px ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--dim);
  min-width: 84px;
  margin-right: auto;
  text-align: left;
}

/* The timeline canvas: one horizontally-scrolling surface carrying
   ruler, lanes, and playhead at a shared pixels-per-second scale, so
   everything stays aligned by construction. Lane names are sticky-left
   (width mirrored in render.js NAME_W). */
.rai-tp-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  border-radius: 6px;
  background: color-mix(in srgb, var(--dim) 5%, transparent);
}

.rai-tp-canvas {
  position: relative;
  min-width: 100%;
}

.rai-tp-ruler {
  position: relative;
  height: 18px;
  border-bottom: 1px solid var(--border);
  /* Press-and-drag the time-strip to scrub, the way a monitor is driven off
     the ruler rather than a slider (Raina). */
  cursor: ew-resize;
  /* Claim the gesture from the browser's pan/zoom, or on a touch device
     pointermove stops arriving mid-scrub and the drag dies as a stutter
     (Fathom's dock-splitter fix, same class). */
  touch-action: none;
}

.rai-tp-tick {
  position: absolute;
  top: 2px;
  bottom: 0;
  border-left: 1px solid color-mix(in srgb, var(--dim) 45%, transparent);
  padding-left: 3px;
  font: 9px ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--dim);
  white-space: nowrap;
}

.rai-tp-lanes {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 0;
}

.rai-tp-lane {
  display: flex;
  align-items: center;
}

/* Frames fill the clip's width at their native aspect, and the label sits
   over them behind a scrim so it stays readable on bright footage. The
   scrim is on the LABEL rather than the whole block: dimming every frame
   to make a few words legible would undo the reason the frames are there. */
.rai-tp-clip.has-strip {
  background-size: auto 100%;
  background-repeat: repeat-x;
  background-position: left center;
}

.rai-tp-clip.has-strip {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
  color: #fff;
}

.rai-tp-lane-name {
  position: sticky;
  left: 0;
  z-index: 3;
  width: 104px;
  margin-right: 8px;
  flex: 0 0 104px;
  font: 11px ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--dim);
  /* LEFT, not right: the icon is the last thing standing as the column
     narrows, so it lives in the always-opaque left zone. */
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  /* One line always: a lane must not be taller because its NAME is
     longer — height differences should mean something about content,
     never about labels. */
  white-space: nowrap;
  /* Transparent: the OPAQUE part is the ::before plate (desktop rule
     below), sized to --lane-name-w, so the film that scrolled under the
     slot shows through the remainder. The flex slot stays 104px — the
     strip origin never moves, which is why hover can restore the name
     without shifting a clip. */
  background: transparent;
  padding-right: 4px;
}

/* ── The lane label shrinks as the strip scrolls — the Mac's
   trackInfoColumnWidth, in web terms. DESKTOP ONLY (>640); the phone
   rule below keeps its 20px-token treatment unchanged. The plate is the
   visible/opaque label; --lane-name-w is driven from scrollLeft in JS. */
@media (min-width: 641px) {
  .rai-tp-lane-name::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: var(--lane-name-w, 104px);
    background: var(--panel, var(--bg));
    transition: width 60ms linear;
    /* The plate stays a hit target — hover restores, right-click opens the
       lane menu — even though the slot around it does not (below). */
    pointer-events: auto;
  }
  /* THE EXPOSED FILM MUST BE LIVE. The slot still occupies 104px but is
     transparent past the plate; if it kept taking the hits there, a clip the
     user can SEE in that strip could not be clicked, dragged, trimmed or
     selected — visible-but-untouchable, worse than the shutter it replaced
     (Sonar). So the slot passes pointer events THROUGH; the plate and the
     label content re-enable them for themselves. */
  .rai-tp-lane-name { pointer-events: none; }
  .rai-tp-lane-name > .rai-tp-lane-icon,
  .rai-tp-lane-name > .rai-tp-lane-label,
  .rai-tp-lane-name > .rai-tp-lane-lock { pointer-events: auto; }
  /* icon + title paint ABOVE the plate (an absolute ::before otherwise
     paints over in-flow content within the same box). */
  .rai-tp-lane-name > .rai-tp-lane-icon,
  .rai-tp-lane-name > .rai-tp-lane-label,
  .rai-tp-lane-name > .rai-tp-lane-lock {
    position: relative;
    z-index: 1;
  }
  /* 50–88px: icon only. The glyph is identity; two clipped letters are noise. */
  .rai-tp-lanes.lane-names-mini .rai-tp-lane-label { display: none; }
  /* Hover restores the full name as an OVERLAY — the plate grows to full and
     the title returns — WITHOUT touching the 104px slot, so no clip moves. */
  .rai-tp-lane-name:hover::before { width: 104px; }
  .rai-tp-lanes.lane-names-mini .rai-tp-lane-name:hover .rai-tp-lane-label { display: inline; }
  /* Not during a drag: widening the column mid-drag would move every clip
     under the finger (the same rule the Mac and the phone name-plate keep). */
  .rai-timeline-player.dragging .rai-tp-lane-name:hover::before {
    width: var(--lane-name-w, 104px);
  }
  .rai-timeline-player.dragging .rai-tp-lanes.lane-names-mini .rai-tp-lane-name:hover .rai-tp-lane-label {
    display: none;
  }
}

@media (max-width: 640px) {
    /* Chrome that costs a fixed number of pixels grows as a PROPORTION when
       the viewport shrinks: 104px is 7% of a 1400px canvas and 35% of a
       294px one. So the column isn't too wide — it's the wrong KIND of thing
       at this size, and the answer is not a narrower column. A lane is
       needed by name at two moments, not continuously: when you're orienting
       and when you're targeting a drop. At rest a token distinguishes; the
       name arrives when a clip is in the air. (Sonar's spec.) Recovers 84px
       of a 294px canvas — 29% more timeline. */
    .rai-tp-lane-name {
      width: 20px;
      flex: 0 0 20px;
      margin-right: 4px;
      padding-right: 0;
      color: transparent;
      position: relative;
    }

    .rai-tp-lane-name::before {
      content: "";
      position: absolute;
      inset: 3px 4px;
      border-radius: 3px;
      background: var(--clip-edge, var(--dim));
      opacity: 0.85;
    }

    /* While a clip is in the air the lanes are the subject, so they say what
       they are — over the strip rather than by widening the column, because
       widening it mid-drag would move every clip under the user's finger. */
    /* The element's own text stays transparent — the ::after carries the
       name. Un-hiding it showed the 20px token's clipped "Syst" bleeding
       out behind the label. */
    .rai-timeline-player.dragging .rai-tp-lane-name {
      z-index: 5;
      overflow: visible;
    }

    .rai-timeline-player.dragging .rai-tp-lane-name::after {
      content: attr(data-lane);
      position: absolute;
      left: 24px;
      top: 50%;
      transform: translateY(-50%);
      white-space: nowrap;
      font: 11px ui-monospace, "SF Mono", Menlo, monospace;
      color: var(--fg);
      background: var(--panel, var(--bg));
      border: 1px solid var(--border);
      border-radius: 4px;
      padding: 1px 6px;
      pointer-events: none;
    }
}

.rai-tp-strip {
  position: relative;
  flex: 1;
  /* Driven by the ⇕ control. The fallback is the height this lane had
     before it was adjustable, so an older player renders unchanged. */
  height: var(--tp-lane-h, 22px);
  background: color-mix(in srgb, var(--dim) 8%, transparent);
  border-radius: 4px;
}

/* A clip is a FRAME, not a block (the Mac's clip drawing): the border
   is the trim handle, the fill identifies the kind, and the border
   color carries state — accent on hover, orange everywhere while ⌘
   arms overwrite, so the destructive mode is visible on the objects
   it will affect. No minimum width: a short clip looks short. */
/* Where the cut has no picture. Diagonals because that is the edit
   suite's own word for "deliberately nothing" — the same argument as
   keeping the word "Bin": our audience already reads this pattern.

   A gap is a SPAN, so this spans the whole extent rather than marking a
   point; how much black there is IS the thing you need to read, and a
   flag at one end answers a different question.

   Low contrast on purpose. It sits BEHIND the clips (z-index 0 against
   their auto), and it must not compete with the filmstrips — this is the
   quiet layer that says "nothing here", not an alarm. The Release banner
   is where the number gets stated. */
/* The selected clip, and the only place a remove affordance appears.
   Ring rather than a fill change, because the block already carries a
   filmstrip and a lane colour — both of which mean something. */
.rai-tp-clip.selected {
  outline: 2px solid var(--accent, #5cf09d);
  outline-offset: 1px;
  z-index: 3;
}

/* ⛔ A 16px BUTTON INSIDE A PAGE THAT GIVES EVERY BUTTON A 44px MINIMUM.
   Raina, 2026-10-04, on a screenshot: "strange item in my timeline?" — a black
   pill lying across her footage. It is this button, the ☰ that opens a clip's
   commands.
   Measured by Margaret in Chromium AND WebKit, touch and mouse, with a causal
   control: the rect is 16x44, computed min-height 44px, inline style none.
   Setting min-height:0 on the element alone gives 16x16; removing it returns
   44. Swapping the ☰ for an "x" changes nothing, so it is a MINIMUM-SIZE
   CLAMP and not the glyph's line box — the two look identical on screen and
   need opposite fixes, which is why both were measured before either was
   touched.
   The clamps are MINE and they are right where they are: timeline-studio.html
   gives `button,a` a 44px minimum so the page's own controls can be hit with a
   thumb, and rai.css does the same for coarse pointers. `.rai-tp-kill` beats
   them on `height` and `padding` by specificity — and declared no MINIMUM, so
   the one property it left unset was the one that decided the box.
   ⚠ IT IS NOT EXEMPT FROM BEING REACHABLE, it is exempt from being a PAGE
   BUTTON. It sits inside a clip that is often 60px wide; a 44px target covers
   the picture it is meant to annotate. The thumb-sized path to the same menu
   is the long press, and on a phone the verb row — both of which open the
   identical menu, so nothing is behind a 16px target alone. */
.rai-tp-kill {
  position: absolute;
  top: 1px;
  right: 1px;
  width: 16px;
  height: 16px;
  min-width: 16px;
  min-height: 16px;
  padding: 0;
  line-height: 14px;
  font-size: 10px;
  border-radius: 3px;
  cursor: pointer;
  border: 1px solid var(--danger, #fb7185);
  background: var(--panel, #171c24);
  color: var(--danger, #fb7185);
}

.rai-tp-kill:hover {
  background: var(--danger, #fb7185);
  color: var(--panel, #171c24);
}

/* What that removal just did. Sits under the transport, beside the lanes
   it refers to, and clears itself — it reports an event, and an event that
   stays becomes furniture. */
.rai-tp-note {
  display: none;
  padding: 4px 8px;
  font-size: 11px;
  color: var(--dim, #8a93a0);
  border-left: 2px solid var(--warn, #fcd34d);
  margin: 4px 0 0 0;
}

.rai-tp-note.shown { display: block; }

/* The holes' own layer, so they can be re-derived without the clips being
   rebuilt (render.js: fillHoles). `display: contents` and not a positioned
   box ON PURPOSE: an absolutely-positioned child of a `display: contents`
   parent resolves against the nearest POSITIONED ancestor, which is still
   .rai-tp-strip — so every hole's top/bottom/left/width, its stacking and
   its pointer-events are byte-for-byte what they were when the holes were
   direct children. A wrapper with a box of its own would have been a second
   containing block to keep in agreement with the first. */
.rai-tp-gaps {
  display: contents;
}

.rai-tp-gap {
  position: absolute;
  top: 2px;
  bottom: 2px;
  z-index: 0;
  pointer-events: none;
  border-radius: 3px;
  /* Ink, not white: the stripes were rgba(255,255,255,.10) — authored
     on the dark theme, invisible the day Halo (light) became default.
     hatch-truth measured 0.0% painted pixels and it was correct: white
     on white. Deriving from --text keeps the hatch visible on every
     theme's ground, current and future. */
  background: repeating-linear-gradient(
    -45deg,
    color-mix(in srgb, var(--text) 13%, transparent) 0 3px,
    transparent 3px 8px
  );
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 8%, transparent);
}

.rai-tp-clip {
  position: absolute;
  top: 2px;
  bottom: 2px;
  border-radius: 3px;
  /* ⛔ `background-color`, NEVER the `background` SHORTHAND, on anything
     that can also carry a filmstrip. The shorthand resets background-size,
     -repeat and -position to their initial values, and `.rai-tp-clip.has-strip`
     above sets exactly those three. Measured by Margaret, 2026-10-04
     (REF-F4EA58FA), from COMPUTED STYLE in both engines at both phone
     sizes: video clips read `auto 100% / repeat-x / 0% 50%` and audio clips
     read `auto / repeat / 0% 0%` — so a waveform drew at its natural size,
     tiled, anchored top-left, in a 64px lane. The image was there the whole
     time; it was being drawn as noise.
     ⚠ SPECIFICITY DOES NOT SAVE THIS AND I TALKED MYSELF INTO THINKING IT
     DID. `.rai-tp-clip.rai-tp-audio` is (0,2,0), the SAME as
     `.rai-tp-clip.has-strip`, and it comes LATER in the file, so it wins.
     I compared has-strip against the (0,1,0) rule and stopped there.
     Reading a cascade is guessing; the computed style is the measurement. */
  background-color: var(--clip-fill, #4a9eff88);
  border: 1px solid var(--clip-edge, rgba(255, 255, 255, 0.35));
  font: 9px ui-monospace, "SF Mono", Menlo, monospace;
  color: rgba(255, 255, 255, 0.88);
  padding: 3px 4px;
  /* The block used to clip everything (overflow:hidden) so a long label
     could not spill. The label now clips ITSELF (below), which frees the
     block's children to overhang — the trim grips need that (Sonar: a
     silhouette that sits proud of the outline is the one signal that
     does not compete for the grip's five pixels). Nothing else in the
     block relied on the clip: no child uses a negative offset. */
  overflow: visible;
  white-space: nowrap;
}

/* A hole whose cause was recorded (#544; Sonar's marker). Amber, never red: the playhead is the 2px red line
   (.rai-tp-playhead), and a hole must never be mistaken for it. Inset further than a clip so the lane's rhythm breaks
   exactly where the film does. Focusable so a screen reader meets it in reading order. */
.rai-tp-gap.rai-tp-gap-why {
  top: 4px;
  bottom: 4px;
  pointer-events: auto;
  border: 1px dashed color-mix(in srgb, var(--warn, #fcd34d) 70%, transparent);
  overflow: hidden;
}
.rai-tp-gap.rai-tp-gap-why:focus-visible { outline: 2px solid var(--warn, #fcd34d); outline-offset: 1px; }
.rai-tp-gap-label {
  font-size: 11px;
  line-height: 1.2;
  padding: 2px 4px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* too narrow for two words: a downward notch on the lane's top edge, a shape and never a line */
.rai-tp-gap.rai-tp-gap-point { border: none; background: none; overflow: visible; }
.rai-tp-gap.rai-tp-gap-point::before {
  content: "";
  position: absolute;
  top: -4px;
  left: -5px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid var(--warn, #fcd34d);
}
.rai-tp-gap.rai-tp-gap-point::after { content: ""; position: absolute; inset: -12px -18px; }
.rai-tp-stage-note {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 16px;
  text-align: center;
  color: #f4f4f5;
  font-size: 14px;
  pointer-events: none;
}
.rai-tp-stage-note[hidden] { display: none; }
.rai-tp-stage-note strong { font-size: 16px; }
.rai-tp-label {
  display: block;
  overflow: hidden;
  white-space: nowrap;
}

/* TRIM HANDLES — the Mac's TrimmerOverlayCanvas (Raina, 7/23/25),
   transcribed. There the selected clip is framed and the frame FATTENS
   at both ends into a grab bar with two short grip lines; here the
   block already carries its own border and filmstrip, so we draw the
   bars alone and let the existing selection ring be the frame.

   5px is not a taste choice — it is beginDrag's hit zone, and the Mac's
   trimHandleWidth, which are the same number on purpose. The grip lines
   are the Mac's: 12px tall, centred, dark at 0.67 alpha.

   Never a hover-only affordance: it appears with SELECTION, so the
   trim edge can be found by looking rather than by discovering it with
   the cursor. */
/* #115: a destructive control must not become the clip. The ✕ and ☰
   are a fixed 16px, so below a certain clip width they cover the body
   and clicking to SELECT lands on delete. Hidden by width — the
   dangerous one needs margin, the reversible one only needs to fit. */
.rai-tp-clip.no-kill .rai-tp-kill:not(.rai-tp-clipmenu) { display: none; }
.rai-tp-clip.no-menu .rai-tp-clipmenu { display: none; }

.rai-tp-trim {
  position: absolute;
  /* SILHOUETTE (Sonar, measured): the value fix took grip-vs-outline
     from 1.00:1 to 1.13:1 — real, and thin — and the grip has exactly
     five pixels (the hit zone, the Mac's width) in which to also draw
     three lines and two gaps. Lightening asks those five pixels to carry
     one more signal; sitting PROUD of the outline asks nothing of them:
     it changes the shape, not the contents of the band. 3px above and
     below the block, 2px outside its edge. pointer-events stay none, and
     beginDrag measures offsetX against the block, so the overhang
     changes no hit test. */
  top: -3px;
  bottom: -3px;
  width: 5px;
  /* LIGHTER than the selection outline, same hue (Sonar measured grip
     and outline as the identical value, contrast 1.00 — at 5px the grip
     read as the border getting thicker). Separated by VALUE, not hue:
     hue cannot do this work in five pixels; value can. The grip now
     sits ON the border as an object rather than thickening it. */
  background: color-mix(in srgb, var(--accent, #5cf09d) 60%, white);
  /* beginDrag measures offsetX against the BLOCK; a handle that took
     the press would report its own coordinates and every body grab
     inside 5px of an edge would become a trim. */
  pointer-events: none;
  z-index: 2;
}
.rai-tp-trim.left { left: -2px; border-radius: 3px 0 0 3px; }
.rai-tp-trim.right { right: -2px; border-radius: 0 3px 3px 0; }
.rai-tp-trim::after {
  content: "";
  position: absolute;
  left: 1px;
  right: 1px;
  top: 50%;
  height: 12px;
  transform: translateY(-50%);
  border-left: 1px solid rgba(0, 0, 0, 0.67);
  border-right: 1px solid rgba(0, 0, 0, 0.67);
}
/* The Mac hides handles during a drag (showHandles: !isDraggingClip) —
   mid-gesture the ghost is the subject and a grab bar on it invites a
   second grab that cannot happen. */
.rai-tp-clip.dragging .rai-tp-trim { opacity: 0; }
/* Too narrow to trim: beginDrag forces a move, so the handles go away
   rather than promise an edge that will slide the whole clip. */
.rai-tp-clip.no-trim .rai-tp-trim { display: none; }

/* ── SELECTION REVEALS, IT DOES NOT BUILD ──────────────────────────────
   The grips, the ✕ and the ☰ are now built for every clip that COULD
   have them and hidden until the clip is selected, so changing the
   selection is a class toggle instead of a full rebuild of the lane
   stack (Reel's ruling as Timeline DRI; Drift measured the old cost as
   one full teardown per click).

   THE TWO CLASSES ARE NOT THE SAME QUESTION and collapsing them would
   regress real behaviour:
     .selected  every member of the selection
     .primary   the ONE clip the ✕ and ☰ belong to — they were on
                `selectedItem` before, never on all of selectedSet, and a
                multi-selection must not grow a Remove button per clip
     .solo      only while fewer than two clips are selected; the grips
                suppress at a multi-selection exactly as they did

   `display: none` rather than opacity: a hidden ✕ must not be clickable,
   and a hidden grip must not eat the press that beginDrag reads. */
.rai-tp-trim { display: none; }
.rai-tp-clip.selected.solo .rai-tp-trim { display: block; }

.rai-tp-kill { display: none; }
.rai-tp-clip.primary .rai-tp-kill { display: inline-flex; }

/* The width guards still win over the reveal — a selected clip too narrow
   to hold its own affordances shows none, which is why these come last
   and repeat the narrow rules above. */
.rai-tp-clip.no-trim .rai-tp-trim { display: none; }
.rai-tp-clip.no-kill .rai-tp-kill:not(.rai-tp-clipmenu) { display: none; }
.rai-tp-clip.no-menu .rai-tp-clipmenu { display: none; }
/* ⌘/Ctrl overwrite arms the same orange the block wears. */
.rai-tp-clip.overwrite .rai-tp-trim { background: rgb(255, 128, 0); }

.rai-tp-clip.rai-tp-image { background-color: var(--clip-fill, #47c78788); }
.rai-tp-clip.rai-tp-audio { background-color: var(--clip-fill, #c7a44788); }

.rai-tp-clip.editable { cursor: grab; }
.rai-tp-clip.editable:hover { border-color: var(--text); }
.rai-tp-clip.dragging {
  cursor: grabbing;
  z-index: 2;
  opacity: 0.9;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
  border-color: var(--text);
}

/* Overwrite: the ACTOR wears the Mac's arming orange; the VICTIMS —
   the actual overlap set, computed per move — wear danger in the
   fill. Louder where it matters; clips the drop cannot touch say
   nothing. */
.rai-tp-clip.overwrite {
  background: color-mix(in srgb, rgb(255, 128, 0) 55%, transparent);
  border-color: rgb(255, 128, 0);
}
.rai-tp-clip.doomed {
  background: color-mix(in srgb, var(--danger, #fb7185) 60%, transparent);
  border-color: var(--danger, #fb7185);
}
/* The clamp is saying no: the dragged clip wears the refusal while
   the cursor is past where it may go, and drops it the moment the
   move is legal again. Distinguishable-from-outside rule. */
.rai-tp-clip.dragging.refused {
  box-shadow: 0 0 0 2px var(--danger, #fb7185);
  filter: saturate(0.6);
}

/* ⇧ over a too-small gap: the clip narrows to what fits — the accent
   says a cut is being offered, not refused. */
.rai-tp-clip.dragging.fitting {
  box-shadow: 0 0 0 2px var(--accent, #5cf09d);
}

/* ⌥-drag: the block previews a COPY — dashed, like every platform's
   copy affordance, and the cursor says so. The original never moves,
   so the dashed one is the only thing in motion. */
.rai-tp-clip.dragging.duplicating {
  cursor: copy;
  border-style: dashed;
  border-width: 2px;
  border-color: var(--text, #fff);
}

/* Cross-lane drag: the lane the drop will land on says so while the
   clip is in the air — targeting is when the name is load-bearing. */
.rai-tp-lane.drop-target .rai-tp-strip {
  box-shadow: inset 0 0 0 2px var(--accent, #5cf09d);
  border-radius: 3px;
}

/* The clip menu — the commands, visible. Fixed so it escapes the
   scroller's clipping (the strip's right end taught us what clipping
   does to a click), and its hint column is the teaching surface: every
   row that has a faster path names it. */
.rai-tp-menu {
  position: fixed;
  /* Above every card and dock: the docks the composition work mounts
     beside the stage carry their own stacking contexts, and a menu that
     loses that war swallows no clicks while LOOKING clickable — the
     clip-menu gate found it as "rai-card intercepts pointer events". */
  z-index: 1000;
  min-width: 208px;
  padding: 4px;
  border-radius: 8px;
  border: 1px solid var(--line, #2c3442);
  background: var(--panel, #171c24);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  font: 12px system-ui, sans-serif;
  /* A fixed-position menu taller than the screen is a menu whose last
     rows do not exist: the page behind it scrolls, the menu does not.
     Both halves are needed — the clamp in menuTop keeps the TOP on
     screen, this keeps the BOTTOM reachable once it is there. */
  max-height: calc(100dvh - 16px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.rai-tp-menu-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: 5px 10px;
  border-radius: 5px;
  color: var(--text, #e6ebf2);
  cursor: pointer;
  white-space: nowrap;
}
.rai-tp-menu-item:hover:not(.disabled):not(.header) {
  background: var(--accent, #5cf09d);
  color: #10141a;
}
.rai-tp-menu-item.header {
  cursor: default;
  font-weight: 600;
  opacity: 0.85;
}
.rai-tp-menu-item.disabled {
  opacity: 0.35;
  cursor: default;
}
.rai-tp-menu-hint {
  font: 10px ui-monospace, "SF Mono", Menlo, monospace;
  opacity: 0.55;
}
.rai-tp-menu-item:hover:not(.disabled):not(.header) .rai-tp-menu-hint {
  opacity: 0.8;
}
.rai-tp-menu-sep {
  height: 1px;
  margin: 4px 6px;
  background: var(--line, #2c3442);
}

/* A lane switched off for render says so on its face: dimmed, struck
   name, and the ⊘. The clips stay fully interactive — the lane is out of
   the FILM, not out of the editor. */
.rai-tp-lane.render-off .rai-tp-lane-name {
  opacity: 0.5;
  text-decoration: line-through;
}
.rai-tp-lane.render-off .rai-tp-lane-name::after {
  content: " ⊘";
  text-decoration: none;
}
.rai-tp-lane.render-off .rai-tp-strip {
  opacity: 0.55;
  background-image: repeating-linear-gradient(
    -45deg, transparent 0 10px, rgba(255, 255, 255, 0.04) 10px 20px);
}

/* A locked clip is furniture: dimmer, a lock in its corner, and the
   grab cursor gone — the hand learns before the refusal note has to. */
.rai-tp-clip.locked {
  opacity: 0.7;
  cursor: default !important;
}
.rai-tp-clip.locked::after {
  content: "🔒";
  position: absolute;
  top: 1px;
  left: 3px;
  font-size: 9px;
}

/* The seam badge — the Creator's bowtie at a transition. Sits on the
   incoming clip's head, straddling the seam. */
.rai-tp-seam {
  position: absolute;
  top: 50%;
  left: -9px;
  transform: translateY(-50%);
  z-index: 4;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--accent, #5cf09d);
  background: var(--panel, #171c24);
  color: var(--accent, #5cf09d);
  font-size: 10px;
  line-height: 16px;
  cursor: pointer;
}
.rai-tp-seam:hover {
  background: var(--accent, #5cf09d);
  color: #10141a;
}

/* Track Forward: every clip that will ride shows it while the hand is
   down, and the mode tints the surface's cursor. */
.rai-timeline-player.forward-mode .rai-tp-clip.editable {
  cursor: e-resize;
}
.rai-tp-clip.forward-moving {
  box-shadow: 0 0 0 2px var(--accent, #5cf09d);
  opacity: 0.85;
}

/* The Creator's zoom bar: the thumb IS the viewport. */
.rai-tp-zoombar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}
.rai-tp-zoomtrack {
  position: relative;
  flex: 1;
  /* A flex item's default `min-width: auto` refuses to shrink below its
     content. The track is empty so that is 0 — but the FOUR buttons beside
     it carry 44px minimums on a phone, and without this they push the row
     wider than the bar and spill. The track is the part that should give. */
  min-width: 0;
  height: 12px;
  border-radius: 6px;
  background: var(--line, #2c3442);
  /* The structural half of the overrun fix (the arithmetic half is the
     clamp in syncZoombar). A thumb that is wrong can now only be wrong
     INSIDE its track — it can never again paint over the buttons. Both
     halves on purpose: the clamp makes it correct, this makes it
     incapable, and the next person to touch the arithmetic gets the
     second one free. */
  overflow: hidden;
}
.rai-tp-zoomthumb {
  position: absolute;
  top: 0;
  bottom: 0;
  min-width: 24px;
  border-radius: 6px;
  background: var(--muted, #8b96a8);
  cursor: grab;
}
.rai-tp-zoomhandle {
  position: absolute;
  top: 1px;
  bottom: 1px;
  width: 8px;
  cursor: ew-resize;
  border-radius: 5px;
}
.rai-tp-zoomhandle.left { left: 0; }
.rai-tp-zoomhandle.right { right: 0; }
.rai-tp-zoomhandle::after {
  content: "‖";
  font-size: 8px;
  line-height: 10px;
  display: block;
  text-align: center;
  color: var(--panel, #171c24);
}

/* Lane identity: icon + robot badge, small enough to collapse with the
   name column on a phone. */
.rai-tp-lane-icon {
  font-size: 10px;
  margin-right: 4px;
  opacity: 0.9;
}
.rai-tp-lane-lock {
  font-size: 9px;
}

/* The selected clip's ☰ sits left of its ✕. */
.rai-tp-clipmenu {
  right: 19px;
  color: var(--text, #e6ebf2);
  border-color: var(--line, #2c3442);
}

.rai-tp-playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: #ff4a4a;
  left: 84px;
  z-index: 2;
  pointer-events: none;
}

/* Mint (accent) while a drag is pinned to it — the one property that says
   why the clip jumped (the Mac's PlayheadView). Accent, NOT warn: snapping
   is the system helpfully overriding your exact input, a thing to KNOW not
   be warned about — and the snap TOGGLE already lights accent when armed, so
   "snapping is on" (toggle) and "snapping is happening" (playhead) now speak
   one colour (Sonar's ruling, RAI-DESIGN-LANGUAGE.md 10195a804). */
.rai-tp-playhead.snapping { background: var(--accent, #5cf09d); }

/* The grab handle: a small tab in the ruler band so you can seize the
   playhead itself and drag it, not just press the strip. The 2px line
   stays pointer-events:none so it never eats a lane click; the handle
   re-enables pointer-events on itself (a child can, even under a
   none parent), giving a real 14px hit target on a 2px line. */
.rai-tp-playhead-grab {
  position: absolute;
  top: -1px;
  left: -6px;
  width: 14px;
  height: 15px;
  cursor: ew-resize;
  pointer-events: auto;
  touch-action: none;
  background: #ff4a4a;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  border-radius: 2px 2px 0 0;
}
/* Sonar's touch ladder (the dock splitter): on a coarse pointer the HIT
   target grows to 44px while the painted triangle stays thin — clip-path
   redraws the same shape inside the wider frame, so only the target grows. */
@media (pointer: coarse) {
  /* Sonar's ladder widened the target; the HEIGHT was still 15px, which is
     half a thumb. It can be 44 only because the ruler it sits in is now 44
     (end-of-file block) — a taller grab over an 18px strip would hang into
     the lanes and eat the taps that select a clip, which is the same
     mistake one layer down. clip-path keeps the painted triangle thin
     inside the bigger frame, exactly as she built it. */
  .rai-tp-playhead-grab {
    width: 44px;
    left: -21px;
    height: 44px;
    /* ⚠ AND THE PAINTED TRIANGLE STAYS THIN, which the percentage
       clip-path does NOT do once the box is square: polygon(…, 50% 100%)
       scales with the frame, so height:44px alone turned her slim arrow
       into a 44x44 wedge sitting over the time strip. Pinning the apex in
       PIXELS keeps the shape she drew while the box behind it grows —
       which is her own stated rule for this control, "only the target
       grows", and the reason the ladder uses clip-path at all. */
    clip-path: polygon(0 0, 100% 0, 50% 15px);
  }
}
/* Mirrors the playhead line's snapping colour so the two never disagree
   (Sonar's ruling: accent, not warn — see the line rule above). */
.rai-tp-playhead.snapping .rai-tp-playhead-grab { background: var(--accent, #5cf09d); }

/* Asset-drop landing indicator: a mint line at the time a dragged asset
   would be placed, and a quiet hot ring on the scroller while a payload
   hovers. Accent = "this is where it lands", the engaged/positive family. */
.rai-tp-dropmark {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent, #5cf09d);
  pointer-events: none;
  z-index: 3;
}
.rai-tp-scroll.rai-tp-drop-hot {
  outline: 2px dashed color-mix(in srgb, var(--accent, #5cf09d) 70%, transparent);
  outline-offset: -2px;
}
/* Skimming: where you are looking, as distinct from where the playhead
   is. Lighter and thinner than the playhead, and dimmed clock while it
   shows the skim time — the Mac's own treatment (white at .8). */
.rai-tp-skimline {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.65);
  z-index: 2;
  pointer-events: none;
}
.rai-tp-clock.skimming, .rai-tp-bar .skimming { opacity: 0.8; }

.rai-tp-zoom { width: auto; padding: 0 8px; }
.rai-tp-snap { width: auto; padding: 0 8px; font: 12px ui-monospace, Menlo, monospace; }
.rai-tp-snap.on { border-color: var(--ok, #57ab5a); color: var(--ok, #57ab5a); }

/* One card layout for featured and shelf alike (Sonar F5) — the
   grid track constrains the card; the card must NEVER be sized by
   its text (Sonar F1: intrinsic max-content of clamped copy blew a
   flex chain to 1488px and 400px past the viewport). */
/* Flex-wrap, NOT grid auto-fill: Chromium 151 miscomputes a
   repeat(auto-fill, …) grid inside nested <details> and inflates the
   outer fold ~150px past its last child (measured: auto-fill 147px
   dead, fixed columns/flex 13px). Same geometry: 240px minimum,
   grow-to-fill, capped near today's card width. */
.rai-launch-hero {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 18px;
}
/* Fixed basis, NO grow: grow distributes per-row, so a 4-card row
   gets fatter cards than the 5-card row above it — uneven widths AND
   a ragged edge, the worse half of each option. Uniform size reads
   "same kind of thing"; short rows simply end early (Sonar). */
.rai-launch-hero > *,
.rai-launch-rest > * {
  flex: 0 0 300px;
  min-width: 0;
}

.rai-launch-card {
  position: relative;
  min-width: 0;
  max-width: 100%;
  width: 100%;
  /* Cards are buttons, but their copy is prose: undo the global
     button nowrap (which turned descriptions into one unwrappable
     line and let them sail past the card — the clamp only works on
     text that wraps), and clip anything that still escapes. */
  white-space: normal;
  overflow: hidden;
  overflow-wrap: anywhere;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
  text-align: left;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel, var(--bg));
  color: inherit;
  cursor: pointer;
}

/* Retire ✕ — absent at rest so it can never be mistaken for the
   star: revealed by pointer hover on the card or keyboard focus
   inside it, and heavier than the star once shown (danger color on
   its own hover/focus). Opacity keeps it in the tab order —
   display:none would make it keyboard-unreachable (Sonar,
   first-screen-measured.md §5). */
.rai-card-retire {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font-size: 13px;
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
}
.rai-card-wrap:hover .rai-card-retire,
.rai-card-wrap:focus-within .rai-card-retire {
  opacity: 0.75;
  pointer-events: auto;
}
.rai-card-retire:hover,
.rai-card-retire:focus-visible {
  opacity: 1;
  color: var(--danger);
  background: var(--border);
}
.rai-card-retire:focus-visible {
  outline: 2px solid var(--danger, #fb7185);
  outline-offset: 1px;
}
/* No hover means no reveal gesture, and focus lands only after a tap
   that already boots the app — on touch the ✕ stays visible at rest,
   dim, instead of becoming unreachable or an accident. */
@media (hover: none) {
  .rai-card-retire {
    opacity: 0.4;
    pointer-events: auto;
  }
}

.rai-launch-card:hover { border-color: var(--accent, #4a9eff); }
.rai-launch-card strong { font-size: 14px; }
.rai-launch-card span { color: var(--dim); font-size: 12px; }

.rai-launch-label {
  font: 11px ui-monospace, Menlo, monospace;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 6px 0 8px;
}

.rai-launch-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.rai-launch-chip {
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: inherit;
  font: 12px ui-monospace, Menlo, monospace;
  cursor: pointer;
}

.rai-launch-chip:hover { border-color: var(--accent, #4a9eff); }
.rai-launch-chip.on { border-color: var(--accent, #4a9eff); }

.rai-link-btn {
  display: inline-block;
  text-decoration: none;
  text-align: center;
}

/* Flex-wrap for the same Chromium auto-fill bug the hero works
   around — the shared child rule sits beside the hero's block. */
.rai-launch-rest {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.rai-launch-card.rai-launch-small {
  padding: 10px 12px;
}

/* Positioning context for a card and its retire button — a button
   can't nest in a button, so owned cards get a sibling. */
/* Flex column so the card button fills the wrap's grid-stretched
   height: a two-line description grows its card, and without this
   the NEIGHBOURS keep their shorter height — 14px ragged bottoms in
   any row where copy wraps (Sonar, first-screen-measured.md §4). */
.rai-card-wrap {
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.rai-card-wrap > .rai-launch-card {
  flex: 1;
  box-sizing: border-box;
}

.rai-launch-card.rai-launch-small strong { font-size: 13px; }

.rai-launch-card span {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
}

.rai-launch-card strong {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rai-launch-marquee {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
  margin-bottom: 14px;
  padding: 18px 20px;
  border: 1px solid var(--accent, #4a9eff);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent, #4a9eff) 10%, var(--panel, var(--bg)));
  color: inherit;
  cursor: pointer;
  text-align: left;
}

.rai-launch-marquee:hover {
  background: color-mix(in srgb, var(--accent, #4a9eff) 18%, var(--panel, var(--bg)));
}

.rai-marquee-play {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent, #4a9eff);
  color: #fff;
  font-size: 16px;
}

.rai-marquee-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.rai-marquee-text strong { font-size: 16px; }
.rai-marquee-text span { color: var(--dim); font-size: 13px; }

.rai-tp-edit { width: auto; padding: 0 10px; font: 12px ui-monospace, Menlo, monospace; }
.rai-tp-edit.on { border-color: var(--accent, #4a9eff); }

/* Clip Details — the Mac inspector's Start / Duration / End rows, in the
   transport bar while one clip is selected. */
.rai-tp-details { display: inline-flex; align-items: center; gap: 8px; margin-left: 6px;
  font: 12px ui-monospace, Menlo, monospace; color: var(--dim); }
.rai-tp-details[hidden] { display: none; }
.rai-tp-tc { display: inline-flex; align-items: center; gap: 2px; }
.rai-tp-tc > span { margin-right: 2px; }
.rai-tp-tc input { width: 9.5ch; font: inherit; text-align: right; color: inherit;
  background: transparent; border: 1px solid var(--line, #3a3a3a); border-radius: 4px; padding: 1px 4px; }
.rai-tp-tc input:focus { outline: none; border-color: var(--accent, #4a9eff); color: var(--fg, #eee); }
.rai-tp-nudge { padding: 0 5px; }
.rai-tp-tc-count { padding: 0 4px; }

/* ── THE PHONE'S VERB ROW, and the touch sizes the whole player owes a
   finger. ──────────────────────────────────────────────────────────────

   TWO MEDIA QUERIES DOING TWO DIFFERENT JOBS, deliberately not merged:

     (max-width: 640px)  governs LAYOUT. A narrow window is a narrow
                         window whatever is pointing at it, and a
                         desktop user who drags their window narrow
                         wants the stacked transport too.
     (pointer: coarse)   governs TARGET SIZE, at any width. A 44px row
                         is correct on a touchscreen laptop at 1920 and
                         on a tablet in landscape; the LAYOUT would be
                         wrong at those widths, which is exactly why
                         `(max-width: 640px), (pointer: coarse)` as one
                         OR-ed list is the version NOT to write — it
                         puts a phone layout on a 27-inch touch monitor.

   This is the CSS half of Fathom's ruling: the device decision in JS
   chooses what BOOTS, these choose what is VISIBLE and how big — and
   they hold on their own if the JS half ever fails to recognise a
   phone, which is the only reason a backstop is worth having.

   The verb row itself needs no query: it is only BUILT when the page
   decided phone, so it cannot appear on a desktop by a CSS accident. */
.rai-tp-verbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  width: 100%;
}

.rai-tp-verbs-name {
  /* The clip's own name, first, because every verb beside it says
     "this one" and a person must be able to see WHICH one without
     hunting for the highlight on an 11px block. Ellipsised rather
     than wrapped: the row's height is the thing a thumb aims at and
     it must not move when a label is long. */
  flex: 1 1 100%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg, #eee);
  font: 600 13px system-ui, sans-serif;
}

.rai-tp-verb {
  /* Words, not glyphs, and the width to hold them: Sonar's standing
     rule is that an unlabelled control cannot be learned, and on a
     phone there is no hover to explain one. */
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: 44px;
  padding: 0 10px;
  font: 13px system-ui, sans-serif;
  white-space: nowrap;
}

@media (max-width: 640px) {
  /* The inspector becomes its own full-width band under the transport
     rather than an inline tail on it — it carries the verbs now, and a
     verb a thumb cannot reach is a verb that is not there. */
  .rai-tp-details {
    display: flex;
    flex: 0 0 100%;
    flex-wrap: wrap;
    margin-left: 0;
    row-gap: 8px;
    /* BELOW the transport, above nothing: the reading order is see it,
       find the moment, change that moment (Sonar's ordering), so the
       verbs sit under the thing that found the moment. */
    order: 1;
  }

  /* One big play, centred, on its own line under the clock — with undo
     beside it, because undo is the one verb that must be reachable with
     nothing selected and the verb row is only there when something is.
     `order` rather than a DOM move: the bar's children are built by the
     widget for every surface, and re-parenting them per device is how
     two layouts start drifting. */
  .rai-tp-bar > .rai-tp-play:not(.rai-tp-more):not(.rai-tp-verb) {
    order: 0;
    min-height: 44px;
    min-width: 44px;
  }

  .rai-tp-bar {
    justify-content: center;
  }

  /* ⋯ belongs BESIDE play and undo, not on a line of its own: it is a
     transport control that happens to open a drawer, and a lone button
     on its own row reads as a section. The drawer it opens does get its
     own full-width row — a cluster of eight controls inline with play
     would be the dense bar this change exists to undo. */
  .rai-tp-more {
    order: 0;
    min-height: 44px;
    min-width: 44px;
  }

  .rai-tp-view.open {
    flex: 0 0 100%;
    order: 2;
    row-gap: 8px;
  }

  .rai-tp-view.open > .rai-tp-play {
    min-height: 44px;
  }

  /* THE WIDGET'S ONE CHANNEL FOR SAYING WHY NOTHING HAPPENED, and at
     11px dim it is present without being read. Split refuses with
     "move the playhead inside the selected clip first" — a complete,
     correct sentence that answers the exact question a person has after
     tapping a verb and seeing no change. A phone verb whose refusal is
     invisible is a button that silently does nothing, which is the
     shipped-success-toast defect with the colours reversed. */
  .rai-tp-note {
    font-size: 13px;
    padding: 8px 10px;
    color: var(--fg, #eee);
  }

  /* The lanes get the room the retired controls were using. */
  .rai-tp-stage {
    max-height: clamp(180px, calc(100dvh - 380px), 540px);
  }
}

@media (pointer: coarse) {
  /* The clip menu is twenty-one commands in rows a mouse can hit and a
     finger cannot: 5px of padding on a 13px line is a 23px row. This is
     the only path to most of those commands on a phone, so the rows are
     the thing that has to give. */
  .rai-tp-menu-item {
    min-height: 44px;
    padding: 10px 14px;
  }

  .rai-tp-menu {
    max-width: min(92vw, 320px);
  }

  .rai-tp-menu-item > span:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The zoom bar stays on a phone (there is no pinch yet) and it is the
     control a person reaches for most, because at fit scale a six-second
     shot is eleven pixels. Its magnifiers were mouse-sized. */
  .rai-tp-zoombar .rai-tp-play {
    min-height: 44px;
    min-width: 44px;
  }

  /* THE NUDGE ARROWS ARE THE TRIM, so they are not optional chrome. This
     inspector's Duration row is the numeric form of dragging a clip's
     edge — which is the gesture a phone cannot reach, because the grips
     are suppressed below 18px of block. At 5px of padding the arrows
     were a mouse target sitting where the only trim on the device is. */
  .rai-tp-nudge {
    min-height: 44px;
    min-width: 44px;
    padding: 0 10px;
  }

  /* ⚠ THE SLIDER WAS THE ONE I MISSED, and it was the one touched most:
     every button here was raised to 44px and the range input was in neither
     query, so it kept the UA default height of roughly 16-20px — the control
     I had deliberately given its own full-width row had become the SMALLEST
     target in the transport. It is gone now rather than raised (Raina,
     2026-10-29: "we do not need the slider since they can grab the cursor"),
     and the target that replaces it is the playhead's own grab handle, which
     this stylesheet already takes to 44x44 on a coarse pointer. */
  .rai-tp-tc input {
    min-height: 44px;
  }
}


/* ── phone layout: the top bar is a dev console; on a phone it
   collapses to the picker + status, and the launcher gets the full
   width. ── */
#topbar { flex-wrap: wrap; }
#status { flex-basis: 100%; order: 10; }
#apps {
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  /* Spans the workspace row (grid-area library) — the 900px cap
     predates the move out of the Stage column. #workspace's own
     max-width governs. */
  grid-area: library;
  box-sizing: border-box;
}

@media (max-width: 640px) {
  #server-url, #app-name, #connect, #wordmark { display: none; }
  #topbar { gap: 6px; padding: 8px 10px; }
  #app-picker { flex: 1; min-width: 0; }
  #apps { padding: 10px; }
  .rai-launch-hero, .rai-launch-rest { flex-direction: column; }
  .rai-launch-hero > *, .rai-launch-rest > * { flex-basis: auto; max-width: none; }
  .rai-launch-card { width: 100%; box-sizing: border-box; }
  .rai-launch-marquee { padding: 14px 16px; }
}

.rai-studio-label { margin-top: 2px; }

.rai-studio-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  max-width: 864px;   /* the measure #studio's 900px cap used to give it */
  margin-bottom: 20px;
}

/* Studio tiles are navigation — a list and two editors under a MY
   STUDIO label — so they wear the neutral card treatment, not the
   accent. Measured before this: the three tiles carried ~52k px² of
   accent, 17× the topbar and effectively the page's whole visual
   budget, while the tools that actually make shows sat below with
   none — visual hierarchy inverted from task hierarchy, and the copy
   ("pick a tool below") pointing the other way. The accent belongs to
   do-this-next, and a nav bar is never that. (Sonar, punch-list 5.) */
.rai-studio-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 10px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: color-mix(in srgb, var(--panel, var(--bg)) 55%, transparent);
  color: inherit;
  font: 600 12.5px ui-sans-serif, system-ui, Arial, sans-serif;
  cursor: pointer;
}
.rai-studio-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-align: center;
  min-width: 0;
}
.rai-studio-text strong { font-weight: 600; }
.rai-studio-desc {
  font-weight: 400;
  font-size: 11.5px;
  color: var(--dim);
  text-wrap: balance;
}

.rai-studio-tile:hover {
  background: var(--panel, var(--bg));
  border-color: color-mix(in srgb, var(--text) 25%, var(--border));
}

.rai-studio-icon {
  width: 30px;
  height: 30px;
  fill: none;
  stroke: var(--accent, #4a9eff);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (max-width: 640px) {
  .rai-studio-row { grid-template-columns: repeat(2, 1fr); }
}

/* ── The Stage: where the running script lives, between the studio
   row and the library. ── */
/* ONE shared container, Sonar 2026-08-22: every top-level section takes the
   SAME centred measure, and the page gutter is the 'lg token measured from
   THAT container's edge -- not the viewport's. Centring ONCE (here and on
   #workspace, same geometry) is what keeps the earlier 2560px finding true
   -- a left-pinned cap leaves 1142px of black 'stuck in the corner' -- while
   still giving Kane the single unmoving spine. #studio used to centre itself
   at a DIFFERENT cap (900), which is exactly why its left edge never agreed
   with the workspace's. The 900px measure it wanted is now the ROW's
   max-width, aligned to the container's content edge. */
#studio { padding: 14px var(--space-lg) 0; max-width: var(--measure); margin: 0 auto; box-sizing: border-box; }

/* The workspace grammar:
        [       top controls        ]
        [ left | STAGE     | right  ]
        [      bottom controls      ]
   Docks hold accessory scripts (surface "panel"), each a live RAI app
   beside the stage, like the Mac inspector beside the canvas. Left and
   right are vertical inspectors; top and bottom are horizontal control
   strips. Empty docks vanish and the grid collapses around them. */
#workspace {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
  grid-template-areas:
    "top     top     top"
    "left    stage   right"
    "bottom  bottom  bottom"
    "library library library";
  align-items: start;
  column-gap: 14px;
  /* Centered, never pinned: at 2560px a left-pinned 1400px workspace
     left 1142px of black on the right — "stuck in the corner". Auto
     margins convert the cap into a measure. */
  margin: 0 auto;
  padding: 0 var(--space-lg);
  max-width: var(--measure);
  box-sizing: border-box;
}

/* Widen stage lifts the WORKSPACE cap too — it lifted only #stage's,
   so above 1440 dragging the window wider moved nothing while 70vh
   kept tracking height forever. One axis responding and the other
   frozen is what "doesn't feel reactive" was. */
/* ONE CONTROL, TWO DECISIONS -- now separated. "Collapse docks" used to
   set BOTH `how much screen does this app get` and `do I want my Bin and
   Assets`, because at 1440 they coincide: there is no spare width, so
   widening really does cost the docks. At 2560 they do not coincide --
   there were 580px of empty gutter on EACH side, so the docks would have
   fit with room to spare. The control was charging a person their Bin to
   buy width that was already sitting empty beside it.
   The width decision now belongs to --measure, which answers to the
   viewport. This class collapses docks, which is what its button says. */
#workspace.stage-wide {
  /* max-width deliberately NOT set here: see above. */
}

#dock-top { grid-area: top; }
#dock-left { grid-area: left; }
#dock-right { grid-area: right; }
#dock-bottom { grid-area: bottom; }
#workspace > #stage { grid-area: stage; }

.rai-dock {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* Mirror #stage's top margin so the columns share a baseline:
     eyebrow label at the same height, box tops flush. */
  margin-top: 6px;
}

/* Side inspectors: fixed width, pinned while the stage scrolls —
   and ONLY while the stage scrolls. The aside stretches to the
   stage row and acts as the travel bound; the inner wrapper is the
   sticky element. (Chromium bounds a sticky grid item to the grid
   CONTAINER, not its grid area, so a sticky aside kept riding down
   over the library shelf — Raina's asset-view overlap.) */
#dock-left, #dock-right {
  /* --dock-w-left / --dock-w-right are set by a drag (main.js) and fall
     back to the token, so an undragged dock is exactly what it always
     was and a dragged one needs no extra class. */
  width: var(--dock-w-self, var(--dock-w));
  align-self: stretch;
  position: relative;   /* the split handle rides this box's inner edge */
}
#dock-left  { --dock-w-self: var(--dock-w-left,  var(--dock-w)); }
#dock-right { --dock-w-self: var(--dock-w-right, var(--dock-w)); }

/* ── THE DOCK SPLIT HANDLE ──────────────────────────────────────────
   Raina, 2026-08-25: "I want the last column on all platforms to be a
   Split View so I can resize."

   PAINT THIN, GRAB FAT (Sonar). The hairline is 2px; the pointer target
   is the whole 14px column gap on a fine pointer and 44px on a coarse
   one. The FRAME grows with the input class, the PAINT never does —
   a 44px painted bar would be a wall between the dock and the work.

   It rides the dock's INNER BOUNDARY rather than being a column of its
   own: the grid already puts a 14px gap there, so the handle fills a gap
   that exists instead of adding two more. It is also why this is a
   property of a BOUNDARY and not of a side — the leading handle is the
   same element mirrored, not a second implementation. */
.rai-dock-split {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 14px;
  z-index: 3;
  cursor: col-resize;
  /* Without this a drag on a touch screen scrolls the page instead of
     resizing, and pointermove stops arriving mid-gesture. */
  touch-action: none;
}
#dock-left  .rai-dock-split { right: -14px; }
#dock-right .rai-dock-split { left: -14px; }

/* Visible at rest, brighter under the hand. A control that shows nothing
   until you have already grabbed it is one nobody discovers — and this
   one has no other affordance announcing that the column can move. */
.rai-dock-split::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--border);
  opacity: 0.55;
  transition: opacity 120ms ease, background-color 120ms ease;
}
.rai-dock-split:hover::before,
.rai-dock-split:focus-visible::before,
.rai-dock-split.dragging::before {
  opacity: 1;
  background: var(--accent, #4a9eff);
}
.rai-dock-split:focus-visible {
  outline: 2px solid var(--accent, #4a9eff);
  outline-offset: 2px;
}

/* The touch ladder: 44px of target, still 2px of paint. Offsets keep the
   hairline centred on the same boundary — a target that grows off-centre
   resizes from a line that is not where the person aimed. */
@media (pointer: coarse) {
  .rai-dock-split { width: 44px; }
  #dock-left  .rai-dock-split { right: -29px; }
  #dock-right .rai-dock-split { left: -29px; }
}

/* A collapsed rail keeps its handle: dragging the rail back out is how
   you restore it, which is the half of the gesture that makes the
   splitter and "Collapse docks" one control instead of two. */
#workspace.stage-wide .rai-dock-split { z-index: 4; }

/* ABOUT TO RAIL. The dock stops following the pointer at its 300px floor
   — dragging it through 250, 200, 150 would be showing widths we refuse
   to persist — so without a signal the drag would just look STUCK.
   SHOW THE OUTCOME, DO NOT WARN ABOUT IT (Sonar's ruling, 2026-08-25).
   My first version painted this in the WARN colour, and that was the
   `e-resize`-on-ripple-mode mistake made an hour after diagnosing it:
   railing is not an error and not destructive — it is a normal,
   reversible outcome the person may well want, and a warn colour borrows
   a meaning from the error family to say so. The handle still THICKENS,
   which correctly says "something changes here"; it says it in the
   accent, which is the vocabulary of a control rather than a fault. */
.rai-dock-split.will-rail::before {
  opacity: 1;
  background: var(--accent, #4a9eff);
  width: 4px;
  margin-left: -2px;
}

/* The dock previews what a release will produce, rather than the handle
   describing it: the contents ease toward the rail treatment so the
   person SEES the collapse coming and can back out of it by dragging
   right. Anticipatory, not cautionary. */
.rai-dock.rail-preview .rai-dock-sticky {
  opacity: 0.3;
  transition: opacity 120ms ease;
}
.rai-dock.rail-preview {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  transition: background-color 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .rai-dock.rail-preview,
  .rai-dock.rail-preview .rai-dock-sticky { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .rai-dock-split::before { transition: none; }
}
#dock-left .rai-dock-sticky, #dock-right .rai-dock-sticky {
  position: sticky;
  top: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* "Something's wrong" — quiet until needed, far right of the bar. */
#report-wrong { margin-left: auto; }
#report-dialog {
  background: var(--panel, #161a22);
  color: inherit;
  border: 1px solid var(--edge, #2a3140);
  border-radius: 10px;
  padding: 18px;
  width: min(440px, calc(100vw - 40px));
}
#report-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
#report-form { display: flex; flex-direction: column; gap: 12px; }
#report-form h2 { font-size: 15px; margin: 0; }
#report-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
#report-form textarea { resize: vertical; font: inherit; }
#report-context { margin: 0; font-size: 12px; }
#report-note { margin: 0; font-size: 13px; }
.report-actions { display: flex; gap: 10px; justify-content: flex-end; }

/* ── THE MODAL SURFACE ─────────────────────────────────────────────
   Over the stage, dimming and BLOCKING what is behind it. The <dialog>
   element supplies the blocking (top layer + inert background); this
   only has to look like the mode it already is.

   PADDING LIVES ON THE FRAME, NOT ON THE DIALOG. Click-outside tests
   `e.target === dialog`, and a padded dialog makes its own gutter part
   of the element — so the ring around the card would read as INSIDE
   and swallow the click every person tries first. */
#rai-modal {
  padding: 0;
  border: none;
  background: none;
  max-width: none;
  max-height: none;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
#rai-modal::backdrop { background: rgba(0, 0, 0, 0.55); }
.rai-modal-frame {
  box-sizing: border-box;
  width: min(920px, calc(100vw - 48px));
  max-height: calc(100vh - 96px);
  margin: 48px auto;
  display: flex;
  flex-direction: column;
  background: var(--panel, #161a22);
  color: var(--text);
  border: 1px solid var(--border, #2a3140);
  border-radius: 14px;
  padding: 18px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
}
.rai-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 12px;
}
.rai-modal-head h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}
/* A VISIBLE exit, not only Escape: an exit you cannot see is not
   named. Same anatomy as a panel's close so the two read as one
   system. */
.rai-modal-close {
  border: none;
  background: none;
  color: var(--dim);
  font-size: 13px;
  cursor: pointer;
  padding: 4px 7px;
  border-radius: 6px;
}
.rai-modal-close:hover { color: var(--text); background: var(--panel-2, transparent); }
.rai-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  font-size: 13px;
}

/* Control strips: panels sit side by side, wider than tall. */
.rai-dock-strip {
  flex-direction: row;
  align-items: start;
  gap: 14px;
  margin-bottom: 6px;
}
.rai-dock-strip .rai-panel-slot { flex: 1 1 0; min-width: 0; }
.rai-dock-strip .rai-panel-body { max-height: 32vh; }

/* `display: flex` above beats the UA's [hidden] rule — restate it,
   or empty docks occupy the row as invisible 300px columns. */
.rai-dock[hidden] { display: none; }

/* A docked panel repeats the Stage's anatomy — eyebrow label, then
   the framed box — so the columns align by construction. */
.rai-panel-slot {
  display: flex;
  flex-direction: column;
}

.rai-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font: 11px ui-monospace, Menlo, monospace;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  margin: 6px 0 8px;
}

.rai-panel-close {
  border: none;
  background: none;
  color: var(--dim);
  font-size: 12px;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 5px;
}
.rai-panel-close:hover { color: var(--text); background: var(--panel-2, transparent); }

.rai-panel-body {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel, var(--bg));
  padding: 6px;
  max-height: 78vh;
  overflow-y: auto;
  font-size: 13px;
}

#stage {
  margin: 6px 0 18px;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 900px;
  box-sizing: border-box;
}

/* THE 900px CAP IS A READING MEASURE, and lifting it is a SEPARATE
   decision from collapsing the docks — they were one class, and that
   bundling is what put Chad and Raina on opposite sides of the same bug.
   Collapsing the docks obviously frees the cap (claiming those columns is
   the whole point), but an app that merely DECLARES it wants a wide stage
   should get the room without anyone's Bin and Assets being spent for it.
   `wide-stage` is that second, quieter answer: more room for the work,
   docks untouched. See applyStageWidth in main.js for which is which. */
#workspace.stage-wide > #stage,
#workspace.wide-stage > #stage { max-width: none; }

/* Bundle stack — companions declared in :with mount UNDER the head app
   in the stage column (Raina: "bundles/packages should stack"). Same
   card anatomy as a dock panel, full stage width, reading top-to-
   bottom as one package. */
#stage-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 14px;
}
#stage-stack[hidden] { display: none; }
#stage-stack > .rai-panel-slot {
  width: 100%;
  min-width: 0;
}

#stage > main {
  min-height: 180px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel, var(--bg));
  padding: 6px;
}

#stage-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 168px;
  font-size: 13px;
}

/* Narrow screens: the workspace stacks — top strip, stage, then the
   inspectors, then the bottom strip (the inspector drops below the
   canvas, same as the Mac's compact layouts). Sticky docks would
   fight the scroll here; unpin them. */
@media (max-width: 900px) {
  #workspace {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "top" "stage" "right" "left" "bottom" "library";
  }
  #stage { width: 100%; }
  .rai-dock, #dock-left, #dock-right {
    position: static;
    width: 100%;
    margin-bottom: 14px;
  }
  .rai-dock-strip { flex-direction: column; }
}

@media (max-width: 640px) {
  #studio { margin-left: auto; margin-right: auto; padding-left: var(--space-lg); padding-right: var(--space-lg); }
  #workspace { margin: 0 auto; }
  #studio { padding: 10px var(--space-lg) 0; margin: 0 auto; }
}

.rai-shelf {
  border: 1px solid var(--border);
  border-radius: 10px;
  margin-bottom: 8px;
  background: var(--panel, var(--bg));
  min-width: 0;
  max-width: 100%;
}

/* Full width comes from #apps's column-flex align-items:stretch;
   min-width:0 stops the cards' intrinsic width sailing the shelf
   past the viewport. Height is content's alone: #apps is a COLUMN,
   so any flex-grow/basis here stretches the fold to the grid row's
   height and the border draws the emptiness as a 500px dead panel
   (Sonar, first-screen-measured.md §1 — a bordered container ends
   where its content ends). */
.rai-library {
  flex: 0 0 auto;
  min-width: 0;
  max-width: 100%;
}

/* Chromium hides a closed details' content via content-visibility,
   which skips paint but leaves geometry in layout — the phantom
   boxes inflated the fold ~150px past its last visible child.
   display:none takes the hidden content out of layout entirely, so
   the bordered fold ends where its content ends. */
.rai-shelf:not([open]) > :not(summary) {
  display: none;
}

.rai-shelf > summary {
  cursor: pointer;
  padding: 10px 14px;
  font: 600 12px ui-monospace, Menlo, monospace;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  user-select: none;
}

.rai-shelf[open] > summary { color: inherit; }
.rai-shelf > .rai-launch-hero,
.rai-shelf > .rai-launch-rest { padding: 4px 12px 12px; }

/* The #id display rule beats the UA's [hidden] — restate it, or a
   successful sign-in leaves the banner stuck on screen. */
#auth-banner[hidden] {
  display: none;
}

#auth-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  /* OVERLAY, never in flow: the health check resolves ~7s after
     load, and an in-flow banner shifted the whole workspace 88px
     under a user who had already started reading (Sonar's CLS
     find — hidden for a week because every instrument's settle
     timer waited out the shift). The Stage's position must not
     depend on an async fetch. */
  position: fixed;
  /* BOTTOM, not top: fixed-at-top occludes whatever leads the page
     (Sonar measured 72% coverage — out-of-flow stops moving things
     and starts covering them). Reserving space instead would carve a
     permanent 78px hole into every HEALTHY load for a banner that is
     usually absent. The bottom edge carries no structural content;
     an outage toast belongs there. */
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  width: min(900px, calc(100vw - 36px));
  z-index: 60;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
  box-sizing: border-box;
  padding: 10px 14px;
  border: 1px solid #b8860b;
  border-radius: 10px;
  background: color-mix(in srgb, #b8860b 12%, var(--panel, var(--bg)));
  font-size: 13px;
}

#auth-banner button {
  padding: 4px 14px;
  border: 1px solid #b8860b;
  border-radius: 6px;
  background: #b8860b;
  color: #fff;
  font-weight: 600;
  cursor: pointer;
}

#auth-banner a { color: var(--accent, #4a9eff); }

.rai-library { padding-bottom: 4px; }
.rai-library > summary {
  font-size: 13px;
  letter-spacing: 0.1em;
  padding: 12px 14px;
}
.rai-library > .rai-shelf { margin: 0 10px 8px; }

/* (outline …) — the hierarchical folder view */
.rai-tree { display: flex; flex-direction: column; gap: 1px; }
.rai-tree-row { display: flex; align-items: center; gap: 6px; padding: 2px 4px; border-radius: 6px; min-width: 0; }
.rai-tree-row:hover { background: var(--panel-2); }
.rai-tree-selected { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.rai-tree-summary { cursor: pointer; list-style-position: outside; }
.rai-tree-summary > .rai-tree-row { display: inline-flex; width: calc(100% - 18px); vertical-align: middle; }
.rai-tree-children { display: flex; flex-direction: column; gap: 1px; }
.rai-tree-leaf-row { margin-left: 14px; }

/* thumbnail with no source yet: quiet placeholder, no broken glyph */
img.rai-media-missing { visibility: hidden; }
.rai-media { background: var(--panel-2); min-height: 20px; }

/* drag & drop affordances */
.rai-drag-source { cursor: grab; }
.rai-drag-source:active { cursor: grabbing; }
.rai-tree-drop-hot > .rai-tree-row,
.rai-tree-drop-hot { outline: 1px dashed var(--accent); border-radius: 6px; }

/* right-click context menu */
.rai-context-menu {
  position: fixed;
  z-index: 1000;
  min-width: 180px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px;
  box-shadow: 0 12px 32px rgba(0,0,0,.45);
  font-size: 13px;
}
.rai-context-item { padding: 6px 10px; border-radius: 5px; cursor: pointer; }
.rai-context-item:hover { background: var(--panel); }
.rai-context-divider { height: 1px; background: var(--border); margin: 4px 6px; }

#dev-controls { display: flex; gap: 8px; align-items: center; }
#dev-controls[hidden] { display: none; }

/* A row containing labeled fields: buttons and dropdowns align with
   the INPUT (bottom of the labeled block), not the block's center —
   the floating-button misalignment in forms like New Project. */
.rai-row:has(.rai-field-labeled) {
  align-items: flex-end;
}

/* The <a download> variant of a button renders identically. The base
   button chrome lives on the `button` ELEMENT selector, so an anchor
   needs the full set restated — class alone only gets variants. */
a.rai-button {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  padding: 6px 12px;
  cursor: pointer;
}

a.rai-button:hover {
  border-color: var(--accent);
}

/* Media that failed to load — same footprint, a quiet glyph instead
   of an invisible broken <img>. */
.rai-media-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  font-size: 28px;
  opacity: 0.45;
  background: color-mix(in srgb, var(--panel-2) 60%, transparent);
  border-radius: 8px;
}

/* The LIVE chyron — a filled lower-third strip, unmistakably a
   status, not a control. (banner :level 'live "LIVE") */
.rai-banner[data-level="live"] {
  /* #64: was a filled var(--warn) pill with near-black text — the
     description of a primary button on a STATUS (Drift moused to it),
     and 3.38:1 in halo (14px/700 is not large text; AA floor 4.5).
     Now consistent with its three siblings, which were already
     outline chips: border + text tinted --warn (5.27:1 halo, 12.46:1
     neuroglass, measured on the 55%-opaque card ground), where
     `warning` tints border only and keeps --text — same grammar,
     distinguishable, nothing that reads as pressable. (Sonar's
     measurement + fix, 2026-08-20.) */
  border-color: var(--warn);
  color: var(--warn);
  font-weight: 700;
  letter-spacing: 2px;
  text-align: center;
  text-transform: uppercase;
  padding: 4px 12px;
}

/* Labeled fields in rows must keep a usable width — without a floor
   the long-caption sibling crushes the field to a word-per-line
   sliver (the Screening Room publish row). CHILD selector on purpose:
   the descendant form reached into nested columns, where the flex
   main axis is vertical and a width-authored basis resolves as a
   HEIGHT — a one-line caption rendering 147px tall and absorbing the
   column's slack. That was the "dead space" in Deep Dive's cards. */
.rai-row > .rai-field-labeled {
  flex: 1 1 auto;
  min-width: 18ch;
}

.rai-row > .rai-caption {
  /* A ~24ch basis: with row wrapping on, a caption that can't get a
     readable column takes its own line rather than going one word
     per line down the row's right edge. */
  flex: 1 1 24ch;
  min-width: 0;
}

/* Audio players need their controls' full width — crushed into a
   narrow grid cell the transport becomes unusable. */
audio.rai-media {
  width: 100%;
  min-width: 0;
  height: 32px;
}

/* ── Since You Left — the arrival surface (design/since-you-left.md).
   Lives inside the Stage root; a single left-aligned column (centring
   would read as a different kind of object), one filled card on the
   one thing with a primary action, plain lists elsewhere. Must fit
   the Stage without scrolling in the maximal case. */
#since-you-left {
  max-width: 600px;
  min-width: 0;
  padding: 18px 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: flex-start;
  text-align: left;
  overflow-wrap: anywhere;
}

.syl-greet {
  margin: 0;
  font-size: 15px;
}

.syl-body {
  margin: 0;
  max-width: 46em;
}

.syl-alt { margin: 0; font-size: 13px; }

.syl-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  min-width: 0;
}

/* Same anatomy as the workspace's MY STUDIO / THE STAGE labels. */
.syl-head {
  font: 11px ui-monospace, Menlo, monospace;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Zone 1: the only filled surface. Copy is prose in a user-named
   tool — same escape defenses as the shelf cards. */
.syl-card {
  width: 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  overflow: hidden;
  overflow-wrap: anywhere;
}

.syl-card strong {
  font-size: 14px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.syl-blurb {
  color: var(--dim);
  font-size: 12px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
}

.syl-meta {
  color: var(--dim);
  font-size: 12px;
}

.syl-card .syl-primary { align-self: flex-end; }

.syl-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}

.syl-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  min-width: 0;
}

.syl-list li::before {
  content: "·  ";
  color: var(--dim);
}

/* The reserved make-a-new-tool entry is a quiet text action, not a
   chromed button — stated, not sold. */
.syl-vibe {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: 13px;
  text-align: left;
  white-space: normal;
}

.syl-vibe:hover { text-decoration: underline; }

.syl-dismiss {
  align-self: flex-end;
  background: none;
  border: none;
  padding: 4px 0;
  color: var(--dim);
  font-size: 12px;
}

.syl-dismiss:hover { color: var(--text); text-decoration: underline; }

/* ── Running strip — named chips for booted Stage scripts (Sonar,
   running-scripts.md). Current chip filled, rest quiet; overflow
   scrolls inside the strip, never the page. ── */
#stage-head {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

#stage-head .rai-launch-label { flex: 0 0 auto; }

/* On a phone the strip is the only control in this row that a user
   NEEDS: with two apps open, the second tab's close button sat off
   the clipped edge and could not be reached at all (Raina's report,
   Sonar's measurement — 159px of strip inside 390px of screen).
   The two things flanking it are both dead weight at this width:
   "The Stage" labels the only thing on screen, and "Widen stage"
   promises width the stage already has — at ≤900px the docks stack
   BELOW the stage, so the control's real effect there is hiding
   panels, which is not what its words say. A control whose label
   is false at a width is worse than one that is absent. */
@media (max-width: 640px) {
  #stage-head .rai-launch-label,
  #stage-widen {
    display: none;
  }
  #running-strip { flex: 1 1 auto; min-width: 0; }
  /* And the bar stops being sticky here. Wrapped to three rows it is
     137px of dev chrome — a third of the screen — pinned over
     whatever the page scrolls to, which is how the running tabs
     ended up UNDER it and unreachable however wide the strip got.
     Out of flow stops moving things and starts covering them; on a
     phone the stage is the product and the bar is not worth a third
     of it, so it scrolls away like everything else. */
  #topbar { position: static; }
}

#running-strip {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  padding-bottom: 2px;
  margin: 6px 0 8px;
}

#running-strip[hidden] { display: none; }

.rai-run-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel);
  padding: 0 2px 0 0;
}

.rai-run-chip.current {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, var(--panel));
}

.rai-run-title {
  background: none;
  border: none;
  padding: 3px 4px 3px 10px;
  font-size: 12px;
  color: var(--text);
  cursor: pointer;
}

.rai-run-stop {
  background: none;
  border: none;
  border-radius: 999px;
  width: 22px;
  height: 22px;
  padding: 0;
  font-size: 11px;
  line-height: 1;
  color: var(--dim);
  cursor: pointer;
}

.rai-run-stop:hover { color: var(--danger); }
.rai-run-stop:focus-visible,
.rai-run-title:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ── Favorite star — top-left of every card, always visible in both
   states (hover-only fails discovery and touch). Maximum separation
   from retire ✕ at top-right: a reversible toggle never neighbours
   an irreversible delete (Sonar, favorites-and-wide-stage.md). ── */
.rai-card-star {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--dim);
  font-size: 14px;
  line-height: 1;
  opacity: 0.6;
  cursor: pointer;
}

.rai-card-star:hover { opacity: 1; background: var(--border); }
.rai-card-star.on { color: var(--accent); opacity: 1; }
.rai-card-star:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  opacity: 1;
}

/* Clear the star's corner so titles never sit under it. */
.rai-card-wrap .rai-launch-card { padding-left: 34px; }

/* ── Wide Stage — docks collapse to rails, never vanish (Sonar,
   favorites-and-wide-stage.md §2b). The rail keeps the panel titles
   readable vertically and the whole rail restores on click, so a
   user who forgets the toggle still has an obvious way back. ── */
#stage-widen {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 11px;
  padding: 3px 10px;
  color: var(--dim);
}

#stage-widen:hover { color: var(--text); }
#stage-widen[hidden] { display: none; }

#workspace.stage-wide > #dock-left,
#workspace.stage-wide > #dock-right {
  width: 32px;
  min-width: 32px;
  overflow: hidden;
  cursor: pointer;
  position: sticky;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel);
  min-height: 220px;
}

/* Panels keep RUNNING inside the rail — hidden, not unmounted, so
   their long-poll loops and state stay alive. */
#workspace.stage-wide > .rai-dock > * {
  display: none;
}

/* ...EXCEPT THE SPLIT HANDLE, which is how you get the dock back.
   The rule above hides a collapsed dock's contents so the rail can show
   just its vertical label — correct for panels, and it also swallowed
   the handle, so a dock dragged shut could never be dragged open again.
   Half a gesture is worse than none: the person who collapsed it by
   dragging reaches for the same edge to undo it, and the button is not
   where their hand is. */
#workspace.stage-wide > .rai-dock > .rai-dock-split {
  display: block;
}

/* AND IT HAS TO COME INSIDE THE BOX TO SURVIVE, which is not obvious and
   cost a second round. A collapsed dock sets `overflow: hidden` to clip
   its contents into a rail — and the handle normally rides at
   `right: -14px`, entirely OUTSIDE that box, so it was clipped away and
   received no pointer events at all.
   It still had layout: getBoundingClientRect and Playwright's
   bounding_box() both reported a perfectly good 14x262 box sitting where
   the handle should be. AN ELEMENT CLIPPED BY AN ANCESTOR MEASURES
   EXACTLY LIKE ONE YOU CAN GRAB — the probe agreed with me and the mouse
   did not. Pinning it inside the rail's own edge is what makes
   drag-the-rail-back-out reachable. */
#workspace.stage-wide > #dock-left > .rai-dock-split {
  right: 0;
  left: auto;
}
#workspace.stage-wide > #dock-right > .rai-dock-split {
  left: 0;
  right: auto;
}

#workspace.stage-wide > .rai-dock::after {
  content: attr(data-rail);
  display: block;
  writing-mode: vertical-rl;
  padding: 10px 0 0;
  margin: 0 auto;
  font: 10px ui-monospace, Menlo, monospace;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--dim);
  white-space: nowrap;
}

#workspace.stage-wide > .rai-dock:hover::after { color: var(--text); }

/* Site-wide notice — a live heads-up someone chose to raise. */
#site-notice {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 18px 0;
  padding: 8px 14px;
  border: 1px solid var(--warn, #fcd34d);
  border-radius: 10px;
  background: color-mix(in srgb, var(--warn, #fcd34d) 12%, transparent);
  color: var(--text);
  font-size: 13px;
}
#site-notice[hidden] { display: none; }

/* The maintenance door — full-page, friendly, one action. */
#maintenance-door {
  max-width: 480px;
  margin: 18vh auto 0;
  padding: 28px 32px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--panel);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: center;
}
#maintenance-door h2 { margin: 0; font-size: 20px; }
#maintenance-door p { margin: 0; font-size: 14px; }
#knock-btn {
  font-size: 15px;
  padding: 10px 26px;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
  border-radius: 10px;
}
#knock-btn:disabled { opacity: 0.6; }

/* The retro host on the maintenance door: tiny canvas scaled up with
   hard pixel edges. */
#retro-host {
  width: 192px;
  height: 144px;
  image-rendering: pixelated;
  border: 1px solid var(--border);
  border-radius: 8px;
}
#maintenance-door .syl-fomo {
  color: var(--dim);
  font-style: italic;
  font-size: 13px;
  min-height: 2.6em;
  transition: opacity 0.4s;
}

/* The live broadcast host on the door: the script's sprite canvas
   scaled into a fixed TV frame; whatever overflows is offscreen
   broadcast, not page scroll. */
#retro-host-live {
  width: 100%;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #0a0d14;
}
/* The script frames itself (640×880, face centered) — the frame's
   only job is to scale it down whole, never crop. An explicit px
   bound, because max-height:100% against an auto parent binds to
   nothing (the first door build chopped the chin exactly this way). */
#retro-host-live .rai-sprite-canvas,
#retro-host-live canvas {
  max-width: 100%;
  max-height: 400px;
  width: auto;
  display: block;
  margin: 0 auto;
}

/* The running strip advertises its own scroll: a thin real scrollbar
   plus JS-driven edge fades (see updateStripFades) — a bare mid-word
   clip under the widen button reads as overdraw, not overflow. */
#running-strip {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
  /* Chips snap whole: a partially-scrolled chip shows a bare ✕ — a
     script-killing control with no name beside it, and a destructive
     action must be legible before it's clickable (Sonar). */
  scroll-snap-type: x mandatory;
}
.rai-run-chip { scroll-snap-align: start; }
#running-strip::-webkit-scrollbar { height: 5px; }
#running-strip::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
}
#running-strip::-webkit-scrollbar-track { background: transparent; }

/* Variant divergence: 'plain must not render byte-identical to
   'secondary (it did — Sonar's gallery critic measured matching
   backgrounds AND borders, which erased the recessive arm of any
   two-button choice). Plain is the quiet one: no fill, no border. */
.rai-button[data-variant="plain"] {
  background: transparent;
  border-color: transparent;
  /* Not bare --dim: that measured 4.42:1 on --panel — under WCAG
     AA's 4.5 (Sonar, computed). The mix clears ~7:1 on every ground
     while staying under half of 'secondary's weight — recession
     comes from contrast with neighbours, not from illegibility. And
     it derives from the theme tokens, so wire themes keep the same
     relationship. */
  color: color-mix(in srgb, var(--dim) 60%, var(--text));
}
.rai-button[data-variant="plain"]:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--border);
}


/* Native selects are the one control that ships light-mode chrome
   into a dark UI unless explicitly told otherwise — the gallery's
   two selects measured as the brightest elements on the page
   (Sonar). Dark chrome + an inline chevron; the data: URI is
   CSP-safe. */
.rai-picker {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 30px 6px 10px;
  font: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23848c9a' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
.rai-picker:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Asset-card names: the head shrinks with an ellipsis, the tail (the
   last two dot-segments — the discriminating part of generated names)
   never does. The split spans come from the drag-source renderer;
   min-width:0 on the container lets it shrink inside its flex row. */
.rai-asset-name {
  display: flex;
  min-width: 0;
}
.rai-asset-name-head {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rai-asset-name-tail {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* ── The phone is not a narrow desktop ──────────────────────────────
   Collapsing isn't designing (Sonar's phone pass): the one-column
   fallback held together but kept desktop numbers. These are the
   mechanical halves — touch sizes, the page owns the scroll, rails
   become rows. Stack order and empty-panel collapsing are product
   decisions and live elsewhere. */

@media (pointer: coarse) {
  /* 29px is the house control height and it's a desktop number; 44pt
     is the floor a finger needs — in BOTH axes, and for every control
     element, not just <button>: the app-picker <select> is the phone's
     primary navigation and the ↗ asset links are its most numerous
     small target. Padding grows the TARGET, not the glyph. */
  /* input/textarea joined after the THIRD miss of this hand-listed
     set (select+a, then icon width, then text fields). If it misses
     a fourth time, stop enumerating: tag touch targets at the
     renderer instead (Sonar's standing proposal). */
  button,
  select,
  summary,
  input,
  textarea,
  [role="button"],
  a.rai-button {
    min-height: 44px;
    padding-top: 10px;
    padding-bottom: 10px;
  }
  .rai-panel-close,
  .rai-card-retire,
  .rai-card-star,
  .rai-run-stop,
  .rai-icon-only,
  .rai-toast-undo,
  #workspace.stage-wide > .rai-dock::after {
    min-width: 44px;
    min-height: 44px;
  }
}

@media (max-width: 640px) {
  /* One scroll surface: a 70vh inner scrollbox on a device that
     already scrolls costs the one gesture phones do perfectly. */
  .rai-scroll {
    max-height: none;
  }
  /* Collapsed dock rails are a desktop shape — sideways labels on
     ~450px strips read as empty black bars in a single column. As
     rows they keep their tap-to-reopen job at one line's cost. */
  #workspace.stage-wide > #dock-left,
  #workspace.stage-wide > #dock-right {
    width: auto;
    min-width: 0;
    min-height: 0;
    position: static;
  }
  #workspace.stage-wide > .rai-dock::after {
    writing-mode: horizontal-tb;
    /* Orientation leftovers, killed on purpose: the desktop rail's
       `margin: 0 auto` centred a VERTICAL label across a 32px strip —
       carried into a 321px row it parked the label dead centre while
       TOOLS & LIBRARY sat left. Values chosen because of an
       orientation must die with the orientation. */
    margin: 0;
    padding: 12px 14px;
  }
  /* The library fold must share the strips' edges — #apps's own
     horizontal padding inset it 10px and narrowed it 20px, the one
     box in the stack on different edges. */
  #apps {
    padding-left: 0;
    padding-right: 0;
  }
}

/* The theme picker, under Account. Swatches show the theme rather than
   naming it: the choice IS the appearance, so a label is a description
   of something the reader could simply be shown. */
#theme-ctl { position: relative; display: inline-block; }
#theme-btn .theme-btn-dot {
  display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  margin-right: 6px; vertical-align: baseline;
  background: var(--accent, currentColor);
}
#theme-btn .theme-btn-caret { margin-left: 6px; opacity: 0.7; }
#theme-pop {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  background: var(--panel, #161b22); border: 1px solid var(--edge, #30363d);
  border-radius: 8px; padding: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}

.theme-pick-title {
  margin: 18px 0 8px;
  font: 500 13px/1.3 system-ui, sans-serif;
  color: var(--dim, #8b93a1);
}

.theme-pick {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.theme-swatch {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 10px 14px;
  border: 1px solid var(--border, #2c333d);
  border-radius: 10px;
  font: 500 13px/1 system-ui, sans-serif;
  cursor: pointer;
}

/* The selected theme reads as selected from its ring, not from its
   colours — the swatch's own background is the theme being offered, so
   it cannot also carry the selection state. */
.theme-swatch[aria-checked="true"] {
  outline: 2px solid var(--accent, #5cf09d);
  outline-offset: 2px;
}

.theme-swatch:focus-visible {
  outline: 2px solid var(--accent, #5cf09d);
  outline-offset: 2px;
}

.theme-swatch-dot {
  width: 14px;
  height: 14px;
  border-radius: 999px;
  flex: 0 0 auto;
}

/* Fountain: plain text by format, so a monospace measure wide enough
   that scene headings and dialogue keep their column positions. */
.rai-fountain {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  line-height: 1.45;
  min-height: 220px;
  width: 100%;
  white-space: pre-wrap;
  tab-size: 4;
}

/* image-preview: zoom is a real behaviour, capped by :max-zoom. */
.rai-image-zoomable { transition: transform 120ms ease-out; transform-origin: center center; }
.rai-image-zoomed { position: relative; z-index: 5; }

/* A live alternative inside the note (Kane #125): the second verb, offered
   where the first was reported. */
.rai-tp-note-act {
  margin-left: 8px;
  padding: 0 6px;
  font-size: 11px;
  line-height: 16px;
  border: 1px solid var(--dim, #8a93a0);
  border-radius: 3px;
  background: transparent;
  color: var(--fg, #e6e9ee);
  cursor: pointer;
}
.rai-tp-note-act:hover { border-color: var(--accent, #fcd34d); }

/* An unbound knob: the binding could not be resolved, so the control is
   shown INACTIVE rather than at a value. Deliberately reads as "not
   available" and not as "set to minimum" — the whole defect this exists to
   fix was a dial that looked settled while nothing had set it. No arc is
   painted (render.js removes it); this only has to stop the ring reading
   as a live control. */
.rai-knob-unbound {
  opacity: 0.38;
  cursor: not-allowed;
  filter: grayscale(1);
}
.rai-knob-unbound:hover {
  /* No hover response: a control that cannot take a value must not
     answer the pointer as though it might. */
  opacity: 0.38;
}

/* A control whose binding did not resolve. THE DIMMING IS THE OBSERVABLE:
   for checkbox, toggle, picker and slider the broken state and the correctly
   withheld state are the same pixels, so without a visible treatment a
   refusal could ship, change nothing on screen, and pass any check that
   looks at the page. This is what verification keys on. */
.rai-unbound {
  opacity: 0.38;
  cursor: not-allowed;
  filter: grayscale(1);
}
.rai-unbound input,
.rai-unbound select,
.rai-unbound textarea {
  cursor: not-allowed;
}

/* ── THE SCRUB TARGET SHE WAS ACTUALLY AIMING AT ──────────────────────────
   AT THE END OF THE FILE ON PURPOSE, AND THAT IS THE WHOLE POINT OF THIS
   COMMENT. The first version of this rule sat near the top, and
   `@media (pointer: coarse)` ADDS NO SPECIFICITY — so the base
   `.rai-tp-ruler { height: 18px }` at its own line simply won, being later.
   It measured as 18px on a real phone while reading as fixed in the diff.
   Anything that overrides a base rule for this widget has to come after it.

   Raina, 2026-10-03: "i move some clips by accident on the phone WHILE I WAS
   TRYING TO DRAG THE TIMELINE CURSOR." Measured by Tiki: `.rai-tp-ruler` had
   exactly one rule in the stylesheet — 18px, no coarse override anywhere —
   sitting directly above lanes this branch had raised to 64px BECAUSE 18px
   is too small for a thumb.

   ⚠ THAT ASYMMETRY DID NOT ONLY MAKE THE ACCIDENT WORSE, IT MADE IT MORE
   LIKELY: I enlarged the wrong target and left the right one at mouse size,
   so the easiest thing to hit was the thing she did not want. Without this,
   the arming gate stops the accidental edit and her intended scrub still
   fails — now as a horizontal pan where she wanted a playhead move. */
@media (pointer: coarse) {
  .rai-tp-ruler {
    height: 44px;
  }

  /* The ticks keep their own size; only the strip they live on grows, so
     the time labels sit at the foot of a band a thumb can land on. */
  .rai-tp-tick {
    display: flex;
    align-items: flex-end;
    height: 100%;
    padding-bottom: 2px;
  }
}
