:root {
  /* Opt-in extra bottom clearance stratum-ui's AppShellLayout adds under
   * `@media (display-mode: standalone)` (see its .stratum-app-shell-nav and
   * .stratum-app-shell-fab rules) — undefined, it's a no-op. Matches
   * frontend/src/index.css's own value exactly. env(safe-area-inset-bottom)
   * itself is handled directly elsewhere (e.g. PageHeaderMobile's top
   * padding) via viewport-fit=cover in index.html; this is *additional*
   * padding on top of that, specifically for the installed-PWA bottom nav. */
  --pwa-nav-pad: 24px;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  /* calc(var(--vh, 1vh) * 100), NOT plain 100% — matches frontend/src/index.css exactly.
   * iOS Safari (standalone PWA in particular) has a known relayout-staleness bug where a
   * CSS-native height:100%/100vh on html/body can go stale across a client-side (SPA) route
   * change, leaving body shorter than the real viewport on some transitions but not others —
   * this was the root cause of demo pages showing extra "gap" below content and html/body not
   * covering the full screen in Safari's Web Inspector. --vh (main.tsx) is a JS-computed value
   * read fresh from window.innerHeight/screen.height, immune to that staleness since it isn't
   * a CSS percentage resolving against the initial containing block at all. */
  height: calc(var(--vh, 1vh) * 100);
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
/* Native-scroll: every route in this app (every demo + the gallery) uses AppShellLayout's
 * `scrollMode="native"`/the equivalent per-page relaxation on mobile — see media/frontend's
 * ReadPage migration for the original fix (iOS Safari's native text-selection UI breaking
 * inside a scrolled nested container) and its own index.css's `body.native-scroll` for the
 * same override. Unlike frontend, there's no per-route toggle needed here: since EVERY route
 * is native-scroll (not a phased migration), this can just apply unconditionally rather than
 * being gated behind a body class that some routes opt out of. `body`'s own overflow IS the
 * viewport's overflow (html never sets its own overflow here either), so this is the one
 * override that lets the rest of each page's own height:auto/overflow:visible chain
 * (AppShellLayout, each demo's own scroll container, .gallery-page) actually reach real
 * document-level scrolling instead of clipping. Desktop is untouched. */
@media (max-width: 767px) {
  body {
    overflow: visible;
  }
}

#root {
  height: 100%;
}

/* Shared "descriptive paragraph under a page title" convention — every demo
 * that has one (icons, toast-notifications, …) should use this rather than
 * hand-tuning its own size, so they read consistently with each other and
 * with the gallery's own header (.gallery-header p, GalleryPage.css). */
.demo-intro {
  margin: 0 0 24px;
  max-width: 640px;
  color: var(--ink-3);
  font-size: 14px;
  line-height: 1.6;
}
:root {
  --bg: #fbfaf7;
  --surface: #ffffff;
  --surface-2: #f4f3ef;
  --ink: #141414;
  --ink-2: #3d3d3e;
  --ink-3: #75757a;
  --ink-4: #a4a4a9;
  --line: #e8e7e2;
  --line-2: #dcdbd5;
  --accent: #0a84ff;
  --accent-soft: #e7f0ff;
  --accent-alt: #ff9500;
  --warn: #ff3b30;
  --ok: #32c759;
  --mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
  /* Safe-area insets via variables rather than raw env() so dev tools — e.g.
     ViewportPreview's iPhone simulation — can override them inside a preview
     iframe. Values are identical to the env() calls they wrap. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  /* Past the top safe area and the blur an iOS PWA's status bar casts below it: where something laid
     over the whole screen (a full-screen view, the image viewer) starts its content. 0 elsewhere. */
  --safe-top-clear: calc(var(--safe-top) + min(var(--safe-top), 22px));
  /* Default minimum bottom inset to clear the iPhone home-indicator/drag handle.
     Separate from --safe-bottom (the PWA/env-specified inset) — consumers take
     max() of the two so the drag handle never collides with content, while a
     larger PWA-specified inset is still respected. ViewportPreview sets this to
     12px in its iPhone simulation; otherwise 0. */
  --safe-bottom-default: 0px;
  /* Effective bottom inset: the larger of the drag-handle default and the
     PWA/env-specified inset. Bottom-anchored UI (nav, sheets, toasts) uses
     this so the home indicator never collides with content. */
  --safe-bottom-effective: max(var(--safe-bottom-default), var(--safe-bottom));
  /* The flash that shows where a link landed (utils/flash.ts: flashRange; ToolCallBlock's flash):
     this yellow at this strength, mixed into the page's background behind the text alone, held, then
     easing out. One definition, so every flash looks the same. */
  --stratum-flash-color: #ffd60a;
  --stratum-flash-strength: 55%;
  --stratum-flash-hold: 1.2s;
  --stratum-flash-fade: 0.8s;
  /* The easing out: quadratic, so it lingers, then goes (flashRange steps the same curve). */
  --stratum-flash-ease: cubic-bezier(0.5, 1, 0.89, 1);
  /* Code's colors by kind of token (CodeView's syntax highlighting), after GitHub's light theme. */
  --syntax-keyword: #cf222e;
  --syntax-string: #0a3069;
  --syntax-comment: #6e7781;
  --syntax-constant: #0550ae;
  --syntax-function: #8250df;
  --syntax-parameter: #953800;
  --syntax-inserted: #116329;
  --syntax-deleted: #82071e;
  --syntax-changed: #953800;
}

/* flashRange's text, its strength stepped each frame through --stratum-flash-now. Mixed into the
   page's background rather than made translucent: WebKit paints it once per text box, and where two
   overlap, at a line's start, a translucent one doubled up into a darker strip. */
::highlight(stratum-flash) {
  background-color: color-mix(in srgb, var(--stratum-flash-color) var(--stratum-flash-now, var(--stratum-flash-strength)), var(--bg));
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0a0a0b;
    --surface: #141416;
    --surface-2: #1e1e21;
    --ink: #f5f5f7;
    --ink-2: #c7c7cc;
    --ink-3: #8e8e93;
    --ink-4: #636366;
    --line: #2a2a2e;
    --line-2: #3a3a3f;
  }
}

/* No focus ring on anything: the apps drive focus themselves (a list's highlight, the composer), and
   the browser's blue outline on a tapped or clicked control - a nav item, a row - only gets in the
   way. Text fields keep their own focus look (TextInput's :focus-within). */
:focus-visible {
  outline: none;
}
.stratum-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  /* Also used on <a>/<Link> (e.g. a "View" link styled as a button) — no default link underline. */
  text-decoration: none;
  border-radius: 8px;
  padding: 6px 12px;
  border: 1px solid transparent;
  cursor: pointer;
  transition:
    background-color 0.15s,
    border-color 0.15s,
    color 0.15s;
}
.stratum-button svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  /* Icons leave their weight to the caller; at the default 1 they read thin beside the label. One
     that sets its own (e.g. i-crop) keeps it. */
  stroke-width: 1.75;
}
.stratum-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.stratum-button--secondary {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink);
}
.stratum-button--secondary:hover:not(:disabled) {
  background: var(--surface-2);
}

.stratum-button--primary {
  background: var(--accent);
  color: #fff;
}
.stratum-button--primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 88%, black);
}

.stratum-button--destructive {
  background: var(--warn);
  color: #fff;
}
.stratum-button--destructive:hover:not(:disabled) {
  background: color-mix(in srgb, var(--warn) 88%, black);
}

.stratum-button--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--accent);
  padding: 6px 8px;
  /* ghost buttons sit in header/title-row contexts (next to a 28-32px page
     title), not compact toolbars — 15px reads better there than the 13px
     the other variants use. */
  font-size: 15px;
}
.stratum-button--ghost:hover:not(:disabled) {
  background: var(--surface-2);
}

/* An "outline" warn — same chrome as secondary, but warn-tinted border/text
   instead of a solid destructive fill. For a lower-emphasis warning action
   (e.g. "Retry" on a failed row) where the solid `destructive` variant would
   read as too heavy. */
.stratum-button--warn {
  background: var(--surface);
  border-color: rgba(255, 69, 58, 0.25);
  color: var(--warn);
}
.stratum-button--warn:hover:not(:disabled) {
  background: var(--surface-2);
}
.stratum-select-overlay {
  position: relative;
  display: inline-flex;
}
.stratum-select-overlay select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}
.stratum-select-overlay select:disabled {
  cursor: not-allowed;
}
.stratum-edit-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 36px;
  border-bottom: 1px solid var(--line);
  background: var(--accent-soft);
}
.stratum-edit-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 25;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px calc(6px + var(--safe-bottom, 0px));
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  background: rgba(251, 250, 247, 0.92);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
}
@media (prefers-color-scheme: dark) {
  .stratum-edit-bar {
    border-top-color: rgba(255, 255, 255, 0.06);
    background: rgba(10, 10, 11, 0.92);
  }
}
@media (min-width: 768px) {
  .stratum-edit-bar {
    display: none;
  }
}
.stratum-edit-count {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.stratum-edit-spacer {
  flex: 1;
}
.stratum-edit-action svg {
  width: 16px;
  height: 16px;
  /* One weight for every action's icon (they leave it to the caller). */
  stroke-width: 2;
}
/* The archive box has more lines than a check or a cross, so it reads heavier at the same weight. */
.stratum-edit-action use[href='#i-archive'] {
  stroke-width: 1.75;
}
.stratum-edit-action--destructive {
  color: var(--warn);
}

/* EditMenu: its root is a <span> (SelectOverlay's), not a form control, so Button's `:disabled` rules
   can't see it - the same values via a class, and its hover held at rest while disabled (the repeated
   class only out-ranks Button's own `:hover:not(:disabled)`). */
.stratum-edit-menu--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.stratum-button--secondary.stratum-edit-menu.stratum-edit-menu--disabled:hover {
  background: var(--surface);
}
.stratum-button--ghost.stratum-edit-menu.stratum-edit-menu--disabled:hover {
  background: transparent;
}
.stratum-filter-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 0 2px;
  overflow-x: auto;
  scrollbar-width: none;
}
.stratum-filter-bar::-webkit-scrollbar {
  display: none;
}
.stratum-filter-bar > * {
  flex-shrink: 0;
  white-space: nowrap;
}
.stratum-filter-button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border: 1.5px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
}
.stratum-filter-button svg {
  width: 14px;
  height: 14px;
  stroke-width: 1.8;
}
.stratum-filter-button-count {
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--accent);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  color: #fff;
}
.stratum-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-2);
}
.stratum-filter-chip-remove {
  margin-left: 1px;
  padding: 0;
  border: none;
  background: none;
  font-size: 13px;
  line-height: 1;
  color: var(--ink-3);
  cursor: pointer;
}
.stratum-filter-chip-remove:hover {
  color: var(--ink);
}
.stratum-filter-reset {
  padding: 4px 2px;
  border: none;
  background: none;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--accent);
  cursor: pointer;
}
.stratum-filter-reset:hover {
  text-decoration: underline;
}
/* No @media (min-width) breakpoint anywhere in this file, on purpose — this
   panel stays bottom-docked at every width, unlike Sheet's mobile/desktop
   split (see BottomPanel.tsx's own doc). */
.stratum-bottom-panel {
  position: fixed;
  /* Plain `bottom: 0` pins this against the LAYOUT viewport, which iOS
     Safari does NOT shrink when the on-screen keyboard opens (only
     `window.visualViewport` does) — a text input inside this panel (e.g.
     CharacterListSheet's "Merge into…" filter) would otherwise stay pinned
     below where the keyboard now covers, same bug Sheet had before it
     tracked hooks/visualViewportVar.ts's shared var. Offsets up by exactly
     the gap between the full layout-viewport height and the live visual-
     viewport height — that gap is 0 (this reduces to plain `bottom: 0`)
     whenever the keyboard is closed or the var is unset (no visualViewport
     support, or before the listener's first frame). */
  bottom: calc(100vh - var(--stratum-vvh, 100vh));
  /* Clear of AppShellLayout's rail (desktop), so it centres over the main column - where the page's
     content is - on every page, with nothing to set up. 0 outside the shell, and on mobile. */
  left: var(--stratum-app-shell-rail-width, 0px);
  right: 0;
  z-index: 100;
  margin-inline: auto;
  /* An app can widen (or narrow) every panel under an element at once - e.g. to its content column. */
  max-width: var(--stratum-bottom-panel-max-width, 720px);
  /* Same live-var cap as Sheet.css's own max-height — without it, the
     panel's height allowance (50vh/420px) stays computed against the full
     pre-keyboard viewport even once `bottom` above has moved it up, so on a
     short enough remaining visible area it could still grow past the real
     top of the visible screen instead of scrolling its own body. */
  max-height: min(50vh, 420px, calc(var(--stratum-vvh, 100vh) * 0.9));
  /* It covers the app shell's bottom nav and a page's accessory (a composer), and its body keeps its
     own bottom padding: no page clearance for them in it - an InfiniteScrollFooter or
     AppShellBottomSpacer in it would otherwise leave a page's worth of empty space at its end. */
  --stratum-app-shell-content-inset-bottom: 0px;
  --stratum-app-shell-bottom-chrome: 0px;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: 18px 18px 0 0;
  border: 1px solid var(--line-2);
  border-bottom: none;
}

/* Docked to a column of the page's own (desktop): inside an element with this class - positioned, e.g.
   `position: relative` - a panel centres over that element instead of the main column, e.g. a reader's
   text column beside a side rail. Already clear of the app shell's rail there, so no offset for it.
   Mobile keeps the viewport. */
@media (min-width: 768px) {
  .stratum-bottom-panel-dock .stratum-bottom-panel {
    position: absolute;
    left: 0;
  }
}

.stratum-bottom-panel-header {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* Fixed floor + vertically centred close button (below), so the header's
     height and the close button's spacing above/below it are identical no
     matter what the title contains (plain text vs. a drill-down back button). */
  min-height: 36px;
  box-sizing: border-box;
  padding: 0 48px 0 20px;
  background: var(--surface);
  border-radius: 18px 18px 0 0;
}

.stratum-bottom-panel-title {
  /* Lets a long title shrink within the header (which already leaves room for the close button),
     so a title that ellipsizes its own text gets cut off instead of running under the ×. */
  min-width: 0;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}

/* The title and its actions, side by side on one line (the header itself spreads its children apart). */
.stratum-bottom-panel-heading {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

/* `actions`: muted meta text, an em dash between each piece that renders - by CSS, not by counting
   children, since a switch that doesn't apply renders nothing. The dash is an inline-block, so a
   piece's hover underline doesn't run through it. */
.stratum-bottom-panel-actions {
  display: flex;
  align-items: baseline;
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  color: var(--ink-3);
}
.stratum-bottom-panel-actions:empty {
  display: none;
}
.stratum-bottom-panel-actions > * + *::before {
  content: '—';
  display: inline-block;
  margin: 0 0.3em;
  color: var(--ink-3);
}

/* With onBack: the arrow before the title, the close button's size and hover. Negative margins keep
   it from making the header taller than a plain title's (the header sizes to its in-flow content),
   and line the arrow glyph up with the body's left padding. */
.stratum-bottom-panel-title--back {
  /* Lined up by the title's text, the arrow alone centred: a centred row takes its baseline from its
     first item, and the arrow, having no text, would lend its bottom edge instead - dropping the
     actions beside the title too low. */
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.stratum-bottom-panel-title-text {
  min-width: 0;
}
.stratum-bottom-panel-back {
  flex-shrink: 0;
  align-self: center;
  width: 28px;
  height: 28px;
  margin: -5px 0 -5px -5px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}
.stratum-bottom-panel-back:hover {
  background: var(--surface-2);
  color: var(--ink);
}
.stratum-bottom-panel-back svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
}

.stratum-bottom-panel-close {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 14px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--ink-3);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  transition: background-color 0.15s, color 0.15s;
}
.stratum-bottom-panel-close:hover {
  background: var(--surface-2);
  color: var(--ink);
}

/* BottomPanelSubtitle's line, under the header and outside the body, so it stays as the body scrolls.
   Inline text that wraps: an action in it is muted text too (see the app's rule for meta lines). */
.stratum-bottom-panel-subtitle {
  padding: 0 20px 10px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-3);
}
.stratum-bottom-panel-subtitle:empty {
  display: none;
}

.stratum-bottom-panel-body {
  /* Fills a panel held taller than its content (see BottomPanel's height), so the spare room sits
     inside the scroll region rather than as a gap below it. */
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* max(), not addition — a normal baseline (18px, same as this always was
     before any safe-area handling existed) for desktop/non-PWA contexts,
     where env(safe-area-inset-bottom) is 0px and would otherwise leave NO
     bottom padding at all. On iOS PWA full-screen mode, the actual inset
     (~34px on a notched device) is bigger than 18px and wins instead —
     without max() (i.e. plain addition), that stacked BOTH amounts on top
     of each other, which read as too much. */
  padding: 0 20px max(18px, var(--safe-bottom-effective));
}

/* BottomPanelSwitch: a view switch among the title's actions. */
.stratum-bottom-panel-switch {
  flex-shrink: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 400;
  color: var(--ink-3);
  cursor: pointer;
}
.stratum-bottom-panel-switch:hover {
  text-decoration: underline;
}

/* BottomPanelSelect: its trigger underlines on hover like a switch - the select laid over it takes the
   hover, so it's the wrapper's. */
.stratum-bottom-panel-select:hover .stratum-bottom-panel-switch {
  text-decoration: underline;
}
/* A column whose ToolOutputs shrink, each by its share of its own height (flex-shrink weighs by
   size), while the rest keeps its height. */
.stratum-fit-height {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.stratum-fit-height > * {
  flex-shrink: 0;
}
/* Each one shrinks down to its floor: its label and Show all, and three lines of its text or all of it
   if less (ToolOutput measures it into --stratum-tool-output-min) - so a one-line command is never
   cut, and the output gives up the room instead. Without a floor of its own, a flex item's minimum is
   its whole content, and it wouldn't shrink at all. */
.stratum-fit-height > .stratum-tool-output {
  flex-shrink: 1;
  min-height: var(--stratum-tool-output-min, 0px);
  display: flex;
  flex-direction: column;
}
.stratum-fit-height > .stratum-tool-output > * {
  flex-shrink: 0;
}
/* A tail output (the one being watched) gives up its height last: the others shrink to their floors
   first, then it does. Weighting them up rather than it down: shrink factors summing under 1 would
   leave part of the overflow unshrunk. */
.stratum-fit-height > .stratum-tool-output:not(.stratum-tool-output--tail) {
  flex-shrink: 1000;
}
/* Show all at its own width, on the left, as outside a FitHeight - not stretched across, centred. */
.stratum-fit-height > .stratum-tool-output > .stratum-tool-output-more {
  align-self: flex-start;
}
.stratum-fit-height > .stratum-tool-output > pre {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
/* Room to spare (--stratum-fit-room, from FitHeight): a tail output's rows reach into it, showing more
   of its latest lines - raising only its cap, so it grows by that much at most and the panel doesn't.
   Output that already shows whole stays as it is. 20px: its padding. */
.stratum-fit-height > .stratum-tool-output--tail > pre {
  max-height: calc(var(--stratum-tool-output-rows, 1e4) * 1lh + 20px + var(--stratum-fit-room, 0px));
}
/* A few px in from the card below it, on a page (the Sessions list, Settings) - except in a bottom
   panel, below. */
.stratum-section-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 4px 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.stratum-section-label-trailing {
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
}
/* Its top padding separates it from the section above; first in its container, there's none, and
   the container's own padding (e.g. a page's, under its header) is room enough. */
.stratum-section-label:first-child {
  padding-top: 0;
}

/* A label and what it labels as one block (LabelledSection). The label keeps its space above it -
   it's always first in this block, which would otherwise drop it (:first-child above) - except when
   the block itself comes first in its container. And 8px below it: with the next label's 20px above,
   sections sit 28px apart, wherever they are (a settings page's groups, a list's sections). */
.stratum-labelled-section {
  display: flex;
  flex-direction: column;
  padding-bottom: 8px;
}
.stratum-labelled-section > .stratum-section-label {
  padding-top: 20px;
}
.stratum-labelled-section:first-child > .stratum-section-label {
  padding-top: 0;
}

/* In a bottom panel: flush, lined up with the panel's title just above. */
.stratum-bottom-panel .stratum-section-label {
  padding-inline: 0;
}

/* A count right after the label (`count`). */
.stratum-section-label-count {
  margin-left: 8px;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--ink-4);
}

/* Over full-width rows (`inset="rows"`): lined up with the rows' own text, as a page's content is. */
.stratum-section-label--rows {
  padding-inline: 20px;
}
@media (min-width: 768px) {
  .stratum-section-label--rows {
    padding-inline: 36px;
  }
}

/* A settings group keeps its own 8px below it only when it isn't in a labelled section, which has
   that already. */
.stratum-labelled-section > .stratum-settings-group {
  margin-bottom: 0;
}
.stratum-settings-group {
  margin: 0 0 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
}

.stratum-settings-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 56px;
  padding: 14px 16px;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
}
/* Hairline between rows, starting after the icon column when there is one. A border, not a 1px
   background: at a fractional zoom Chrome rounds a box's edges to whole device pixels, so a 1px
   box at a fractional offset paints 1 or 2 pixels thick depending on the row; a border's width
   rounds the same everywhere. */
.stratum-settings-row + .stratum-settings-row::before {
  content: '';
  position: absolute;
  top: 0;
  left: 16px;
  right: 0;
  border-top: 1px solid var(--line);
}
.stratum-settings-row--icon + .stratum-settings-row::before {
  left: 58px;
}
.stratum-settings-row--tappable {
  cursor: pointer;
  transition: background 0.12s;
}
.stratum-settings-row--tappable:active {
  background: var(--surface-2);
}

.stratum-settings-row-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  background: var(--accent-soft);
  color: var(--accent);
}
.stratum-settings-row-icon svg {
  width: 16px;
  height: 16px;
  stroke-width: 1.8;
}
.stratum-settings-row-icon--neutral {
  background: var(--surface-2);
  color: var(--ink-2);
}
.stratum-settings-row-icon--warn {
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  color: var(--warn);
}

.stratum-settings-row-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stratum-settings-row-title {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.25;
  color: var(--ink);
}
.stratum-settings-row-value {
  font-size: 13px;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}
.stratum-settings-row-value--mono {
  font-family: var(--mono);
  font-size: 12.5px;
  letter-spacing: -0.01em;
}
.stratum-settings-row-trailing {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}
.stratum-skeleton {
  display: block;
  width: 100%;
  height: 0.9em;
  background: linear-gradient(90deg, var(--surface-2) 25%, color-mix(in srgb, var(--surface-2) 55%, var(--surface)) 50%, var(--surface-2) 75%);
  background-size: 300% 100%;
  animation: stratum-skeleton-shimmer 1.4s ease-in-out infinite;
}
.stratum-skeleton--text {
  border-radius: 6px;
}
.stratum-skeleton--circle {
  border-radius: 50%;
}
.stratum-skeleton--block {
  border-radius: 12px;
}
@keyframes stratum-skeleton-shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0 0;
  }
}
/* A loading row's placeholder, on the value's line. */
.stratum-deploy-status-skeleton {
  display: inline-block;
  vertical-align: middle;
}
.stratum-detail-list {
  margin: 0;
  display: flex;
  flex-direction: column;
}

