/* Set UI. Same design system as topograph: 8px grid, Figtree, flat bar
   sliders, glass panel cards, light and dark themes off one set of tokens.
   The only intentional divergence is corner radius: topograph is near-square
   at 1px, Set rounds to 8px on containers and 4px on controls.

   The theme paints chrome only. Everything you export (stage background,
   lights, floor) is scene state and never follows the UI theme. */
/* THE FOUC GUARD'S OTHER HALF. The pages hide the body in an inline
   <style> and this file showing up is what reveals it: Firefox paints before
   a pending stylesheet has arrived and drew the raw document. Equal
   specificity to the inline rule, and later in the document, so this wins.
   Keep it in THIS file rather than in the last stylesheet a page happens to
   load: every page loads styles.css second, and the rest arrive with it. */
.css-wait body { visibility: visible }


:root {
  /* 8px grid */
  --space-0_5: 4px;
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 40px;
  --space-6: 48px;
  --space-7: 56px;

  /* Control heights. NOT the spacing scale doing double duty: these size the
     controls, and tuning the gap between two rows must never resize the
     buttons standing in them. Three tiers, each measured against real
     content. 32 is a sidebar button, field, colour well and segmented
     choice. 28 is the compact row, which is the device pills, the shot tools
     and everything in the timeline bar: a 32px `.btn` stood a few pixels
     proud of the field and the select either side of it. 40 is the
     affirmative in a modal. */
  --control-h: 32px;
  --control-h-sm: 28px;
  --control-h-lg: 40px;

  /* Type */
  --font: 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --mono: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-h1: 20px;
  --font-h2: 16px;
  --font-body: 14px;
  --font-body-small: 12px;
  /* Below body-small the sizes stop being a scale and become roles, which is
     why they are named for the role rather than numbered. Label is a device
     chip and a catalogue button; caption is the fps readout, the media meta
     line and the progress note; micro is a kbd key in the Help card. All
     three were raw px in six places before they were named. */
  --font-label: 13px;
  --font-caption: 11px;
  --font-micro: 10px;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Leading. Tight heads the sidebar, snug sets a caption, body sets anything
     that wraps. A fourth value, 1.45, had drifted onto .note and is folded
     into body here. Buttons keep a literal `/ 1` in their font shorthand:
     that is optical centring in a fixed-height box, not a rule about text. */
  --leading-tight: 1.2;
  --leading-snug: 1.4;
  --leading-body: 1.5;

  /* ONE RAMP, INDEXED FROM BOTH ENDS. These greys are not chosen, they are
     SOLVED: the ink steps are placed against the worst composite ground the
     glass can render over (its own alpha over a black stage in light, over a
     white one in dark), with margin over the 4.5 AA floor. Everything else is
     spaced around them. That is what makes contrast a property of DISTANCE
     ALONG THE SCALE rather than of a hand-tuned hex, and it is why none of
     these carry a "was #xxxxxx, bumped for AA" comment: the previous palette
     was an accretion of exactly those fixes, seven spot values in light and
     seven in dark with a hole through the entire midtone between them.
     Steps 03, 06 and 09 are that hole filled. Nothing uses them yet, and that
     is the point: there is now something to reach for.
     Every step is exactly neutral (r = g = b). Set's chrome is a viewing
     surround for colour-critical work, so a tint here would bias how the
     render beside it reads, the same reason a photo editor's chrome is grey.
     The old palette was not warm either: four of twenty tokens carried a
     tint, at chroma 0.002 to 0.005, at four DIFFERENT hues. That was noise,
     not a direction. */
  --grey-00: #090909;
  --grey-01: #141414;
  --grey-02: #2b2b2b;
  --grey-03: #404040;
  --grey-04: #565656;
  --grey-05: #6c6c6c;
  --grey-06: #7f7f7f;
  --grey-07: #949494;
  --grey-08: #aeaeae;
  --grey-09: #c9c9c9;
  --grey-10: #e4e4e4;
  --grey-11: #f2f2f2;
  --grey-12: #ffffff;

  /* Light: the ramp read from the top. Named on the AXIS (ground, surface,
     ink) rather than by vibe, so a name cannot lie about which one it is on.
     `--bg-secondary` used to mean a raised well and said background. */
  --ground: var(--grey-11);
  --surface: var(--grey-12);
  --surface-raised: var(--grey-10);
  --ink: var(--grey-01);
  --ink-2: var(--grey-04);
  --ink-3: var(--grey-05);
  /* THE ACCENT IS A GROUND OR AN EDGE. Never ink, never a bare hairline,
     never translucent. That rule is what lets it be ONE colour rather than a
     theme pair, so both values below are identical in light and dark and a
     selected chip is the same pixels in either.
     Signal yellow measures 1.59:1 against the white panel, so as a mark it
     fails even the 3:1 a boundary needs, and the brightest yellow that
     clears 3:1 there is #bc8d00, a dark gold: forcing it to be a mark is
     exactly what produced the #956300 amber this replaces, which measured
     within a whisker of the theoretical ceiling for that job. A fill carries its own ground,
     so the requirement moves to the ink ON it, where near-black is 11.62:1.
     It also separates from --rec by LIGHTNESS (0.26 in OKLCH) rather than by
     hue. The amber sat 46 degrees from the record red at 0.05 lightness
     apart, which is the recipe for two colours reading as one, and it read
     as a dull version of it in the timeline bar where the two meet.
     Not translucent, because a film takes the colour of whatever is behind
     it: a 22 percent yellow wash goes olive over near-black, which is the
     identity break this rule exists to close. Use a solid edge instead.
     RETIRED FROM SELECTION (2026-08-24): the chips, the active shot and the
     keyed select all wore it and Adi called the yellow off; selection is
     the seg's ink border and semibold everywhere now. The pair stays
     declared because design-system.html paints its own chrome with it and
     reads it through the CSSOM; everything above remains the bar anything
     reaching for it has to clear. */
  --accent: #ffc512;
  --on-accent: #141414;
  /* Denser than topograph's 0.7: its canvas is paper, ours is whatever is
     being rendered, and captions have to stay legible over a black stage. */
  /* DERIVED FROM THE SURFACE, not a literal beside it. The glass IS the
     surface with alpha, and while it was written out by hand the two
     disagreed: the dark glass stayed on the old #181818 after the surface
     moved to #141414, which put the composite ground four counts light and
     dropped dark ink-3 to 4.41:1, under AA. The ink steps are solved against
     this ground, so the ground has to be the one the ramp actually makes. */
  --panel-glass: color-mix(in srgb, var(--surface) 94%, transparent);
  /* A whisper, not a stroke. Topograph draws its panels with no visible
     outline at all (its light border is white on white glass) and lets the
     fill difference against the canvas be the edge. Set's ground is the
     render, so wherever the stage differs from the glass the fill carries
     the edge the same way, and the hairline only has work to do in the one
     case the fill cannot: white cards over the white boot stage. There it
     measures about 1.14:1, present on a close look and silent otherwise,
     which is the intent: an outline that asserts itself only where there
     would otherwise be no edge at all. */
  --panel-glass-border: rgba(0, 0, 0, 0.06);
  --border-color: rgba(0, 0, 0, 0.13);
  --border-strong: rgba(0, 0, 0, 0.26);
  --slider-track: rgba(0, 0, 0, 0.12);
  --hover: rgba(0, 0, 0, 0.05);
  --active: rgba(0, 0, 0, 0.09);
  /* Two meanings, two names, one value today. `--rec` MARKS A TIME (the
     playhead, a key, the armed record button) and `--danger` DESTROYS WORK.
     They were one token, so the next person to tune the danger red would have
     silently repainted the timeline. */
  --rec: #d93b30;
  --danger: #d93b30;

  --shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15);

  /* Radius: containers 8, controls 4, insets 2. The third tier is what a fill
     sitting INSIDE a control gets, where the control's own 4 would read as a
     second rounded box in a small space: a chip's close box, the media
     thumbnail, the switch knob, a kbd key. It was raw in seven places before
     it was named. */
  --radius: 8px;
  --radius-sm: 4px;
  --radius-xs: 2px;

  --transition: all 0.2s ease;
}