.stratum-detail-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
}
.stratum-detail-row:first-child {
  border-top: none;
  padding-top: 2px;
}

.stratum-detail-text {
  flex: 1 1 auto;
  min-width: 0;
}

.stratum-detail-label {
  font-size: 12px;
  color: var(--ink-3);
}

.stratum-detail-value {
  margin: 2px 0 0;
  font-size: 14px;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.stratum-detail-value--mono {
  font-family: var(--mono);
  font-size: 13px;
  word-break: break-all;
}

.stratum-detail-copy {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  margin-right: -6px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  transition: background-color 0.15s, color 0.15s;
}
.stratum-detail-copy:hover {
  background: var(--surface-2);
  color: var(--ink);
}
.stratum-detail-copy svg {
  width: 17px;
  height: 17px;
  stroke-width: 1.75;
}
.stratum-detail-copy--copied {
  color: var(--accent);
}
.stratum-detail-copy--copied svg {
  stroke-width: 2.25;
}
.stratum-detail-copy--failed {
  color: var(--warn);
}
.stratum-text-input {
  display: flex;
  align-items: center;
  border: 1.5px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  overflow: hidden;
  transition:
    border-color 0.15s,
    box-shadow 0.15s;
}

/* Focus ring formula matches SignInPage's .si-input/.otp-cell elsewhere in
   media-app-hetzner — .sr-text-input (this component's origin) had no focus
   state at all, a real accessibility gap, so this reuses an existing
   in-codebase pattern rather than inventing a new one. :focus-within, not
   :focus, since focus always lands on the inner <input>, not this wrapper. */
.stratum-text-input:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
}

.stratum-text-input:has(input:disabled) {
  opacity: 0.5;
}

.stratum-text-input input {
  flex: 1;
  min-width: 0;
  border: none;
  background: transparent;
  padding: 9px 10px;
  font-family: inherit;
  font-size: 14px;
  color: var(--ink);
  outline: none;
}
.stratum-text-input input::placeholder {
  color: var(--ink-4);
}
.stratum-text-input input:disabled {
  cursor: not-allowed;
}

.stratum-text-input-prefix {
  padding: 0 0 0 10px;
  font-size: 14px;
  color: var(--ink-3);
  flex-shrink: 0;
}
.stratum-sheet-backdrop {
  position: fixed;
  inset: 0;
  /* Overrides the `bottom: 0` implied by `inset: 0` above (CSS drops
     `bottom` in favor of an explicit `height` when both are set alongside
     `top` — the standard over-constrained resolution) with the live visual-
     viewport height hooks/visualViewportVar.ts's shared listener maintains,
     so this backdrop (and the sheet flex-aligned to its bottom edge below)
     shrinks to track an iOS Safari on-screen keyboard instead of staying
     sized for the full pre-keyboard screen. Falls back to the old plain
     `100%` (full fixed-position containing block, same as `inset: 0` alone
     gave before) when the var is unset — no visualViewport support, or
     before that listener has run its first frame. */
  height: var(--stratum-vvh, 100%);
  z-index: 100;
  background: rgba(0, 0, 0, 0.3);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.22s ease;
}
.stratum-sheet-backdrop.open {
  opacity: 1;
}
@media (min-width: 768px) {
  .stratum-sheet-backdrop {
    align-items: center;
  }
}

.stratum-sheet {
  width: 100%;
  /* min() with the same live var the backdrop above uses (falls back to
     plain 80dvh when unset) — without this, a sheet tall enough to hit its
     own cap could still overflow past the shrunk backdrop's TOP edge once
     the keyboard eats space below (flexbox doesn't clip by default), rather
     than the body's own overflow-y:auto scrolling internally like it's
     meant to. */
  max-height: min(80dvh, calc(var(--stratum-vvh, 100dvh) * 0.8));
  background: var(--surface);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.15);
  transform: translateY(100%);
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  display: flex;
  flex-direction: column;
}
.stratum-sheet.open {
  transform: translateY(0);
}
@media (min-width: 768px) {
  .stratum-sheet {
    max-width: 380px;
    border-radius: 16px;
    transform: translateY(12px) scale(0.98);
  }
  .stratum-sheet.open {
    transform: translateY(0) scale(1);
  }
  /* Two-class selector for a safe specificity win over the plain .stratum-sheet rule above,
     regardless of source order. See Sheet.tsx's `size` prop doc for when to use this. */
  .stratum-sheet.stratum-sheet--wide {
    max-width: 520px;
  }
}

.stratum-sheet-grip {
  width: 36px;
  height: 4px;
  background: var(--line-2);
  border-radius: 2px;
  margin: 12px auto 10px;
  flex-shrink: 0;
}
@media (min-width: 768px) {
  .stratum-sheet-grip {
    display: none;
  }
}

.stratum-sheet-head {
  flex-shrink: 0;
  padding: 0 18px 8px;
}
@media (min-width: 768px) {
  .stratum-sheet-head {
    padding: 22px 24px 8px;
  }
}
/* When footer sits directly against head with no body in between (a plain
   confirm prompt — ConfirmSheet's own shape, title+description only), the
   footer's own top divider otherwise reads as cramped against the
   description right above it: head's bottom padding (8px, tuned for the
   normal case of a body's own top spacing taking over from there) is much
   smaller than the footer's own top padding below the divider (18px/20px).
   Matches that instead, so the divider sits evenly spaced on both sides.
   Scoped via :has() so it only fires for this exact "straight from head to
   footer" shape — a Sheet WITH body content (MoveVersionSheet,
   SourcePickerSheet, RegionCropSheet, ...) already gets its spacing from
   the body's own padding instead, and is unaffected. */
.stratum-sheet-head:has(+ .stratum-sheet-footer) {
  padding-bottom: 18px;
}
@media (min-width: 768px) {
  .stratum-sheet-head:has(+ .stratum-sheet-footer) {
    padding-bottom: 20px;
  }
}

.stratum-sheet-title {
  margin: 0 0 8px;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.stratum-sheet-description {
  margin: 0;
  font-size: 14px;
  color: var(--ink-3);
  line-height: 1.5;
  /* Sized to its text. A sheet whose description changes line count at runtime reserves the
     height itself, so the sheet doesn't jump (see RegionCropSheet's description). */
}

.stratum-sheet-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Top padding is NOT 0 — see the comment below on why a scrollable
     container needs a bit of room on every edge for whatever sits flush
     against it. 6px comfortably covers the accent focus ring's own 4px
     spread (TextInput.css's `:focus-within` box-shadow) with a little to
     spare for anything similar. */
  padding: 6px 18px 26px;
}
@media (min-width: 768px) {
  .stratum-sheet-body {
    padding: 6px 24px 22px;
  }
}
/* General gotcha this guards against: `overflow-y: auto` clips anything
   painted outside this element's own box, INCLUDING a child's decorative
   bleed (a focus ring's box-shadow/outline, a selection ring, ...) — not
   just the child's actual content. It can't tell "this pixel is the child's
   own box" apart from "this pixel is a shadow/outline the child painted
   outside its box"; both are outside the child's border box, so both get
   clipped the same way once they cross this container's edge. An
   `autoFocus`ed input (or anything else with its own focus/selection ring)
   placed as the FIRST scrollable child — no sibling above it to absorb the
   bleed — will have that ring's top edge clipped flat unless this padding
   (or equivalent room) exists, purely from being flush against the
   scrollable box's own edge, regardless of how the ring itself is drawn.
   Confirmed for real: MoveVersionSheet's TextInput (character-sheet demo)
   had its focus glow cut off at the top before this fix, from `autoFocus`
   landing on it as this body's very first child. Same reasoning applies to
   ANY future scrollable container in this design system that might host a
   focusable/selectable child flush against its edge — pad for the bleed,
   don't rely on the child's own margin (a child can't push itself outside
   its own overflow-clipped ancestor no matter how it's styled). */

.stratum-sheet-footer {
  flex-shrink: 0;
  display: flex;
  gap: 10px;
  padding: 18px 18px calc(18px + var(--safe-bottom-effective));
  border-top: 1px solid var(--line);
}
@media (min-width: 768px) {
  .stratum-sheet-footer {
    flex-direction: row-reverse;
    padding: 20px 24px 20px;
  }
  .stratum-sheet-footer button {
    flex: 1;
  }
}

.stratum-sheet-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Needed for correct sizing inside `footer` (a row-direction flex
     container there — see SheetActions.tsx's own doc): without an explicit
     width, this div sizes to its own content as a flex ITEM of footer,
     instead of filling it, leaving its buttons narrow rather than the full
     width they get for free when used as Sheet's plain-block `children`
     instead (ConfirmSheet's own usage). Harmless no-op in the `children`
     case, which was already effectively 100% wide there regardless. */
  width: 100%;
}
@media (min-width: 768px) {
  .stratum-sheet-actions {
    flex-direction: row-reverse;
  }
  .stratum-sheet-actions button {
    flex: 1;
  }
}

.stratum-sheet-btn {
  padding: 13px 14px;
  border-radius: 12px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  border: none;
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 0.15s;
}
.stratum-sheet-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--surface-2) 92%, black);
}
.stratum-sheet-btn--destructive {
  background: var(--warn);
  color: #fff;
}
.stratum-sheet-btn--destructive:hover:not(:disabled) {
  background: color-mix(in srgb, var(--warn) 88%, black);
}
.stratum-sheet-btn--accent {
  background: var(--accent);
  color: #fff;
}
.stratum-sheet-btn--accent:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 88%, black);
}
.stratum-sheet-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
@media (min-width: 768px) {
  .stratum-sheet-btn {
    padding: 10px 14px;
    font-size: 14px;
  }
}
.stratum-choice-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.stratum-choice {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  text-align: left;
  cursor: pointer;
  transition:
    background-color 0.15s,
    border-color 0.15s;
}
.stratum-choice:hover {
  background: var(--surface-2);
}
/* Selected: the accent's border and a tick at the end. */
.stratum-choice--selected,
.stratum-choice--selected:hover {
  border-color: var(--accent);
  background: var(--accent-soft, var(--surface-2));
}
.stratum-choice-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stratum-choice-label {
  font-weight: 500;
}
.stratum-choice-detail {
  font-size: 13px;
  color: var(--ink-3);
}
.stratum-choice-check {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  stroke-width: 2.25;
  color: var(--accent);
  visibility: hidden;
}
.stratum-choice--selected .stratum-choice-check {
  visibility: visible;
}
/* --stratum-app-shell-content-inset-bottom (see AppShellLayout) is 0 on
   desktop, where the mobile bottom nav is display:none, so this collapses to
   its flat breathing-room default there without needing a media query. On
   mobile it reserves exactly the bar's real height plus that same default. */
.stratum-infinite-scroll-footer {
  min-height: var(--stratum-app-shell-content-inset-bottom, 0px);
}

/* Clear of the list it follows, as much room above as below. */
.stratum-infinite-scroll-loading {
  padding: 20px 0;
  text-align: center;
  color: var(--ink-3);
  font-size: 14px;
}
.stratum-toast-wrap {
  position: fixed;
  bottom: calc(80px + var(--safe-bottom-effective));
  left: 16px;
  right: 16px;
  z-index: 200;
  pointer-events: none;
  display: flex;
  justify-content: center;
}
@media (min-width: 768px) {
  .stratum-toast-wrap {
    bottom: 24px;
  }
}

.stratum-toast {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--ink);
  color: var(--bg);
  padding: 11px 14px;
  border-radius: 12px;
  font-size: 14px;
  font-weight: 500;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.22);
  pointer-events: auto;
  max-width: 480px;
}
.stratum-toast-msg {
  flex: 1;
}
.stratum-toast-close {
  background: transparent;
  border: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.5;
  padding: 0;
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.stratum-toast-close:hover {
  opacity: 1;
}
.stratum-thumb {
  display: block;
  width: 100%;
  border-radius: 18px;
  overflow: hidden;
  position: relative;
  background: var(--surface-2);
}
.stratum-thumb img,
.stratum-thumb video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Poster overlay sits under the video and stays visible until the video's
   'playing' event fires (adds .stratum-video-ready) — see MediaThumb.tsx for
   why this replaces the native `poster` attribute. */
.stratum-thumb video {
  position: relative;
  z-index: 1;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.stratum-thumb video.stratum-video-ready {
  opacity: 1;
}
.stratum-thumb-video-poster {
  position: absolute;
  inset: 0;
  z-index: 0;
}
/* ThumbImage: the image stays transparent over a shimmering Skeleton that fills the tile until it's
   fully loaded and decoded, then fades in whole. */
.stratum-thumb-skeleton.stratum-skeleton {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
}
.stratum-thumb .stratum-thumb-img {
  opacity: 0;
  transition: opacity 0.25s ease;
}
.stratum-thumb .stratum-thumb-img--ready {
  opacity: 1;
}
/* .stratum-thumb-img listed too: `.stratum-thumb .stratum-thumb-img` above outranks a plain `img`
   selector, and its opacity-only transition would make the hover zoom jump instead of ease. */
.stratum-thumb--zoom img,
.stratum-thumb--zoom .stratum-thumb-img,
.stratum-thumb--zoom video {
  transition:
    transform 0.2s ease,
    opacity 0.25s ease;
}
.stratum-thumb--zoom:hover img,
.stratum-thumb--zoom:active img {
  transform: scale(1.03);
}
.stratum-thumb--tall {
  aspect-ratio: 4 / 5;
}
@media (min-width: 768px) {
  .stratum-thumb--tall {
    aspect-ratio: 1;
    border-radius: 12px;
  }
}

.stratum-thumb-novel-cover {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
}
.stratum-thumb-novel-icon {
  width: 30%;
  max-width: 48px;
  aspect-ratio: 1;
  color: var(--ink-3);
}

.stratum-thumb-duration,
.stratum-thumb-char-count {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 2;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  font-family: var(--mono);
  font-size: 11px;
  padding: 3px 7px;
  border-radius: 5px;
  backdrop-filter: blur(8px);
}
.stratum-cta-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  border: 1px solid transparent;
  background: var(--accent);
  color: #fff;
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
  transition:
    background 0.12s,
    transform 0.08s;
}

/* Size tiers — see CtaButton.tsx's `size` doc for when each applies. Only the
   metrics differ; fill, press feedback and disabled treatment are shared above. */
.stratum-cta-button--md {
  gap: 10px;
  padding: 14px 16px;
  border-radius: 14px;
  font-size: 15px;
}

.stratum-cta-button--compact {
  gap: 8px;
  padding: 11px 16px;
  border-radius: 12px;
  font-size: 14px;
}
.stratum-cta-button:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 88%, black);
}
.stratum-cta-button:active:not(:disabled) {
  transform: scale(0.985);
}
.stratum-cta-button:disabled {
  background: var(--line);
  color: var(--ink-4);
  cursor: not-allowed;
}
.stratum-cta-button svg {
  width: 16px;
  height: 16px;
}
.stratum-cta-button--compact svg {
  width: 15px;
  height: 15px;
}

/* Ghost variant — same box (size tiers above still apply), no fill/border, muted text. Declared
   after the base/hover/disabled rules above so it wins the specificity tie on `background`/`color`. */
.stratum-cta-button--ghost {
  background: none;
  color: var(--ink-3);
}
.stratum-cta-button--ghost:hover:not(:disabled) {
  background: var(--surface-2);
}
.stratum-cta-button--ghost:disabled {
  background: none;
  color: var(--ink-4);
}
.stratum-photo-viewer {
  position: fixed;
  inset: 0;
  z-index: 1000;
  /* Fades (--stratum-photo-viewer-backdrop, 1 to 0) while the image is dragged away to close, so the
     page shows through. */
  background: rgb(251 250 247 / var(--stratum-photo-viewer-backdrop, 1));
  overflow: hidden;
}
/* While the image is dragged away to close: the controls out of the way. */
.stratum-photo-viewer--dismissing .stratum-photo-viewer-glass,
.stratum-photo-viewer--dismissing .stratum-photo-viewer-close,
.stratum-photo-viewer--dismissing .stratum-photo-viewer-copy {
  opacity: 0;
  pointer-events: none;
}

/* The current video's controls (VideoControls), at the bottom while it's snapped into place. */
.stratum-photo-viewer-video-controls {
  position: absolute;
  left: 50%;
  bottom: max(18px, var(--safe-bottom-effective));
  z-index: 10;
  width: min(520px, calc(100% - 36px));
  transform: translateX(-50%);
}

.stratum-photo-viewer-canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: grab;
  /* Safari-only bug workaround: WebKit can cache a stale backdrop-filter snapshot of a <canvas>
     that's being redrawn purely via drawImage()/requestAnimationFrame (not a real DOM layout
     change) — the glass buttons over it stop visually updating until something else forces a
     repaint (e.g. switching tabs away and back). Chrome doesn't have this bug. Forcing the canvas
     onto its own GPU compositing layer is the documented fix — translateZ(0) has no visible
     effect on its own (0 offset), it's purely there to trigger layer promotion. */
  transform: translateZ(0);
  will-change: transform;
}

/* Kept mounted (rather than unmounted) while the image loads, so the
   container's real layout size is available for canvas sizing/fit-to-view —
   just not shown until it's actually ready to display. */
.stratum-photo-viewer--loading {
  visibility: hidden;
  pointer-events: none;
}

/* Mobile-first: bottom-right, where a bottom-nav-hiding FAB would sit in the
   consuming app — this overlay always fully covers any bottom nav. */
.stratum-photo-viewer-close {
  position: absolute;
  right: 18px;
  bottom: 18px;
  z-index: 10;
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.18);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  /* Part of the same Safari staleness workaround as .stratum-photo-viewer-canvas's own
     transform: translateZ(0) — see that rule's comment. */
  will-change: backdrop-filter;
  color: #fff;
  cursor: pointer;
  transition:
    opacity 0.18s ease,
    background-color 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease;
}

.stratum-photo-viewer-close:hover {
  background: rgba(255, 255, 255, 0.28);
}

/* Applied when the sampled canvas pixels behind the button read as light
   (e.g. the near-white letterbox around a fitted image) — see MediaViewer.tsx. */
.stratum-photo-viewer-close--on-light {
  border-color: rgba(0, 0, 0, 0.03);
  background: rgba(20, 20, 20, 0.05);
  color: #141414;
}

.stratum-photo-viewer-close--on-light:hover {
  background: rgba(20, 20, 20, 0.1);
}

/* Applied when the backdrop can't be sampled at all (tainted canvas — see
   MediaViewer.tsx). Uses its own dark fill for contrast instead of
   guessing light-vs-dark, so it stays legible over both the image and the
   viewer's light background. */
.stratum-photo-viewer-close--adaptive-fallback {
  border-color: rgba(255, 255, 255, 0.12);
  background: rgba(20, 20, 20, 0.55);
  color: #fff;
}

.stratum-photo-viewer-close--adaptive-fallback:hover {
  background: rgba(20, 20, 20, 0.68);
}

@media (max-width: 767px) {
  .stratum-photo-viewer-close--controls-hidden {
    opacity: 0;
    pointer-events: none;
  }
  /* Phones close by dragging the image away instead, unless the viewer asks for the button
     (mobileCloseButton). */
  .stratum-photo-viewer-close--not-mobile {
    display: none;
  }
}

@media (min-width: 768px) {
  .stratum-photo-viewer-close {
    top: max(16px, var(--safe-top));
    right: max(16px, var(--safe-right));
    bottom: auto;
    width: 36px;
    height: 36px;
  }
}

/* The copy button (`copyable`) sits beside the close button: left of it on wide screens; on phones in
   its corner, or left of it when mobileCloseButton shows that too. */
.stratum-photo-viewer-copy--beside-close {
  right: 84px;
}

@media (min-width: 768px) {
  .stratum-photo-viewer-copy,
  .stratum-photo-viewer-copy--beside-close {
    right: calc(max(16px, var(--safe-right)) + 44px);
  }
}

/* ── Crop mode (MediaViewer's own `crop` prop) ──────────────────────
   Positioned via inline style (left/top/width/height in px), recomputed every draw() call to
   track the image through pan/zoom/pinch — see MediaViewer.tsx's own updateCropOverlay.
   Same spotlight-mask trick as RegionCropSheet.css's own .stratum-region-crop-selection (a single
   element's own box-shadow, spread past the frame's edges and clipped by the viewer container's
   overflow: hidden), just inverted here: RegionCropSheet's frame clips the shadow itself, while
   here the whole .stratum-photo-viewer container does. */
.stratum-photo-viewer-crop-rect {
  position: absolute;
  z-index: 5;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
  cursor: move;
  touch-action: none;
}

/* Shared look — shape/position come from the --corner/--edge-h/--edge-v variants below, each
   paired with one of the four --nw/--ne/--sw/--se/--n/--s/--e/--w placement classes (see
   MediaViewer.tsx's own CROP_HANDLES). */
.stratum-photo-viewer-crop-handle {
  position: absolute;
  z-index: 6;
  background: #fff;
  border: 1.5px solid rgba(0, 0, 0, 0.35);
  touch-action: none;
}

/* Corners — round grab dots, resize both of their own two edges at once. */
.stratum-photo-viewer-crop-handle--corner {
  width: 22px;
  height: 22px;
  margin: -11px;
  border-radius: 50%;
}
.stratum-photo-viewer-crop-handle--nw {
  top: 0;
  left: 0;
  cursor: nwse-resize;
}
.stratum-photo-viewer-crop-handle--se {
  bottom: 0;
  right: 0;
  cursor: nwse-resize;
}
.stratum-photo-viewer-crop-handle--ne {
  top: 0;
  right: 0;
  cursor: nesw-resize;
}
.stratum-photo-viewer-crop-handle--sw {
  bottom: 0;
  left: 0;
  cursor: nesw-resize;
}

/* Single-edge handles — pill-shaped bars centered along one side only, for resizing just that
   side without disturbing the other three (the corners already cover "resize two at once"). */
.stratum-photo-viewer-crop-handle--edge-h {
  left: 50%;
  width: 28px;
  height: 10px;
  margin-left: -14px;
  border-radius: 5px;
  cursor: ns-resize;
}
.stratum-photo-viewer-crop-handle--n {
  top: 0;
  margin-top: -5px;
}
.stratum-photo-viewer-crop-handle--s {
  bottom: 0;
  margin-bottom: -5px;
}
.stratum-photo-viewer-crop-handle--edge-v {
  top: 50%;
  width: 10px;
  height: 28px;
  margin-top: -14px;
  border-radius: 5px;
  cursor: ew-resize;
}
.stratum-photo-viewer-crop-handle--w {
  left: 0;
  margin-left: -5px;
}
.stratum-photo-viewer-crop-handle--e {
  right: 0;
  margin-right: -5px;
}

/* No background scrim (tried a linear-gradient here initially) — the buttons themselves already
   carry their own adaptive glass (see --on-light above, sampled per button), same as the plain
   close button relies on with no scrim behind it either; a scrim on top just muddied the glass
   effect (everything behind it read as darker than it actually was) for no legibility benefit
   the per-button adaptation doesn't already cover. */
.stratum-photo-viewer-crop-footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  display: flex;
  justify-content: center;
  gap: 10px;
  padding: 14px max(16px, var(--safe-right)) max(14px, var(--safe-bottom-effective)) max(16px, var(--safe-left));
}
/* CtaButton defaults to width: 100% (sensible for a stacked Sheet footer like
   RegionCropSheet's own, wrong here — this is a floating side-by-side row over the full-screen
   viewer, not a narrow stacked sheet) — width: auto sizes each button to its own label instead of
   stretching to fill this row, and flex: none stops it from being asked to grow/shrink at all. */
.stratum-photo-viewer-crop-footer .stratum-cta-button {
  flex: none;
  width: auto;
}

/* Same translucent glass look as .stratum-photo-viewer-close (the plain-viewer-mode close
   button) — variant="ghost"'s own transparent-background/var(--ink-3) treatment reads as
   invisible-ish against this footer's backdrop, same reason the close button itself isn't styled
   that way either. Shared by both Cancel and Use-this-crop — deliberately no distinct "primary"
   treatment for the confirm action (an accent-tinted variant was tried, and removed as
   unwanted — both read as equally-weighted actions here). */
.stratum-photo-viewer-crop-btn {
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.25);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  /* Part of the same Safari staleness workaround as .stratum-photo-viewer-canvas's own
     transform: translateZ(0) — see that rule's comment. */
  will-change: backdrop-filter;
  color: #fff;
}
.stratum-photo-viewer-crop-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.28);
}

/* Applied when the canvas pixels sampled behind each button individually (see
   MediaViewer.tsx's own periodic-resample effect, cancelOnLight/confirmOnLight) read as
   light — same light-on-dark → dark-on-light flip as .stratum-photo-viewer-close--on-light, just
   tuned for a text button instead of an icon-only circle. No separate --adaptive-fallback variant
   here (unlike the close button) — if sampling isn't available, staying on the default (white
   text) style is an acceptable fallback rather than adding a third state. */
.stratum-photo-viewer-crop-btn--on-light {
  background: rgba(20, 20, 20, 0.08);
  border-color: rgba(0, 0, 0, 0.05);
  color: #141414;
}
.stratum-photo-viewer-crop-btn--on-light:hover:not(:disabled) {
  background: rgba(20, 20, 20, 0.14);
}

/* Several images (onPrev / onNext / position): which one this is, at the top, and on desktop arrow
   buttons at the sides (phones swipe instead). The same adaptive glass as the close button; tapping
   the image fades them out and back, on desktop too. */
.stratum-photo-viewer-glass {
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(255, 255, 255, 0.18);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  will-change: backdrop-filter;
  color: #fff;
  transition:
    opacity 0.18s ease,
    background-color 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease;
}
.stratum-photo-viewer-glass--on-light {
  border-color: rgba(0, 0, 0, 0.03);
  background: rgba(20, 20, 20, 0.05);
  color: #141414;
}
.stratum-photo-viewer-glass--fallback {
  border-color: rgba(255, 255, 255, 0.12);
  background: rgba(20, 20, 20, 0.55);
  color: #fff;
}
.stratum-photo-viewer-glass--hidden {
  opacity: 0;
  pointer-events: none;
}
.stratum-photo-viewer-position {
  position: absolute;
  /* Clear of an iOS PWA's status bar and its blur, as a full-screen view's content is. */
  top: max(18px, var(--safe-top-clear));
  left: 50%;
  z-index: 10;
  transform: translateX(-50%);
  padding: 5px 12px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.stratum-photo-viewer-nav {
  display: none;
}
@media (min-width: 768px) {
  .stratum-photo-viewer-nav {
    position: absolute;
    top: 50%;
    z-index: 10;
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    padding: 0;
    border-radius: 50%;
    cursor: pointer;
    transform: translateY(-50%);
  }
  .stratum-photo-viewer-nav:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.28);
  }
  .stratum-photo-viewer-glass--on-light.stratum-photo-viewer-nav:hover:not(:disabled) {
    background: rgba(20, 20, 20, 0.1);
  }
  .stratum-photo-viewer-glass--fallback.stratum-photo-viewer-nav:hover:not(:disabled) {
    background: rgba(20, 20, 20, 0.68);
  }
  /* At either end: still there, so the pair doesn't jump around, but its arrow is faint. */
  /* Like stratum-ui's Button when disabled. */
  .stratum-photo-viewer-nav:disabled {
    cursor: not-allowed;
  }
  .stratum-photo-viewer-nav:disabled svg {
    opacity: 0.3;
  }
  .stratum-photo-viewer-nav--prev {
    left: 18px;
  }
  .stratum-photo-viewer-nav--next {
    right: 18px;
  }
}
.stratum-glass-icon-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
}
.stratum-glass-icon-btn svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.2;
}
.stratum-glass-icon-btn--sm {
  width: 28px;
  height: 28px;
}
.stratum-glass-icon-btn--sm svg {
  width: 13px;
  height: 13px;
}
/* As MediaViewer's close button (.stratum-photo-viewer-close:hover). */
.stratum-glass-icon-btn:hover {
  background: rgba(255, 255, 255, 0.28);
}
.stratum-glass-icon-btn.stratum-photo-viewer-glass--on-light:hover {
  background: rgba(20, 20, 20, 0.1);
}
.stratum-video-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Faded out piece by piece (see VideoControls), so only clicks are off here. */
.stratum-video-controls--hidden {
  pointer-events: none;
}
/* The seek bar and times, in a glass pill as tall as the buttons beside it. */
.stratum-video-controls-bar {
  display: flex;
  flex: 1;
  align-items: center;
  gap: 10px;
  min-width: 0;
  height: 34px;
  padding: 0 14px;
  box-sizing: border-box;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stratum-video-controls-time {
  flex: none;
}
/* A thin track in the glass's own color, filled up to where it's got (--stratum-video-progress). */
.stratum-video-controls-seek {
  flex: 1;
  min-width: 0;
  height: 20px;
  margin: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}
.stratum-video-controls-seek:disabled {
  cursor: default;
}
.stratum-video-controls-seek::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(
    to right,
    currentColor var(--stratum-video-progress, 0%),
    color-mix(in srgb, currentColor 30%, transparent) var(--stratum-video-progress, 0%)
  );
}
.stratum-video-controls-seek::-webkit-slider-thumb {
  width: 12px;
  height: 12px;
  margin-top: -4px;
  border: none;
  border-radius: 50%;
  background: currentColor;
  -webkit-appearance: none;
  appearance: none;
}
.stratum-video-controls-seek::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: color-mix(in srgb, currentColor 30%, transparent);
}
.stratum-video-controls-seek::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: currentColor;
}
.stratum-video-controls-seek::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: currentColor;
}
/* A cut-off end of a sideways-scrolling line fades out (useEdgeFade marks which), eased as the app
   shell's bottom fade; growing and shrinking as it scrolls there and back. */
@property --stratum-edge-fade-start {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
@property --stratum-edge-fade-end {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
.stratum-edge-fade {
  --fs: var(--stratum-edge-fade-start);
  --fe: var(--stratum-edge-fade-end);
  -webkit-mask-image: var(--stratum-edge-fade-mask);
  mask-image: var(--stratum-edge-fade-mask);
  --stratum-edge-fade-mask: linear-gradient(
    to right,
    rgba(0, 0, 0, 0) 0,
    rgba(0, 0, 0, 0.28) calc(var(--fs) * 0.5),
    rgba(0, 0, 0, 0.62) calc(var(--fs) * 0.75),
    #000 var(--fs),
    #000 calc(100% - var(--fe)),
    rgba(0, 0, 0, 0.62) calc(100% - var(--fe) * 0.75),
    rgba(0, 0, 0, 0.28) calc(100% - var(--fe) * 0.5),
    rgba(0, 0, 0, 0) 100%
  );
  transition:
    --stratum-edge-fade-start 0.15s,
    --stratum-edge-fade-end 0.15s;
}
.stratum-edge-fade[data-cut-start] {
  --stratum-edge-fade-start: var(--stratum-edge-fade-size, 28px);
}
.stratum-edge-fade[data-cut-end] {
  --stratum-edge-fade-end: var(--stratum-edge-fade-size, 28px);
}
.stratum-spinner {
  display: inline-block;
  flex-shrink: 0;
  vertical-align: middle;
  border-radius: 50%;
  border: 2px solid var(--line-2);
  border-top-color: var(--accent);
  animation: stratum-spinner-spin 0.7s linear infinite;
}
/* In a line of text: a little gap before the words it's beside. */
.stratum-spinner--sm {
  width: 12px;
  height: 12px;
  border-width: 1.5px;
  margin-right: 6px;
}
.stratum-spinner--md {
  width: 20px;
  height: 20px;
}
@keyframes stratum-spinner-spin {
  to {
    transform: rotate(360deg);
  }
}
/* ── Mobile sticky header ──
   `fixed`, not `absolute`: this is mobile-only (display:none below, see the media query
   further down), and pinning it to the real viewport rather than a positioned ancestor is
   what keeps it correctly stuck at the top for a page whose ancestor chain grows taller
   than the viewport (AppShellLayout's `scrollMode="native"` opt-in — see its own doc) —
   and renders identically to the old `position: absolute` for every page that ISN'T
   native-scroll, since their containing ancestor stays exactly viewport-sized regardless
   (same reasoning as AppShellLayout.css's own .stratum-app-shell-nav/-fab). */
.stratum-page-header-mobile {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: rgba(251, 250, 247, 0.88);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  border-bottom: 1px solid transparent;
  transition:
    transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.22s,
    border-bottom-color 0.18s;
}
.stratum-page-header-mobile--scrolled {
  border-bottom-color: rgba(0, 0, 0, 0.08);
}
.stratum-page-header-mobile--hidden {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-color-scheme: dark) {
  .stratum-page-header-mobile {
    background: rgba(10, 10, 11, 0.88);
  }
}
@media (min-width: 768px) {
  .stratum-page-header-mobile {
    display: none !important;
  }
}

/* Fills in for the header above once it's slid away (scrolled down): a soft tint + glass blur along the
   top edge, standing in for the scroll-edge effect real iOS now draws itself under the status bar. So it
   exists ONLY inside the dev mobile-view emulator (ViewportPreview marks the emulated app's <html> with
   data-viewport-preview) — never on a real phone, and never in a plain browser window that's merely been
   narrowed to a mobile width. Kept as its own fixed element rather than baked into
   .stratum-page-header-mobile's background so its crossfade can run opposite the header's (visible
   exactly when the header isn't). `pointer-events: none` since it's purely decorative — it never
   intercepts taps meant for the content scrolling underneath it, including PageHeaderMobile's own
   onBackgroundClick handling. Capped at 70% of --bg even at its top edge. The falloff uses several
   intermediate stops (an "eased" gradient) rather than a plain two-stop one — a straight linear gradient
   reads as fading much faster than its opacity numbers suggest, which made the earlier version look like
   it ended in an abrupt line. */
.stratum-page-header-mobile-fade {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  height: calc(var(--safe-top) + 66px);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--bg) 70%, transparent) 0%,
    color-mix(in srgb, var(--bg) 61%, transparent) 19%,
    color-mix(in srgb, var(--bg) 48%, transparent) 34%,
    color-mix(in srgb, var(--bg) 34%, transparent) 47%,
    color-mix(in srgb, var(--bg) 21%, transparent) 59%,
    color-mix(in srgb, var(--bg) 12%, transparent) 71%,
    color-mix(in srgb, var(--bg) 6%, transparent) 84%,
    color-mix(in srgb, var(--bg) 2%, transparent) 93%,
    transparent 100%
  );
  /* Slight glass blur behind the tint, faded out toward the bottom by the mask (which clips the
     blur and the gradient alike) so there's no hard edge — the look of iOS's own scroll-edge
     effect. */
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s;
}
.stratum-page-header-mobile-fade--visible {
  opacity: 1;
}
/* Shown only in the emulator (see the doc above). */
html[data-viewport-preview] .stratum-page-header-mobile-fade {
  display: block;
}

/* 30px (not a plain 16px) below the safe-area inset: iOS Safari's standalone
   web apps now blur a strip along the top edge of the screen, which softened
   the top of the title at 16px. .stratum-page-header-spacer measures the header,
   so only its fallback (before the first measure) assumes this. */
.stratum-page-header-mobile-inner {
  padding: calc(30px + var(--safe-top)) 20px 14px;
}
.stratum-page-header-mobile-inner:has(> .stratum-tabs:last-child) {
  padding-bottom: 0;
}

.stratum-page-header-title-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
}

/* Top-aligned against the title's cap-height, not bottom-aligned against the
   subtitle's baseline like the row's own `align-items: flex-end` default. */
.stratum-page-header-action {
  align-self: flex-start;
  flex-shrink: 0;
}

.stratum-page-header-title {
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink);
  /* Shrink-wraps to the actual title text instead of a plain block's default `width: auto`
     (which fills the whole column, even past the visible glyphs, whenever a sibling like the
     subtitle needs that width) — still wraps onto multiple lines past this width exactly like
     `auto` did, just without silently widening the div's own (and thus its click target's) box
     beyond the rendered text for a short title. Load-bearing for
     PageHeaderMobile's onBackgroundClick (see its own doc) — without this, tapping to the
     right of a short title still hits this div, not the header's background. */
  width: fit-content;
  max-width: 100%;
  /* Every header title is one line, cut off with an ellipsis; the full title opens in a
     BottomPanel on tap (HeaderTitle in PageHeader.tsx), or scrolls sideways (--scroll). */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stratum-page-header-title--wrap {
  white-space: normal;
  overflow: visible;
}
.stratum-page-header-title--scroll {
  overflow-x: auto;
  overflow-y: hidden;
  text-overflow: clip;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.stratum-page-header-title--scroll::-webkit-scrollbar {
  display: none;
}
.stratum-page-header-title--clickable {
  cursor: pointer;
}
/* The flex item wrapping title + subtitle must be allowed to shrink below its content's width,
   or a long title pushes the action off-screen instead of truncating. */
.stratum-page-header-title-row > div:first-child,
.stratum-page-header-desktop-inner > div:first-child {
  min-width: 0;
}
/* No title yet (still loading): a title and subtitle line's room, blank. */
.stratum-page-header-pending {
  visibility: hidden;
}

.stratum-page-header-subtitle {
  font-family: inherit;
  font-size: 13px;
  color: var(--ink-3);
  margin-top: 2px;
}

/* ── Desktop sticky header ──
   Needs to be mounted inside a scrolling ancestor for `position: sticky` to
   stick within it. */
.stratum-page-header-desktop {
  display: none;
  position: sticky;
  top: 0;
  z-index: 10;
  border-bottom: 1px solid transparent;
  transition:
    background-color 0.18s,
    border-bottom-color 0.18s,
    box-shadow 0.18s,
    transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.22s;
}
.stratum-page-header-desktop--scrolled {
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  border-bottom-color: var(--line);
}
/* Opt-in only (see PageHeaderDesktopProps.hidden) — sticky still reserves
   this box's layout slot at its "home" position in the flow, so hiding the
   visually-pinned instance via transform just reveals the content that was
   underneath it, same visual result as the mobile absolute+transform case. */
.stratum-page-header-desktop--hidden {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-color-scheme: dark) {
  .stratum-page-header-desktop--scrolled {
    background: rgba(20, 20, 22, 0.88);
  }
}
@media (min-width: 768px) {
  .stratum-page-header-desktop {
    display: block;
  }
}

.stratum-page-header-desktop-inner {
  padding: 24px 36px 16px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
}
/* The bar row (e.g. a FilterBar) under the title, aligned with it. */
.stratum-page-header-desktop-bar {
  margin-top: -10px;
  padding: 0 36px 12px;
}
/* A bar of Tabs ends in the active tab's own underline, so the header stops right under it: no
   padding below, on desktop here and on mobile in the header's inner padding. */
.stratum-page-header-desktop-bar:has(> .stratum-tabs) {
  padding-bottom: 0;
}
.stratum-page-header-desktop .stratum-page-header-title {
  font-size: 32px;
}

/* Reserves the mobile header's height in normal flow, since the header itself
   is position: absolute and would otherwise overlap the content below it. */
.stratum-page-header-spacer {
  /* The fixed mobile header's actual height, measured by PageHeaderMobile (a bar, children, a
     wrapped title all count); the constant is a title and subtitle's, until it's measured. */
  height: var(--stratum-page-header-mobile-height, calc(86px + var(--safe-top)));
}
@media (min-width: 768px) {
  .stratum-page-header-spacer {
    display: none;
  }
}

/* Back crumb (PageHeaderCrumb): reads as part of the subtitle — same font and color — and is
   underlined on hover, like the subtitle's other links. */
.stratum-page-header-crumb {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.stratum-page-header-crumb:hover {
  text-decoration: underline;
}

/* Full title in the built-in title panel: wraps freely, at reading size. */
.stratum-page-header-title-full {
  padding-top: 4px;
  font-size: 17px;
  line-height: 1.5;
  color: var(--ink);
  overflow-wrap: break-word;
}

/* The subtitle's loading Spinner (`loading`): right after its last piece, its gap on that side. */
.stratum-page-header-spinner.stratum-spinner {
  margin-right: 0;
  margin-left: 6px;
}
.stratum-checkbox {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 0;
  font-family: inherit;
  font-size: 14px;
  color: var(--ink-2);
  cursor: pointer;
  background: none;
  border: none;
  width: 100%;
  text-align: left;
}
.stratum-checkbox--checked {
  color: var(--ink);
}
.stratum-checkbox:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.stratum-checkbox-box {
  width: 18px;
  height: 18px;
  border-radius: 5px;
  border: 1.5px solid var(--line-2);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition:
    background 0.12s,
    border-color 0.12s;
}
.stratum-checkbox--checked .stratum-checkbox-box {
  background: var(--accent);
  border-color: var(--accent);
}

.stratum-checkbox-box svg {
  width: 11px;
  height: 11px;
  color: #fff;
  opacity: 0;
  transition: opacity 0.1s;
}
.stratum-checkbox--checked .stratum-checkbox-box svg {
  opacity: 1;
}
.stratum-badge {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.stratum-badge--ok {
  background: #dff6e4;
  color: #2b7a3a;
}
.stratum-date-field {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1.5px solid var(--line);
  background: var(--surface);
  cursor: pointer;
  position: relative;
}
.stratum-date-field--has-value {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.stratum-date-field:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}
.stratum-date-field-icon {
  width: 15px;
  height: 15px;
  color: var(--ink-3);
  flex-shrink: 0;
}
.stratum-date-field--has-value .stratum-date-field-icon {
  color: var(--accent);
}
.stratum-date-field-display {
  font-size: 13px;
  color: var(--ink-2);
  flex: 1;
}
.stratum-date-field--has-value .stratum-date-field-display {
  color: var(--accent);
  font-weight: 500;
}
.stratum-date-field input[type='date'] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%;
}
.stratum-date-field input[type='date']:disabled {
  /* Browsers don't reliably show a disabled native input's own `cursor`
     value on hover. Let events fall through to the label instead, whose
     `:has(input:disabled)` rule above already sets not-allowed correctly. */
  pointer-events: none;
}
.stratum-date-field input[type='date']::-webkit-calendar-picker-indicator {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.stratum-date-field-clear {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 15px;
  color: var(--accent);
  padding: 0;
  line-height: 1;
  position: relative;
  z-index: 1;
}
.stratum-date-field-clear:disabled {
  /* Otherwise this more-specific rule would keep showing `pointer` on hover
     even though the button (a separate, higher z-index sibling of the native
     input) is no longer clickable — overriding the label's own
     :has(input:disabled) cursor: not-allowed above. */
  cursor: not-allowed;
}
.stratum-slider-track-wrap {
  position: relative;
  height: 28px;
  margin: 6px 8px 0;
  touch-action: none;
}
.stratum-slider-track {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 3px;
  transform: translateY(-50%);
  background: var(--line-2);
  border-radius: 2px;
}
.stratum-slider-fill {
  position: absolute;
  top: 50%;
  height: 3px;
  transform: translateY(-50%);
  background: var(--line-2);
  border-radius: 2px;
  transition: background 0.15s;
}
.stratum-slider--active .stratum-slider-fill {
  background: var(--accent);
}
/* Hit target is deliberately much bigger than the visible dot (44px is the
   accepted minimum comfortable touch target) — a smaller box makes it easy
   to miss the thumb on a touchscreen and grab the track/nothing instead. */
.stratum-slider-thumb {
  position: absolute;
  top: 50%;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translate(-50%, -50%);
  cursor: grab;
  touch-action: none;
}
.stratum-slider-thumb:active {
  cursor: grabbing;
}
.stratum-slider-thumb-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--line-2);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  transition: border-color 0.15s;
}
.stratum-slider--active .stratum-slider-thumb-dot {
  border-color: var(--accent);
}
.stratum-slider-vals {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
  padding: 4px 6px 0;
  text-transform: capitalize;
}
.stratum-slider--active .stratum-slider-vals {
  color: var(--ink-2);
}
.stratum-range-rail {
  position: relative;
  height: 28px;
  margin: 6px 8px 0;
  touch-action: none;
}
.stratum-range-track,
.stratum-range-fill {
  position: absolute;
  top: 50%;
  height: 3px;
  transform: translateY(-50%);
  border-radius: 2px;
  background: var(--line-2);
}
.stratum-range-track {
  left: 0;
  right: 0;
}
.stratum-range-fill {
  transition: background 0.15s;
}
.stratum-range--active .stratum-range-fill {
  background: var(--accent);
}
/* The hit target is much bigger than the dot (44px, a comfortable touch target), so a finger
   doesn't grab the track instead. */
.stratum-range-thumb {
  position: absolute;
  top: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
  cursor: grab;
  touch-action: none;
}
.stratum-range-thumb:active {
  cursor: grabbing;
}
.stratum-range-thumb-dot {
  width: 16px;
  height: 16px;
  border: 2px solid var(--line-2);
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  transition: border-color 0.15s;
}
.stratum-range--active .stratum-range-thumb-dot {
  border-color: var(--accent);
}
.stratum-range-vals {
  display: flex;
  justify-content: space-between;
  padding: 4px 6px 0;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
}
.stratum-range--active .stratum-range-vals {
  color: var(--ink-2);
}
.stratum-range--disabled,
.stratum-range--disabled .stratum-range-thumb {
  pointer-events: none;
  cursor: not-allowed;
}
.stratum-icon-button {
  border: none;
  background: transparent;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stratum-icon-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.stratum-icon-button--square {
  width: 34px;
  height: 34px;
  border-radius: 8px;
  color: var(--ink-2);
  transition: background 0.12s;
}
.stratum-icon-button--square:hover:not(:disabled) {
  background: var(--surface-2);
  color: var(--ink);
}
.stratum-icon-button--square svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.8;
}

.stratum-icon-button--circle {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  color: var(--ink-2);
}
.stratum-icon-button--circle:hover:not(:disabled) {
  background: var(--surface-2);
}
.stratum-icon-button--circle svg {
  width: 20px;
  height: 20px;
}

.stratum-icon-button--warn {
  color: var(--warn);
}
/* Chrome comes from IconButton's own .stratum-icon-button classes, and the
   overlay-select mechanism from SelectOverlay's own .stratum-select-overlay
   class (see IconMenu.tsx). IconButton's own :hover never fires here though:
   SelectOverlay's invisible <select> sits on top and receives the pointer
   instead of the button underneath — same issue as RotateControl's
   disabled-hover override. Re-trigger the hover state via the wrapper span. */
.stratum-icon-menu:hover .stratum-icon-button {
  background: var(--surface-2);
  color: var(--ink);
}
.stratum-popover-menu {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
}

/* In <body>, fixed beside the trigger (placed inline from its position). */
.stratum-popover-menu-card {
  position: fixed;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  min-width: 190px;
  padding: 6px;
  border: 1px solid var(--line-2);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.14);
}