[data-theme="dark"] {
  /* THE SAME RAMP, READ FROM THE OTHER END. Dark is not a second palette: it
     is the same thirteen greys indexed in reverse, which is why it needs no
     values of its own and cannot drift away from light.
     Two deliberate asymmetries, both earned. The raised well goes UP from the
     surface rather than down, because a well in a dark theme has to be
     LIGHTER than what it sits on: at a step below, the shot thumbnails and
     the kbd keys had no ground at all. And the ink tiers sit two steps apart
     rather than one, because the glass is translucent and the stage sets the
     ground: the worst case is dark chrome over a light stage, which is the
     boot default. That spacing is also what makes the tier REAL. The old dark
     secondary and muted were 1.13x apart, a rounding error wearing two names,
     where light's were a genuine step.
     No accent here on purpose. It is ONE colour in both themes: that is what
     a ground buys, and it is what a theme pair kept failing to be. */
  --ground: var(--grey-00);
  --surface: var(--grey-01);
  --surface-raised: var(--grey-02);
  --ink: var(--grey-10);
  --ink-2: var(--grey-08);
  --ink-3: var(--grey-07);
  --panel-glass: color-mix(in srgb, var(--surface) 90%, transparent);
  /* Same whisper as light: near-black glass over a near-black render is the
     case the fill cannot carry, and a faint rim is the only edge that works
     there (a darker blur adds nothing, see --shadow below). */
  --panel-glass-border: rgba(255, 255, 255, 0.07);
  --border-color: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.24);
  /* Pitched against the panel as it actually renders, not against
     --panel-glass on its own. The glass is translucent, so the stage behind
     it sets the ground, and Set boots on a WHITE backdrop in both themes:
     0.9 x 20 + 0.1 x 255 composites to rgb(43,43,43), not rgb(20,20,20).
     The old #808080 / #7d7d7d measured 3.39:1 and 3.25:1 there. These reach
     5.31:1 and 4.70:1 on that worst-case ground, and 7.17:1 and 6.35:1 over
     a black stage. Raising the glass alpha instead cannot get there alone. */
  --slider-track: rgba(228, 228, 228, 0.12);
  --hover: rgba(255, 255, 255, 0.06);
  --active: rgba(255, 255, 255, 0.12);
  --rec: #ff453a;
  --danger: #ff453a;

  /* Elevation in the dark theme cannot be a darker shadow: the surfaces it
     lifts are already near black and the render behind them can be too, so a
     blacker blur adds nothing (the modal measured 1.10:1 over its own scrim).
     The rim is what carries the edge here, and the blur only carries the
     depth. */
  --shadow: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.09);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* Set by Theme.apply for the two frames a theme swap takes. Every control
   transitions `all`, so a swap otherwise animates each colour independently
   and the UI spends 200ms wearing neither palette. Switching mode should be
   instant; the movement in this app belongs to the camera, not the chrome. */
[data-theme-switching] *,
[data-theme-switching] *::before,
[data-theme-switching] *::after {
  transition: none !important;
}

html, body {
  height: 100%;
  overflow: hidden;
  background: var(--ground);
  font-family: var(--font);
  color: var(--ink);
  font-size: var(--font-body);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
}

*:focus { outline: none; }

/* NO FOCUS OUTLINE, ANYWHERE. This app draws none: not on a button, a select,
   a slider, a switch, the stage or the ruler. It carried one for a long time
   and the whole of that history is in the log; what settled it is that a ring
   is a stroke the interface never asked for, drawn round whatever the last
   click happened to land on, and it kept turning up in ordinary mouse work
   however carefully the :focus-visible rules were tuned. `*:focus` above
   removes the browser's own and this removes ours; author styles beat the UA
   stylesheet whatever its specificity, so those two lines are the whole rule.
   A control that needs to show it has focus says so in its OWN language
   instead, the way the select turns its chevron over. Anything new that needs
   to must do the same, and must not reach for an outline. */
*:focus-visible { outline: none; }


.ui { user-select: none; -webkit-user-select: none; }

/* ---------- stage ---------- */

#stage {
  position: fixed;
  inset: 0;
  background: var(--ground);
}

#view {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

/* The screens' halo on a Flat or Fade backdrop. Those two are painted by the
   stylesheet rather than by three, so a spot light has nothing to land on:
   this canvas is that light, worked out by hand and laid UNDER the WebGL
   frame, so everything on stage occludes it exactly as it occludes the
   backdrop colour it is painted over. It carries the colour too, because a
   canvas cannot add to a stylesheet underneath it; the CSS background stays
   where it is, so a frame this misses shows the backdrop rather than a hole.
   It is never in the DOM at all unless the Ambient switch is throwing one. */
.halo-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* Composition and reference guides are DOM-only. The exporter reads the
   WebGL canvas directly, so none of this presentation layer can leak into a
   saved image or video. */
.guide-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
}

.guide-frame {
  position: relative;
  overflow: hidden;
}

.guide-frame.has-frame {
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.34), inset 0 0 0 1px rgba(255, 255, 255, 0.72);
}

.guide-reference,
.guide-lines,
.guide-safe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.guide-reference { object-position: center; }

.guide-lines {
  color: #808080;
  mix-blend-mode: difference;
}

.guide-thirds .guide-lines {
  background:
    linear-gradient(to right, transparent calc(33.333% - .5px), currentColor calc(33.333% - .5px), currentColor calc(33.333% + .5px), transparent calc(33.333% + .5px), transparent calc(66.666% - .5px), currentColor calc(66.666% - .5px), currentColor calc(66.666% + .5px), transparent calc(66.666% + .5px)),
    linear-gradient(to bottom, transparent calc(33.333% - .5px), currentColor calc(33.333% - .5px), currentColor calc(33.333% + .5px), transparent calc(33.333% + .5px), transparent calc(66.666% - .5px), currentColor calc(66.666% - .5px), currentColor calc(66.666% + .5px), transparent calc(66.666% + .5px));
}

.guide-center .guide-lines {
  background:
    linear-gradient(to right, transparent calc(50% - .5px), currentColor calc(50% - .5px), currentColor calc(50% + .5px), transparent calc(50% + .5px)),
    linear-gradient(to bottom, transparent calc(50% - .5px), currentColor calc(50% - .5px), currentColor calc(50% + .5px), transparent calc(50% + .5px));
}

.guide-grid .guide-lines {
  background-image:
    linear-gradient(to right, currentColor 1px, transparent 1px),
    linear-gradient(to bottom, currentColor 1px, transparent 1px);
  background-size: 12.5% 12.5%;
}

.guide-safe {
  inset: 5%;
  width: 90%;
  height: 90%;
  border: 1px dashed #808080;
  mix-blend-mode: difference;
}

/* ---------- top bar ---------- */

#bar {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  right: var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  pointer-events: none;
  z-index: 20;
}

#bar > * { pointer-events: auto; }

.spacer { flex: 1; }

/* Holds the compact-width panel toggle and nothing else, so it stays out of
   the way until that breakpoint. */
#actions { display: none; }

/* ---------- buttons ---------- */

.btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  height: var(--control-h);
  padding: 0 var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: var(--weight-semibold) var(--font-body-small) / 1 var(--font);
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition);
  -webkit-tap-highlight-color: transparent;
}

.btn-primary {
  background: var(--ink);
  color: var(--surface);
}

.btn-primary:hover { opacity: 0.85; }

.btn-secondary {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--border-color);
}

.btn-secondary:hover { border-color: var(--border-strong); }

/* The affirmative button on a confirm that throws work away. `danger` was
   already a parameter on ui.confirm and rendered as an ordinary secondary,
   so the one dialog that most needed to look different looked the same.
   The label takes --surface for the same reason .btn-primary does: --danger is
   a deep red in light and a bright one in dark, so a fixed white label would
   read 4.55:1 in one theme and 3.41:1 in the other. Flipped it is 4.55 and
   5.41. */
.btn-danger {
  background: var(--danger);
  color: var(--surface);
  border-color: transparent;
}

.btn-danger:hover { opacity: 0.85; }

/* A take running: one red for everything that marks a time, the ruler's. */
.btn.taking {
  background: var(--rec);
  color: var(--surface);
  border-color: transparent;
}

/* The Transform card's phone group (remote.js): its rows are the body's own
   and space by its gap, and hidden it leaves no gap behind. */