.stratum-popover-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.stratum-popover-menu-item:hover {
  background: var(--surface-2);
}
.stratum-popover-menu-item svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  stroke-width: 1.8;
  color: var(--ink-3);
}
.stratum-tabs {
  display: flex;
  gap: 18px;
  padding-top: 8px;
  overflow-x: auto;
  scrollbar-width: none;
}
.stratum-tabs::-webkit-scrollbar {
  display: none;
}
.stratum-tab {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 0 7px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--ink-3);
  cursor: pointer;
}
.stratum-tab:hover {
  color: var(--ink-2);
}
.stratum-tab--active,
.stratum-tab--active:hover {
  border-bottom-color: var(--accent);
  color: var(--accent);
}
.stratum-tab-count {
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  color: #fff;
}
.stratum-link-button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.stratum-link-button:hover:not(:disabled) {
  text-decoration: underline;
}
.stratum-link-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.stratum-link-button--warn {
  color: var(--warn);
}
.stratum-link-button--inherit {
  color: inherit;
}
.stratum-link-button--regular {
  font-weight: inherit;
}
.stratum-url-link {
  color: var(--ink-3);
  font-family: var(--mono);
  text-decoration: none;
}
.stratum-url-link:hover {
  text-decoration: underline;
}
.stratum-file-picker {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 14px;
  border-radius: 10px;
  border: 1.5px dashed var(--line-2);
  background: var(--surface);
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
  transition:
    background 0.12s,
    border-color 0.12s,
    color 0.12s;
}
.stratum-file-picker svg {
  width: 18px;
  height: 18px;
  color: var(--ink-3);
  flex-shrink: 0;
}
.stratum-file-picker:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.stratum-file-picker--has-files {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}
.stratum-file-picker--has-files svg {
  color: var(--accent);
}
.stratum-app-shell {
  /* The rail's width (0 where there's none: mobile), for what's positioned against the viewport but
     belongs over the main column - BottomPanel centres over it, not the whole window. */
  --stratum-app-shell-rail-width: 0px;
  display: flex;
  height: calc(var(--vh, 1vh) * 100);
  background: var(--bg);
  overflow: hidden;
}

/* ── Desktop rail ── */
.stratum-app-shell-rail {
  display: none;
  flex-shrink: 0;
  width: var(--stratum-app-shell-rail-width);
  box-sizing: border-box;
  /* A phone in landscape: clear the Dynamic Island or notch when it's on the left, and the home
     indicator below. env() is 0 where there's none. */
  padding: 14px 0 calc(18px + var(--safe-bottom)) var(--safe-left);
  flex-direction: column;
  align-items: center;
  gap: 4px;
  border-right: 1px solid var(--line);
  background: var(--surface-2);
  height: calc(var(--vh, 1vh) * 100);
  /* Any number of pages: past what fits, the rail scrolls (no scrollbar), as the mobile nav does
     sideways. A wheel over it scrolls it then, not the page (useWheelForward in AppShellLayout). */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.stratum-app-shell-rail::-webkit-scrollbar {
  display: none;
}
@media (min-width: 768px) {
  .stratum-app-shell {
    --stratum-app-shell-rail-width: calc(72px + var(--safe-left));
  }
  .stratum-app-shell-rail {
    display: flex;
  }
}

.stratum-app-shell-rail-btn {
  /* Never squeezed to fit: the rail scrolls instead. */
  flex-shrink: 0;
  width: 48px;
  height: 52px;
  border: none;
  background: transparent;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--ink-3);
  cursor: pointer;
  font-family: inherit;
  font-size: 10px;
  font-weight: 500;
  text-decoration: none;
  transition:
    background 0.15s,
    color 0.15s;
}
.stratum-app-shell-rail-btn svg {
  width: 22px;
  height: 22px;
  stroke-width: 1.7;
  padding: 7px;
  box-sizing: content-box;
  border-radius: 10px;
  transition:
    background 0.15s,
    color 0.15s;
}
.stratum-app-shell-rail-btn:hover:not(.stratum-app-shell-rail-btn--active) {
  color: var(--ink-2);
}
.stratum-app-shell-rail-btn--active {
  color: var(--accent);
  font-weight: 600;
}
.stratum-app-shell-rail-btn--active svg {
  background: var(--accent-soft);
  color: var(--accent);
}

.stratum-app-shell-rail-fab {
  margin-top: auto;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--accent);
  border: none;
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(10, 132, 255, 0.35);
  transition: background-color 0.15s;
}
.stratum-app-shell-rail-fab:hover {
  /* Same color-mix darken-on-hover as Button's primary variant (see
     Button.css) — the FAB previously had no hover feedback at all. */
  background: color-mix(in srgb, var(--accent) 88%, black);
}
.stratum-app-shell-rail-fab svg {
  width: 20px;
  height: 20px;
  /* Same weight as the mobile FAB's icon, without each app setting it on its own svg. */
  stroke-width: 2;
}
.stratum-app-shell-rail-fab--alt {
  background: var(--accent-alt);
  box-shadow: 0 4px 12px rgba(255, 149, 0, 0.35);
}
.stratum-app-shell-rail-fab--alt:hover {
  background: color-mix(in srgb, var(--accent-alt) 88%, black);
}

/* ── Main column ── */
.stratum-app-shell-main {
  flex: 1;
  min-width: 0;
  position: relative;
  height: calc(var(--vh, 1vh) * 100);
  /* The height a page has below the shell's own chrome, for one that needs it as a length (a mobile page
     that bounds itself to the viewport): the viewport, less a banner. Use it, not 100vh or --vh. */
  --stratum-app-shell-page-height: calc(var(--vh, 1vh) * 100);
  overflow: hidden;
  /* Single shared source of truth for "how much bottom clearance a page's
     own scroll container needs" — nav height (0 on desktop) plus a flat
     breathing-room default. Consumers (AppShellBottomSpacer,
     InfiniteScrollFooter, or a page's own padding-bottom) should read this
     directly rather than re-deriving it from --stratum-app-shell-nav-height
     themselves, so the default only ever needs tuning in one place. */
  --stratum-app-shell-content-inset-bottom: calc(var(--stratum-app-shell-bottom-chrome) + 100px);
  /* Desktop: the height of an accessory's fade band, with `fade` (see below). */
  --stratum-app-shell-accessory-fade: 64px;
  /* What covers the bottom edge of the page: on mobile the nav, and a page's AppShellAccessory on
     top of it; on desktop (no nav) just the accessory. */
  --stratum-app-shell-bottom-chrome: calc(
    var(--stratum-app-shell-nav-height, 0px) + var(--stratum-app-shell-accessory-height, 0px) + var(--stratum-app-shell-bottom-fade, 0px) +
      var(--stratum-app-shell-keyboard-room, 0px)
  );
}
/* Mobile, with an accessory's `mobileFade`: its fade band above the glass covers the page too. */
@media (max-width: 767px) {
  .stratum-app-shell--accessory-mobile-fade .stratum-app-shell-main {
    --stratum-app-shell-bottom-fade: 32px;
  }
}
@media (min-width: 768px) {
  .stratum-app-shell-main {
    display: flex;
    flex-direction: column;
    /* Nothing over the page (an accessory sits under it), except an accessory's fade band (below). */
    --stratum-app-shell-bottom-chrome: 0px;
    /* A phone in landscape: clear the Dynamic Island or notch when it's on the right. */
    box-sizing: border-box;
    padding-right: var(--safe-right);
  }
  .stratum-app-shell--accessory-fade .stratum-app-shell-main {
    --stratum-app-shell-bottom-chrome: var(--stratum-app-shell-accessory-fade);
  }
}

/* A phone in landscape (the wide layout on a short touch screen; not a desktop or a tablet): with the
   keyboard up for the accessory, only a strip of the page shows above it, so the page's header stays
   out of the way, scrolling up included, until the keyboard goes down. */
@media (min-width: 768px) and (max-height: 500px) and (pointer: coarse) {
  .stratum-app-shell--keyboard .stratum-page-header-desktop {
    transform: translateY(-100%);
    opacity: 0;
    pointer-events: none;
  }
}

/* The routed page: the whole main column, less a desktop accessory's room below it. */
.stratum-app-shell-page {
  height: 100%;
}
/* Not positioned, on purpose: something a page docks with position: absolute (a BottomPanel kept off
   the rail) then docks to the main column, accessory included, so it opens over the accessory. */
/* Desktop: a column, as the main column itself is, so a page's own children lay out as they did when
   they sat straight in it - a scroll container with `flex: 1; min-height: 0` gets the page's height
   (and scrolls) instead of growing to its content, which the main column would clip unscrollably. */
@media (min-width: 768px) {
  .stratum-app-shell-page {
    flex: 1 1 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
}

/* ── A page's AppShellAccessory ──
   Mobile: the top of the bottom chrome, above the nav (.stratum-app-shell-bottom carries the glass).
   Desktop: in the main column, under the page. */
.stratum-app-shell-accessory:empty {
  display: none;
}
.stratum-app-shell-accessory {
  flex-shrink: 0;
}
/* Desktop: no nav, so only a page's accessory, under the page. Not glass: the page's own background
   (`background`, the default) or nothing at all; with `fade`, a band above it where the page fades
   out into it, as on claude.ai. Only its content
   takes clicks; the rest (the fade, the room around the content) lets them through to the page, its
   scrollbar included. */
@media (min-width: 768px) {
  .stratum-app-shell-accessory {
    pointer-events: none;
    /* A phone in landscape: above the home indicator. Part of the accessory, so its measured height
       (what a page clears, and what sits just above it) counts this room too. */
    padding-bottom: var(--safe-bottom);
  }
  /* Not with the keyboard up: it covers the home indicator, and the accessory sits right on it. */
  .stratum-app-shell--keyboard .stratum-app-shell-accessory {
    padding-bottom: 0;
  }
  /* The page's background behind it (AppShellAccessory's `background`, on by default). */
  .stratum-app-shell--accessory-background .stratum-app-shell-accessory {
    background: var(--bg);
  }
  .stratum-app-shell-accessory > * {
    pointer-events: auto;
  }
  /* The fade (AppShellAccessory's `fade`, on by default): a band above it, over the end of the page, fading from
     transparent into the page's background - eased, so it starts barely there and only turns solid
     at the accessory. Paints only; takes no clicks, so the page's scrollbar under it still works. */
  .stratum-app-shell--accessory-fade .stratum-app-shell-accessory {
    position: relative;
  }
  .stratum-app-shell--accessory-fade .stratum-app-shell-accessory::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: var(--stratum-app-shell-accessory-fade);
    background: linear-gradient(
      to bottom,
      color-mix(in srgb, var(--bg) 0%, transparent) 0,
      color-mix(in srgb, var(--bg) 8%, transparent) 25%,
      color-mix(in srgb, var(--bg) 28%, transparent) 50%,
      color-mix(in srgb, var(--bg) 62%, transparent) 75%,
      var(--bg) 100%
    );
    pointer-events: none;
  }
}

/* Desktop, a page with an accessory (a chat's composer): its bottom panels dock to the main column -
   not the viewport, which would center them across the rail too - so they open over the accessory, in
   line with the page. Absolute against the main column, so the page mustn't position an ancestor of
   them (.stratum-app-shell-page isn't, on purpose). Their width: --stratum-bottom-panel-max-width,
   e.g. the accessory's own. */
@media (min-width: 768px) {
  .stratum-app-shell--has-accessory .stratum-bottom-panel {
    position: absolute;
    /* Already inside the main column: no rail to clear. */
    left: 0;
  }
}

/* ── Native-scroll opt-in (see AppShellLayoutProps.scrollMode's own doc) ──
   Mobile only (max-width: 767px) — desktop always keeps the 'nested' layout above,
   regardless of scrollMode, since the bug this exists for (iOS Safari's native
   text-selection UI breaking inside a scrolled nested container) is mobile-Safari-specific.
   Lets `children`'s own content grow past the viewport height instead of being clipped by
   this shell, so the real `document`/`body` becomes the scrolling element — pairs with the
   consuming page's own CSS relaxing ITS scroll container's height/overflow the same way
   (see media/frontend's ReadPage.css for the first example), and with
   media/frontend/src/index.css's `body.read-native-scroll` override, which the consumer is
   responsible for toggling (this component has no way to reach `document.body` itself). */
@media (max-width: 767px) {
  .stratum-app-shell--native-scroll {
    height: auto;
    overflow: visible;
  }
  .stratum-app-shell--native-scroll .stratum-app-shell-main,
  .stratum-app-shell--native-scroll .stratum-app-shell-page {
    height: auto;
    overflow: visible;
  }
}

/* ── Mobile bottom chrome: a page's accessory (if any) on top of the nav, as ONE pane ──
   The glass (background + backdrop blur), the top line and the slide-away live on this one
   container, not on the accessory and the nav separately: two elements blurring what's behind them
   each blur only their own box, so they meet at a visible seam however alike they're styled. */
.stratum-app-shell-bottom {
  /* `fixed`, not `absolute`: correct once .stratum-app-shell-main can grow taller than the
     viewport (see --native-scroll above) — it needs to stay pinned to the actual viewport bottom
     while the document scrolls underneath it, not the bottom of the full (possibly much taller)
     content. On pages that AREN'T native-scroll, .stratum-app-shell-main stays exactly
     viewport-sized regardless, so `fixed` vs `absolute` render identically there too. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  background: rgba(251, 250, 247, 0.88);
  backdrop-filter: saturate(1.5) blur(20px);
  -webkit-backdrop-filter: saturate(1.5) blur(20px);
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  /* Always-on (not just while --hidden is animating): forces this onto its own GPU-composited
     layer, same reasoning as .stratum-app-shell-fab's own base `transform: translateY(0)` above —
     a plain, non-composited position:fixed element is the one observed to desync from the real
     viewport bottom on iOS Safari PWAs after the WKWebView is suspended in the background for a
     while and then resumed without a full reload (it renders mid-content instead of pinned to the
     true bottom; only fully closing/reopening the PWA — a fresh paint — fixes it otherwise). */
  transform: translateY(0);
  /* `will-change`/`backface-visibility` (confirmed, separate from the resume-desync issue above):
     rapidly reversing this transition — the real trigger is scrolling direction flipping back and
     forth, e.g. while reading, which flips useScrollHeader's `navHidden` mid-slide — can leave
     this element visually STUCK partway through the animation on iOS Safari, since
     interrupting/reversing a `transform` transition forces WebKit to re-derive this element's
     `backdrop-filter` compositing on the fly. Pinning it to a stable, dedicated layer up front
     avoids that recompute. Confirmed via a real device reproduction. */
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
.stratum-app-shell-bottom--hidden {
  transform: translateY(100%);
}
.stratum-app-shell-bottom.stratum-app-shell-bottom--borderless {
  border-top-color: transparent;
}
@media (prefers-color-scheme: dark) {
  .stratum-app-shell-bottom {
    background: rgba(10, 10, 11, 0.88);
    border-top-color: rgba(255, 255, 255, 0.06);
  }
}
/* Desktop: none of the mobile chrome - no nav, no glass, no line - just a page's accessory, in the
   main column under the page, which ends right above it, as if this container weren't there.
   Nothing at all without an accessory. */
@media (min-width: 768px) {
  .stratum-app-shell-bottom {
    display: contents;
  }
  .stratum-app-shell:not(.stratum-app-shell--has-accessory) .stratum-app-shell-bottom {
    display: none;
  }
}
.stratum-app-shell--edit-mode .stratum-app-shell-bottom {
  display: none;
}
/* Mobile, with an accessory's `mobileFade` (on by default): the glass reaches up a band further
   than the accessory, and a mask fades that band in - transparent at its top, full glass by the
   accessory - so content scrolling down fades into the glass rather than meeting its edge. One
   element (one blur), masked, so there's no seam; the accessory and nav sit below the band,
   untouched. The band takes no clicks. Eased, like the desktop fade. */
@media (max-width: 767px) {
  .stratum-app-shell--accessory-mobile-fade .stratum-app-shell-bottom {
    padding-top: var(--stratum-app-shell-bottom-fade, 32px);
    --stratum-app-shell-fade-mask: linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 0.08) calc(var(--stratum-app-shell-bottom-fade, 32px) * 0.25),
      rgba(0, 0, 0, 0.28) calc(var(--stratum-app-shell-bottom-fade, 32px) * 0.5),
      rgba(0, 0, 0, 0.62) calc(var(--stratum-app-shell-bottom-fade, 32px) * 0.75),
      #000 var(--stratum-app-shell-bottom-fade, 32px)
    );
    -webkit-mask-image: var(--stratum-app-shell-fade-mask);
    mask-image: var(--stratum-app-shell-fade-mask);
    pointer-events: none;
  }
  .stratum-app-shell--accessory-mobile-fade .stratum-app-shell-bottom > * {
    pointer-events: auto;
  }
}

.stratum-app-shell-nav {
  display: flex;
  padding: 6px;
  /* Home-indicator clearance: the real safe-area inset on device (34px on
     modern iPhones), the simulated one inside ViewportPreview's iPhone frame.
     env() is 0 where there's no inset, so this is a no-op on desktop. Total bottom
     padding is capped at 24px: on a real iOS PWA the OS already clears the
     home indicator, so the full 34px manual inset is unnecessary — the
     retained 18px keeps a reasonable minimum in simulated views, while
     smaller/no insets keep their natural smaller value. */
  padding-bottom: calc(6px + min(var(--safe-bottom-effective), 18px));
  /* Any number of pages: past what fits, the row scrolls sideways (no scrollbar) rather than
     squeezing each button narrower. */
  overflow-x: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
}
.stratum-app-shell-nav::-webkit-scrollbar {
  display: none;
}
@media (min-width: 768px) {
  .stratum-app-shell-nav {
    display: none !important;
  }
}

.stratum-app-shell-nav-btn {
  /* A few grow evenly to fill the row; many keep this width and scroll (see the nav above). */
  flex: 1 0 60px;
  padding: 8px 0 6px;
  border: none;
  background: transparent;
  color: var(--ink-3);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  border-radius: 10px;
}
.stratum-app-shell-nav-btn svg {
  width: 24px;
  height: 24px;
  stroke-width: 1.8;
}
.stratum-app-shell-nav-btn--active {
  color: var(--accent);
}

/* ── Mobile floating primary action ── */
/* Hides via `transform`, not `bottom`, to match the nav bar above: iOS
   Safari's WebKit doesn't reliably repaint an absolute-positioned element
   inside an overflow:hidden ancestor when only top/bottom/left/right
   changes, so an animated `bottom` can visually "stick" on real iOS
   devices even though the transition fires fine in desktop responsive
   mode. `transform` is GPU-composited and layout-independent, so it
   always animates smoothly. */
.stratum-app-shell-fab {
  /* fixed, not absolute — same reasoning as .stratum-app-shell-nav's own doc above. */
  position: fixed;
  right: 18px;
  /* 8px above the nav bar's top edge: nav base height (content + padding = 67px:
     54px button + 6px top pad + 6px bottom pad + 1px border) + effective
     safe-area + 8px gap. Scales with safe-area automatically; no
     standalone-specific override needed. */
  --fab-bottom: calc(75px + var(--safe-bottom-effective));
  --fab-hidden-bottom: 18px;
  bottom: var(--fab-bottom);
  z-index: 30;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: #fff;
  box-shadow:
    0 6px 16px rgba(10, 132, 255, 0.4),
    0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transform: translateY(0);
  transition:
    transform 0.24s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.15s;
}
.stratum-app-shell-fab:hover {
  /* Same color-mix darken-on-hover as Button's primary variant (see
     Button.css) — the FAB previously had no hover feedback at all. */
  background: color-mix(in srgb, var(--accent) 88%, black);
}
.stratum-app-shell-fab--nav-hidden {
  transform: translateY(calc(var(--fab-bottom) - var(--fab-hidden-bottom)));
}
.stratum-app-shell-fab--alt {
  background: var(--accent-alt);
  box-shadow:
    0 6px 16px rgba(255, 149, 0, 0.4),
    0 2px 4px rgba(0, 0, 0, 0.1);
}
.stratum-app-shell-fab--alt:hover {
  background: color-mix(in srgb, var(--accent-alt) 88%, black);
}
.stratum-app-shell-fab:active {
  transition-duration: 0.15s;
  transform: scale(0.94);
}
.stratum-app-shell-fab--nav-hidden:active {
  transform: translateY(calc(var(--fab-bottom) - var(--fab-hidden-bottom))) scale(0.94);
}
.stratum-app-shell-fab svg {
  width: 24px;
  height: 24px;
  stroke-width: 2;
}
/* FAB is hidden on desktop (nav is mobile-only). */
@media (min-width: 768px) {
  .stratum-app-shell-fab {
    display: none;
  }
}
.stratum-app-shell--edit-mode .stratum-app-shell-fab {
  display: none;
}

/* Opt-in (AppShellLayoutProps.navBorderless). Two classes so it also beats the
   dark-mode border color above. */

/* ── Nav item badge (AppShellNavItem.badge) ── */
.stratum-app-shell-nav-icon {
  position: relative;
  display: flex;
}
.stratum-app-shell-badge {
  position: absolute;
  top: -3px;
  right: -7px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 8px;
  background: var(--warn);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 2px var(--bg);
}
/* The rail's icon is padded for its active highlight, so the badge sits on the padding box. */
.stratum-app-shell-rail-btn .stratum-app-shell-badge {
  top: 2px;
  right: 1px;
  box-shadow: 0 0 0 2px var(--surface-2);
}

/* The document, frozen while a page has an accessory (mobile): nothing to scroll, so nothing - an
   on-screen keyboard coming up - can scroll it. The page scrolls its own container. :root (not
   html) out-ranks an app's own mobile body rules (e.g. `html:not(.x) body { overflow: visible }`). */
@media (max-width: 767px) {
  :root.stratum-frozen-document {
    overflow: hidden;
  }
  :root.stratum-frozen-document body {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
  }
}

/* ── A banner above the page ──
   The main column becomes a column on a phone too, so the page takes what the banner leaves instead of
   the full height. The banner takes the notch's room, so the page's own header (which clears it with
   --safe-top) doesn't clear it twice. */
.stratum-app-shell-banner {
  flex-shrink: 0;
  /* Over the mobile page header, which is fixed to the top of the viewport and slides up under it. */
  position: relative;
  z-index: 11;
  background: var(--bg);
  padding-top: var(--safe-top);
}
/* A strip across the column, not a card: the banner's own border and corners go, leaving a line under it. */
.stratum-app-shell-banner > .stratum-status-banner {
  border-width: 0 0 1px;
  border-radius: 0;
  padding: 9px 16px;
}
@media (min-width: 768px) {
  .stratum-app-shell-banner {
    padding-top: 0;
  }
}
.stratum-app-shell--has-banner .stratum-app-shell-main {
  display: flex;
  flex-direction: column;
  /* What a page that must be viewport-tall (not just fill its parent) gets less the banner. */
  --stratum-app-shell-page-height: calc(var(--vh, 1vh) * 100 - var(--stratum-app-shell-banner-height, 0px));
}
.stratum-app-shell--has-banner .stratum-app-shell-page {
  --safe-top: 0px;
  --safe-top-clear: 0px;
  flex: 1 1 0;
  min-height: 0;
  /* What the banner leaves of the column, as a length rather than the flex size alone: a page's own
     height: 100% only resolves against a definite height, which a flexed one isn't in every browser
     (Safari left the scroll container as tall as its content, so it never scrolled). */
  height: calc(100% - var(--stratum-app-shell-banner-height, 0px));
}
/* The mobile page header (fixed to the viewport's top) starts below the banner. */
.stratum-app-shell--has-banner .stratum-page-header-mobile,
.stratum-app-shell--has-banner .stratum-page-header-mobile-fade {
  top: var(--stratum-app-shell-banner-height, 0px);
}
.stratum-app-shell-bottom-spacer {
  height: calc(var(--stratum-app-shell-content-inset-bottom, 0px) + var(--stratum-app-shell-bottom-spacer-extra, 0px));
}
.stratum-recording-indicator {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Reset native <button> chrome — the pill itself is the stop button when a stop action is
   available (see RecordingIndicator.tsx), not just the small red mark inside it.
   `font-family: inherit` only, NOT the `font` shorthand — `font` resets every sub-property
   including size/weight, and since this class has the same specificity as (and is declared
   after) .stratum-recording-indicator above, it silently won: the pill's own
   font-size/font-weight never actually applied whenever a stop action was present (i.e. always,
   while actually recording), it just inherited whatever size the surrounding header happened to
   use. Only font-family needs resetting here — buttons get a distinct UA-default font stack that
   visually clashes with the page's own; size/weight are this component's own explicit choices. */
.stratum-recording-indicator--button {
  border: none;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.12s;
}
.stratum-recording-indicator--button:hover {
  background: color-mix(in srgb, var(--ink) 8%, var(--surface-2));
}

.stratum-recording-indicator-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--warn);
  flex-shrink: 0;
  animation: stratum-recording-pulse 1.4s ease-in-out infinite;
}

.stratum-recording-indicator-label {
  color: var(--warn);
  letter-spacing: 0.04em;
}

/* width is set inline per-render (see RecordingIndicator.tsx) — inline-block so it actually
   reserves that space instead of being ignored the way a plain inline element would. Explicit
   left-align since the parent pill is a flex container (default cross-axis centering doesn't
   apply here, but pinning it removes any doubt/sub-pixel drift as the digits inside redraw each
   second — otherwise-identical-width text can still visibly wobble a fraction of a pixel if the
   browser centers it within the box on every re-render). */
.stratum-recording-indicator-time {
  display: inline-block;
  text-align: left;
}

.stratum-recording-indicator-stop {
  /* Now the ONLY red mark in the pill (the status dot is dropped whenever this button is
     present — see RecordingIndicator.tsx) — safe to be a plain solid circle again since
     there's no second red shape left for it to be confused with. A plain <span> now, not its
     own nested button — the whole pill is the button (.stratum-recording-indicator--button),
     this is just its visual mark. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 17px;
  height: 17px;
  margin-left: 1px;
  border-radius: 50%;
  background: var(--warn);
  color: #fff;
}
.stratum-recording-indicator-stop svg {
  width: 11px;
  height: 11px;
  fill: currentColor;
}

@keyframes stratum-recording-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.35;
    transform: scale(0.85);
  }
}

/* Frame picker sheet (FramePickerSheet.tsx): live preview, filmstrip scrub bar, step/random controls. */

.stratum-frame-picker-frame {
  position: relative;
  width: 100%;
  height: 100%;
}

/* Captured frame of a real video: fills its tile, cropped like the app's thumbnails. */
.stratum-frame-picker-frame--real {
  background: #111;
}
.stratum-frame-picker-frame--real img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The live preview letterboxes instead, so a portrait clip shows whole. */
.stratum-frame-picker-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}
/* ── The picker sheet ── */
.stratum-frame-picker-preview {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: 12px;
  overflow: hidden;
  background: #000;
}
/* Time sits on the frame itself (like a video player's) instead of a separate heavy readout. */
.stratum-frame-picker-preview-time {
  position: absolute;
  left: 10px;
  bottom: 10px;
  padding: 4px 9px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #fff;
  font: 600 13px var(--mono);
  font-variant-numeric: tabular-nums;
}
.stratum-frame-picker-preview-total {
  font-weight: 400;
  opacity: 0.7;
}
.stratum-frame-picker-spinner {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  animation: stratum-frame-picker-spin 0.7s linear infinite;
}
@keyframes stratum-frame-picker-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Filmstrip scrub bar. */
.stratum-frame-picker-scrub {
  -webkit-tap-highlight-color: transparent;
  position: relative;
  margin-top: 16px;
  height: 44px;
}
.stratum-frame-picker-strip {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 2px;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
}
.stratum-frame-picker-strip .stratum-frame-picker-frame {
  opacity: 0.8;
}
.stratum-frame-picker-playhead {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 4px;
  margin-left: -2px;
  border-radius: 2px;
  background: #fff;
  box-shadow: 0 0 0 1.5px var(--accent), 0 2px 6px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}
/* Where the thumbnail currently comes from — a dashed line through the strip (a button: clicking it
   jumps back there). Narrow, so it barely takes any scrubbing area away. */
.stratum-frame-picker-current-mark {
  -webkit-tap-highlight-color: transparent;
  position: absolute;
  top: -4px;
  bottom: -4px;
  z-index: 1;
  width: 10px;
  margin-left: -5px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.stratum-frame-picker-current-mark::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 4px;
  border-left: 2px dashed rgba(255, 255, 255, 0.95);
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7));
}
.stratum-frame-picker-current-mark:hover::after {
  border-left-color: var(--accent);
}
.stratum-frame-picker-range {
  /* iOS flashes a translucent grey box over a tapped element — the whole invisible range input here. */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  position: absolute;
  inset: -6px 0;
  width: 100%;
  height: calc(100% + 12px);
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  touch-action: none;
}
.stratum-frame-picker-scrub:has(.stratum-frame-picker-range:focus-visible) .stratum-frame-picker-strip {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.stratum-frame-picker-scale {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12px;
  font: 11px var(--mono);
  color: var(--ink-3);
}
.stratum-frame-picker-scale-legend {
  -webkit-tap-highlight-color: transparent;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 6px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.stratum-frame-picker-scale-legend:hover {
  color: var(--ink);
  background: var(--surface-2);
}
/* Legend swatch matching the dashed marker. */
.stratum-frame-picker-current-dot {
  height: 11px;
  border-left: 2px dashed currentColor;
}

/* Stepper: one segmented pill instead of loose links. */
.stratum-frame-picker-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 18px 0 22px;
}
.stratum-frame-picker-stepper {
  display: inline-flex;
  border-radius: 999px;
  background: var(--surface-2);
  padding: 3px;
}
.stratum-frame-picker-stepper button {
  -webkit-tap-highlight-color: transparent;
  border: 0;
  background: none;
  padding: 7px 12px;
  border-radius: 999px;
  color: var(--ink);
  font: 500 13px var(--mono);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.stratum-frame-picker-stepper button:hover {
  background: var(--bg);
}
.stratum-frame-picker-stepper button:active {
  transform: scale(0.96);
}
/* Grid of thumbnail-forward tiles (screen-share-picker style, e.g. Chrome's "Share your
   screen" dialog) — the thumbnail is the primary visual element, title/app name are a small
   caption below it, not the other way around like a typical settings-style list row. */
.stratum-source-picker-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

.stratum-source-picker-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  /* The classic grid/flexbox "min-width: auto" trap: a grid item's default minimum width is
     based on its content's intrinsic (unshrinkable) size, so a column whose caption text is
     longer than the other (e.g. "Weekly Sync — Zoom Meeting") can force its own track wider,
     stealing width from the sibling column at wide (desktop-dialog) sheet widths — confirmed by
     hand: narrow/mobile widths looked fine, wide ones didn't. Zeroing it here lets the row (and
     its ellipsis-truncated text below) actually shrink to the grid's intended equal 1fr split. */
  min-width: 0;
}

.stratum-source-picker-thumb {
  position: relative;
  width: 100%;
  border-radius: 10px;
  border: 1.5px solid var(--line);
  overflow: hidden;
  background: var(--surface-2);
  transition: border-color 0.12s;
}
/* Classic padding-top aspect-ratio technique (16:10 → 10/16 = 62.5%) instead of the `aspect-ratio`
   property directly on this box — nested inside a CSS Grid row (whose cells stretch to match
   each other's cross-axis size by default), a 100%-width `aspect-ratio` box can resolve its
   height ambiguously (definite cross-size from the grid's own stretch vs. the width-driven size
   `aspect-ratio` wants) — confirmed by hand: these tiles rendered at a fraction of their intended
   height, plainly wider than 16:10, until switched to this. The padding-top hack has no such
   ambiguity: it's a plain percentage of the box's own width, unrelated to any cross-axis stretch. */
.stratum-source-picker-thumb::before {
  content: '';
  display: block;
  padding-top: 62.5%;
}
/* :not(...selected) here isn't just belt-and-suspenders: `:hover` gives this selector one MORE
   simple selector than the plain `.stratum-source-picker-row--selected` rule below, so without
   it this rule's higher specificity wins even when the row is selected too — hovering a selected
   tile would visibly clear its accent outline back to the plain hover color. Confirmed by hand. */
.stratum-source-picker-row:hover:not(.stratum-source-picker-row--selected) .stratum-source-picker-thumb {
  border-color: var(--line-2);
}
.stratum-source-picker-row--selected .stratum-source-picker-thumb {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.stratum-source-picker-thumb-inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-4);
}
.stratum-source-picker-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.stratum-source-picker-thumb svg {
  width: 28px;
  height: 28px;
}

/* Overlaid on the thumbnail's corner rather than trailing the row — only shown once selected
   (an empty-circle affordance on every unselected tile read as clutter across a whole grid of
   them, unlike the single-column list this replaced). */
.stratum-source-picker-check {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  opacity: 0;
  transform: scale(0.8);
  transition:
    opacity 0.12s,
    transform 0.12s;
}
.stratum-source-picker-row--selected .stratum-source-picker-check {
  opacity: 1;
  transform: scale(1);
}
.stratum-source-picker-check svg {
  width: 11px;
  height: 11px;
}

.stratum-source-picker-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  padding: 0 2px;
}

.stratum-source-picker-title {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stratum-source-picker-app {
  font-size: 11px;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Loading state (SourcePickerSheet's `loading` prop) — same grid/tile shape as the real content
   (thumb box at the same 16:10 padding-top ratio, two text-line placeholders sized to roughly
   match the title/app rows below it), so nothing jumps in size once the real sources replace it. */
.stratum-source-picker-skeleton {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.stratum-source-picker-skeleton-thumb {
  width: 100%;
  padding-top: 62.5%;
  border-radius: 10px;
  background: var(--surface-2);
  animation: stratum-source-picker-skeleton-pulse 1.4s ease-in-out infinite;
}
.stratum-source-picker-skeleton-line {
  height: 12px;
  border-radius: 4px;
  background: var(--surface-2);
  animation: stratum-source-picker-skeleton-pulse 1.4s ease-in-out infinite;
}
.stratum-source-picker-skeleton-line--short {
  width: 60%;
  height: 10px;
}
@keyframes stratum-source-picker-skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

/* Shown instead of the grid when `sources` is empty (and not `loading`) — a caller whose list can
   legitimately come back with nothing (e.g. narrowed to a specific allowlist of apps/windows, none
   of which happen to be open right now) previously left this area a silent blank gap between the
   description and the footer buttons, indistinguishable from a bug. */
.stratum-source-picker-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 32px 16px;
  color: var(--ink-3);
}
.stratum-source-picker-empty svg {
  width: 28px;
  height: 28px;
  color: var(--ink-4);
}
.stratum-source-picker-empty p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.5;
  max-width: 320px;
}

.stratum-source-picker-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 100%;
}
/* Both footer buttons are CtaButton instances now (the confirm action and a `variant="ghost"`
   Cancel — see SourcePickerSheet.tsx), so a single rule targeting the shared class covers both,
   rather than two near-duplicate one-off classNames. Outranks Sheet.css's own
   `.stratum-sheet-footer button` desktop rule (which would otherwise stretch these via flex: 1,
   splitting the footer's height between them in this column layout) regardless of stylesheet
   import order — two classes beats one. */
.stratum-source-picker-footer .stratum-cta-button {
  flex: none;
}
.stratum-region-crop-frame {
  display: block;
  position: relative;
  width: 100%;
  /* No fixed aspect-ratio, no object-fit — the frame is just a plain wrapper around the `<img>`
     below, which renders at its own natural aspect ratio (`height: auto`). Normal block flow
     then makes the frame exactly as tall as the rendered image, every time. (The skeleton
     variant below still needs its own placeholder aspect ratio, since there's no image yet to
     size around.) */
  border-radius: 10px;
  overflow: hidden;
  background: #000;
  cursor: pointer;
  /* Reset default <button> chrome — this is a plain clickable preview, not a styled button. */
  border: none;
  padding: 0;
  font: inherit;
  text-align: inherit;
  appearance: none;
}
.stratum-region-crop-frame img {
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none;
}

/* Loading state (RegionCropSheet's `loading` prop, or `imageSrc` not ready yet) — needs its own
   placeholder aspect ratio since there's no real image yet for the frame to size around. */
.stratum-region-crop-frame--skeleton {
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  cursor: default;
  animation: stratum-region-crop-skeleton-pulse 1.4s ease-in-out infinite;
}
@keyframes stratum-region-crop-skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

/* Spotlight-mask trick: a single element's own box-shadow, spread far past the frame's edges
   and clipped by the frame's `overflow: hidden`, dims everything outside the selection without
   needing separate mask elements for each of the four surrounding edges. */
.stratum-region-crop-selection {
  position: absolute;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}

.stratum-region-crop-reset {
  display: block;
  margin: 10px auto 0;
  background: none;
  border: none;
  padding: 6px 10px;
  border-radius: 8px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-3);
  cursor: pointer;
  transition:
    color 0.12s,
    background-color 0.12s;
}
.stratum-region-crop-reset:hover:not(:disabled) {
  color: var(--ink);
  background: var(--surface-2);
}
.stratum-region-crop-reset:disabled {
  color: var(--ink-4);
  cursor: not-allowed;
}

.stratum-region-crop-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 100%;
}
/* Same "grid the shared class" convention as SourcePickerSheet.css's own footer — see its
   comment for why this beats Sheet.css's own `.stratum-sheet-footer button` desktop rule. */
.stratum-region-crop-footer .stratum-cta-button {
  flex: none;
}

.stratum-region-crop-description {
  display: block;
  min-height: calc(1.5em * 2);
}
.stratum-transcript-feed {
  flex: 1 1 auto;
  min-height: 240px;
  max-height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  /* Between separate transcript entries (each its own .stratum-transcript-line), not the gap
     within one — that's .stratum-transcript-text-group's own smaller gap between a line's text
     and its translation. Bumped from 10px: felt cramped once furigana/word-spacing was rendering
     (a genuine multi-row segment already reads as visually dense on its own) — a clearer gap here
     helps tell "a new wrapped row of the same segment" apart from "a whole new segment." */
  gap: 20px;
  padding: 0;
  /* Font size and line-height both match dictionary-lookup demo's .dl-paragraph
     (stratum-ui-demos) — this is Japanese transcript text by default (see the app's ja_JP
     locale). This component renders plain text on its own, but a `renderText` consumer (electron's
     RecorderApp, via useTranscriptDictionary.tsx) annotates it with real furigana — 2.2 gives
     that reading row room without the wrapped rows of one long segment visibly crowding into the
     row above them (confirmed cramped at a plain-text-tuned 1.6 once real ruby/rt readings were
     actually rendering). A consumer with no furigana at all would find this roomier than it
     needs, but there isn't currently one — TranscriptFeed has exactly one real consumer today. */
  font-size: 20px;
  line-height: 2.2;
}

.stratum-transcript-feed-empty {
  margin: auto;
  color: var(--ink-4);
  font-size: 14px;
}

.stratum-transcript-line {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
}

.stratum-transcript-time {
  flex-shrink: 0;
  min-width: 34px;
  box-sizing: border-box;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-4);
  border-radius: 4px;
  /* Horizontal padding (not a fixed width) so longer timestamps like 61:20
     grow the pill instead of touching its edges. */
  padding: 2px 6px;
  text-align: center;
  transition: background-color 0.15s, color 0.15s;
}

/* When the timestamp is hidden and only the suffix (e.g. speaker indicator) shows,
   left-align it instead of the timestamp pill's centered text. */
.stratum-transcript-time--suffix-only {
  flex-basis: 100%;
  text-align: left;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
  margin-bottom: 0;
}

/* Replay-only (WatchPage): badges the segment matching the video's current playback position —
   scoped to just the timestamp, not the whole line (a real, confirmed problem: recoloring the
   WHOLE LINE's background/text made ordinary transcript text read as a hyperlink/highlighted
   content, and made the entire line look like one big click target when only the timestamp
   actually is one — see onSegmentClick's own doc). A small colored badge on the time itself reads
   as "here's where we are" without implying anything about the line's own text. */
.stratum-transcript-time--active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
}

/* Replay-only (WatchPage): only the timestamp is a seek target when onSegmentClick is passed —
   not the whole line, which is where a separate word-click-to-look-up interaction lives instead. */
.stratum-transcript-time--clickable {
  cursor: pointer;
}
/* The hover wash is for idle timestamps only — the active line's own blue pill keeps its
   color on hover instead of flashing gray. */
.stratum-transcript-time--clickable:not(.stratum-transcript-time--active):hover {
  background: var(--surface-2);
}

.stratum-transcript-text-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Lets the text/translation spans wrap and ellipsis correctly inside the flex row instead of
     forcing the row wider than its container — the classic flex min-width:auto trap. */
  min-width: 0;
}

.stratum-transcript-text {
  color: var(--ink);
}

/* Deliberately no distinct styling for .stratum-transcript-line--volatile .stratum-transcript-text
   — an earlier version dimmed it to var(--ink-3) to signal "in progress", but for Japanese text
   (this app's ja_JP locale) that read as low-contrast/harder to parse rather than as a status
   indicator, so the in-progress line is now fully opaque, same as a finalized one. Also no
   font-style: italic for the same reason CJK has no real italic tradition — browsers just
   synthetically skew the glyphs, which reads as broken rather than stylistic. */

.stratum-transcript-translation {
  /* Dimmer than the main text — a translation is always secondary to its source line, finalized
     or not. Same no-italics reasoning as above extended to translated text too, for visual
     consistency between the two lines rather than because English itself lacks an italic
     tradition. Sizing matches dictionary-lookup demo's own .dl-line-translation proportion under
     its same 20px/2.2 main line. */
  color: var(--ink-4);
  font-size: 15px;
  line-height: 1.6;
}

/* Mobile: timestamp on its own row above the text, like a divider between
   entries. Translation on its own row below the Japanese, matching desktop.
   Placed at end of file so it wins the cascade over the base rules above. */
@media (max-width: 767px) {
  .stratum-transcript-line {
    display: block;
  }
  .stratum-transcript-time {
    display: flex;
    justify-content: flex-start;
    align-items: baseline;
    min-width: 0;
    padding: 0;
    margin-bottom: 0;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    background: none;
    border-radius: 0;
    text-align: left;
    color: var(--ink-3);
  }
  .stratum-transcript-time--active {
    background: none;
    color: var(--accent);
  }
  .stratum-transcript-time--clickable:not(.stratum-transcript-time--active):hover {
    background: none;
  }
  .stratum-transcript-text-group {
    display: block;
  }
  .stratum-transcript-translation {
    display: block;
    margin-top: 6px;
  }
}

/* No desktop padding override — .stratum-transcript-feed has padding: 0,
   .wpd-body (or the consumer) provides all spacing to avoid stacking. */

/* How far what's shown keeps its content (a table's header labels) down from the screen's top: past
   the safe area, and past the blur an iOS PWA's status bar casts below it. */