.remote-box { display: contents; }
.remote-box[hidden] { display: none; }

/* The QR the phone scans: BLACK ON A WHITE TILE IN BOTH THEMES, a
   deliberate exception like the Alpha checker, because phone cameras read
   dark on light and an inverted code fails on some scanners. SVG, so no
   theme redraw. */
.remote-qr {
  display: block;
  width: 208px;
  height: 208px;
  margin: 0 auto;
  border-radius: var(--radius-card, 8px);
  background: #ffffff;
  padding: 8px;
  box-sizing: border-box;
}
.remote-qr svg { display: block; width: 100%; height: 100%; }

/* The quiet tier, not an invisible one. Ghost used to be bare text until the
   pointer found it, which left New and Save as reading as labels in a card
   full of real buttons. It keeps the secondary's edge and drops its fill:
   still a step back from Save, but visibly pressable at rest. */
.btn-ghost {
  color: var(--ink-2);
  border-color: var(--border-color);
}

.btn-ghost:hover {
  background: var(--hover);
  color: var(--ink);
  border-color: var(--border-strong);
}

.btn-lg { height: var(--control-h-lg); font-size: var(--font-body); }

.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.4; pointer-events: none; }

.icon-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-h);
  height: var(--control-h);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}

.icon-btn:hover { color: var(--ink-2); }
.icon-btn:active { transform: scale(0.95); }

/* The half-filled disc reads as the theme it will switch to. */
#theme-toggle svg { transition: transform 0.4s ease; }
[data-theme="dark"] #theme-toggle svg { transform: rotate(180deg); }

/* ---------- sidebar ---------- */

/* The column runs off BOTH edges of the window rather than stopping short
   of them: cards bleed past the edge while scrolling instead of being cut
   off inside a container that ends inside it. The scroller stands on the
   window's top and bottom, and the 16px the header rests at is PADDING on
   the scroller, so it scrolls away with the header; it was `top: 16px`
   until 2026-09-07, which cut every card at a hard line 16px under the
   window's edge on the way up while the bottom let them flow out. The
   trailing padding on the body is the same idea at the other end. */
#panel {
  position: fixed;
  top: 0;
  right: var(--space-2);
  bottom: 0;
  padding-top: var(--space-2);
  width: 336px;
  display: flex;
  flex-direction: column;
  z-index: 15;
  pointer-events: none;
  /* THE WHOLE COLUMN SCROLLS, header included: a pinned header had the cards
     sliding under it, cut off mid-word, where one flow reads as a list. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

#panel::-webkit-scrollbar { display: none; }

#panel > * { pointer-events: auto; }

/* The header carries the same glass as the cards below it. topograph can set
   its wordmark straight on the canvas because that canvas is paper; here the
   stage is whatever is being rendered, so the brand needs its own ground. */
#panel-head {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
  height: var(--space-6);
  padding: 0 var(--space-1) 0 var(--space-2);
  margin-bottom: var(--space-1);
  background: var(--panel-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--panel-glass-border);
  border-radius: var(--radius);
}

#panel-head h1 {
  font-size: var(--font-h1);
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  line-height: var(--leading-tight);
  color: var(--ink);
}

/* The wordmark opens the About page. Focus shows in the link's own language,
   its colour, the shift the theme toggle beside it makes under the pointer,
   and never as an outline: `*:focus-visible` at the top covers a link too. */
#panel-head h1 a { color: inherit; text-decoration: none; transition: var(--transition); }
#panel-head h1 a:hover,
#panel-head h1 a:focus-visible { color: var(--ink-2); }


#panel-body {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding-bottom: var(--space-2);
}

/* ---------- panel cards ---------- */

/* Glass, a 1px border, and NO drop shadow, the same treatment topograph gives
   its sidebar cards. The cards used to carry a lift as well, but #panel is a
   scroller with overflow-x hidden, so each card's blur was clipped at the
   sidebar's edges and read as a floating grey band under the card rather
   than depth. The border is what carries the edge, on the head and the cards
   both, which is why it sits at 0.12 in light rather than a subtler 0.06. */
.panel {
  flex: none;
  background: var(--panel-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--panel-glass-border);
  border-radius: var(--radius);
  padding: 0 var(--space-2);
}

[data-theme="dark"] .panel { border-color: var(--panel-glass-border); }

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--space-2) 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--ink-3);
  font: var(--weight-semibold) var(--font-body-small) / 1 var(--font);
  text-transform: uppercase;
  letter-spacing: 1px;
  transition: color 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}

.panel-header:hover { color: var(--ink); }

.panel-title { pointer-events: none; }

.panel-chevron {
  flex-shrink: 0;
  transition: transform 0.2s ease;
  pointer-events: none;
}

.panel-header[aria-expanded="false"] .panel-chevron { transform: rotate(-90deg); }

/* The bottom padding matches the card's 16px sides: the last row used to sit
   8px off the edge while everything beside it sat 16, and the slider rows
   made it obvious because their bar is the lowest ink in the row. */
.panel-body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.2s ease, padding-bottom 0.2s ease;
  padding-bottom: var(--space-2);
}

.panel-body[aria-hidden="true"] {
  grid-template-rows: 0fr;
  padding-bottom: 0;
}

/* A card revealed above the view opens with no ease (ui.revealCard). */
.panel.no-ease .panel-body { transition: none; }

.panel-body-inner {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

#fps {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--font-caption);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- control groups ---------- */

/* Rows are spaced by the column's gap, not their own margin: a contextual row
   that hides itself then costs nothing, with no stale margin left behind. */
.control-group {
  flex: none;
  min-height: 0;
}

/* Contextual rows hide themselves. This has to beat the display on .inline. */
.control-group[hidden] { display: none; }

.control-group > .cap {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-1);
  font-size: var(--font-body-small);
  font-weight: var(--weight-medium);
  color: var(--ink-2);
  margin-bottom: var(--space-0_5);
  line-height: var(--leading-snug);
}

/* THE TAIL OF A CAPTION IS ONE CLUSTER AT THE RIGHT END. A caption is its
   text followed by whatever rides the right end: the readout, a colour well,
   a reset, the key mark, a link. `space-between` above is right for the
   common two-piece caption and WRONG the moment there are three: it spread
   them evenly, so a Colour row (readout AND well) showed its Kelvin in the
   middle of the caption, and arming Rec did the same to every keyable row,
   the readout drifting to the centre as the mark appeared. The first element
   child is the first tail piece (the caption's text is a bare text node), and
   an auto margin there absorbs the free space before anything is
   distributed, so the tail stands flush right with its own gap inside it
   whatever it is made of. */
.control-group > .cap > :first-child { margin-left: auto; }

/* Sits at the right end of a caption, where a slider puts its readout. It is
   only in the DOM while it would do something, so it is never a dead control,
   and it stays faint until the row is being worked on. */
.cap-reset {
  display: inline-flex;
  align-self: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-2);
  opacity: 0.5;
  cursor: pointer;
  transition: opacity 120ms ease, color 120ms ease;
}

.cap-reset[hidden] { display: none; }

/* The key mark, in the same right-end slot as the reset and with the same
   manners: faint until the row is being worked on, and in the DOM only while
   it would do something, which here means only while Rec is armed. Filled in
   the ruler's own red when the row has a key at the playhead, because it
   marks the same thing the dots on the ruler do, and it is a DISC for the
   same reason: those dots are discs. */
.cap-key {
  display: inline-flex;
  align-self: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-2);
  fill: none;
  opacity: 0.5;
  cursor: pointer;
  transition: opacity 120ms ease, color 120ms ease;
}

.cap-key[hidden] { display: none; }

.control-group:hover > .cap > .cap-key { opacity: 0.75; }

.cap-key:hover,
.cap-key:focus-visible { opacity: 1; color: var(--ink); }

.cap-key.on {
  color: var(--rec);
  fill: var(--rec);
  opacity: 1;
}

.control-group:hover > .cap > .cap-reset { opacity: 0.75; }

.cap-reset:hover,
.cap-reset:focus-visible {
  opacity: 1;
  color: var(--ink);
}

/* A caption-level way into reference, in the same right-end slot as the
   reset. Unlike the reset it is always relevant, so it is always there, and
   it underlines because it acts like a link (opens the guide), not like a
   button that changes state. */