.stratum-fullscreen {
  --stratum-fullscreen-top-inset: var(--safe-top-clear);
}
/* MediaViewer's backdrop (.stratum-photo-viewer), holding what's shown edge to edge. */
.stratum-fullscreen-body {
  width: 100%;
  height: 100%;
}
/* The title: MediaViewer's position pill, but tappable (a menu) and cut short when long. */
.stratum-fullscreen-title {
  max-width: calc(100% - 140px);
  pointer-events: auto;
}
.stratum-fullscreen-title.stratum-photo-viewer-glass--hidden {
  pointer-events: none;
}
.stratum-fullscreen-title-text {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stratum-fullscreen-title-chevron {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  stroke-width: 2.4;
}
/* controls="bottom": the title, the content's own controls (the slot) and Close in one row along the
   bottom, on every screen - Close keeps its size (a phone's large one, desktop's small one) and its
   hiding (with the rest on a phone, never on desktop). */
.stratum-fullscreen-bar {
  position: absolute;
  left: max(18px, var(--safe-left, 0px));
  right: max(18px, var(--safe-right, 0px));
  bottom: max(18px, var(--safe-bottom, 0px));
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: none;
}
.stratum-fullscreen-bar .stratum-fullscreen-title {
  position: static;
  min-width: 0;
  max-width: none;
  overflow: hidden;
  transform: none;
}
.stratum-fullscreen-bar-slot {
  display: flex;
  min-width: 0;
  pointer-events: auto;
}
.stratum-fullscreen-bar .stratum-photo-viewer-close {
  position: static;
  flex-shrink: 0;
  margin-left: auto;
  /* The row passes taps through to what's under it; its controls take their own. */
  pointer-events: auto;
}
@media (max-width: 767px) {
  .stratum-fullscreen-bar .stratum-photo-viewer-close--controls-hidden {
    pointer-events: none;
  }
}

/* Controls hidden (a tap on what's shown): what's in the bottom row's slot, and whatever the content
   marks as a control, fade out with the title - and stop taking taps. Each fades itself, never a
   wrapper around it: glass (backdrop-filter) under a fading ancestor can't blur what's behind it until
   that ancestor is opaque again, so it would fade in unblurred and then pop - mark the glass element
   itself. Close has its own rule (hidden with them on a phone, never on desktop). */
.stratum-fullscreen .stratum-fullscreen-bar-slot > *,
.stratum-fullscreen [data-fullscreen-control] {
  /* As the glass's own (MediaViewer.css): its tone changes ease too. */
  transition:
    opacity 0.18s ease,
    background-color 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease;
}
.stratum-fullscreen--controls-hidden .stratum-fullscreen-bar-slot > *,
.stratum-fullscreen--controls-hidden [data-fullscreen-control] {
  opacity: 0;
  pointer-events: none;
}

/* A table fills the screen edge to edge, as a diagram does, under its controls along the bottom
   (controls="bottom", its row count among them): its header row sticks at the top, clear of the status
   bar's area in an iOS PWA (which blurs what's under it) by its own padding, its background still
   reaching the screen's top; and past its last row, when it overflows, there's room to scroll it above
   the controls. */
.stratum-fullscreen .stratum-csv--full {
  border-radius: 0;
  --stratum-csv-top-inset: var(--stratum-fullscreen-top-inset);
  --stratum-csv-end-room: calc(64px + var(--safe-bottom, 0px));
}
.stratum-fullscreen .stratum-csv--full .stratum-csv-scroll {
  border-radius: 0;
}
@media (max-width: 767px) {
  .stratum-fullscreen .stratum-csv--full {
    --stratum-csv-end-room: calc(86px + var(--safe-bottom, 0px));
  }
}
.stratum-csv {
  display: flex;
  flex-direction: column;
  position: relative;
  min-width: 0;
}
.stratum-csv--full {
  height: 100%;
  min-height: 0;
}
/* Floating over the table's bottom corners: the row count (left), the controls (right). Lifted clear
   of the home indicator when the table reaches the screen's bottom edge (FileViewer in a panel). */
.stratum-csv-count,
.stratum-csv-actions {
  position: absolute;
  bottom: calc(10px + var(--stratum-csv-bottom-inset, 0px));
  z-index: 3;
}
.stratum-csv-count {
  left: 10px;
  max-width: calc(100% - 80px);
  padding: 7px 12px;
  border-radius: 999px;
  overflow: hidden;
  font-size: 12px;
  white-space: nowrap;
  text-overflow: ellipsis;
  pointer-events: none;
}
/* In a full screen's row of controls (FullscreenView controls="bottom"): laid out in the row. */
.stratum-csv-count--in-bar {
  position: static;
  max-width: 100%;
}
/* A full screen on a phone has no room for it beside the title and Close. */
@media (max-width: 767px) {
  .stratum-csv--full .stratum-csv-count,
  .stratum-csv-count--in-bar {
    display: none;
  }
}
.stratum-csv-actions {
  right: 10px;
  display: flex;
  gap: 6px;
}
/* Past the last row, when the rows overflow: room to scroll it above the floating count and controls. */
.stratum-csv-end {
  height: var(--stratum-csv-end-room, calc(52px + var(--stratum-csv-bottom-inset, 0px)));
}
/* No rubber band: pulled past its top on iOS, the sticky header slid down off the top and the scroll
   indicator showed in the gap above it - an overlay scrollbar draws over any content, so no background
   there could hide it. */
.stratum-csv-scroll {
  overflow: auto;
  overscroll-behavior: none;
  max-height: var(--stratum-csv-max-height, 320px);
  border-radius: 8px;
  background: var(--surface-2);
}
.stratum-csv--full .stratum-csv-scroll {
  flex: 1;
  min-height: 0;
  max-height: none;
}
.stratum-csv-scroll {
  display: grid;
  grid-template-columns: var(--stratum-csv-gutter) minmax(var(--stratum-csv-body-w), 1fr);
  grid-template-rows: auto minmax(var(--stratum-csv-body-h), 1fr) auto;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-2);
}
/* The header row sticks at the very top. Under something floating over the table's top (a full
   screen's title), it's taller by that much, its labels below it: the title floats on the header's own
   background, and no row ever shows above the header. */
.stratum-csv-corner,
.stratum-csv-head {
  position: sticky;
  top: 0;
  grid-row: 1;
  box-sizing: border-box;
  height: calc(27px + var(--stratum-csv-top-inset, 0px));
  background: var(--surface-2);
  box-shadow: inset 0 -1px 0 var(--line-2);
}
.stratum-csv-corner {
  left: 0;
  z-index: 3;
  grid-column: 1;
}
.stratum-csv-head {
  z-index: 2;
  grid-column: 2;
}
/* Row numbers stick at the left. */
.stratum-csv-nums {
  position: sticky;
  left: 0;
  z-index: 1;
  grid-column: 1;
  grid-row: 2;
  background: var(--surface-2);
}
.stratum-csv-body {
  position: relative;
  grid-column: 2;
  grid-row: 2;
}
.stratum-csv-end {
  grid-column: 1 / -1;
  grid-row: 3;
}
.stratum-csv-th,
.stratum-csv-num,
.stratum-csv-td {
  position: absolute;
  box-sizing: border-box;
  padding: 4px 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stratum-csv-th {
  bottom: 0;
  height: 27px;
  font-weight: 600;
  color: var(--ink);
}
.stratum-csv-num,
.stratum-csv-td {
  border-bottom: 1px solid var(--line);
}
.stratum-csv-num {
  left: 0;
  right: 0;
  color: var(--ink-4);
  text-align: right;
  user-select: none;
}
.stratum-md {
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.stratum-md > * {
  margin: 0 0 12px;
}
.stratum-md > :last-child {
  margin-bottom: 0;
}
.stratum-md h3,
.stratum-md h4,
.stratum-md h5 {
  margin-top: 18px;
  font-weight: 600;
  line-height: 1.35;
}
.stratum-md h3 {
  font-size: 17px;
}
.stratum-md h4 {
  font-size: 15px;
}
.stratum-md h5 {
  font-size: 14px;
  color: var(--ink-2);
}
.stratum-md > :first-child {
  margin-top: 0;
}
.stratum-md ul,
.stratum-md ol {
  padding-left: 22px;
}
.stratum-md li + li {
  margin-top: 4px;
}
/* A list nested in an item: just under its text, spaced as items are. */
.stratum-md li > ul,
.stratum-md li > ol {
  margin: 4px 0 0;
}
.stratum-md hr {
  border: none;
  border-top: 1px solid var(--line);
}
.stratum-md-code {
  font-family: var(--mono);
  font-size: 0.86em;
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--surface-2);
}
.stratum-md-pre {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.5;
  padding: 12px 14px;
  border-radius: 10px;
  background: var(--surface-2);
  overflow-x: auto;
}
.stratum-md-link {
  color: var(--accent);
  text-decoration: none;
}
.stratum-md-link:hover {
  text-decoration: underline;
}
.stratum-md-ref {
  color: var(--accent);
}
/* A file reference that opens (onFileRef): a link's look, as a button. */
.stratum-md-file {
  display: inline;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: var(--accent);
  text-align: inherit;
  cursor: pointer;
  overflow-wrap: anywhere;
}
.stratum-md-file:hover {
  text-decoration: underline;
}
.stratum-md-file .stratum-md-code {
  color: var(--accent);
}

/* A table keeps its natural width - words whole, long text wrapping at a readable width - and
   scrolls sideways when that's wider than the column, rather than squeezing its columns down to a
   letter each (the text above may break anywhere; a table's cells don't). */
.stratum-md-table-wrap {
  overflow-x: auto;
  /* Swiped or trackpad-scrolled sideways, without a scrollbar under the table. */
  scrollbar-width: none;
}
.stratum-md-table-wrap::-webkit-scrollbar {
  display: none;
}
.stratum-md-table {
  width: max-content;
  border-collapse: collapse;
  font-size: 14px;
  line-height: 1.45;
}
/* Table cells ignore max-width, so their content carries it: past this, text wraps. A word longer
   than that (a class name in `code`) widens the cell to fit it instead of spilling over the next one:
   min-width wins over max-width. */
.stratum-md-cell {
  max-width: 24em;
  min-width: min-content;
  overflow-wrap: normal;
}
.stratum-md-table th,
.stratum-md-table td {
  padding: 6px 12px;
  border: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}
.stratum-md-table th {
  font-weight: 600;
  background: var(--surface-2);
}

/* tableFullscreen: a wide table's full-screen button, floating at its bottom right (as a diagram's). */
.stratum-md-table-box {
  position: relative;
}
/* The last column wider by the button's room, so scrolled to the end, the button sits over the
   column's own empty edge - its borders and header running under it - not its text. */
.stratum-md-table-box--expandable > .stratum-md-table-wrap > .stratum-md-table :is(th, td):last-child {
  padding-right: calc(12px + 36px);
}
.stratum-md-table-expand {
  position: absolute;
  right: 8px;
  bottom: 8px;
}
/* The table over the whole screen: scrolling both ways, kept clear of an iOS PWA's status bar (which
   blurs what's under it) by the top padding, with room past its last row to scroll it clear of the
   controls along the bottom. */
.stratum-md-table-full {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  overflow: auto;
  padding: var(--stratum-fullscreen-top-inset, 0px) 16px calc(96px + var(--safe-bottom, 0px));
  /* The viewer's own backdrop shows through, as under an image or a diagram. */
  background: transparent;
}
/* Centred on the screen while it's smaller than it, both ways (auto margins in a flex column); a
   larger one starts at the top left and scrolls, the margins giving way. */
.stratum-md-table-full {
  display: flex;
  flex-direction: column;
}
.stratum-md-table-full .stratum-md-table {
  margin: auto;
}
/* Its header row pins below the status bar's area once it scrolls, the rows passing under it hidden
   there by the backdrop's colour above each header cell - invisible while the table sits lower down
   (a short one, centred), where its header needs no room above it. */
.stratum-md-table-full .stratum-md-table th {
  position: sticky;
  top: var(--stratum-fullscreen-top-inset, 0px);
  z-index: 1;
}
.stratum-md-table-full .stratum-md-table th::before {
  content: '';
  position: absolute;
  left: -1px;
  right: -1px;
  bottom: calc(100% + 1px);
  height: var(--stratum-fullscreen-top-inset, 0px);
  background: var(--bg);
}

.stratum-md blockquote {
  padding-left: 12px;
  border-left: 3px solid var(--line);
  color: var(--ink-2);
}
.stratum-md blockquote > * {
  margin: 0 0 8px;
}
.stratum-md blockquote > :last-child {
  margin-bottom: 0;
}
/* Paragraphs in a list item that has several: spaced as the item's own lines. */
.stratum-md li > p {
  margin: 0;
}
.stratum-md li > p + p,
.stratum-md li > p + pre,
.stratum-md li > pre {
  margin-top: 6px;
}
/* A task list's checkbox, read-only, before its item's text. */
.stratum-md-task {
  margin: 0 6px 0 0;
  vertical-align: -1px;
}

/* An image shown in place (showImages): fitting the column, centered, rounded like the app's cards. */
.stratum-md-img {
  display: block;
  max-width: 100%;
  margin: 12px auto;
  border-radius: 10px;
}
.stratum-chat-msg {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.stratum-chat-msg--user {
  align-items: flex-end;
}
.stratum-chat-msg--user .stratum-chat-msg-body {
  max-width: min(560px, 85%);
  padding: 10px 14px;
  border-radius: 18px 18px 6px 18px;
  background: var(--accent-soft);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.stratum-chat-msg-body--button {
  border: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.stratum-chat-msg-body--button:hover {
  text-decoration: underline;
}

.stratum-chat-msg--assistant .stratum-chat-msg-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.stratum-chat-msg-meta {
  font-size: 11px;
  color: var(--ink-4);
  padding: 0 4px;
}

.stratum-chat-msg--note .stratum-chat-msg-body {
  /* Same size as the thread's text (Markdown); italic marks it as the harness talking. */
  font-size: 15px;
  font-style: italic;
  line-height: 1.6;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}
.stratum-chat-divider {
  align-self: center;
  padding: 4px 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-4);
}
.stratum-composer {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 4px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 24px;
  background: var(--surface);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.stratum-composer:focus-within {
  border-color: var(--line-2);
}
.stratum-composer--disabled {
  opacity: 0.6;
}

.stratum-composer-input {
  flex: 1;
  min-width: 0;
  min-height: 36px;
  padding: 8px 6px;
  border: none;
  outline: none;
  resize: none;
  background: transparent;
  font: inherit;
  /* The chat's own 15px (ChatMessage). Under 16px, iOS Safari zooms the page into a focused field
     unless the viewport sets maximum-scale=1, as momo and the demos do. */
  font-size: 15px;
  line-height: 20px;
  color: var(--ink);
  /* When the text overflows and scrolls, don't chain past its ends into the page. */
  overscroll-behavior: contain;
  text-overflow: ellipsis;
}
/* A phone in landscape (the wide layout on a short touch screen): two lines at most, scrolling past
   that, so a long message leaves the strip of chat above the keyboard in view. Wins over the height
   the field grows to (ChatComposer's maxHeight). */
@media (min-width: 768px) and (max-height: 500px) and (pointer: coarse) {
  .stratum-composer-input {
    box-sizing: border-box;
    max-height: 56px;
  }
}
.stratum-composer-input::placeholder {
  color: var(--ink-4);
  /* Always one line: a placeholder too long for the field is cut with an ellipsis, not wrapped. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stratum-composer-btn,
.stratum-composer-send {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 50%;
  cursor: pointer;
}
.stratum-composer-btn {
  background: transparent;
  color: var(--ink-3);
}
.stratum-composer-btn--disabled {
  cursor: default;
  opacity: 0.5;
}
.stratum-composer-btn:hover:not(:disabled, .stratum-composer-btn--disabled) {
  background: var(--surface-2);
  color: var(--ink);
}
.stratum-composer-btn svg {
  width: 20px;
  height: 20px;
  stroke-width: 1.8;
}

/* Dictating: the mic itself, red and pulsing - a stop icon would read as a second Stop next to
   the running one. Tapping it again stops. */
.stratum-composer-btn--dictating,
.stratum-composer-btn--dictating:hover:not(:disabled) {
  background: var(--warn);
  color: var(--surface);
  animation: stratum-composer-pulse 1.2s ease-in-out infinite;
}

.stratum-composer-send {
  background: var(--ink);
  color: var(--surface);
  transition: opacity 0.15s;
}
.stratum-composer-send:disabled {
  opacity: 0.25;
  cursor: default;
}
.stratum-composer-send svg {
  width: 18px;
  height: 18px;
}
.stratum-composer-send--stop svg {
  width: 18px;
  height: 18px;
}

/* Images attached to the next message: a row of thumbnails across the top of the composer. */
.stratum-composer-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  width: 100%;
  padding: 4px 4px 2px;
}
.stratum-composer-attachment {
  position: relative;
}
.stratum-composer-attachment-img {
  display: block;
  width: 56px;
  height: 56px;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  cursor: pointer;
}
.stratum-composer-attachment-img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.stratum-composer-attachment-img--loading {
  animation: stratum-composer-pulse 1.2s ease-in-out infinite;
  cursor: default;
}
@keyframes stratum-composer-pulse {
  50% {
    opacity: 0.5;
  }
}
.stratum-composer-attachment-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  background: var(--ink);
  color: var(--surface);
  cursor: pointer;
}
.stratum-composer-attachment-remove svg {
  width: 10px;
  height: 10px;
  stroke-width: 3;
}
/* One line of text — name, a space, summary — cut off with an ellipsis as a whole. */
.stratum-tool {
  display: block;
  width: 100%;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 5px 0;
  border: none;
  background: none;
  font: inherit;
  /* Same size and color as the thread's text (Markdown), so a row reads as part of it. */
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  /* A selection stopping inside this clipped line blanks the whole page's paint in Safari (Option-Shift-Left
     from the line below, until it reaches the row's end); one crossing it is fine. So the row is never part of a
     selection, as a button usually isn't. */
  -webkit-user-select: none;
  user-select: none;
}

.stratum-tool-name {
  font-weight: 600;
}

/* Running shows as a trailing ellipsis, so a new call doesn't flash. Failure shows in the name. */
.stratum-tool-ellipsis {
  color: var(--ink-3);
}
.stratum-tool--error .stratum-tool-name {
  color: var(--warn);
}
.stratum-tool--denied .stratum-tool-name {
  color: var(--ink-4);
  text-decoration: line-through;
}
.stratum-tool--active .stratum-tool-name {
  color: var(--accent);
}

/* `flash`: the same flash as flashRange's (utils/flash.ts), from the same variables
   (styles/tokens.css): the text alone, as tall as its line and as wide as its words, held for
   --stratum-flash-hold (the delay, filled backwards), then easing out over --stratum-flash-fade. */
.stratum-tool-text--flash {
  animation: stratum-tool-flash var(--stratum-flash-fade) var(--stratum-flash-ease) var(--stratum-flash-hold) both;
}
@keyframes stratum-tool-flash {
  from {
    background-color: color-mix(in srgb, var(--stratum-flash-color) var(--stratum-flash-strength), var(--bg));
  }
  to {
    background-color: color-mix(in srgb, var(--stratum-flash-color) 0%, var(--bg));
  }
}

/* Screen-reader-only status. */
.stratum-tool-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.stratum-tool-output,
.stratum-tool-section {
  min-width: 0;
}
.stratum-tool-output-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-4);
  margin-bottom: 4px;
}
.stratum-tool-output pre {
  margin: 0;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.stratum-tool-output-text {
  display: block;
}
/* Loading: placeholder lines at the text's own line height (1.5 x 12px), so the block is the size the
   lines it stands in for would be. */
.stratum-tool-output-placeholder {
  display: flex;
  align-items: center;
  height: 1.5em;
}
/* The bars sit on the block's own --surface-2 background, which is Skeleton's colour too: a step
   darker here, so they show (as CodeView's placeholder does). */
.stratum-tool-output-loading .stratum-skeleton {
  background: linear-gradient(90deg, var(--line) 25%, color-mix(in srgb, var(--line) 35%, var(--surface-2)) 50%, var(--line) 75%);
  background-size: 300% 100%;
}
/* Its latest lines at the bottom: past its rows (--stratum-tool-output-rows, from maxLines) or cut to fit
   (FitHeight), it loses its first ones. Rows on screen, not lines, so lines that wrap don't change its
   height; and never shorter than it has been (--stratum-tool-output-held), so live output stays put.
   20px: its padding. */
.stratum-tool-output--tail pre {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  max-height: calc(var(--stratum-tool-output-rows, 1e4) * 1lh + 20px);
  min-height: var(--stratum-tool-output-held, 0px);
  overflow: hidden;
}
/* Its first rows on screen (--stratum-tool-output-rows, from maxRows), wrapped ones counted, the rest
   cut off at the bottom. */
.stratum-tool-output--rows pre {
  max-height: calc(var(--stratum-tool-output-rows) * 1lh + 20px);
  overflow: hidden;
}
.stratum-tool-output--error pre {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 8%, var(--surface-2));
}
.stratum-tool-output-more {
  margin-top: 4px;
  padding: 2px 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--accent);
  cursor: pointer;
}
.stratum-tool-output-more:hover {
  text-decoration: underline;
}

/* With an action: the row keeps its one-line cut-off, the action at its end. */
.stratum-tool-with-action {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}
.stratum-tool-with-action > .stratum-tool {
  flex: 1 1 auto;
  width: auto;
}
.stratum-tool-action {
  flex-shrink: 0;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--accent);
  cursor: pointer;
  white-space: nowrap;
}
.stratum-tool-action:hover {
  text-decoration: underline;
}
/* Syntax highlighting (syntax.ts), for CodeView and DiffView: each kind of token Shiki finds, in
   stratum's colors. Plain text and punctuation keep the code's own --ink-2. */
::highlight(stratum-syntax-keyword) {
  color: var(--syntax-keyword);
}
::highlight(stratum-syntax-string),
::highlight(stratum-syntax-string-expression) {
  color: var(--syntax-string);
}
::highlight(stratum-syntax-comment) {
  color: var(--syntax-comment);
}
::highlight(stratum-syntax-constant) {
  color: var(--syntax-constant);
}
::highlight(stratum-syntax-function) {
  color: var(--syntax-function);
}
::highlight(stratum-syntax-parameter) {
  color: var(--syntax-parameter);
}
::highlight(stratum-syntax-link) {
  color: var(--accent);
}
::highlight(stratum-syntax-inserted) {
  color: var(--syntax-inserted);
}
::highlight(stratum-syntax-deleted) {
  color: var(--syntax-deleted);
}
::highlight(stratum-syntax-changed) {
  color: var(--syntax-changed);
}

/* The same colors as classes, for a long file's lines, which carry their spans in the line itself (useLineSpans). Kept
   apart from the rules above: a browser without ::highlight drops a whole rule it can't read. */
.stratum-syntax-keyword {
  color: var(--syntax-keyword);
}
.stratum-syntax-string,
.stratum-syntax-string-expression {
  color: var(--syntax-string);
}
.stratum-syntax-comment {
  color: var(--syntax-comment);
}
.stratum-syntax-constant {
  color: var(--syntax-constant);
}
.stratum-syntax-function {
  color: var(--syntax-function);
}
.stratum-syntax-parameter {
  color: var(--syntax-parameter);
}
.stratum-syntax-link {
  color: var(--accent);
}
.stratum-syntax-inserted {
  color: var(--syntax-inserted);
}
.stratum-syntax-deleted {
  color: var(--syntax-deleted);
}
.stratum-syntax-changed {
  color: var(--syntax-changed);
}
.stratum-code {
  min-width: 0;
  border-radius: 8px;
  /* clip, not hidden: hidden would make it the scroll box its sticky head sticks in. Clip doesn't round
     what it cuts in every browser (Safari), so the head and body round their own outer corners. */
  overflow: clip;
  background: var(--surface-2);
}
/* Stays at the top of what scrolls the code (a panel's body) as the reader scrolls down. */
.stratum-code-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  border-top-left-radius: inherit;
  border-top-right-radius: inherit;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
}
/* One line, scrolling sideways when it's longer - opened at its end (the file's name), see CodeView. */
.stratum-code-path {
  min-width: 0;
  font-family: var(--mono);
  color: var(--ink-2);
  white-space: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
.stratum-code-path::-webkit-scrollbar {
  display: none;
}
/* Pinned, the box carries on above the panel's top edge, under the head - so its rounding would show
   the code scrolling past, not the panel: the corners outside the curve are painted in what's behind
   the box (a panel's surface; --stratum-code-backdrop for another). */
.stratum-code-head::before,
.stratum-code-head::after {
  content: '';
  position: absolute;
  top: 0;
  width: 8px;
  height: 8px;
  pointer-events: none;
}
.stratum-code-head::before {
  left: 0;
  background: radial-gradient(circle at 100% 100%, transparent 7.5px, var(--stratum-code-backdrop, var(--surface)) 8px);
}
.stratum-code-head::after {
  right: 0;
  background: radial-gradient(circle at 0 100%, transparent 7.5px, var(--stratum-code-backdrop, var(--surface)) 8px);
}
.stratum-code-note {
  color: var(--ink-3);
}
.stratum-code-body {
  margin: 0;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
  padding: 6px 0;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-2);
  overflow-x: auto;
}
/* Every line as wide as the longest (and at least the box), so a marked line's background runs the
   whole way when scrolled sideways, not just across the part first in view. */
.stratum-code-lines {
  width: max-content;
  min-width: 100%;
}
/* Wrapping (wrap): lines as wide as the box, each wrapping within its text, beside its number. */
.stratum-code--wrap .stratum-code-lines {
  width: auto;
}
/* A long file's lines (VirtualLines): only the ones near the screen are mounted, each placed by a transform inside a box as
   tall as all of them (its height set in the style). */
.stratum-code-lines--virtual {
  position: relative;
}
.stratum-code-lines--virtual > .stratum-code-line {
  position: absolute;
  top: 0;
  left: 0;
  width: max-content;
  min-width: 100%;
}
.stratum-code--wrap .stratum-code-lines--virtual > .stratum-code-line {
  width: 100%;
}
.stratum-code--wrap .stratum-code-line {
  display: flex;
  white-space: pre-wrap;
}
.stratum-code--wrap .stratum-code-num {
  flex-shrink: 0;
}
.stratum-code--wrap .stratum-code-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
.stratum-code-line {
  padding: 0 12px 0 0;
  white-space: pre;
}
.stratum-code-num {
  display: inline-block;
  width: var(--stratum-code-gutter, 4ch);
  padding-right: 1ch;
  box-sizing: border-box;
  text-align: right;
  color: var(--ink-4);
  -webkit-user-select: none;
  user-select: none;
}
.stratum-code-line--marked {
  background: color-mix(in srgb, var(--accent-alt) 18%, transparent);
}
.stratum-code-line--marked .stratum-code-num {
  color: var(--ink-2);
}
/* Loading: placeholder lines at the code's line height, so the frame doesn't change size once the
   text arrives. */
/* Cut to fit what scrolls it (see CodeView), so the lines past its bottom edge are hidden. */
.stratum-code-loading {
  overflow: hidden;
}
.stratum-code-loading .stratum-code-line {
  display: flex;
  align-items: center;
  height: calc(12px * 1.55);
  overflow: hidden;
}
.stratum-code-loading .stratum-code-num {
  flex-shrink: 0;
}
/* The placeholder bars sit on the code's own --surface-2 background, which is Skeleton's colour too:
   a step darker here, so they show. */
.stratum-code-loading .stratum-skeleton {
  background: linear-gradient(90deg, var(--line) 25%, color-mix(in srgb, var(--line) 35%, var(--surface-2)) 50%, var(--line) 75%);
  background-size: 300% 100%;
}
/* Couldn't be shown: the placeholder lines held still, faded, with the message in a card centred over
   them (they fit what scrolls them, so it's centred in view). */
.stratum-code--failed {
  position: relative;
}
.stratum-code--failed .stratum-skeleton {
  animation: none;
  opacity: 0.6;
}
.stratum-code-message {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px;
}
/* A long message (many files to pick from) scrolls inside the card, since the frame doesn't scroll. */
.stratum-code-message-card {
  box-sizing: border-box;
  max-width: 420px;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 18px;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--surface);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-2);
  text-align: center;
  overflow-wrap: anywhere;
}
/* An EmptyState as the message (an icon, a title, the details) sits on the card's own padding. */
.stratum-code-message-card .stratum-empty-state {
  padding: 4px 0;
}
.stratum-diagram {
  min-width: 0;
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface-2);
}
.stratum-diagram--full {
  height: 100%;
  border-radius: 0;
  background: transparent;
}
.stratum-diagram-viewport {
  position: relative;
  height: var(--stratum-diagram-height, 300px);
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}
.stratum-diagram--full .stratum-diagram-viewport {
  height: 100%;
}
.stratum-diagram-viewport:active {
  cursor: grabbing;
}
.stratum-diagram-canvas {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
}
.stratum-diagram-canvas > svg,
.stratum-diagram-canvas > img {
  display: block;
  max-width: none;
}
/* Labels select as text (a drag from one doesn't pan); everything else grabs. */
.stratum-diagram-viewport {
  user-select: none;
  -webkit-user-select: none;
}
.stratum-diagram-canvas text {
  cursor: text;
  user-select: text;
  -webkit-user-select: text;
}
/* Find highlights: under their label, never taking the pointer. */
.stratum-diagram-canvas rect.stratum-diagram-hit {
  fill: color-mix(in srgb, #ffd60a 45%, transparent);
  pointer-events: none;
}
.stratum-diagram-canvas rect.stratum-diagram-hit--current {
  fill: var(--accent-alt);
}
/* The small view's own controls (a full-screen button), in its corner. */
.stratum-diagram-actions {
  position: absolute;
  right: 10px;
  /* Lifted clear of the home indicator when it reaches the screen's bottom edge (FileViewer). */
  bottom: calc(10px + var(--stratum-diagram-bottom-inset, 0px));
  display: flex;
  gap: 6px;
  cursor: default;
}
/* Find: a glass bar under the full-screen title (at the top once the controls hide). */
.stratum-diagram-find {
  position: absolute;
  top: 12px;
  left: 50%;
  z-index: 11;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 2px;
  width: min(420px, calc(100% - 24px));
  padding: 4px 4px 4px 14px;
  box-sizing: border-box;
  border-radius: 999px;
  cursor: default;
  user-select: auto;
}
.stratum-diagram--full .stratum-diagram-find {
  top: 64px;
  transition: top 0.18s ease;
}
.stratum-diagram--full .stratum-diagram-find--lifted {
  top: 18px;
}
.stratum-diagram-find-input {
  flex: 1;
  min-width: 0;
  padding: 6px 0;
  border: none;
  outline: none;
  background: transparent;
  font: inherit;
  /* 16px: iOS zooms the page into smaller inputs on focus. */
  font-size: 16px;
  color: inherit;
}
.stratum-diagram-find-input::placeholder {
  color: inherit;
  opacity: 0.55;
}
.stratum-diagram-find-count {
  flex-shrink: 0;
  padding: 0 6px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}
.stratum-diagram-find-btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.stratum-diagram-find-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
.stratum-diagram-find-btn svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.2;
}
.stratum-diagram-find-btn--up svg {
  transform: rotate(180deg);
}
.stratum-diagram--error {
  padding: 14px;
}
.stratum-diagram-error {
  font-size: 13px;
  color: var(--ink-3);
}
/* Why a file shows as text instead of in its viewer (a .puml that won't draw). */
.stratum-file-viewer-hint {
  display: block;
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--ink-3);
}

/* In a panel's title: switches between a file's viewer and its text. Reads as the title's muted meta
   text - no button look - and only underlines on hover. */

/* A table or diagram fills the rest of a BottomPanel, down to its bottom edge as a long text runs: the
   panel body, and whatever wraps the viewer in it (a page, an app's body div), become a column it
   grows in - not a PageStack page, which stays boxless (display: contents), and not for a viewer on a
   hidden page under the one showing. Its floating controls lift
   clear of the home indicator instead of the body padding that would stop it short. */
.stratum-bottom-panel-body:has(.stratum-file-viewer-fill:not([hidden] *)),
.stratum-bottom-panel-body :has(.stratum-file-viewer-fill:not([hidden] *)):not(.stratum-page-stack-page) {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.stratum-bottom-panel-body:has(.stratum-file-viewer-fill:not([hidden] *)) {
  padding-bottom: 0;
}
.stratum-file-viewer-fill {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 200px;
  /* Partly into the home indicator's area (an iOS PWA's inset), not wholly above it. */
  --stratum-csv-bottom-inset: max(var(--safe-bottom-default), calc(var(--safe-bottom) - 16px));
  --stratum-diagram-bottom-inset: max(var(--safe-bottom-default), calc(var(--safe-bottom) - 16px));
}
.stratum-file-viewer-fill > .stratum-csv,
.stratum-file-viewer-fill > .stratum-diagram {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.stratum-file-viewer-fill .stratum-csv-scroll {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.stratum-file-viewer-fill .stratum-diagram-viewport {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
}

/* A page adds no box of its own: its content lays out as if it were the panel body's child. */
.stratum-page-stack-page {
  display: contents;
}
.stratum-page-stack-page[hidden] {
  display: none;
}
.stratum-diff {
  min-width: 0;
  border-radius: 8px;
  background: var(--surface-2);
}
.stratum-diff-head-in {
  border-radius: 8px 8px 0 0;
  background: var(--surface-2);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
}
/* The path: one line that scrolls sideways to show the rest (no scrollbar), fading where it's cut off
   (useEdgeFade), as the patch heading does. */
.stratum-diff-path {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  color: var(--ink-2);
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}
.stratum-diff-path::-webkit-scrollbar {
  display: none;
}
.stratum-diff-count {
  font-family: var(--mono);
  font-weight: 600;
}
.stratum-diff-count--add {
  color: var(--ok);
}
.stratum-diff-count--del {
  color: var(--warn);
}
.stratum-diff-body {
  /* Its own corners: .stratum-diff can't clip (overflow would stop a PatchView's heads sticking). */
  border-radius: 0 0 8px 8px;
  margin: 0;
  padding: 6px 0;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-2);
  overflow-x: auto;
}
/* Every line as wide as the longest (and at least the box), so an added or removed line's background
   runs the whole way when scrolled sideways, not just across the part first in view (as CodeView's). */
.stratum-diff-lines {
  width: max-content;
  min-width: 100%;
}
.stratum-diff-line {
  padding: 0 12px 0 0;
  white-space: pre;
}
.stratum-diff-op {
  display: inline-block;
  width: 24px;
  text-align: center;
  color: var(--ink-4);
  user-select: none;
}
.stratum-diff-line--add {
  background: color-mix(in srgb, var(--ok) 14%, transparent);
}
.stratum-diff-line--add .stratum-diff-op {
  color: var(--ok);
}
.stratum-diff-line--del {
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}
.stratum-diff-line--del .stratum-diff-op {
  color: var(--warn);
}

/* PatchView: a patch's files, one DiffView-like block each. */
.stratum-patch {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.stratum-diff-note {
  color: var(--ink-3);
}
.stratum-diff-line--hunk {
  padding: 4px 12px;
  color: var(--ink-4);
}
.stratum-diff-line--hunk:not(:first-child) {
  margin-top: 4px;
  border-top: 1px solid var(--line);
}
.stratum-diff-empty {
  margin: 0;
  color: var(--ink-3);
}

/* wrap: lines as wide as the box, each wrapping within its text, beside its +/− (as CodeView's wrap). */
.stratum-diff--wrap .stratum-diff-lines {
  width: auto;
}
.stratum-diff--wrap .stratum-diff-line:not(.stratum-diff-line--hunk) {
  display: flex;
}
.stratum-diff--wrap .stratum-diff-op {
  flex-shrink: 0;
}
.stratum-diff--wrap .stratum-diff-text,
.stratum-diff--wrap .stratum-diff-line--hunk {
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* PatchView scrolled: its heading at the top of what scrolls it, and each file's head under that while
   the file's changes go by (the next file's pushes it on). Backgrounds so the changes pass beneath. */
.stratum-patch {
  --stratum-patch-heading-height: 0px;
}
.stratum-patch--heading {
  --stratum-patch-heading-height: 30px;
}
.stratum-patch-heading {
  position: sticky;
  top: 0;
  z-index: 2;
  box-sizing: border-box;
  height: var(--stratum-patch-heading-height);
  margin: 0;
  padding: 4px 0 6px;
  font-size: 13px;
  line-height: 20px;
  color: var(--ink-3);
  background: var(--surface);
}
.stratum-patch .stratum-diff-head {
  position: sticky;
  top: var(--stratum-patch-heading-height);
  z-index: 1;
  /* Square, in the panel's own color, around the rounded head: stuck over the changes, its corners
     show the panel, not the lines scrolling under them (and at the file's top, the same as before). */
  background: var(--surface);
}
/* Its text: one line that scrolls sideways to show the rest (no scrollbar), fading where it's cut off
   (useEdgeFade). Masked on its own, so the heading's background stays solid over the changes. */
.stratum-patch-heading-text {
  display: block;
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: none;
}
.stratum-patch-heading-text::-webkit-scrollbar {
  display: none;
}

/* A patch's line numbers (as in the file: the new one, the old one for a removed line), muted, before
   the +/-; kept beside its line when it wraps. */
.stratum-diff-num {
  display: inline-block;
  min-width: 3ch;
  padding: 0 4px 0 12px;
  text-align: right;
  color: var(--ink-4);
  user-select: none;
}
.stratum-diff--wrap .stratum-diff-num {
  flex-shrink: 0;
}
/* onHunk: a hunk's header is a link to the whole file there - plain text, underlined on hover. */
.stratum-diff-line--hunk-link {
  display: block;
  width: 100%;
  border: none;
  border-radius: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.stratum-diff-line--hunk-link:hover {
  color: var(--ink-2);
  text-decoration: underline;
}

/* onFile: a file's path is a link to the file as it is now - the same mono text, underlined on hover. */
.stratum-diff-path-link {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  white-space: nowrap;
  text-align: left;
  cursor: pointer;
}
.stratum-diff-path-link:hover {
  color: var(--ink);
  text-decoration: underline;
}
.stratum-thinking {
  align-self: flex-start;
  padding: 2px 0;
  font-size: 14px;
}
.stratum-thinking-label {
  background: linear-gradient(90deg, var(--ink-3) 0%, var(--ink-4) 40%, var(--ink) 50%, var(--ink-4) 60%, var(--ink-3) 100%);
  background-size: 250% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: stratum-thinking-shimmer 1.6s linear infinite;
}
@keyframes stratum-thinking-shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: -150% 0;
  }
}
.stratum-permission {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  border: 1px solid color-mix(in srgb, var(--accent-alt) 45%, var(--line));
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent-alt) 6%, var(--surface));
  min-width: 0;
}
.stratum-permission--allowed,
.stratum-permission--denied {
  border-color: var(--line);
  background: var(--surface);
}

.stratum-permission-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.stratum-permission-icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  stroke-width: 1.8;
  color: var(--accent-alt);
}
.stratum-permission--allowed .stratum-permission-icon,
.stratum-permission--denied .stratum-permission-icon {
  color: var(--ink-3);
}
.stratum-permission-title {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}
.stratum-permission-tool {
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--ink-2);
}

.stratum-permission-body {
  min-width: 0;
}

.stratum-permission-actions {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 8px;
}

.stratum-permission-decision {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-3);
}
.stratum-permission--allowed .stratum-permission-decision {
  color: var(--ok);
}
/* Desktop only: below 768px the page shows its filters in a Sheet instead. */
.stratum-filter-rail {
  display: none;
}
@media (min-width: 768px) {
  .stratum-filter-rail {
    display: block;
    width: 260px;
    flex-shrink: 0;
    border-right: 1px solid var(--line);
    height: 100%;
    overflow-y: auto;
    scrollbar-width: none;
  }
  .stratum-filter-rail::-webkit-scrollbar {
    display: none;
  }
  .stratum-filter-rail--collapsed {
    width: 0;
    border-right: none;
    overflow: hidden;
  }
}
.stratum-filter-rail-inner {
  padding: 20px 16px 40px;
}
.stratum-filter-rail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 16px;
}
.stratum-filter-rail-title {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink);
}
.stratum-filter-rail-count {
  font-size: 11px;
  font-weight: 500;
  color: var(--accent);
}
.stratum-filter-rail-reset {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--accent);
  cursor: pointer;
}
.stratum-filter-rail-reset:hover {
  text-decoration: underline;
}

/* Groups as media's search filters draw them: a hairline between each, its label above. */
.stratum-filter-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 0;
  border-top: 1px solid var(--line);
}
.stratum-filter-group:first-child {
  border-top: none;
  padding-top: 0;
}
.stratum-filter-group-label {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 2px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.stratum-filter-group-aux {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-3);
}

/* FilterSheetFooter: media's search sheet footer. */
.stratum-filter-footer {
  /* Fills the Sheet's footer row, which lays its children out as a row of their own widths. */
  flex: 1;
  width: 100%;
  display: flex;
  gap: 10px;
}
.stratum-filter-footer-reset {
  padding: 12px 18px;
  border: 1.5px solid var(--line-2);
  border-radius: 12px;
  background: transparent;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
}
.stratum-filter-footer-show {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 18px;
  border: none;
  border-radius: 12px;
  background: var(--accent);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: #fff;
  cursor: pointer;
}
.stratum-filter-footer-count {
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.25);
  font-size: 13px;
}
/* Same card as PermissionPrompt: tinted while it needs an answer, plain once answered. */
.stratum-question {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
  border: 1px solid color-mix(in srgb, var(--accent-alt) 45%, var(--line));
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent-alt) 6%, var(--surface));
  min-width: 0;
}
/* The questions: spaced as the card spaces its parts. */
.stratum-question-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.stratum-question--answered {
  border-color: var(--line);
  background: var(--surface);
}
.stratum-question-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.stratum-question-icon {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  stroke-width: 1.8;
  color: var(--accent-alt);
}
.stratum-question--answered .stratum-question-icon {
  color: var(--ink-3);
}
.stratum-question-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}

.stratum-question-item {
  margin: 0;
  padding: 0;
  border: none;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.stratum-question-text {
  padding: 0;
  /* A legend sits outside the fieldset's flex gap, so it needs its own space below. */
  margin-bottom: 10px;
  font-size: 15px;
  line-height: 1.45;
  color: var(--ink);
}
.stratum-question-chip {
  display: inline-block;
  margin-right: 8px;
  padding: 1px 7px;
  border-radius: 6px;
  background: var(--surface-2);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  vertical-align: 2px;
  color: var(--ink-2);
}

.stratum-question-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.stratum-question-option {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  cursor: pointer;
}
.stratum-question-item:disabled .stratum-question-option {
  cursor: default;
}
.stratum-question-option--unpicked {
  opacity: 0.55;
}
.stratum-question-option--selected {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}
.stratum-question-option > input {
  margin: 3px 0 0;
  accent-color: var(--accent);
}
.stratum-question-option-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stratum-question-option-label {
  font-size: 14.5px;
  font-weight: 500;
  color: var(--ink);
}
.stratum-question-option-description {
  font-size: 13px;
  color: var(--ink-3);
}
.stratum-question-other {
  margin-top: 6px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  outline: none;
}
.stratum-question-other:focus {
  border-color: var(--accent);
}

.stratum-question-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.stratum-question-note {
  margin: 0;
  font-size: 13px;
  color: var(--ink-3);
}

/* "2 of 4", at the end of the title row while answering several. */
.stratum-question-step {
  margin-left: auto;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
}

/* Answerable: never taller than most of the screen still visible (above an on-screen keyboard, see
   --stratum-vvh), the questions scrolling inside, the title row and the buttons staying put. */
.stratum-question--interactive {
  display: flex;
  flex-direction: column;
  max-height: calc(var(--stratum-vvh, 100vh) * 0.6);
}
.stratum-question--interactive .stratum-question-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.stratum-question--interactive .stratum-question-head,
.stratum-question--interactive .stratum-question-actions {
  flex-shrink: 0;
}
.stratum-status-dot {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-4);
}
.stratum-status-dot--busy {
  background: var(--accent-alt);
}
/* Working: a short arc circling the dot. Kept with Reduce Motion on - it's what tells working apart
   from the steady 'tasks'. Drawn outside the dot, so it doesn't change the dot's size in a layout. */
.stratum-status-dot--busy::after {
  content: '';
  position: absolute;
  inset: -4px;
  border: 1.5px solid transparent;
  border-top-color: var(--accent-alt);
  border-radius: 50%;
  animation: stratum-status-dot-orbit 0.9s linear infinite;
}
.stratum-status-dot--tasks {
  background: var(--accent-alt);
}
.stratum-status-dot--attention {
  background: var(--accent);
}
.stratum-status-dot--error {
  background: var(--warn);
}
.stratum-status-dot--ok {
  background: var(--ok);
}
.stratum-status-dot--offline {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--ink-4);
}
@keyframes stratum-status-dot-orbit {
  to {
    transform: rotate(360deg);
  }
}
.stratum-meter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}
.stratum-meter-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.stratum-meter-bar {
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background: var(--line);
  overflow: hidden;
}
.stratum-meter-bar > span {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: var(--accent);
}
.stratum-meter-bar--warning > span {
  background: var(--accent-alt);
}
.stratum-meter-bar--critical > span {
  background: var(--warn);
}
.stratum-meter-percent {
  min-width: 4ch;
  text-align: right;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.stratum-meter-caption {
  font-size: 13px;
  color: var(--ink-3);
}
.stratum-todo-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.stratum-todo {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 5px 0;
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink);
}
/* A ring the size of a Checkbox's box, lined up with the text's first line. */
.stratum-todo-mark {
  width: 16px;
  height: 16px;
  margin-top: 1.5px;
  border-radius: 50%;
  border: 1.5px solid var(--line-2);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  box-sizing: border-box;
}
.stratum-todo-mark svg {
  width: 10px;
  height: 10px;
}
.stratum-todo--done .stratum-todo-mark {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.stratum-todo--done .stratum-todo-text {
  color: var(--ink-3);
  text-decoration: line-through;
}
.stratum-todo--active .stratum-todo-mark {
  border-color: var(--accent);
  animation: stratum-todo-pulse 1.6s ease-in-out infinite;
}
.stratum-todo--active .stratum-todo-text {
  font-weight: 600;
}
@keyframes stratum-todo-pulse {
  50% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  }
}
.stratum-page-content {
  padding: 16px 20px var(--stratum-app-shell-content-inset-bottom, 0px);
}
@media (min-width: 768px) {
  .stratum-page-content {
    padding: 16px 36px var(--stratum-app-shell-content-inset-bottom, 0px);
  }
}
/* A readable column (`maxWidth`), centered inside the padding. */
.stratum-page-content-column {
  margin-inline: auto;
}
/* Full-width rows pad themselves (`fullWidth`). */
.stratum-page-content.stratum-page-content--full-width {
  padding-inline: 0;
}
/* An infinite list's footer reserves the nav's room itself (InfiniteScrollFooter); in a PageContent,
   whose own bottom padding already does, it doesn't again - wherever it is, nested in the page's own wrapper or not (loading placeholders
   can follow it). */