.cap-link {
  display: inline-flex;
  align-self: center;
  padding: 0;
  border: none;
  background: none;
  font-size: var(--font-body-small);
  font-weight: var(--weight-medium);
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  transition: color 120ms ease;
}

.cap-link:hover,
.cap-link:focus-visible { color: var(--ink); }

/* Switches read better beside their caption than under it. */
.control-group.inline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* The caption takes the row's slack so its tail (a switch's key mark) stands
   at the right end beside the control, in the column the sliders' marks make,
   rather than hanging off the label's last letter. */
.control-group.inline > .cap { margin-bottom: 0; flex: 1; }
.control-group.inline > .ctl { flex: none; }

/* A light's own name heads its group of controls. */
.control-group.subhead > .cap {
  color: var(--ink);
  font-weight: var(--weight-semibold);
}

/* Separates the device catalogue from what is already on stage. Space does
   the separating, not a rule: no grey divider lines anywhere in the chrome. */
.control-group.divide {
  padding-top: var(--space-2);
}

.val {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ctl {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
}

.ctl > * { min-width: 0; }
.ctl > .slider,
.ctl > .btn,
.ctl > .select-input,
.ctl > .text-input,
.ctl > input[type="color"] { flex: 1; }
.ctl > .seg { flex: 1; }

/* ---------- sliders ---------- */

.slider {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 6px;
  display: block;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: var(--slider-track);
  cursor: ew-resize;
  overflow: hidden;
}

/* The fill is painted as a background gradient (see paintSlider in ui.js),
   so the thumb itself is invisible: the bar is the value. */
.slider::-webkit-slider-runnable-track {
  height: 6px;
  background: transparent;
  border: none;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 0;
  height: 0;
  background: transparent;
  border: none;
}

.slider::-moz-range-track {
  height: 6px;
  background: transparent;
  border: none;
}

.slider::-moz-range-thumb {
  appearance: none;
  width: 0;
  height: 0;
  background: transparent;
  border: none;
}

.slider::-moz-range-progress { background: transparent; }

.slider:disabled { cursor: default; }

/* ---------- the shade pad ---------- */

/* A stand-in for the viewport, at the viewport's aspect (set from script),
   and the one control in the sidebar that is genuinely two dimensional. It
   keeps the slider's premise rather than growing a thumb: the SHADED REGION
   is the value, painted as a DARK film over a ground a step off the surface,
   and the edge is the one stroke. Dark in both themes, because a shade is a
   darker place and the fill has to mean that: ink at alpha would paint it
   LIGHT on the dark chrome and invert the picture. The ground mixes a little
   ink into the surface rather than naming a grey, so it steps the same way
   in either theme and the film has room to read. Twelve units tall so a row
   of it is a short paragraph, not a second stage; the width follows from the
   aspect. The ground is a FILL, the same rule the cards keep, so the pad has
   no outline to clip. */
.shade-pad {
  position: relative;
  height: 96px;
  aspect-ratio: 16 / 9;
  max-width: 100%;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface) 88%, var(--ink));
  cursor: crosshair;
  touch-action: none;
  user-select: none;
  overflow: hidden;
}

.shade-pad svg {
  display: block;
  width: 100%;
  height: 100%;
}

.shade-pad .sp-dark {
  fill: #000;
  fill-opacity: 0.4;
  /* The strip's two half-planes do not overlap, so an even-odd fill is only
     insurance against the day they do. */
  fill-rule: evenodd;
}

.shade-pad .sp-edge {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5px;
  vector-effect: non-scaling-stroke;
}

.shade-pad .sp-aim {
  fill: none;
  stroke: var(--ink-3);
  stroke-width: 1px;
  vector-effect: non-scaling-stroke;
}

/* The focus reticle over the stage (reticle.js). The layer is the frame:
   it takes the guide overlay's inset beside the sidebar and goes with the
   chrome in preview. It takes no pointer; the ring does. The ring is the
   axis widget's kind of quiet, 0.55 until hover, focus or a drag, and it
   says focus by opacity, its own language, never an outline. IT DOES NOT
   TAKE THE THEME'S INK: it sits on the render, which is scene and not
   chrome, so a theme-coloured ring vanished over a bright screen in dark
   mode. It is ONE hairline and nothing else, and it keeps its contrast by
   DIFFERENCE BLENDING: a white line inverts against whatever it sits on,
   dark over the white sweep, light over a dark screen, a complement over
   colour. A dark hairline either side of a white stroke was tried and read
   as a filled band, and a soft shadow under it still read as one; Adi
   asked for just the line. Its diameter is the aperture's
   (`--d`, set by reticle.js from the f-stop): wide open it is large, stopped
   down small, easing between stops as the slider moves, so the dial can be
   seen doing something to the picture's one lens control. */
.focus-layer {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.focus-ring {
  --d: 36px;
  position: absolute;
  width: var(--d);
  height: var(--d);
  margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2);
  border-radius: 50%;
  border: 1px solid #fff;
  mix-blend-mode: difference;
  opacity: 0.55;
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  user-select: none;
  transition: opacity 140ms ease, width 140ms ease, height 140ms ease, margin 140ms ease;
}

.focus-ring::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: #fff;
}

.focus-ring:hover,
.focus-ring:focus,
.focus-ring.dragging {
  opacity: 1;
}

.focus-ring.dragging { cursor: grabbing; }

/* ---------- fields ---------- */

.select-input,
.text-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  padding: var(--space-1);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  font: 400 var(--font-body) / 1.2 var(--font);
  background: var(--surface);
  color: var(--ink);
  transition: var(--transition);
}

.select-input:hover,
.text-input:hover { border-color: var(--border-strong); }

/* The one select that can go inert in place: the bar's easing control when
   the selected column is switches only (a switch holds, there is nothing to
   ease), and the bar's widths are pinned so it cannot hide. Same tone as a
   disabled button. */
.select-input:disabled { opacity: 0.4; cursor: default; }

/* Every other icon in the app is currentColor and follows the theme. This one
   is a data-URI, so it needs the second copy rather than inheriting. */
[data-theme="dark"] .select-input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%237d7d7d' d='M10.293 3.293L6 7.586 1.707 3.293A1 1 0 00.293 4.707l5 5a1 1 0 001.414 0l5-5a1 1 0 10-1.414-1.414z'/%3E%3C/svg%3E");
}

.select-input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236d6d6d' d='M10.293 3.293L6 7.586 1.707 3.293A1 1 0 00.293 4.707l5 5a1 1 0 001.414 0l5-5a1 1 0 10-1.414-1.414z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-1) center;
  background-size: 12px;
  padding-right: var(--space-4);
  cursor: pointer;
}

/* The chevron says which way the list is about to go, so turning it over is
   how a select says "I am the one you are about to change". It REPLACES the
   ring on this control rather than joining one. Per theme, because the arrow
   at rest is: a single rule here loses to `[data-theme="dark"] .select-input`
   on specificity and the dark chevron stayed pointing down. `:open` joins
   `:focus` because with the list below the control (the block after this)
   the engine moves focus INTO the list, and the arrow has to stay turned
   for as long as the list it points at is open. Inside `:is()`, so an engine
   without `:open` keeps the focus half instead of dropping the rule. */
.select-input:is(:focus, :open) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236d6d6d' d='M1.707 8.707L6 4.414l4.293 4.293a1 1 0 001.414-1.414l-5-5a1 1 0 00-1.414 0l-5 5a1 1 0 101.414 1.414z'/%3E%3C/svg%3E");
}

[data-theme="dark"] .select-input:is(:focus, :open) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%237d7d7d' d='M1.707 8.707L6 4.414l4.293 4.293a1 1 0 001.414-1.414l-5-5a1 1 0 00-1.414 0l-5 5a1 1 0 101.414 1.414z'/%3E%3C/svg%3E");
}

/* ---------- the select's list ---------- */

/* WHERE THE ENGINE ALLOWS IT, THE LIST IS OURS. A native <select> popup is
   drawn by the platform, and on macOS it behaves as an NSPopUpButton menu:
   the list is shifted so the CHECKED item's text lands over the control's
   text, with the checkmark column hanging out to the left of the control,
   and it opens OVER the control rather than under it. So every list in Set
   opened a checkmark's width left of its own control, at the platform's
   radius, ground and type, in a panel where every other edge stands on the
   grid. `appearance: base-select` (Chromium 135 and up, and the WebKit the
   gate runs; Firefox still draws its own) hands the list to the page: it is a popover anchored to the control, as wide as the control
   and flush with its left edge, dropped below it (or raised above it where
   there is no room below), and painted with the same tokens as everything
   else here. Engines without it keep their native popup, the same fallback
   the colour well makes. The option text sits at the control's own inset,
   so the list reads as the control's text continued downward; the mark for
   the current value rides the RIGHT end of its row, the slot a caption
   gives its readout and the control gives its chevron. */
@supports (appearance: base-select) {
  .select-input,
  .select-input::picker(select) {
    appearance: base-select;
  }

  /* The engine's own disclosure glyph. The chevron stays the data-URI above,
     so both paths draw one arrow and the focus rule keeps working. */
  .select-input::picker-icon { display: none; }

  .select-input::picker(select) {
    box-sizing: border-box;
    inline-size: anchor-size(self-inline);
    max-block-size: min(60vh, 400px);
    /* Below the control, and above it only when it does not fit below. The
       engine's default is `most-block-size`, which opens the list on
       whichever side has more room, so the Bitrate list in the Export
       dialog rose over the rows above it with the whole lower half of the
       dialog free. A list that goes down is the one the chevron promises. */
    position-try-order: normal;
    margin: var(--space-0_5) 0;
    padding: var(--space-0_5) 0;
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    box-shadow: var(--shadow-lg);
    overflow: auto;
  }

  [data-theme="dark"] .select-input::picker(select) { border-color: var(--border-strong); }

  .select-input option {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: var(--control-h-sm);
    padding: 0 var(--space-1);
    font: inherit;
    color: var(--ink);
    white-space: nowrap;
    cursor: pointer;
  }

  /* No ring on the list either: the row a key or the pointer is on takes the
     hover ground, the way a button does. */
  .select-input option:hover,
  .select-input option:focus-visible { background: var(--hover); }
  .select-input option:active { background: var(--active); }

  /* The current value reads the way the segmented choice reads its own. */
  .select-input option:checked { font-weight: var(--weight-semibold); }

  /* The column is reserved on every row so the labels stand still. */
  .select-input option::checkmark {
    order: 1;
    content: "";
    flex: none;
    margin-inline-start: auto;
    inline-size: 12px;
    block-size: 12px;
    background: currentColor;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23000' d='M10.293 2.293a1 1 0 011.414 1.414l-6.5 6.5a1 1 0 01-1.414 0l-3.5-3.5a1 1 0 011.414-1.414L4.5 8.086l5.793-5.793z'/%3E%3C/svg%3E") center / contain no-repeat;
    visibility: hidden;
  }

  .select-input option:checked::checkmark { visibility: visible; }
}

.text-input[type="number"] {
  -moz-appearance: textfield;
  font-variant-numeric: tabular-nums;
  /* Drag left or right on the field to change the value. */
  cursor: ew-resize;
}

.text-input[type="number"]::-webkit-inner-spin-button,
.text-input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

body.scrubbing,
body.scrubbing * { cursor: ew-resize !important; }

input[type="color"] {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  /* Capped so a swatch sharing a row with a slider does not eat half of it;
     three of them still fill the row exactly. */
  max-width: 96px;
  height: var(--control-h);
  min-width: 0;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
  /* ONE STROKE. The well is the field's own box, the border every text field
     wears, filled edge to edge with the colour: no padding and no ring on the
     swatch. It wore two until 2026-09-07, this border round 2px of glass and
     an inset hairline on the swatch, which read as a box drawn inside a box.
     The ring was there so a white swatch on a white card would not read as
     empty; the field's border does that job on its own, exactly as it does
     for an empty text field on the same card. */
  padding: 0;
  overflow: hidden;
}

/* A light's colour well sits at the right end of its caption, beside the
   readout, rather than in the control row: warmth and the override are one
   property, so they share one row, and the row underneath has to stay the
   plain 6px bar every other slider row is. Sized to the caption's text
   rather than to a control, or it drags the row's height with it. */
.cap > input[type="color"] {
  flex: 0 0 auto;
  width: 22px;
  height: 12px;
  max-width: none;
  align-self: center;
}

input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
/* The swatch is the fill and nothing else: the field's border is the stroke,
   and the field clips the fill to its own corners. */
input[type="color"]::-webkit-color-swatch {
  border: none;
  border-radius: 0;
}

input[type="color"]::-moz-color-swatch {
  border: none;
  border-radius: 0;
}

/* ---------- colour picker ---------- */

/* The well's own popover (src/colorpicker.js), the select list's terms: the
   list's surface, border, radius and lift, standing 4px under the well and
   flush with its left edge. 272 wide on the 8px grid: a 256 SQUARE for
   saturation and brightness (a square gives the two axes the same travel,
   and the width is what the sidebar has), the slider's own 6px bar for the
   hue, and one row of boxes at the control height: the screen dropper where
   the engine has one, the hex in the readout's mono, and a copy. Above the
   sidebar and the bar (15), the header (20) and a dialog (60), under the
   toast. No opacity field: nothing in Set carries an alpha, and a control
   that cannot act is not there. */
.picker {
  --picker-hue: #f00;
  position: fixed;
  z-index: 65;
  width: 272px;
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  user-select: none;
}

[data-theme="dark"] .picker { border-color: var(--border-strong); }

.picker-sv {
  position: relative;
  height: 256px;
  border-radius: var(--radius-sm);
  background:
    linear-gradient(to top, #000, transparent),
    linear-gradient(to right, #fff, var(--picker-hue));
  cursor: crosshair;
  touch-action: none;
}

/* The hue bar is 6px like every slider, and its hit area reaches 8px into
   the gaps either side (the margins take back the padding), so the rhythm
   stays 8 / 6 / 8 while a finger has 22px to land on. */
.picker-hue {
  position: relative;
  /* Content-box, or the page's border-box sizing lets the padding swallow
     the 6px and the bar paints nothing. */
  box-sizing: content-box;
  height: 6px;
  padding: var(--space-1) 0;
  margin: calc(-1 * var(--space-1)) 0;
  background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
  background-clip: content-box;
  border-radius: var(--radius-sm);
  cursor: ew-resize;
  touch-action: none;
}

/* ONE MARKER FOR BOTH: a 12px ring, the reticle's hairline, white under
   difference so it inverts against whatever colour it stands on; on the bar
   it rides over the 6px, three past each edge. Focus thickens the ring, the
   control's own language; there is no outline on anything. */
.picker-mark,
.picker-knob {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border: 1px solid #fff;
  border-radius: 50%;
  mix-blend-mode: difference;
  pointer-events: none;
}

/* The bar's knob is FILLED with the colour picked, since the bar under it
   shows only the hue: 14px, cut out of the bar by a 2px stroke in the
   popover's own surface, which reads in both themes with no blend. */
.picker-knob {
  top: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border: 2px solid var(--surface);
  background: var(--picker-color, #f00);
  mix-blend-mode: normal;
}

.picker-sv:focus .picker-mark { border-width: 2px; }
.picker-hue:focus .picker-knob { border-width: 3px; }

/* The row is three boxes of one height: a bordered square either side of
   the field, so the dropper and the copy read as the field's own kind. */
.picker-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.picker-hex {
  flex: 1;
  height: var(--control-h);
  font-family: var(--mono);
  font-size: var(--font-body-small);
  text-align: center;
  user-select: text;
}

.picker-btn {
  flex: none;
  width: var(--control-h);
  height: var(--control-h);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.picker-btn:hover { border-color: var(--border-strong); background: var(--hover); }

/* ---------- segmented choice ---------- */

.seg {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-0_5);
  width: 100%;
}

.seg button {
  appearance: none;
  min-width: 0;
  height: var(--control-h);
  padding: 0 var(--space-0_5);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2);
  font: var(--weight-medium) var(--font-caption) / 1 var(--font);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: var(--transition);
}

.seg button:hover { border-color: var(--border-strong); color: var(--ink); }

/* THE OPTION YOU PICKED FROM ITS PEERS WEARS THE ACCENT AS A GROUND, the
   device pill's rule to the letter (2026-09-07): yellow fill, near-black
   ink, the border the same yellow so no stroke shows, the weight untouched.
   It spoke the ink border and a semibold from 2026-08-24; the pill moved to
   the ground first and a seg saying the same thing another way is two
   languages for one idea. Nothing that time owns takes this: Rec, the key
   marks and the keyed select stay red or ink. */
.seg button.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.seg button.on:hover { border-color: var(--accent); color: var(--on-accent); }

/* ---------- switch ---------- */

.tgl {
  position: relative;
  width: 44px;
  height: 24px;
  flex: none;
  cursor: pointer;
}

.tgl input { position: absolute; opacity: 0; inset: 0; cursor: pointer; margin: 0; }

.tgl i {
  position: absolute;
  inset: 0;
  background: var(--slider-track);
  border-radius: var(--radius-sm);
  transition: background 0.2s ease;
}

.tgl i::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-xs);
  background: var(--surface);
  /* The off knob is --surface on a 12% track over a white card, which is
     1.32:1 and effectively invisible. The lift is what separates them. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  transition: transform 0.2s ease, background 0.2s ease;
}

/* Dark used to write #404040 and #808080 inline, the only two theme colours in
   the file that the palette could not reach, and the pair measured 2.63:1
   against the 3:1 WCAG asks of a control. */
[data-theme="dark"] .tgl i { background: #3a3a3a; }
[data-theme="dark"] .tgl i::after { background: #a0a0a0; }

.tgl input:checked + i { background: var(--ink); }
.tgl input:checked + i::after {
  transform: translateX(20px);
  background: var(--surface);
}

[data-theme="dark"] .tgl input:checked + i { background: var(--ink); }
[data-theme="dark"] .tgl input:checked + i::after { background: var(--ground); }


/* ---------- device catalogue and chips ---------- */

.adder { display: flex; flex-wrap: wrap; gap: var(--space-0_5); }

.add-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-0_5);
  height: var(--control-h-sm);
  padding: 0 10px 0 8px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2);
  font: var(--weight-medium) var(--font-body-small) / 1 var(--font);
  cursor: pointer;
  transition: var(--transition);
}