.stratum-page-content .stratum-infinite-scroll-footer {
  min-height: 0;
}
/* Takes the scroll area's place in its column. A one-cell grid that lays the cover over the scroll
   area in that same cell - not positioned, so it never becomes what a docked BottomPanel (absolute
   against the main column, over a page's accessory) or anything else inside positions against. */
.stratum-settle-cover {
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  flex: 1;
  min-height: 0;
}
.stratum-settle-cover > * {
  grid-area: 1 / 1;
  min-height: 0;
}
/* Over the scroll area, not in it: it never moves while the content does. Under the page header, which
   is fixed and above it. The page's own background, so nothing behind shows through. */
/* The header waits with the content: blank, its room kept (as PageHeader with no title yet), so it
   doesn't show the page's title and status over a skeleton and then again over the content. */
.stratum-settle-cover--settling .stratum-page-header-mobile-inner,
.stratum-settle-cover--settling .stratum-page-header-desktop-inner,
.stratum-settle-cover--settling .stratum-page-header-desktop-bar {
  visibility: hidden;
}
.stratum-settle-cover > .stratum-settle-cover-over {
  z-index: 1;
  overflow: hidden;
  background: var(--bg);
}
.stratum-session-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface);
  overflow: hidden;
}

.stratum-session-row {
  display: flex;
  gap: 10px;
  width: 100%;
  padding: 14px 16px;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
}
.stratum-session-row + .stratum-session-row {
  border-top: 1px solid var(--line);
}
button.stratum-session-row,
.stratum-session-row--link {
  cursor: pointer;
}
/* A link row (to): no underline or link colour; the text inside sets its own. */
.stratum-session-row--link {
  color: inherit;
  text-decoration: none;
}
button.stratum-session-row:hover,
.stratum-session-row--link:hover {
  background: var(--surface-2);
}
/* Selected (e.g. in a select mode): the accent tint, so it doesn't read as hover. */
.stratum-session-row--selected,
button.stratum-session-row--selected:hover,
.stratum-session-row--selected.stratum-session-row--link:hover {
  background: var(--accent-soft);
}

/* Part of a run of rows made by one thing (grouped): an accent border down the left edge, drawn inside the
 * row so nothing shifts. */
.stratum-session-row--grouped {
  --group-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--group-color);
}
.stratum-session-row--grouped-1 {
  --group-color: var(--accent-alt);
}
.stratum-session-row--grouped-2 {
  --group-color: var(--ok);
}
.stratum-session-row--grouped-3 {
  --group-color: #af52de;
}
/* The list's corner is square where a grouped row meets it, so the border runs straight to the edge
 * instead of tapering along the curve. */
.stratum-session-list:has(> .stratum-session-row--grouped:first-child) {
  border-top-left-radius: 0;
}
.stratum-session-list:has(> .stratum-session-row--grouped:last-child) {
  border-bottom-left-radius: 0;
}

.stratum-session-row-dot {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  width: 8px;
  padding-top: 7px;
}

.stratum-session-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.stratum-session-row-top {
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.stratum-session-row-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* scrollTitle: scrolls sideways instead of ending in an ellipsis (no scrollbar), fading where cut off. */
.stratum-session-row-title--scroll {
  overflow-x: auto;
  text-overflow: clip;
  scrollbar-width: none;
}
.stratum-session-row-title--scroll::-webkit-scrollbar {
  display: none;
}
.stratum-session-row-time {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--ink-3);
}
.stratum-session-row-preview {
  font-size: 14px;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stratum-session-row-bottom {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.stratum-session-row-meta {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stratum-session-row-badge {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
}

/* Inside a Sheet (white surface), a white bordered card barely separates from it: use the inset
   grouped look instead — soft grey fill, no border, slightly stronger dividers. */
.stratum-sheet .stratum-session-list {
  border-color: transparent;
  background: var(--surface-2);
}
.stratum-sheet .stratum-session-row + .stratum-session-row {
  border-top-color: var(--line-2);
}
.stratum-sheet button.stratum-session-row:hover,
.stratum-sheet .stratum-session-row--link:hover {
  background: color-mix(in srgb, var(--ink) 5%, var(--surface-2));
}

.stratum-status-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  min-width: 0;
}
.stratum-status-banner--warn {
  border-color: color-mix(in srgb, var(--warn) 30%, var(--line));
  background: color-mix(in srgb, var(--warn) 6%, var(--surface));
}
.stratum-status-banner-icon {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  stroke-width: 1.8;
  color: var(--ink-3);
}
.stratum-status-banner--warn .stratum-status-banner-icon {
  color: var(--warn);
}
.stratum-status-banner-lead {
  flex-shrink: 0;
  display: flex;
  width: 18px;
  justify-content: center;
}
.stratum-status-banner-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.stratum-status-banner-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}
/* One line, cut with an ellipsis when it's longer: banners sit above the composer on phones, where
   every line of height counts. */
.stratum-status-banner-description {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--ink-3);
}
.stratum-status-banner-action {
  flex-shrink: 0;
}
.stratum-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 6px;
  padding: 40px 24px;
}
.stratum-empty-state-icon {
  width: 32px;
  height: 32px;
  stroke-width: 1.5;
  color: var(--ink-4);
  margin-bottom: 6px;
}
.stratum-empty-state-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
}
.stratum-empty-state-description {
  max-width: 360px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-3);
}
.stratum-empty-state-action {
  margin-top: 10px;
}
/* Shared dev-only mobile viewport preview (see ViewportPreview.tsx) — an iPhone
 * impersonation for reviewing mobile layouts on desktop: Dynamic Island, a mock
 * iOS status bar and home indicator, plus simulated iPhone PWA safe areas inside
 * the iframe (see the component's applySafeAreaSim). Deliberately quiet: this is
 * a dev tool, not app chrome. */

.vp-toggle {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 1000;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.vp-toggle:hover {
  border-color: var(--ink-3);
}
.vp-toggle-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

/* Closed but loaded: laid out at full size (the app inside needs its real viewport) and invisible. */
.vp-stage--hidden {
  visibility: hidden;
  pointer-events: none;
}
.vp-stage {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 20px;
  background: color-mix(in srgb, var(--ink) 7%, var(--bg));
}

/* Phone body — the black bezel around the screen. Thinner bezels to match
   iPhone 17 Pro Max's edge-to-edge display. */
.vp-device {
  /* Sized by its width and aspect ratio alone: the screen inside is 125% tall (scaled back to 80%), and
     without this a browser counts that 125% towards the device's own height, so the device comes out
     taller than the phone and runs off the window. */
  contain: size;
  max-width: calc(100vw - 40px);
  padding: 8px;
  border-radius: 48px;
  background: #101012;
  box-shadow:
    0 24px 64px rgba(0, 0, 0, 0.35),
    inset 0 0 0 2px rgba(255, 255, 255, 0.06);
}
/* Opt-out of the iPhone look: plain rounded frame like the old preview. */
.vp-device--plain {
  padding: 0;
  border-radius: 36px;
  background: var(--bg);
  border: 1px solid var(--line);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.2);
}
.vp-device--plain .vp-screen {
  border-radius: 36px;
}

.vp-screen {
  position: relative;
  width: 125%;
  height: 125%;
  border-radius: 40px;
  overflow: hidden;
  background: var(--bg);
  /* Zoom everything inside the screen (app content + device chrome) to 80% —
     matches the iOS Safari Page Zoom many users set on their PWAs.
     transform-origin top-left with the 1/0.8 size boost keeps it filling
     the device frame. */
  transform: scale(0.8);
  transform-origin: top left;
}
/* The device's 48px outer curve and the screen's 40px curve leave a
   lens-shaped gap at each corner (the padding box's inner corners are sharp,
   not rounded) where the stage shows through. An 8px black ring following the
   screen's own 40px curve — its outer edge lands exactly at 48px — seals it. */
.vp-device:not(.vp-device--plain) .vp-screen {
  box-shadow: 0 0 0 8px #101012;
}
.vp-frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  background: var(--bg);
}

/* Dynamic Island — always pure black, like the real thing. Sized for iPhone 17
   Pro Max: ~20.76mm wide (vs ~25mm on older models), slimmer profile.
   Counter-scaled to 90% (1.125x within the 80% screen) so it reads slightly
   larger than the app content. */
.vp-island {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%) scale(1.125);
  transform-origin: top center;
  width: 117px;
  height: 32px;
  border-radius: 16px;
  background: #000;
  pointer-events: none;
}

/* Mock iOS status bar: drawn over the app like the real PWA status bar, inside
   the simulated 59px top safe area. Ink-colored so it adapts to dark mode.
   Counter-scaled to 90% (1.125x within the 80% screen); width is 100/1.125 so
   the scaled width still fills the screen. */
.vp-statusbar {
  position: absolute;
  top: 0;
  left: 0;
  width: 88.889%;
  height: 59px;
  display: grid;
  grid-template-columns: 1fr 117px 1fr;
  align-items: center;
  color: var(--ink);
  pointer-events: none;
  transform: scale(1.125);
  transform-origin: top left;
}
.vp-statusbar-time {
  grid-column: 1;
  text-align: center;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}
.vp-statusbar-icons {
  grid-column: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}

.vp-home {
  position: absolute;
  bottom: 9px;
  left: 50%;
  transform: translateX(-50%);
  width: 134px;
  height: 5px;
  border-radius: 3px;
  background: var(--ink);
  pointer-events: none;
}

.vp-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}
.vp-exit {
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.vp-exit:hover {
  border-color: var(--ink-3);
}

/* On real touch devices and narrow viewports the page is already in its
 * mobile layout, so the preview toggle would just sit on top of app chrome. */
@media (pointer: coarse), (max-width: 700px) {
  .vp-toggle {
    display: none;
  }
}
/* Shared transcript token styles — used by useTranscriptDictionary's renderText.
   Moved from media frontend's WatchPage.css so both the real WatchPage and
   the stratum-ui-demos watch-page get token spacing, dict-match highlight,
   and playing underline. */

/* One span per kuromoji token (see useTranscriptDictionary.tsx's renderText) — margin, not an
   inserted space character, so Japanese text still reads/copies as the exact real transcript. */
.wt-token {
  margin-right: 0.4em;
}
.wt-token:last-child {
  margin-right: 0;
}

/* Zero-width furigana spacer prepended to every line — invisible, so it shouldn't act like real
   selectable/interactive text either. */
.wt-furigana-spacer {
  user-select: none;
  cursor: default;
}

/* Dictionary lookup match highlight — a matched word can span several adjacent .wt-token spans
   (a conjugated verb kuromoji split into multiple tokens), so the background bleeds into the
   token's own right margin via an equal-color box-shadow, bridging the gap so adjacent matched
   tokens read as one continuous highlighted run instead of separate islands. Rounded corners
   only at the START and END of a run, via pure CSS sibling matching. */
.wt-token.wt-token--match {
  background: rgba(255, 200, 0, 0.4);
  box-shadow: 0.4em 0 0 0 rgba(255, 200, 0, 0.4);
  border-radius: 0;
}
.wt-token.wt-token--match:not(.wt-token--match + .wt-token--match) {
  border-top-left-radius: 3px;
  border-bottom-left-radius: 3px;
}
.wt-token.wt-token--match:not(:has(+ .wt-token--match)) {
  border-top-right-radius: 3px;
  border-bottom-right-radius: 3px;
  box-shadow: none;
}

/* Underlines whichever token(s) are playing right now (see useTranscriptDictionary.tsx's own doc
   on activeWordRange) — one level more granular than TranscriptFeed's own whole-line
   activeSegmentId highlight. Deliberately just an underline, not a background — background is
   reserved for the dictionary-lookup match above, so the two never read as the same kind of
   thing even on the rare token both apply to at once. */
.wt-token.wt-token--playing {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* Furigana readings are never part of a selection/click target — a real, confirmed Safari
   drag-select regression on ruby-text boxes. pointer-events: none also means a click landing
   visually on the reading resolves (via document.elementFromPoint) to the base text/token
   underneath instead of nothing. */
.wt-token rt {
  -webkit-user-select: none;
  user-select: none;
  pointer-events: none;
}

/* English translation shown under a toggled-on (active) line — mirrors ReadPage's
   per-line translation reveal. */
.wt-translation {
  margin-top: 4px;
  font-size: 0.9em;
  color: var(--ink-2);
  line-height: 1.5;
}
/* DictLookupSheet passes BottomPanel a fixed `maxHeight="180px"` inline style,
   sized for desktop's wider column where entries rarely wrap. Mobile's narrower
   width makes the same content wrap onto more lines, so 180px feels cramped
   there — bump it a bit for mobile only. `:has()` scopes this to the dict
   panel specifically (SelectionPromptSheet is also a bare BottomPanel with its
   own 180px), and `!important` is required since an inline `style` always
   outranks a stylesheet rule regardless of specificity. */
@media (max-width: 767px) {
  .stratum-bottom-panel:has(.dict-lookup-body) {
    max-height: 240px !important;
  }
}

/* No max-height/overflow here — BottomPanel's own body region
   (.stratum-bottom-panel-body) already caps height and scrolls; nesting a
   second scroll container would just double up. */
.dict-lookup-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.dict-status {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--ink-3);
  padding: 8px 0;
}

.dict-entry {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.dict-entry-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.dict-entry-word {
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.dict-entry-reading {
  color: var(--ink-3);
  font-size: 14px;
}
.dict-entry-reason {
  font-size: 13px;
  color: var(--ink-3);
  font-style: italic;
}

.dict-pos-row {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 6px;
}
.dict-pos-badge {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line-2);
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 700;
  font-family: var(--mono);
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 10px;
  line-height: 1.5;
}

.dict-senses {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 6px;
}
.dict-sense {
  display: flex;
  gap: 6px;
  font-size: 14px;
  color: var(--ink);
  line-height: 1.5;
}
.dict-sense-num {
  color: var(--ink-3);
  flex-shrink: 0;
}

/* Separates stacked entries when a lookup returns more than one — a plain
   top border rather than gap+background, matching how other list rows in
   this app (e.g. .read-chapter-item) separate without a full card look. */
.dict-entry + .dict-entry {
  padding-top: 12px;
  border-top: 1px solid var(--line-2);
}

.dict-kanji-section {
  border-top: 1px solid var(--line-2);
  padding-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.dict-kanji-heading {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  margin: 0;
}

.dict-kanji-entry {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.dict-kanji-char {
  font-size: 28px;
  line-height: 1;
  flex-shrink: 0;
}
.dict-kanji-detail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.dict-kanji-onkun {
  font-size: 13px;
  color: var(--ink-3);
}
.dict-kanji-meanings {
  font-size: 14px;
  color: var(--ink);
}
.gallery-page {
  /* Native document scroll on mobile (see index.css's own doc) — this page renders standalone
   * outside AppShellLayout/DemoShell entirely (App.tsx's `/` route isn't nested under it), so
   * unlike every demo it relaxes its own height/overflow directly instead of going through
   * AppShellLayout's scrollMode prop. iOS's installed-PWA (apple-mobile-web-app-capable, see
   * index.html) standalone mode scrolls document.body just fine — verified via a real-device
   * test (a since-removed /scroll-test diagnostic route) — the earlier own-scroll-container
   * version of this rule predates that finding. Desktop keeps the old clamped/scrolling
   * behavior, unaffected.
   *
   * This element is deliberately full-width (no max-width here) so that on desktop, where it's
   * also the scroll container (below), its scrollbar renders flush against the real viewport
   * edge rather than at the edge of the centered content column — the centering/max-width
   * lives on .gallery-page-inner instead. */
}
@media (min-width: 768px) {
  .gallery-page {
    height: calc(var(--vh, 1vh) * 100);
    overflow-y: auto;
  }
}

.gallery-page-inner {
  max-width: 1080px;
  margin: 0 auto;
  /* Unlike every demo page (wrapped in AppShellLayout, whose PageHeaderMobile
   * already reserves top safe-area space via its own env() padding), the
   * gallery has no such header — needs its own env(safe-area-inset-top) or
   * its "stratum-ui demos" heading sits flush against the status bar in
   * installed-PWA/edge-to-edge mode (viewport-fit=cover in index.html is what
   * makes this resolve to a real value on iOS). */
  padding: calc(48px + var(--safe-top)) 24px 96px;
}

.gallery-header {
  margin-bottom: 36px;
}

.gallery-header h1 {
  font-size: 32px;
  margin: 0 0 8px;
  color: var(--ink);
}

.gallery-header p {
  margin: 0 0 20px;
  color: var(--ink-3);
  max-width: 560px;
}

.gallery-header .stratum-text-input {
  max-width: 360px;
}

.gallery-empty {
  color: var(--ink-3);
}

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}

.gallery-card {
  display: block;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
  background: var(--surface);
  transition:
    transform 0.15s ease,
    border-color 0.15s ease;
}

.gallery-card:hover {
  transform: translateY(-2px);
  border-color: var(--line-2);
}

.gallery-card-thumb {
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  overflow: hidden;
}

.gallery-card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gallery-card-body {
  padding: 16px 18px 18px;
}

.gallery-card-body h2 {
  font-size: 17px;
  margin: 0 0 6px;
  color: var(--ink);
}

.gallery-card-body p {
  margin: 0 0 12px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-3);
}

.gallery-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.gallery-tag {
  font-family: var(--mono);
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
}
/* The rail's look with a nav list as long as the demos' (one entry per demo): AppShellLayout scrolls
 * it when it overflows; this pins the FAB at the bottom with a fade above it, and anchors the list to
 * the bottom when it fits.
 *
 * Scoped under .stratum-app-shell (specificity 0,2,0) rather than bare .stratum-app-shell-rail
 * (0,1,0) on purpose: stratum-ui's own stylesheet is injected *after* this file's bundle at
 * runtime, so on equal specificity its base rules (e.g. the padding shorthand that resets
 * padding-bottom) would silently win. */
@media (min-width: 768px) {
  .stratum-app-shell .stratum-app-shell-rail {
    /* Room below the last nav link so it stays clear of the fixed-position
     * FAB and the ::after fade overlay below: 44px button + 18px original
     * rail bottom padding + ~16px breathing room = 78px of solid backdrop,
     * plus the 40px gradient fade above it (78–118px). The last item parks
     * right at the top edge of the fade — fully legible, never washed out —
     * and can still scroll fully clear when the rail overflows. */
    padding-bottom: 118px;
  }

  .stratum-app-shell .stratum-app-shell-rail-btn:first-child {
    /* Bottom-anchors the nav list when it *fits*: the auto margin absorbs
     * any free space above the list, so on tall viewports the last item
     * parks right at the top edge of the fade overlay instead of leaving a
     * stretch of dead rail below it. Safe for the overflowing case too —
     * auto margins resolve to 0 once the content overflows, so the rail
     * keeps scrolling top-aligned exactly as before (justify-content:
     * flex-end would instead trap the top items out of scroll reach). */
    margin-top: auto;
  }

  .stratum-app-shell .stratum-app-shell-rail-fab {
    /* stratum-ui's own margin-top: auto only pushes this to the bottom when
     * the nav list *fits* — once it overflows (see overflow-y: auto above),
     * this button is just the last item in the same scrolling flex column,
     * gap: 4px from the last nav link, and would scroll away with everything
     * else. position: sticky was tried and rejected: within the *same*
     * scrolling container, a sticky element still visually collides with
     * whatever content is scrolling past its stuck position — here that's
     * the last nav link or two, covered as they scroll underneath it, only
     * 4px of clearance away. position: fixed is viewport-relative and fully
     * ignores the rail's own scroll instead, which is what the padding-bottom
     * above reserves clearance for. left/bottom reproduce the exact resting
     * position stratum-ui's own margin-top: auto + rail padding produced. */
    position: fixed;
    left: 14px; /* (72px rail width - 44px fab width) / 2, centers it */
    bottom: 18px; /* stratum-ui's own .stratum-app-shell-rail padding-bottom */
    margin-top: 0;
    z-index: 5;
  }

  /* Guarantees a fully opaque backdrop (same --surface-2 as the rail itself)
   * behind the FAB all the way down to the bottom edge of the screen, with a
   * gradient fade above that easing the last bit of real scrolled content
   * into it — same technique as mirai/renderer's Sidebar.tsx
   * (.sidebar-footer::before): a pointer-events: none overlay, not a mask,
   * positioned like the FAB (fixed, viewport-relative, so it can't be
   * affected by the rail's own scroll). One element spanning the full
   * reserved zone (118px) rather than two: pixel-length gradient stops give
   * a dead-solid 0–78px (covering the FAB's own footprint plus any safe-area
   * space below it) and a fade
   * from 78–118px into the actual scrollable content above.
   * color-mix() (rather than the `transparent` keyword) keeps the faded end
   * at the same hue as the solid end so the transition doesn't pick up a
   * muddy tint partway through, and stays correct in both themes for free
   * since --surface-2 itself flips under prefers-color-scheme: dark. */
  .stratum-app-shell .stratum-app-shell-rail::after {
    content: '';
    position: fixed;
    left: 0;
    bottom: 0;
    width: 72px;
    height: 118px;
    background: linear-gradient(
      to top,
      var(--surface-2) 0px,
      var(--surface-2) 78px,
      color-mix(in srgb, var(--surface-2) 0%, transparent) 118px
    );
    pointer-events: none;
    z-index: 4;
  }
}
/* Matches frontend's shared .page-loading/.spinner exactly (AppShell.css) —
 * same "still loading" convention used inside Library/Downloads while their
 * first fetch is in flight, here covering the lazy-loaded demo chunk itself
 * (see DemoRoute.tsx) so navigating from the gallery never looks like a hang
 * with zero feedback. height: 100% since this renders inside DemoShell's
 * .stratum-app-shell-main — DemoShell persists across demo switches, so only
 * the content area (not the rail/FAB alongside it) should show this. */
.demo-route-loading {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--ink-3);
  font-size: 14px;
  background: var(--bg);
}
/* Mobile: .stratum-app-shell-main is native-scroll-relaxed to height:auto (see
   DemoShell.tsx/AppShellLayout.css), so the height:100% above resolves against an
   indefinite-height parent and computes as if unset — this collapsed the box toward zero
   height, leaving the centered spinner rendered at the natural top of the content area
   instead of vertically centered, with no PageHeader in this fallback state to reserve
   safe-area-top space, so it landed right under (or behind) the iOS status bar. An absolute
   length (not a percentage) sidesteps the indefinite-parent issue entirely — same fix/reasoning
   as OnboardingFlowDemo.css's own min-height override. Desktop keeps height:100% above,
   unaffected (.stratum-app-shell-main stays definite-height there regardless). */
@media (max-width: 767px) {
  .demo-route-loading {
    height: calc(var(--vh, 1vh) * 100);
  }
}

.demo-route-spinner {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--line-2);
  border-top-color: var(--accent);
  animation: demo-route-spin 0.7s linear infinite;
  flex-shrink: 0;
}

@keyframes demo-route-spin {
  to {
    transform: rotate(360deg);
  }
}
/*$vite$:1*/