.add-btn::before {
  content: "+";
  font-size: var(--font-label);
  line-height: 1;
  color: var(--ink-3);
}

.add-btn:hover {
  border-color: var(--border-strong);
  color: var(--ink);
}

.add-btn:hover::before { color: var(--ink); }

.add-btn:disabled { opacity: 0.4; cursor: default; }

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-0_5);
}

.chip {
  display: inline-flex;
  align-items: center;
  height: var(--control-h-sm);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  font-size: var(--font-body-small);
  transition: var(--transition);
}

.chip-main {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: var(--weight-medium) var(--font-body-small) / 1 var(--font);
  padding: 0 var(--space-0_5) 0 10px;
  height: 100%;
  cursor: pointer;
}

.chip:hover { border-color: var(--border-strong); color: var(--ink); }

/* THE DEVICE PILL WEARS THE ACCENT AS A GROUND, and nothing else changes:
   the yellow fill, the near-black ink on it, the border the same yellow so
   no stroke shows, the weight untouched. This pill and ONLY this pill,
   which Adi chose on 2026-09-07 (the active shot and the keyed select were
   painted with it the same day and called back within the hour: the row
   went to a yellow slab, and neither is the thing the yellow is for). They
   keep the seg's language, the ink border. Before 2026-08-24 the accent was
   a ground here and a 3px EDGE on those two, and the edge is what read as a
   mark. Never an edge. */
.chip.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.chip.on .x { color: var(--on-accent); }
.chip.on:hover { border-color: var(--accent); color: var(--on-accent); }
.chip.on .x:hover { background: rgba(0, 0, 0, 0.08); color: var(--on-accent); }

.chip .x {
  appearance: none;
  border: 0;
  background: transparent;
  width: 20px;
  height: 20px;
  margin-right: 4px;
  border-radius: var(--radius-xs);
  display: grid;
  place-items: center;
  color: var(--ink-3);
  font-size: var(--font-label);
  line-height: 1;
  cursor: pointer;
}

.chip .x:hover { background: var(--hover); color: var(--ink); }

.note {
  font-size: var(--font-body-small);
  color: var(--ink-3);
  line-height: var(--leading-body);
  /* A status line carries a file name the user chose, and a long one is a
     single unbreakable token: without this it ran to the card border and was
     sliced mid-character with nothing to say it had been. */
  overflow-wrap: anywhere;
}

/* ---------- named shots ---------- */

.shot-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.shot-item {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: var(--space-1);
  padding: var(--space-1);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* The seg's word for "this one": the border goes to ink. It wore a 3px
   accent edge once, and for an hour on 2026-09-07 the accent as a ground,
   which turned the row into a yellow slab: the yellow is the device pill's
   and nothing else's. */
.shot-item.active {
  border-color: var(--ink);
}

/* Stretched, not fixed at 44px: it spans the name row and the tools row, and
   a hard height left a hole under it whenever those two did not add up to
   exactly 44. Stretching keeps it flush whatever the rows come to. */
.shot-pick {
  grid-row: 1 / span 2;
  align-self: stretch;
  width: 64px;
  padding: 0;
  overflow: hidden;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  cursor: pointer;
}

.shot-thumb {
  display: block;
  width: 100%;
  height: 100%;
  background-position: center;
  background-size: cover;
}

.shot-name { height: var(--control-h-sm); padding: var(--space-0_5) var(--space-1); font-size: var(--font-body-small); }
.shot-tools { display: flex; min-width: 0; gap: var(--space-0_5); }
.shot-tool { flex: 1; height: var(--control-h-sm); padding: 0 var(--space-0_5); font-size: var(--font-body-small); }

/* Delete is not a third reorder button: it reads in the warning colour so the
   irreversible one is not the same as the two that are not. It used to carry
   a rule to its left to break a run of three borderless buttons; the ghost
   tier wears its own border now, so the rule went with the run. */
.shot-tool-danger {
  color: var(--danger);
}

.shot-tool-danger:hover { background: var(--hover); color: var(--danger); }
.reference-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- media well ---------- */

.media-well {
  appearance: none;
  width: 100%;
  color: inherit;
  font: inherit;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-1);
  cursor: pointer;
  transition: var(--transition);
}

.media-well:hover { border-color: var(--ink); }

.media-well .thumb {
  width: 48px;
  height: 32px;
  border-radius: var(--radius-xs);
  background: var(--surface-raised) center/cover no-repeat;
  border: 1px solid var(--border-color);
  flex: none;
}

.media-well .meta { min-width: 0; }

.media-well .meta b {
  display: block;
  font-size: var(--font-body-small);
  font-weight: var(--weight-semibold);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.media-well .meta span { font-size: var(--font-caption); color: var(--ink-3); }

.media-row { display: flex; gap: var(--space-1); align-items: center; width: 100%; }
.media-row .media-well { flex: 1; min-width: 0; }
.media-x { flex: none; width: 32px; padding: 0; font-size: 18px; line-height: 1; }

/* ---------- timeline ---------- */

#timeline {
  position: fixed;
  left: var(--space-2);
  right: 368px;
  bottom: var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--panel-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--panel-glass-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* The side inset IS the vertical one. The ruler is the tallest child at
     56px and the controls are 28, so a control sits 8 + 14 = 22px under the
     bar's top edge; the sides used to stop at the bare 8, and Motion stood
     nearly three times further from the top than from the right edge. The
     number is derived rather than written, so a taller ruler or a different
     control height moves both insets together. */
  --tl-inset: calc(var(--space-1) + (var(--space-7) - var(--control-h-sm)) / 2);
  padding: var(--space-1) var(--tl-inset);
  z-index: 15;
}

/* The two transport buttons are a cluster like the controls on the right, at
   the same 4px, and they are a box in the bar's own 28px row (they stood at a
   sidebar button's 32 for a long time). With the inset above, the play box
   sits where Motion's box sits on the far side, and the triangle inside it
   sits where the word Motion does. */
.tl-left { display: flex; align-items: center; gap: var(--space-0_5); flex: none; }

.tl-btn {
  appearance: none;
  border: none;
  width: var(--control-h-sm);
  height: var(--control-h-sm);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  display: grid;
  place-items: center;
  flex: none;
  transition: background 0.15s ease;
}

.tl-btn:hover { background: var(--hover); }
.tl-btn.on { background: var(--active); }
.tl-btn.rec.on { color: var(--rec); background: transparent; }
.tl-btn svg { display: block; }

/* Seven rows of the grid: the playhead badge on 16, the tick row from 24 to
   32, and the THREE key lanes on 38, 44 and 50, with room under the lowest
   for a selected key's ring. It was six rows and two lanes; the scene lane is
   what the seventh bought. The lanes are six apart rather than eight, which
   is what kept this at 56 instead of 64: _geom says why. */
#ruler {
  flex: 1;
  height: var(--space-7);
  min-width: 120px;
  cursor: crosshair;
  border-radius: var(--radius-sm);
  /* The last tick stands the bar's inset from the field beside it, the way
     the first stands it from the rec dot: the ruler insets its ticks by 5px
     (EDGE in timeline.js, half a key mark) and the bar's gap is 8, so the
     rest of the 22 is carried here. */
  margin-right: calc(var(--tl-inset) - var(--space-1) - 5px);
}

.tl-right { display: flex; align-items: center; gap: var(--space-0_5); flex: none; }

.tl-right .select-input {
  width: auto;
  font-size: var(--font-body-small);
  padding: 6px 26px 6px 8px;
}

/* One compact row. The bar's controls are the same 28px the device pills are,
   not the 32px a sidebar button gets: `.btn` stood a few pixels proud of the
   field and the select either side of it. */
.tl-right .btn,
.tl-right .text-input,
.tl-right .select-input { height: var(--control-h-sm); }

/* The key button swaps its label between a plus and a minus, and Figtree's
   plus is wider than its minus. Left to size itself it would shove the ease,
   the preset and the ruler along every time a key was selected, so it is
   held at the row's own 28px square, the transport buttons' box, which puts
   the plus at the selects' 8px inset. It said "+ Key" at 48px until
   2026-09-07; the title and the aria-label carry the word now. */
#tl-key {
  width: var(--control-h-sm);
  padding: 0;
  flex: none;
}

/* The two selects are held the same way, at the width of their WIDEST label
   ("Overshoot", "Camera push-in": 12px Figtree plus the 8px inset, the 26px
   the chevron takes and two hairlines), because once the list is the page's
   own (`appearance: base-select`, the block after the fields) the engine
   sizes a select to the label it is SHOWING rather than to the widest one it
   could show, and a bar whose selects breathed with their value would move
   the ruler on every pick. These are the widths the native control measured
   on the same labels, so in an engine that keeps its native popup nothing
   moves either. A new preset longer than "Camera push-in" has to raise the
   second number. */
#tl-ease { width: 93px; }
#tl-preset { width: 122px; }

/* The easing control changes what it addresses when a key is selected, so it
   says so rather than swapping meaning behind the same face: the ink border,
   the same word the seg and the active shot use. It wore a 3px accent edge,
   and before that a tinted border AND label, which was the accent as ink;
   the accent as a ground here lasted an hour on 2026-09-07 (the yellow is
   the device pill's alone). No font change: the bar's selects are pinned
   to measured widths and a semibold label could outgrow its own box. */
.tl-right .select-input.keyed {
  border-color: var(--ink);
}

/* The duration field is sized to its widest legal value ("59.5", 23px at
   this size: the step is a half second, so four characters are a fact of the
   control and not a corner) at the selects' 8px inset. It was 52px with 4px
   of padding, which left "60" swimming. Fixed rather than fitted to the
   value, for the reason the selects are pinned. */
.tl-right .text-input {
  width: 42px;
  text-align: center;
  font-size: var(--font-body-small);
  padding: 6px var(--space-1);
}

/* ---------- modal ---------- */

#modal-root {
  position: fixed;
  inset: 0;
  display: none;
  place-items: center;
  background: rgba(9, 10, 10, 0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 60;
}

#modal-root.open { display: grid; }

/* Bounded, with the body as the only scroller. The boot gate lets the window
   be 500px tall and Export motion is 563px with a lens row, so an unbounded
   dialog put Render and Cancel below the fold with nothing able to scroll to
   them: #modal-root centres without overflow and html/body are hidden. */
.modal {
  width: 400px;
  max-height: calc(100vh - var(--space-4));
  display: flex;
  flex-direction: column;
  background: var(--surface);
  /* Dark takes the stronger edge: a #131313 modal over a scrim that composites
     to rgb(4,5,5) on a dark render measured 1.10:1 on its fill and 1.35:1 on
     this border, so the dialog had no outline at all. */
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

[data-theme="dark"] .modal { border-color: var(--border-strong); }

.modal header,
.modal footer { flex: none; }

.modal header { padding: var(--space-2) var(--space-2) var(--space-1); }

.modal header h2 {
  font-size: var(--font-h2);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.01em;
}

.modal .m-body {
  padding: var(--space-1) var(--space-2) var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow-y: auto;
  min-height: 0;
}

/* The dialog's notes, one block: consecutive lines, not rows. A hidden
   note leaves no gap behind, the sidebar's rule. */
.modal .notes { display: flex; flex-direction: column; gap: var(--space-1); }
.modal .notes:empty { display: none; }

/* No rule above the buttons: the footer separates from the body by space,
   the same way the sidebar's groups do. */
.modal footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2) var(--space-2);
}

/* The last frame of the session being offered back. Sized by the stage's own
   16:9-ish proportions rather than the shot, so a portrait scene does not make
   the dialog jump. */
.recover-shot {
  width: 100%;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-color);
  background: var(--surface-raised) center/cover no-repeat;
}

/* The capture guide: four titled recipes, a couple of steps each. Sections
   are spaced by the modal body's own gap; inside one, the head and its steps
   hold together on the half step. */
.guide { display: flex; flex-direction: column; gap: var(--space-0_5); }

.guide-head {
  font-size: var(--font-body);
  font-weight: var(--weight-semibold);
  color: var(--ink);
}

.guide-steps {
  margin: 0;
  padding-left: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-0_5);
  font-size: var(--font-body-small);
  line-height: var(--leading-body);
  color: var(--ink-2);
}

.guide-note {
  margin: 0;
  font-size: var(--font-body-small);
  line-height: var(--leading-body);
  color: var(--ink-3);
}

.dims { display: flex; align-items: center; gap: var(--space-0_5); width: 100%; }
.dims .text-input { text-align: center; }
.dims span { color: var(--ink-3); font-size: var(--font-body-small); }

.progress {
  height: 4px;
  border-radius: var(--radius-xs);
  background: var(--slider-track);
  overflow: hidden;
}

.progress i {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--ink);
  transition: width 0.15s linear;
}

.prog-note {
  font-family: var(--mono);
  font-size: var(--font-caption);
  color: var(--ink-2);
  text-align: center;
  font-variant-numeric: tabular-nums;
  margin-top: var(--space-0_5);
}

/* ---------- toast and drop ---------- */

#toast {
  position: fixed;
  left: 50%;
  bottom: 88px;
  transform: translateX(-50%) translateY(8px);
  background: var(--ink);
  color: var(--surface);
  border-radius: var(--radius-sm);
  padding: 10px var(--space-2);
  font-size: var(--font-body-small);
  font-weight: var(--weight-medium);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  z-index: 70;
  box-shadow: var(--shadow-lg);
  max-width: 60vw;
  text-align: center;
}

#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

#drop {
  position: fixed;
  inset: var(--space-1);
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius);
  /* 70% left the subtitle at 1.93:1 over the white stage Set boots on, and
     that line is the only place the app says which files it takes. */
  background: color-mix(in srgb, var(--ground) 92%, transparent);
  backdrop-filter: blur(6px);
  display: none;
  place-items: center;
  z-index: 50;
  pointer-events: none;
}

#drop.show { display: grid; }

#drop p {
  font-size: var(--font-h2);
  font-weight: var(--weight-semibold);
  color: var(--ink);
  text-align: center;
}

#drop p span {
  display: block;
  margin-top: var(--space-0_5);
  font-size: var(--font-body-small);
  font-weight: 400;
  color: var(--ink-2);
}

/* Also glass, for the same reason as the header: it floats over the render. */
/* ---------- axis view ---------- */

/* It sits over the render, which is the thing being judged, so it stays quiet
   until it is looked at. Same lesson as the old #hints strip. */
.axisview {
  position: relative;
  width: 76px;
  height: 76px;
  flex: none;
  border-radius: 50%;
  background: var(--panel-glass);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--panel-glass-border);
  box-shadow: var(--shadow);
  opacity: 0.55;
  transition: opacity 140ms ease;
}

.axisview:hover,
.axisview:focus-within { opacity: 1; }

.axisview svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* One class per axis colours its line and both of its balls, so the widget
   speaks the same red, green, blue as the transform gizmo in the scene. */
.axisview line {
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  opacity: 0.5;
}

.axisview .axis-x { color: #d95757; }
.axisview .axis-y { color: #56b46f; }
.axisview .axis-z { color: #4d84d6; }

.axisview .ball {
  position: absolute;
  width: 13px;
  height: 13px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: currentColor;
  cursor: pointer;
  appearance: none;
}

/* The far half of each axis: present enough to aim at, quiet enough that the
   near ball still reads as the one facing you. */
.axisview .ball.neg {
  width: 9px;
  height: 9px;
  background: color-mix(in srgb, currentColor 45%, transparent);
}

/* ---------- shortcuts card ---------- */

/* Key on the left, what it does on the right: a two-column grid keeps the
   descriptions aligned however many chips a row carries. */
.keys {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: var(--space-0_5) var(--space-1);
  font-size: var(--font-body-small);
}

.keys dt { display: flex; gap: 3px; }
.keys dd { color: var(--ink-2); line-height: var(--leading-body); }

.keys kbd, .gate-card kbd {
  font-family: var(--mono);
  font-size: var(--font-micro);
  font-weight: var(--weight-medium);
  color: var(--ink);
  background: var(--surface-raised);
  border-radius: var(--radius-xs);
  padding: 2px 5px;
}

.card-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1);
  font-size: var(--font-body-small);
  color: var(--ink-3);
}

/* The version is a button that opens the changelog, dressed as the foot's
   links are: the same tone, ink under the pointer, no border and no fill,
   so the foot reads as one line of quiet text with three handles in it. */
.card-foot .version {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  font-family: var(--mono);
  color: var(--ink-2);
  cursor: pointer;
}
.card-foot .version:hover { color: var(--ink); }
.card-foot .card-links { display: flex; gap: var(--space-2); }
.card-foot a { color: var(--ink-2); text-decoration: none; }
.card-foot a:hover { color: var(--ink); }

/* ---------- narrow-window gate ---------- */

#gate, #boot-error {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--space-2);
  background: var(--ground);
}

#gate[hidden], #boot-error[hidden] { display: none; }

/* #bar, #panel and #timeline each set their own `display` for the app shell,
   which otherwise outranks the plain [hidden] the browser applies by
   default; the gate needs them to actually disappear, not just sit under it. */
#bar[hidden], #panel[hidden], #timeline[hidden] { display: none; }

.gate-card {
  max-width: 340px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--space-4);
}

.gate-card h1 {
  font-size: var(--font-h1);
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  color: var(--ink);
}

.gate-card p {
  color: var(--ink-2);
  font-size: var(--font-body);
  line-height: var(--leading-body);
}

/* THE GATE'S SECOND LINE FOLLOWS THE POINTER, the panel's own contextual
   rule: a finger cannot widen a window or reset a zoom, so on a touch
   screen the line says the one thing it can do and the mouse line is not
   there. The primary pointer is the signal and the user agent is not (Safari
   on an iPad claims to be a Mac by design, and under emulation every engine
   does). An iPad with a trackpad still reads as touch, and never reaches the
   gate anyway: even the mini is wider than the 700px threshold either way
   up. A browser that cannot answer the query lands on the mouse line, which
   is never wrong, only longer. */
.gate-touch { display: none; }
@media (hover: none) and (pointer: coarse) {
  .gate-touch { display: block; }
  .gate-mouse { display: none; }
}

.gate-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1);
  margin-top: var(--space-1);
}

/* ---------- compact ---------- */

/* 936, not 900: the bar's own controls need 548px beside the 368px the
   sidebar reserves, plus the 16px inset (they were 550.5 before the bar took
   its 22px side inset, a 28px key button and a 42px duration field, which
   net out to about the same). Below 936 they overflowed the glass card by up
   to 27.5px before the compact rule ever came into effect. */
@media (max-width: 936px) {
  /* The panel it toggles occupies the right edge, so the button goes left. */
  #actions { display: flex; order: -1; }
  #panel { width: min(336px, calc(100vw - 32px)); }
  #panel.compact-hidden { display: none; }
  /* The bar takes the full width here, which means it crosses the sidebar
     rather than sitting beside it. Both are z-index 15 and the bar is later
     in the DOM, so it wins and covers the panel's bottom edge. Reserving its
     footprint on the scroller keeps the last card reachable: 66px of bar,
     its own 16px inset, and the 16px the column already trails by. */
  #timeline { right: var(--space-2); }
  #panel-body { padding-bottom: 98px; }
}

@media (max-width: 680px) {
  #timeline { overflow-x: auto; }
  .modal { width: min(400px, calc(100vw - var(--space-3))); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ---------- view: work area and preview ---------- */

/* Beside the sidebar: the FRAME ends where the timeline already ends, one
   inset short of the sidebar, so the render is composed against pixels no
   chrome covers, while the canvas stays full bleed and keeps drawing under
   the glass (a camera view offset in studio._resize, not a narrower stage:
   a strip of chrome ground there read as a hole in the room). The guides
   are the one piece of chrome that has to know where the frame ends. Compact
   keeps the whole window: below the breakpoint the sidebar can be dismissed,
   and a frame pinched to half a small window helps nobody. */
body.stage-clear .guide-overlay,
body.stage-clear .focus-layer { right: 368px; }
@media (max-width: 936px) {
  body.stage-clear .guide-overlay { right: 0; }
}

/* Preview gives the render the window: the sidebar, the bar with the axis
   widget riding in it, the timeline, and the guides all leave, the guides
   because they are aids for framing the thing this mode shows finished.
   After the cleared rules, so previewing wins the stage's right edge when
   both classes are on the body. Escape or P is the way back, which the
   entry toast says, because once the chrome has left nothing on screen can. */
body.previewing #bar,
body.previewing #panel,
body.previewing #timeline,
body.previewing .guide-overlay,
body.previewing .focus-layer { display: none; }
body.previewing #stage { right: 0; }

/* Focus picking retargets the click, so the cursor has to say so. */

/* ---------- account and licence ---------- */

/* An option a tier cannot use stays in its row, marked rather than gone:
   a hidden 4K sells nothing. Picking one keeps the previous value and the
   gate note under the dialog says which tier it needs. Same quiet as the
   axis widget at rest. */
.seg button.locked { opacity: 0.55; }
.seg button.locked:hover { opacity: 0.8; }

/* The gate under an export dialog: what the current settings need, and
   the one button that gets it. Note left, button at the right end, the
   caption tail rule at dialog scale. */
.gate {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.gate-note {
  flex: 1;
  font-size: var(--font-body-small);
  color: var(--ink-3);
  line-height: var(--leading-body);
}
/* `hidden` has to win over the flex display, or show() cannot hide it. */
.gate[hidden] { display: none; }

/* A licence in the purchase dialog: name and price on the caption line,
   what it carries under it, the button last. Grouped by the modal body's
   own gap; inside, the half step. */
.plan { display: flex; flex-direction: column; gap: var(--space-0_5); }
.plan .cap { display: flex; align-items: baseline; }
.plan .cap .price { margin-left: auto; font-family: var(--font-mono); color: var(--ink-2); }
.plan .btn { align-self: flex-start; }

/* The account rows in Settings. */
.account { display: flex; flex-direction: column; gap: var(--space-1); }
.account-error { color: var(--rec); }
.account-who { display: flex; justify-content: space-between; gap: var(--space-2); min-width: 0; }
.account-who > :first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-who > :last-child { flex: none; }
