/* public/stream/sections.css — shared layout + component classes for the
   Phase 1 web config sections (#sec-alerts / #sec-tips / #sec-loyalty /
   #sec-audio). Ported from the canonical mockup (mockups/omni-mock.css) and
   DESIGN-GUIDELINES §3.5/§3.2; tokens ONLY, no hardcoded color. Loaded once
   alongside control-surface.css. No page-local nav styling lives here. */

/* ═══════════════════════════════════════════════════════════════════════════
   SPACING PRIMITIVES — the ONE vertical-rhythm contract (DESIGN-GUIDELINES §1.4.1)

   THE RULE: a CONTAINER owns the space between its children, via flex/grid
   `gap`. A CHILD never carries its own block margin. Nothing else.

   WHY THIS BLOCK EXISTS (2026-08-15). Spacing bugs kept shipping on page after
   page — "Alerts & Overlays", "Loyalty", the buttons in "Tips", "Public
   Profile" — because the shared layer ran THREE spacing models at once and
   never said which one wins:
     1. gap containers      — .sec-body / .field / .sec-list / .sec-actions
     2. a margin owl        — shell.css `.section > * { margin-bottom }`
     3. neither             — .well and every unclassed wrapper <div>, which
                              are plain blocks, so their children stacked FLUSH
   Model 3 produced the crushed layouts (a Save button welded to the card edge,
   a field label welded to the paragraph above it). Model 2 crossed with model 1
   produced the double gaps (`.aw-preset-section{margin-bottom:16}` inside a
   `gap:12` well renders 28px next to a sibling row's 12px). Authors could not
   tell which model a container used, so each page hand-rolled margins and each
   page landed on a different rhythm.

   The fix is not four page patches. It is: every shared container that STACKS
   children is a gap column BY DEFAULT (no opt-in modifier to forget), and a
   GUARD strips block margins off their direct children so a stray page-local
   `margin-bottom` can never re-enter the additive failure. If you need space
   between two things, put them in a container and pick its gap. If you are
   about to write `margin-top`/`margin-bottom` on a child, you are writing the
   bug this block exists to prevent — see `sections-spacing.test.js`, which
   fails the gate when the count of such violations grows.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The page root. Replaces shell.css's former `.section > * { margin-bottom }`
   owl: that margin ADDED to the gap of every container a page nested inside it
   and forced pages to reach for .sec-mt-4 / inline style="margin-top:…". Loaded
   after shell.css (index.html), so this display wins at equal specificity. */
.section.is-active { display: flex; flex-direction: column; gap: var(--space-4); }

/* The sub-tab pane wrapper. `<div data-pane="…">` is the universal sub-tab
   idiom (alerts, bot, chat, clips, loyalty, merch, moderation, schedule,
   sponsorships, …) and it was ALWAYS unclassed — a plain block holding two or
   three cards, so every one of those pages hand-rolled a margin on the second
   card. It is a stack, and the [hidden] belt keeps an inactive pane closed
   (author `display:flex` beats the UA's `[hidden]{display:none}`). */
.section > [data-pane] { display: flex; flex-direction: column; gap: var(--space-4); }
.section > [data-pane][hidden] { display: none; }

/* The generic named stack, for every other wrapper that groups blocks. Use this
   instead of inventing a page-local `.foo { margin-bottom }` rule. */
.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.stack--tight { gap: var(--space-2); }
.stack--snug { gap: var(--space-3); }
.stack--loose { gap: var(--space-6); }
.stack[hidden] { display: none; }

/* THE GUARD. Direct children of a gap container carry no block margin, so a
   page-local rule (or a UA <p>/<h3> default) cannot add a second, invisible
   layer of spacing on top of the container's gap. `:is()` takes the specificity
   of its heaviest arm (.section.is-active = 0,2,0), which outranks the single-
   class page rules that caused the drift. Only the BLOCK axis is reset —
   `margin-left:auto` (the standard push-to-end idiom) is untouched. */
:is(.section.is-active, .section > [data-pane], .stack, .sec-body, .well,
    .sec-list, .sec-grid, .field, .tile, .sec-item-main, .sec-item-form) > * {
  margin-block: 0;
}
/* STRETCH BELT. A flex column blockifies its children and stretches them across
   the cross axis. That is correct for blocks (cards, fields, rows, grids) and
   WRONG for the inline-level controls this app plants straight into a wrapper: a
   bare <button> that shrink-wrapped in block flow would suddenly span the full
   card the moment its parent became a column. So every container this block
   newly turned into a column pins its inline-level children back to their
   intrinsic width — which reproduces the old block-flow rendering exactly.
   Containers that were ALREADY flex columns (.sec-body, .field, .sec-list, …)
   are deliberately NOT listed: their children already stretch and must keep
   doing so. */
:is(.section.is-active, .section > [data-pane], .stack, .well,
    .aw-group, .aw-preset-section, .aw-style-section)
  > :is(button, a, img, svg, video, audio, span, label, input, select, textarea,
        .btn, .pill, .badge, .chip, .seg, .toggle) {
  align-self: start;
}

/* The ONE sanctioned exception: a group heading earns extra air ABOVE it, which
   a single container gap cannot express per-child. Declared here (0,3,0) so it
   is a deliberate, uniform, system-owned addition rather than a page's guess. */
:is(.section.is-active, .stack, .sec-body, .well) > .sec-grouplabel {
  margin-block: var(--space-2) 0;
}
:is(.section.is-active, .stack, .sec-body, .well) > .sec-grouplabel:first-child {
  margin-block: 0;
}

/* ── grid helpers (DESIGN-GUIDELINES responsive columns) ── */
.sec-grid { display: grid; gap: var(--space-4); align-items: start; }
/* Equal-height variant: cards in the same row stretch to match the tallest, so a
   multi-row dashboard (e.g. Home) has aligned rows with uniform gaps rather than
   ragged voids under shorter cards. The card content stays top-aligned. */
.sec-grid--even { align-items: stretch; }
.sec-grid--even > .card { display: flex; flex-direction: column; }
/* Responsive card grids (Stream-Manager pattern): auto-fit tracks with a per-density
   MIN width mean cards fill the row and reflow naturally, and empty tracks collapse so
   two cards in a wide container stretch to fill instead of leaving dead space. The
   number in each cols-* is the MINIMUM card width; the count is a hint, not a cap.
   auto-fit also handles narrow viewports (tracks drop to 1) so no @media steps are
   needed here. The min(<W>, 100%) guard keeps a single card from overflowing a
   container narrower than <W>. This base (grid) applies to the deliberately
   grid-shaped collections: an equal-height dashboard (.sec-grid--even) and any grid
   with a full-width span child (:has(> .span-2)) — both need row semantics grid
   provides. Everything else opts into masonry below. */
.sec-grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
.sec-grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }
.sec-grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }
/* ── PACK COLUMNS (.sec-pack) — independent cards, NO shared row axis ──
   THE PROBLEM IT SOLVES (owner, 2026-08-16, twice). First: "all the cards are
   full width at all times. We should use multiple columns when the width is
   sufficient". Answered with a .sec-grid — and that reintroduced the second
   complaint, "the columns card stacking is doing the awkward gapping again,
   which we have done away with": a GRID gives every card in a row the height of
   the tallest one, so a very tall card (Social media) leaves a void under each
   short card beside it until the next row starts. On a 3000px monitor auto-fit
   also produced FOUR tracks, which no settings pane wants.

   THE MODEL: two INDEPENDENT column stacks, not tracks of one grid. There is no
   row axis at all, so there is nothing that can couple two cards' heights —
   cards simply flow down their own column. `align-items: flex-start` is the
   structural guarantee (nothing stretches to a neighbour) and the TWO children
   are the cap: no viewport width can conjure a third column, because there is
   no third box. Below ~1040px the two columns wrap to one per line and each
   grows to full width, so the narrow layout is the plain stack it always was —
   with no @media step to keep in sync.

   WHY NOT CSS MULTI-COLUMN (`columns:` + `break-inside: avoid`), the obvious
   masonry answer:
     1. It was ALREADY TRIED HERE AND REVERTED — see the masonry note above:
        a multicol block inside the `.content{overflow-y:auto}` scroller
        miscomputes its height and clips the last card so it cannot be scrolled
        to. That bug is a broken page; this one was only an ugly one.
     2. Multicol has NO row-gap. `gap` sets the COLUMN gap only, so the vertical
        space between cards has to come from a `margin-bottom` on `.card` — a
        block margin on a SHARED class, which is precisely the failure mode the
        SPACING PRIMITIVES contract exists to ban and which sections-spacing.test.js
        ratchets. A .stack column keeps that spacing a container GAP.
     3. Column balancing re-flows cards BETWEEN columns as content changes, so
        adding a link could visibly jump an unrelated card to the other side.
   A JS masonry (grid-auto-rows + measured spans) has none of those faults but
   needs a ResizeObserver re-measure on every content change, and this pane
   mutates constantly (links, custom socials, schedule slots, the embed field).
   Two stacks buy the same packing for no JS and no new failure mode.

   THE TRADE-OFF, stated: reading order is down column one, then down column two
   — the same trade-off multicol would have had. That is fine for INDEPENDENT
   settings cards (no card is a continuation of the one above it) and it is the
   reason this is `.sec-pack` and not a replacement for `.sec-grid`, whose row
   semantics real tabular collections still need. */
.sec-pack { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-4); }
/* `flex: 1 1 32rem` is the whole responsive rule — no @media step to keep in
   sync. As many columns share a line as the container can hold at 32rem + gap
   apiece; the rest wrap. The shell spends 248px on the sidebar and 2×24px on
   `.content` padding, so the folds land at roughly 1330px of viewport (2
   columns) and 1570px (3). The basis is the trade: raising it pushes the folds
   onto ever-wider monitors (26rem bought columns on a 1140px laptop but a
   column too narrow to hold a label, a control and its help text; the owner
   called 26rem cramped on 2026-08-17). min-width:0 keeps one long unbreakable
   string (a pasted URL in a .badge.mono) from widening its column and pushing
   the pane past the viewport — the Music-card precedent.

   THE COLUMN COUNT IS BOUNDED BY THE CHILD COUNT. No CSS turns two stacks into
   three, so a pane that wants a third column on a 3000px monitor ships a third
   `.stack` in its markup (sec-public-profile.js does: six cards, 2/2/2). That
   is the whole point of the model — width is absorbed by ADDING a column, never
   by stretching one and never by capping the page. */
.sec-pack > .stack { flex: 1 1 32rem; min-width: 0; }

/* WIDTH MODEL (owner ruling 2026-08-17: "I still think applying a max width to
   the full page is the wrong way to fix the issue").

   THE PROBLEM this replaces was real: on a ~3000px window a single-column form
   smeared full-bleed — 90-character text inputs, paragraphs no eye can track
   back, and a `.sec-row` border rule running a thousand px past the content it
   belongs to. The FIX was a page-level cap (`#sec-public-profile > *` at
   80→100rem, `#sec-settings …` at 60→90rem), and a cap is the wrong instrument:
   it throws the extra width away, so the pane looks the same at 1600px and at
   3000px and the monitor is wasted. Both caps are GONE — do not reintroduce one;
   the pins in sec-settings.test.js / sec-public-profile.test.js will fail.

   THE MODEL that replaces it, in two halves:
     1. LAYOUT scales with width by adding COLUMNS — `.sec-pack` gets another
        stack, `.sec-grid.cols-*` / `.dest-grid` auto-fit another track. Cards
        and panels FILL their tracks; a wide card is fine.
     2. READABILITY is constrained at the ELEMENT level, below — a URL input
        does not need 2500px and a paragraph does not need 200 characters, so
        the cap lives on the input and on the prose, where the measure actually
        matters, and not on their container.
   Half 2 is what makes half 1 safe: because no single element can smear, a
   column is free to be as wide as the window allows. */

/* ── (2) THE ELEMENT-LEVEL READABILITY CAPS ──
   --w-control: single-line text controls. ~38rem ≈ 608px ≈ 70 characters at the
   14px input font — past that the field stops looking like a field and the
   value's start is nowhere near the caret. Scoped to `.field > …` (the
   label-over-control stack) ON PURPOSE: a control that is a flex item in a row
   (`.sec-formrow > .input`, `.sec-row > .input`, `.input--sm`) is already sized
   by its row's own rule, and capping it there would fight that rule.
   --w-prose: the classic ~60-75 character measure, in `ch` so it tracks the
   font. Applied to the three long-form carriers: card notes, field help, and
   the secondary line of a `.sec-row` text column.
   OPT-OUT: `.input--wide` for a control whose CONTENT is the width (a pasted
   embed snippet, a long URL to verify at a glance, a code textarea). Fixed-size
   media (.settings-qr, .pp-banner) already carries its own size and is
   untouched.

   ── (2b) THE ONE LAYOUT CAP THAT SURVIVED (music setup-first redesign
   2026-08-18; the page cap beside it DELETED the same day) ──
   --w-card: a FIELD-STACK card is a control plus its padding and nothing more.
   38rem control + 2x16px padding = 46rem. Wider and the card stops being a form
   and becomes a page with a form floating in it. This is half 2 one level up: it
   caps an ELEMENT that exists to hold a capped control. It never caps a page.

   🔴 `--w-col: 62rem` IS GONE, and geometry.test.js deletion-pins it. It capped
   `.songs-col` (the Songs & Media settings page) and `.songs-wz` (both wizards),
   which is the SAME instrument the owner rejected for Settings on 2026-08-17
   ("applying a max width to the full page is the wrong way to fix the issue") —
   and it drew the same report on 2026-08-18: "'Songs & Media' for example
   doesn't use the full width. Which makes it look bad especially on the wizard,
   when there's available width." Songs now answers width the way every other
   section does: the card grid and the wizard's option grid ADD A TRACK, and the
   prose inside them is capped where the prose lives. */
:root { --w-control: 38rem; --w-prose: 72ch; --w-card: 46rem; }
.field > .input, .field > .select, .field > .cs-input { max-width: var(--w-control); }
/* A textarea is authored in, not just read, so it gets more room than a
   single-line control — but not the whole monitor. */
.field > .textarea { max-width: calc(var(--w-control) * 1.5); }
.input--wide, .field > .input--wide, .field > .textarea.input--wide { max-width: none; }
.sec-note, .field > .help, .sec-row .grow .s { max-width: var(--w-prose); }

/* ── Masonry card flow (generic card collections) ──
   A CSS-grid row aligns every card to the tallest one, leaving blank space under
   short cards. For INDEPENDENT cards (loyalty / engagement / moderators / etc.) we
   want them to pack upward instead — CSS multi-column does exactly that. We opt in
   only the generic collections: NOT the equal-height dashboard (.sec-grid--even)
   and NOT any grid with a full-width span child (:has(> .span-2), e.g. revenue /
   safety / bot-import), which rely on real grid row/column semantics. Multicolumn
   reading order becomes top-to-bottom then next column (fine for independent cards),
   and vertical spacing comes from child margins, not grid gap. */
/* Masonry (CSS multi-column) was tried here to pack unequal-height cards, but a
   multicol block inside the .content overflow scroller miscomputes height and
   clipped the last card on mobile (can't scroll to the bottom). Reverted to the
   responsive auto-fit grid above — safe + scrolls correctly. On mobile the grid
   collapses to one column so there is no gap to solve; the only gap is on wide
   desktop when card heights differ, which is an acceptable tradeoff vs a broken
   scroll. (A true masonry, if wanted, needs JS — pure CSS is not reliable here.) */

/* Section body: a flex column with token gaps. This base padding only applies to a
   .sec-body used OUTSIDE a .card (standalone); when it sits directly in a .card the
   card already pads, so the override below (.card > .sec-body) zeroes this to avoid
   a doubled inset. */
.sec-body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-4); }
/* Forms inside a section body must space their OWN children (fields + actions):
   .sec-body's gap only separates its direct children (e.g. the <form>), not the
   fields nested inside it, which otherwise stack flush (label jammed under the box
   above, submit button touching the last field). Make the form a gap column too. */
.sec-body form { display: flex; flex-direction: column; gap: var(--space-4); }

/* ── well + tile (DESIGN-GUIDELINES §3.2) ── */
/* A .well IS a stack (SPACING PRIMITIVES above). It used to be padding-only, so
   its children sat flush at 0 — that is the "Connect PayPal welded to the help
   text", "Save settings welded to the card edge" defect, and it kept being
   patched one well at a time (.aw-scenewell carries a 2026-07-08 comment doing
   exactly that). Every .well in this codebase stacks block children, so the gap
   is the default and there is no modifier left to forget. [hidden] children are
   display:none and drop out of the flex flow, so a closed menu adds no phantom
   gap; the belt below keeps a [hidden] WELL closed against this display:flex. */
.well {
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-3);
}
.well[hidden] { display: none; }
/* Retained as a no-op alias: `.well--stack` is now what every .well does. Kept
   so the shipped markup that still says it stays valid (and self-documenting)
   rather than churning six files for nothing. */
.well--stack { display: flex; flex-direction: column; gap: var(--space-3); }
.tile { display: flex; flex-direction: column; gap: var(--space-2); }
.between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
/* The benchmark control row reuses .between but must wrap on narrow screens so
   the Category / value / Compare controls stack instead of cramping onto one row
   (Web Interface Guidelines: no cramped controls). flex-wrap keeps width:100%
   inputs from compressing below a usable size under --bp-mobile. */
.between.wrap { flex-wrap: wrap; align-items: flex-end; }
.between.wrap > .field { flex: 1 1 160px; min-width: 0; }

/* ── row (label + control), reused from the mockup pattern ── */
.sec-row { display: flex; align-items: center; gap: var(--space-3); padding: 10px var(--space-2); border-bottom: 1px solid var(--border); }
.sec-row:last-child { border-bottom: 0; }
.sec-row .grow { flex: 1; min-width: 0; } /* min-width is floored by the GUARD block below */
/* GUARD (2026-08-13, permanent): .input/.select/.textarea/.cs-input are
   width:100% BY DESIGN because they live in column .field stacks. Planted
   bare inside a flex .sec-row, that 100% basis crushes .grow (min-width:0)
   into a one-word-per-line sliver with the label painted mid-paragraph — the
   recurring "messed up text" card bug (latest: the Songs & Media reward rows).
   The markup that causes it is invisible in code review (HTML is concatenated
   across lines), so the fix lives HERE: any full-width control that lands
   directly in a row is bounded to a compact inline field, and the text column
   keeps its space. A row that genuinely wants a wide control uses .sec-copyrow
   (whose own rule sets flex:1) or a nested .field stack — both unaffected. */
.sec-row > .input, .sec-row > .select, .sec-row > .textarea, .sec-row > .cs-input {
  width: auto; flex: 0 1 10rem; min-width: 6rem;
}
/* A bare .lbl in a row (outside .field, which styles its own) — style it like
   the .field caption so it reads as intentional instead of unstyled body text. */
.sec-row > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
/* The text column itself never collapses below readable width even if some
   future sibling ignores the guard above: past this floor the row overflows
   VISIBLY (or wraps, on the touch breakpoint) instead of silently shredding
   the copy one word per line. Loud beats silent. */
.sec-row .grow { min-width: min(45%, 14rem); }
.sec-row .grow .t { font-weight: 600; display: block; }
.sec-row .grow .s { color: var(--text-dim); font-size: 11.5px; display: block; }
/* Row/list health cue (§3.3): the .dot status circle + its text label, so health
   is perceivable without color. The label inherits text color (not the severity
   color) — meaning lives in the word, severity color lives in the dot. */
.sec-health { display: inline-flex; align-items: center; gap: var(--space-1); flex: none; }
.sec-health-label { font-size: 11px; color: var(--text-dim); letter-spacing: .03em; }

/* ── form field + inputs (DESIGN-GUIDELINES §3.5) ── */
/* Label→control breathing room: --space-2 (8px) reads less cramped than the old
   --space-1 (4px) on the dense loyalty/engagement/moderators forms. */
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field > .lbl { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
/* THE POINTS SWITCH (owner request 2026-09-12). Its label holds the checkbox, so
   it needs to be a row rather than the uppercase caption a .field label normally
   is: the checkbox and the words belong on one line and are clicked together. */
.field.loy-switch > .lbl { display: flex; align-items: center; gap: var(--space-1);
  text-transform: none; font-size: 12px; letter-spacing: 0; color: var(--text); cursor: pointer; }
.field.loy-switch > .lbl > input { margin: 0; }

/* Form help is instructional CONTENT, not a decorative hint, so it uses the
   readable --text-dim token (~7.4:1 on --surface-inset) to meet WCAG 1.4.3 AA
   (4.5:1) — --text-faint (~3.4:1) failed for this 11px content text. */
.field > .help { font-size: 11px; color: var(--text-dim); }
.input, .select, .textarea {
  width: 100%; font: inherit; color: var(--text);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 11px 14px;
}
.textarea { min-height: 64px; resize: vertical; font-family: var(--font-mono); font-size: 12px; }
/* Compact inline input (e.g. the chat-history date/search filters) — fits in a
   flex action row instead of stretching full-width. (It used to cite the TikTok
   @username add in Connections; that control was deleted in the 2026-08-16
   settings audit, so the example now names a live one.) */
.input--sm { width: auto; max-width: 12rem; padding: 7px 10px; font-size: 12px; }
.input:focus-visible, .select:focus-visible, .textarea:focus-visible { border-color: var(--border-accent); }
/* READONLY AFFORDANCE. A dim colour alone still reads as an editable box that
   silently ignores typing (Settings → Account username, the 2FA secret, the device
   pairing code — all readonly by design). The surface + default cursor say "this is
   a VALUE, not a field" before the user tries. Border/background stay tokenised. */
.input[readonly] {
  color: var(--text-dim); background: var(--surface); cursor: default;
}
/* Disabled fields must stay READABLE, not vanish. `opacity` (not `color`) is the
   cue on purpose: pane-scoped overrides like `#sec-bot .select` re-declare
   `color` at higher specificity, so a color-only rule would silently not apply
   there — opacity is set by nobody else and therefore lands everywhere. .6 of
   --text over --surface-inset is ≈5.9:1, i.e. still AA. */
.input:disabled, .select:disabled, .textarea:disabled { opacity: .6; cursor: not-allowed; }
/* A .select is a native <select>: restate color-scheme so the browser-drawn
   popup + arrow are dark even if an ancestor ever scopes a lighter scheme, and
   give it the pointer affordance the shared field rule (built for text inputs)
   doesn't. Option/optgroup colors come from the bare `select` base in
   omni-ui.css — do NOT re-declare them per pane. */
.select { color-scheme: dark; cursor: pointer; }
/* Inline select that sits beside a growing .grow label (TTS premium-voice tier,
   TTS per-tier reward): the shared .select width:100% claims the whole flex row
   via its basis and starves the label column to a sliver. --auto sizes the select
   to its content so the label fills the row. Mirrors .input--sm's width:auto. */
.select--auto { width: auto; }

/* ── TTS tier editor: rows + aligned column legend ──
   The editable tier row and its header legend share ONE grid template so the
   Tier name / Tip price / Bits / Unlock reward columns line up. The row keeps
   .sec-row (border + padding); .sec-tier-grid overrides its flex display with the
   grid. minmax(0, *) lets every cell shrink instead of widening the body on narrow
   screens. First/last tracks (auto) hold the #ordinal chip and the Remove button;
   the legend leaves those cells empty. Inputs/selects fill their cell (the reward
   select is .select--auto so it sizes to content within the cell). */
.sec-tier-grid {
  display: grid;
  /* Seven tracks since W3-5: the Pyre-points price is its own column, because it
     is a different currency from the channel-point reward beside it. */
  grid-template-columns: auto minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.6fr) minmax(0, 1.4fr) auto;
  gap: var(--space-3); align-items: center;
}
.sec-tier-legend {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim);
}
/* A tier is now TWO lines (B3): the grid row, and the demoted legacy
   map-a-reward line under it. The block owns the separator so the pair reads as
   one tier instead of two list entries. */
.sec-tier-block > .sec-row { border-bottom: 0; }
.sec-tier-block { border-bottom: 1px solid var(--border); }
.sec-tier-block:last-child { border-bottom: 0; }
/* The channel-point cell: a cost field + Create, or the linked state + Unlink.
   Two controls in one grid track, so a flex line that WRAPS rather than widening
   the grid (the .sec-row > .input guard above does not reach a nested control). */
.sec-tier-mint { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; min-width: 0; }
.sec-tier-mint > .input { width: auto; flex: 1 1 5rem; min-width: 4.5rem; }
/* PER-ROAD ON/OFF (U-4). The switch rides above its road's control, so what
   opens or closes a payment method is attached to that method rather than
   floating in the row. A closed cell DIMS but keeps its price on screen: the
   amount survives the road being switched off, and blanking it would make
   "off" and "free" look identical - the exact confusion the switches exist to
   end. The streamer has to be able to see what is waiting for them. */
.sec-pay-cell { flex-direction: column; align-items: stretch; gap: var(--space-1); }
.sec-pay-head { display: flex; align-items: center; gap: var(--space-2); }
.sec-pay-cell.is-off > .input,
.sec-pay-cell.is-off > [data-tier-mint],
.sec-pay-cell.is-off > [data-tier-points-cell] { opacity: .45; }
.sec-pay-head > .s { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; }
/* Four switches share one row, so this one is tighter than the page default. */
.toggle--sm { transform: scale(.82); transform-origin: left center; }
.sec-tier-mint > .s { color: var(--text-dim); font-size: 11.5px; }
/* The channel-points column states its OWN difference (owner ruling 2026-08-24):
   this is the one price that saves the moment you click it, and the one whose
   refunds Pyre can handle. Said in the cell's own unit line, on its own row
   under the control, rather than by exiling the control somewhere else. */
/* CHILD-SCOPED, both of them: these spans also carry `.s`, and
   `.sec-tier-mint > .s` above is TWO classes — a bare `.sec-tier-unit` /
   `.sec-tier-on` would lose the cascade to it and render dim at 11.5px, which is
   exactly the "styled a class that never won" failure. */
.sec-tier-mint > .sec-tier-unit { flex-basis: 100%; font-size: 11px; color: var(--text-dim); }
/* The linked state reads the same on both panes (parity, 2026-08-24): the tick
   is decoration on a pinned two-word string, never part of it. */
.sec-tier-mint > .sec-tier-on { color: var(--ok); font-weight: 600; white-space: nowrap; }
.sec-tier-on::before { content: "\2713 "; font-weight: 400; }
/* The three-answer unlink question cannot read in one grid track, so the cell
   takes the row while the question is up. `:has()` is already shipped in this
   file (.chat-filter) and the fallback merely wraps, as it did before.
   🔴 THE TARGET IS `[data-tier-mint]`, NOT `.sec-tier-mint`: that class is on TWO
   cells (the channel-points cell AND the Pyre-points cell share the skin), so a
   class-scoped rule spanned the points cell across the row as well — one
   question, two full-width cells. The attribute is on the channel-points cell
   alone. And `dense` is load-bearing, exactly as in the portal twin: with one
   cell spanning all seven tracks, sparse auto-placement strands tracks 5-6 and
   drops Pyre points + Remove onto a row of their own, misaligned under the
   legend. Dense backfills, so the priced row keeps its columns and the question
   sits beneath it. */
.sec-tier-grid:has([data-tts-unlink-ask]) { grid-auto-flow: dense; }
.sec-tier-grid:has([data-tts-unlink-ask]) [data-tier-mint] { grid-column: 1 / -1; }
/* The LEGACY line is deliberately quieter than the tier it hangs off. */
.sec-tier-legacy { padding-top: 0; }
.sec-tier-legacy .s { color: var(--text-dim); font-size: 11.5px; display: block; }
/* THE HAND-MADE-REWARD PATH is the exception, not the alternative: one collapsed
   line until the streamer who made their own reward opens it, and absent
   entirely while a Pyre reward is linked (the two paths must never sit side by
   side). `.sec-row` gives it the row's border and padding. */
/* `display:block` OVERRIDES the `.sec-row` flex it also carries: as a flex
   container the open body became a SIBLING COLUMN beside the summary instead of
   the panel beneath it. `.sec-row` is still what gives it the row's border and
   padding, which is why the class stays. */
.sec-tier-own { padding-top: 0; display: block; }
.sec-tier-own > summary { color: var(--text-dim); cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 6px; user-select: none; }
.sec-tier-own > summary::-webkit-details-marker { display: none; }
.sec-tier-own > summary::before { content: "\25B8"; font-size: 10px; transition: transform .15s; }
.sec-tier-own[open] > summary::before { transform: rotate(90deg); }
.sec-tier-own > summary:hover { color: var(--text); }
.sec-tier-own-body { display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-top: var(--space-2); }
.sec-tier-own-body .s { color: var(--text-dim); font-size: 11.5px; flex-basis: 100%; }

/* ── toggle (track + knob), accessible role="switch" (DESIGN-GUIDELINES §3.5) ── */
.toggle {
  width: 40px; height: 22px; border-radius: var(--r-pill); background: var(--surface-2);
  border: 1px solid var(--border); position: relative; cursor: pointer; flex: none;
  transition: background var(--dur) var(--ease);
}
.toggle::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--text-dim);
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
.toggle.on { background: var(--accent); }
.toggle.on::after { transform: translateX(18px); background: var(--text); }
.toggle[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

/* ── segmented control (binary/enum) (DESIGN-GUIDELINES §3.5) ── */
.seg { display: inline-flex; background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-pill); padding: 3px; }
.seg button {
  border: 0; background: transparent; color: var(--text-dim); font: inherit; font-weight: 600;
  font-size: 11.5px; padding: 5px 12px; border-radius: var(--r-pill); cursor: pointer;
}
.seg button.on { background: var(--accent); color: var(--text); }

/* ── chips row (presets, socials) ── */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--r-pill); background: var(--surface-inset); font-size: 12px; }
.chip button { border: 0; background: transparent; color: var(--text-faint); cursor: pointer; font: inherit; padding: 0; line-height: 1; }
.chip button:hover { color: var(--danger); }

/* ── copy row (browser-source URL) ── */
.sec-copyrow { display: flex; gap: var(--space-2); }
.sec-copyrow .input { flex: 1; }

/* ── INLINE FORM ROW — the "add a thing" editor (label | url | kind | + Add) ──
   WHY THIS EXISTS (owner report 2026-08-16, the Public Profile Links card, which
   rendered as "three detached boxes stacked VERTICALLY and CENTRED with huge
   empty space"). That form was authored `class="sec-row"`, and .sec-row IS a
   flex ROW — but `.sec-body form` above (0,1,1) outranks `.sec-row` (0,1,0) and
   turned it into a COLUMN. Two more shared rules then finished the job on the
   flipped axis: `.sec-row { align-items: center }` survived and, in a column,
   centres children on the CROSS (horizontal) axis, while the `.sec-row > .input`
   guard's `width: auto; flex: 0 1 10rem` became a 10rem HEIGHT on the column's
   main axis. Hence three tall, intrinsic-width, centred boxes.

   The lesson is not "patch that one form": an inline editor must declare its row
   direction at a specificity that BEATS `.sec-body form`, or the next author
   reproduces this exactly. So the row lives here, the winning rule is stated
   explicitly below, and the controls carry their own basis instead of borrowing
   .sec-row's (which is tuned for a label+control row, not an editor).

   flex-wrap + min-width:0 are load-bearing: on a narrow card the fields wrap to
   the next line instead of shredding one word per line (§ the .sec-row GUARD
   above) or overflowing the card. */
.sec-formrow {
  display: flex; flex-direction: row; flex-wrap: wrap;
  align-items: center; gap: var(--space-2);
}
/* THE WINNING RULE (0,2,1 > `.sec-body form`'s 0,1,1). Without it a .sec-formrow
   used as a <form> inside a .sec-body silently becomes a column again. */
.sec-body form.sec-formrow { flex-direction: row; flex-wrap: wrap; gap: var(--space-2); }
/* Controls shrink-to-fit with a sane basis; the one marked `.grow` takes the
   slack. `width: auto` undoes the width:100% these classes carry for column
   .field stacks, and min-width:0 lets them actually shrink before wrapping. */
.sec-formrow > .input, .sec-formrow > .select, .sec-formrow > .cs-input {
  width: auto; flex: 0 1 11rem; min-width: 0;
}
.sec-formrow > .grow { flex: 1 1 14rem; min-width: 0; }
.sec-formrow > .btn { flex: 0 0 auto; }

/* ── inline actions + notes ── */
.sec-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.sec-note { margin: 0; font-size: 12px; color: var(--text-dim); }
.sec-err { margin: 0; font-size: 12px; }
/* A block that answers to a master switch that is OFF: visibly present (the
   stored states survive) but inert. Structure only — no colors of its own. */
.sec-off { opacity: .5; pointer-events: none; }
.sec-empty, .sec-loading { padding: var(--space-4); text-align: center; font-size: 12px; }

/* ── Home: next-step inline guidance + performance-card value rows ── */
.sec-stack { border-bottom: 1px solid var(--border); }
.sec-stack:last-child { border-bottom: 0; }
.sec-stack > .sec-row { border-bottom: 0; }
.sec-step-help { padding: 0 var(--space-2) 10px; line-height: 1.4; }
/* a right-aligned monetary / numeric value paired with a sec-row label */
.sec-amount { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ── variable legend (collapsible token reference in the bot commands tab) ── */
.sec-legend { margin: var(--space-2) 0; font-size: 12px; }
.sec-legend > summary { cursor: pointer; color: var(--text-dim); font-weight: 600; }
.sec-legend > summary:hover { color: var(--text); }
.sec-legend-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.sec-legend-list li { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
/* Variable tokens are CASE-SENSITIVE literals ($user, $(count), …) — the global
   .badge uppercases, which would teach the wrong spelling on this discovery
   surface. Render legend tokens verbatim. */
.sec-legend-list .badge { text-transform: none; letter-spacing: 0; }

/* ── list of items (alerts/widgets/engagements) ── */
.sec-list { display: flex; flex-direction: column; gap: var(--space-2); }
.sec-item { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-inset); }
.sec-item-main { flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: var(--space-1); }
.sec-item-title { font-weight: 600; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.sec-item-meta { font-size: 11px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.sec-item-actions { flex: none; display: flex; gap: var(--space-2); }
/* A DISABLED destination: dim the whole (collapsed) card + a muted "Disabled" badge,
   so enabled/disabled state is visible on the summary without expanding. */
.sec-item.is-disabled { opacity: 0.55; }
.sec-item-title .badge--muted { background: var(--surface-inset); border: 1px solid var(--border); color: inherit; opacity: 0.85; }
/* Phase 4 capability badge (resolution-mismatch surfacing): the warn variant marks
   a destination the current canvas exceeds (no in-cap output set) — same warn
   recipe as .status--warn; the quiet in-cap-output badge reuses .badge--muted
   above. .sec-warn colors the one-line fix hint (and the existing transcode
   warning) in the expanded card. */
.sec-item-title .badge--warn { background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.sec-warn { color: var(--warn); }

/* ── Continuity: input-pool + failover-priority drag surface ──
   Two columns (pool ↔ ordered failover list); HTML5 drag-and-drop. Tokens only;
   wide content scrolls within each column (no body h-scroll). */
.cont-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: var(--space-4); align-items: start; }
@media (max-width: 720px) { .cont-cols { grid-template-columns: 1fr; } }
.cont-col { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.cont-col-head { font-family: var(--font-display); font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-dim); margin: 0; }
.cont-pool, .cont-priority { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-inset); min-height: var(--space-7, 3rem); }
.cont-pool-empty { padding: var(--space-3); text-align: center; font-size: 12px; }
/* Draggable input card (pool + inside a filled row). */
.cont-card { cursor: grab; user-select: none; align-items: center; }
.cont-card.is-dragging { opacity: .5; }
.cont-card:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
/* A filled failover row: the card + scene picker + ▲▼/Remove fallback controls. */
.cont-row { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); }
.cont-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.cont-row-scene { gap: var(--space-2); }
.cont-row-actions { flex: none; }
/* An empty DRAFT row — a dotted "drag a device here" drop target. */
.cont-draft { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-3); border: 1px dashed var(--border-accent); border-radius: var(--r-sm); background: var(--accent-wash); text-align: center; }
.cont-draft-label { font-weight: 600; font-size: 12px; color: var(--text-dim); }
.cont-draft-hint { font-size: 11px; }
/* Liveness dot on an input card (mirrors the Devices UI). Server-resolved
   `online` (relay live-slot or fresh device); inline before the label. */
.cont-dot { display: inline-block; width: 8px; height: 8px; border-radius: var(--r-full, 50%); margin-right: var(--space-2); vertical-align: middle; }
.cont-dot.is-online { background: var(--ok, var(--accent)); }
.cont-dot.is-offline { background: var(--border); }
/* Honesty note shown when the failover list is empty (empty ≠ failover off). */
.cont-empty-note { padding: var(--space-2); font-size: 12px; }

/* ── Continuity: the RECONNECT CARD editor ──
   Content for the pinned terminal tier: the image/video/playlist viewers see when
   every input is dark. Reuses the shell's .seg / .toggle / .btn / .well / .input;
   only the playlist row + picker geometry is new. Tokens only, spacing on the
   --space-* scale. */
.rc-body { display: flex; flex-direction: column; gap: var(--space-3); }
.rc-seg { align-self: flex-start; }
.rc-seg button:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }

/* Honest empty state: it NAMES what is playing (the default card), never a blank panel. */
.rc-empty { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-5); text-align: center; border: 1px dashed var(--border-strong); border-radius: var(--r-sm); }
.rc-empty b { font-size: 14px; color: var(--text); }
.rc-empty span { font-size: 12px; color: var(--text-faint); }

/* The playlist. Rows are draggable (order = playback order); ▲▼/Remove are the
   keyboard-accessible fallback for the same reorder. */
.rc-items { display: flex; flex-direction: column; gap: var(--space-2); }
.rc-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-inset); cursor: grab; }
.rc-item.is-dragging { opacity: .5; }
.rc-item:focus-within { border-color: var(--border-accent); }
.rc-drag { flex: none; font-size: 14px; line-height: 1; color: var(--text-faint); }
.rc-thumb { flex: none; display: grid; place-items: center; width: 52px; height: 30px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--r-chip); background: var(--inset-2); }
.rc-thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; }
.rc-thumb-tag { font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em; color: var(--text-faint); }
.rc-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.rc-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rc-sub { font-family: var(--font-mono); font-size: 11px; }
.rc-durwrap { flex: none; display: flex; align-items: center; gap: var(--space-2); }
.rc-durlabel { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint); }
.rc-dur { width: 5rem; text-align: right; font-family: var(--font-mono); }
.rc-item-actions { flex: none; }
.rc-actions { margin: 0; }

/* Songs & Media (W6.3): the per-row Block menu. A native <details> so opening
   and closing needs no JS and is keyboard-operable for free; the revealed
   panel reuses shell.css's `.menu`/`.menu-item` floating-popover chrome
   (flat, hover-highlighted rows) rather than another stack of bordered
   buttons — the "Add to scene" menu comment further down in this file notes
   why that shape was already retired once. `.rc-blockmenu` only supplies the
   positioning context `.menu`'s `right:0` anchors against, plus hiding the
   native disclosure triangle; `.menu` needs no `[hidden]` toggle here because
   the browser already hides everything but `<summary>` while `<details>` is
   closed. */
.rc-blockmenu { position: relative; display: inline-flex; }
.rc-blockmenu > summary { list-style: none; cursor: pointer; }
.rc-blockmenu > summary::-webkit-details-marker { display: none; }
/* The "Applies to new requests." note under the requester item (review
   finding 6) sits in the menu's flow, not a `.menu-item` row — give it the
   same horizontal rhythm as the rows above it. */
.rc-blockmenu .menu-list > .sec-note { padding: 0 var(--space-3) var(--space-2); }

/* Audio opt-in + total-loop readout: one label/control rhythm, divided by hairlines. */
.rc-row { display: flex; align-items: center; gap: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border); }
.rc-row-lbl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.rc-row-lbl b { font-size: 13px; }
.rc-row-lbl span { font-size: 12px; line-height: 1.5; }
.rc-total { flex: none; padding: var(--space-1) var(--space-3); font-family: var(--font-mono); font-size: 13px; border: 1px solid var(--border); border-radius: var(--r-chip); background: var(--inset-2); }
.rc-over { font-size: 12px; }
[data-rc-audio]:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
.rc-limits { margin: 0; font-size: 12px; line-height: 1.5; }

/* The library picker (same endpoints + raw-body upload as the alert asset field). */
.rc-panel { display: flex; flex-direction: column; gap: var(--space-3); }
.rc-panel[hidden] { display: none; }
.rc-assets { display: flex; flex-direction: column; gap: var(--space-2); max-height: 20rem; overflow-y: auto; }
.rc-asset { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); }

@media (max-width: 720px) {
  .rc-item, .rc-asset { flex-wrap: wrap; }
  .rc-meta { flex: 1 1 100%; order: 1; }
  .rc-durwrap { order: 2; }
  .rc-item-actions { order: 3; margin-left: auto; }
  .rc-row { flex-wrap: wrap; }
}

/* Connection context on a destination card (desktop-overhaul §02 — the retired
   channel dropdown, folded into the cards): which connected account this dest
   streams as, or an honest not-connected state. .dim token = readable muted
   informational text (WCAG 1.4.3), not decorative .faint. */
.sec-item-conn { font-size: 12px; color: var(--text-dim); display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.sec-item-conn--off { color: var(--warn); }
.sec-conn-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.sec-conn-link:hover, .sec-conn-link:focus { text-decoration: underline; }

/* ── Destination card: collapsed summary + click/keyboard-expand editor (P1) ──
   A destination card stacks vertically: an always-visible summary row (the
   expand toggle) over a hidden detail editor. The summary is the keyboard hit
   target (role=button, tabindex 0); the detail reveals ONLY via the
   click/Enter/Space toggle (which drives aria-expanded + [hidden]). The detail
   must NEVER open on hover — a hover-fold-out flapped the editor open as the
   pointer crossed the card. Hover is a purely visual chevron hint. */
/* ── Collapsible CARD heads (U-14 item 5) ──────────────────────────────────
   The same disclosure the destination card below has shipped since P1, applied
   to a whole card: the head is the keyboard hit target (role=button, tabindex
   0) driving `aria-expanded` on itself and `[hidden]` on everything after it.
   NEVER opens on hover, for the reason the destination card records: a
   hover-fold-out flaps open as the pointer crosses. */
.card-head[role="button"] { cursor: pointer; display: flex; align-items: center; gap: var(--space-2); }
.card-head[role="button"] > .sec-card-chevron { margin-left: auto; color: var(--text-dim); }
.card-head[role="button"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card.is-collapsed > .card-head { margin-bottom: 0; }

.sec-item--dest { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.sec-item-summary { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3); cursor: pointer; min-width: 0; }
/* Action buttons in a row stay together and can't push past the card edge; on a
   narrow row they wrap below the text (with the text) instead of overflowing. */
.sec-item-summary > .btn, .sec-item-summary > .sec-item-chevron { flex: 0 0 auto; }
/* The bot rows group their Edit/Remove/chevron into one actions container so the
   summary has a stable 3-child shape (toggle · main · actions); on desktop the
   container is a single inline flex group, so the row reads identical to before.
   On mobile it drops to its own full-width line (see the <=640px block). */
.sec-item-actions-row { display: flex; gap: var(--space-2); align-items: center; flex: none; }
.sec-item-summary:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; border-radius: var(--r-sm); }
.sec-item-summary > .sec-item-main { flex: 1; min-width: 0; }
.sec-item-chevron { flex: none; color: var(--text-dim); font-size: 12px; line-height: 1.6; user-select: none; transition: transform var(--dur-fast) var(--ease); }
.sec-item-detail { display: flex; flex-direction: column; gap: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--border); }
.sec-item-detail[hidden] { display: none; }
/* Hover is a chevron-only hint (pointer devices); it NEVER unhides the editor —
   the click/Enter/Space toggle is the single source of truth for expansion. */
@media (hover: hover) {
  .sec-item--dest:hover .sec-item-chevron { transform: rotate(90deg); }
}
.sec-item--dest.is-expanded .sec-item-chevron { transform: none; }
/* Destination editor: identity fields stack one per row (cleaner than the old
   horizontal wrap). Auto (OAuth) sources show a read-only synced notice instead
   of editable Server URL / Stream key; the Enabled toggle sits in the footer. */
.sec-item-form { display: flex; flex-direction: column; gap: var(--space-3); }
.sec-item-synced { display: flex; align-items: center; gap: var(--space-2); }
.sec-item-enabled { display: inline-flex; align-items: center; gap: var(--space-1); margin-right: auto; font-size: 12px; cursor: pointer; }

/* ── Destinations: the always-present PLATFORM cards (2026-08-15 rework) ──
   Twitch / YouTube / Kick / TikTok render as permanent cards above the custom
   destinations. A platform card is the same .sec-item--dest shell, plus a brand
   mark, an enable toggle in the summary, and (when no destination backs it yet)
   a static header with the one setup step it needs. Tokens only. */

/* Group headings between the two lists. */
.sec-grouplabel {
  font-family: var(--font-display); font-size: 11px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-faint);
  margin: var(--space-4) 0 var(--space-2);
}
.sec-grouplabel:first-child { margin-top: 0; }

/* AUTO-FIT, not `repeat(2, …)` (width model, 2026-08-17). A fixed two-track list
   answers a wider monitor by making each platform card wider — a 1200px row for a
   logo, a name and a Connect button. auto-fit answers it by adding a TRACK, so the
   cards stay card-sized and the set gets shorter. minmax's `min(…, 100%)` floor is
   the standard idiom here (`.sec-grid.cols-2` above) — it keeps a single card from
   overflowing a container narrower than the floor. The @media step below stays: it
   forces ONE column from 840px down, which is earlier than the 26rem floor would
   fold on its own, and that early fold is deliberate for these tall cards. */
.dest-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr)); gap: var(--space-2); }
@media (max-width: 840px) { .dest-grid { grid-template-columns: 1fr; } }

/* The brand mark tile. Colour comes from the canonical --p-* platform tokens
   (DESIGN-GUIDELINES §1.2) — never a local hex — and the glyph inherits
   currentColor from the tile. */
.dest-logo {
  flex: none; width: 34px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-items: center; background: var(--surface-2); color: var(--text);
}
.dest-logo svg { width: 18px; height: 18px; display: block; fill: currentColor; }
.dest-logo--twitch { background: var(--p-twitch); color: var(--text); }
.dest-logo--youtube { background: var(--p-youtube); color: var(--text); }
/* Kick's green is very light — its glyph needs the dark ground, not --text. */
.dest-logo--kick { background: var(--p-kick); color: var(--bg); }
.dest-logo--tiktok { background: var(--p-tiktok); color: var(--text); }
.dest-logo--custom { background: var(--surface-2); color: var(--text-dim); }

.sec-item--platform .sec-item-summary { align-items: center; }
/* A platform card with no destination row yet has nothing to expand: its header
   is static (no role=button, no chevron, default cursor). */
.sec-item-summary--static { cursor: default; }
/* Dim only the SETUP-PENDING platform cards a touch, so the configured ones lead.
   (.is-disabled — an existing row switched off — keeps its stronger dim.) */
.sec-item--platform.is-unset { background: var(--inset-2); }
.sec-item--platform.is-unset .sec-item-title { color: var(--text-dim); }
/* A switched-OFF platform card stays legible: it is a permanent slot with a
   visible OFF switch and an "Off" pill, so it does not need the strong 0.55 dim
   a hidden-away disabled custom destination gets. */
.sec-item--platform.is-disabled { opacity: .85; }

/* "Off" is a neutral state, not a fault: the shared .pill family in omni-ui.css
   only ships ok/warn/danger, so add the muted variant here. */
.pill.mut { background: var(--surface-inset); border: 1px solid var(--border); color: var(--text-dim); }

/* ── Sources: editable per-device priority + routing (P22) ── */
.sec-priority-wrap { display: inline-flex; align-items: center; gap: 4px; }
.sec-priority { display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 4px; border-radius: var(--r-sm); background: var(--accent-wash); color: var(--accent-2); font-family: var(--font-mono); font-size: 11px; font-weight: 700; }
.sec-priority.dim { background: transparent; color: var(--text-dim); }
.sec-pri-btn { padding: 2px 6px; font-size: 10px; line-height: 1; }
.sec-pri-btn:disabled { opacity: .4; cursor: not-allowed; }
.select--sm { padding: 6px 10px; font-size: 11.5px; }
/* Routing badge accent + online dot (used by the Sources device table). */
.badge--info { background: var(--accent-wash); color: var(--accent-2); border-color: var(--border-accent); }
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none; }
/* --success was never a token (the fallback always won); the semantic token is --ok. */
.status-dot--ok { background: var(--ok); }
.status-dot--off { background: var(--text-faint); }

/* ── sub-tab strip (engagement vs loyalty placeholder) ── */
/* Scrolls inside its own container so 5+ tabs never push the body wider than
   the viewport on narrow screens (Web Interface Guidelines: no horizontal body
   scroll). flex-wrap:nowrap keeps the strip one row; overflow-x:auto lets that
   row scroll locally. */
.subtabs { display: flex; flex-wrap: nowrap; gap: var(--space-2); margin-bottom: var(--space-4); border-bottom: 1px solid var(--border); overflow-x: auto; -webkit-overflow-scrolling: touch; }
.subtabs button { flex: none; }
.subtabs button { border: 0; background: transparent; color: var(--text-dim); font: inherit; font-weight: 600; padding: 9px var(--space-4); border-bottom: 2px solid transparent; cursor: pointer; }
.subtabs button.on { color: var(--text); border-bottom-color: var(--accent); }

/* ── spacing utilities (token-based; replace inline el.style.marginTop and
   stray inline style="margin-top:…" so markup carries no hardcoded spacing,
   DESIGN-GUIDELINES §3.9 anti-pattern). ── */
.sec-mt-4 { margin-top: var(--space-4); }

/* ── content-bearing meta text (WCAG 1.4.3): .dim is the readable muted token
   for meta that carries information. .faint is reserved for decorative/optional
   hints; content meta uses .dim against --surface-inset. ── */
.sec-mb-4 { margin-bottom: var(--space-4); }

/* ═══════════════════════════════════════════════════════════════════════
   Phase 3 — Revenue (#sec-revenue) + Analytics (#sec-analytics) sections.
   Tokens ONLY (omni-ui.css :root). Provenance/availability badges are the
   honesty surface: every money figure and every metric is badged.
   ═══════════════════════════════════════════════════════════════════════ */

/* card header strip (used by every section card; previously relied on default
   block flow — defined here so the head reads as a header with its own border). */
.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 0 0 var(--space-3) 0; border-bottom: 1px solid var(--border); margin-bottom: var(--space-3); }
.card-head h2 { margin: 0; font-size: 14px; font-weight: 600; }
/* A .card already supplies the outer space-4 padding (shell.css). A .sec-body
   directly inside it must NOT add its own — that DOUBLED the inset (2× space-4):
   the header sat at 16px while the body sat at 32px, with an oversized bottom gap.
   Plain .card content (no .sec-body) stayed at 16px, so section cards read as
   over-padded and the plain ones as under-padded side by side. The card pads; the
   body only lays out (flex column + gap). The rule below re-adds a small top gap
   under the .card-head divider when a head precedes the body. */
.card > .sec-body { padding: 0; }
.card > .card-head { margin-bottom: 0; }
.card > .card-head + .sec-body { padding-top: var(--space-3); }

/* ── provenance badge (REAL / ESTIMATED / IMPORTED) — extends .badge ── */
.badge.prov { border: 1px solid var(--border); }
.prov--real { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
.prov--estimated { background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.prov--imported { background: var(--accent-wash); color: var(--accent-2); border-color: var(--border-accent); }

/* ── payout reconciliation status — a PERSISTENT record label → .badge (§3.3
   reserves .pill for transient LIVE/CONNECTING/ERROR). Status-derived variant so
   received vs mismatch are visually differentiated, not colour-only: the text
   label (PENDING/RECEIVED/RECONCILED/MISMATCH) always carries the meaning. ── */
.badge.status { border: 1px solid var(--border); }
.status--ok { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
.status--warn { background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.status--danger { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
.status--neutral { background: var(--surface-2); color: var(--text-dim); }

/* ── availability badge (Available / Estimated / N/A) — mockup avail-* set ── */
.availbadge { font-family: var(--font-mono); font-size: 9px; letter-spacing: .06em; text-transform: uppercase; font-weight: 700; padding: 2px 7px; border-radius: var(--r-sm); white-space: nowrap; }
.avail-ok { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok-soft); }
.avail-est { background: color-mix(in srgb, var(--warn) 12%, transparent); color: var(--warn); }
/* N/A is load-bearing honesty content ("no platform API"), not decoration, so it
   must meet WCAG 1.4.3 AA (4.5:1). This 9px bold badge is NOT WCAG "large text",
   so --text-faint (~3.4:1 on --surface) failed — same faint-on-surface mistake
   already fixed for .field > .help. Use --text-dim (~7:1 on --surface, also AA on
   --surface-inset where the legend rows sit in .well). Background is a token-based
   wash (--surface-2 over a tint) rather than a hardcoded hex. */
.avail-na { background: var(--surface-2); color: var(--text-dim); border: 1px solid var(--border); }

/* ── legend (provenance / availability key rows) ── */
.legend { display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: center; font-size: 11.5px; color: var(--text-dim); }
.legend > span { display: inline-flex; align-items: center; gap: 6px; }

/* ── per-currency subtotal blocks (multi-currency: one block per ISO currency,
   never summed across) — layout only, colours come from tokenized child classes */
.cur-totals { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.cur-block { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.cur-block:last-child { border-bottom: 0; }
.cur-block .cur-code { font-weight: 600; color: var(--text); min-width: 3.5em; }
.cur-block .cur-total { font-weight: 600; color: var(--text); }

/* ── platform dot ── */
.pdot { width: 12px; height: 12px; border-radius: 3px; flex: none; display: inline-block; background: var(--surface-2); }
.pdot.twitch { background: var(--p-twitch); }
.pdot.youtube { background: var(--p-youtube); }
.pdot.kick { background: var(--p-kick); }
.pdot.tiktok { background: var(--p-tiktok); }
.pdot.billing { background: var(--accent); }
.inline { display: inline-flex; align-items: center; gap: var(--space-2); }

/* ── data table (revenue breakdown / analytics metrics / payouts) ── */
/* Scrolls inside its own wrapper so a wide table never widens the page body
   on narrow viewports (Web Interface Guidelines: no horizontal body scroll). */
.sec-tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* Audio VOD-split checklist (opt-out): one checkbox per audio source against the
   VOD column. Buses are auto-populated from the live OBS sources — there is no
   manual add/remove (untick to drop a source from the VOD). */
.sec-checklist { display: flex; flex-direction: column; }
.sec-checkrow {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 10px var(--space-2); border-bottom: 1px solid var(--border); cursor: pointer;
}
.sec-checkrow:last-child { border-bottom: 0; }
.sec-checkrow > .grow { flex: 1; min-width: 0; }
.sec-checkrow > input[type="checkbox"] { flex: 0 0 auto; }
/* ONE INLINE CHECKBOX + its caption, as a real <label> so the whole phrase is
   the hit target. Distinct from .sec-checkrow above, which is a bordered list
   ROW; this one rides inside a .sec-row or a .chips group (Schedule & Social
   emits it in both). It shipped 2026-08-19 with NO rule at all — an unstyled
   label whose caption baseline-floated against the 44px controls beside it. */
.sec-check {
  display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0;
  color: var(--text-dim); font-size: 12.5px; line-height: 1.4; cursor: pointer;
}
/* The tick itself wears the shell's accent and a real size (owner report
   2026-08-26: "missing our stylized tickbox" on the custom-voice card). The
   rule shipped as `flex:none; margin:0` only — layout, no skin — so the box
   stayed the UA's own 13px default and drew its check in the browser's blue,
   the one control on a Pyre card that was not Pyre-coloured. Same recipe the
   bot pane's `.ef-check` already uses, in the shared tokens. */
.sec-check > input[type="checkbox"] {
  flex: none; margin: 0; width: 16px; height: 16px;
  accent-color: var(--accent); cursor: pointer;
}
.sec-check > input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The centered default is right for a two-word caption ("Weekly", "Announce");
   it is wrong for a SENTENCE that wraps, where centering floats the tick
   halfway down the paragraph. `--top` lines it up with the caption's first
   line instead — what the custom-voice consent line needs. */
.sec-check--top { align-items: flex-start; }
.sec-check--top > input[type="checkbox"] { margin-top: 1px; }
/* ── Audio matrix: a bus whose OBS source is GONE (missing-source reap, 2026-08-15) ──
   Buses are persisted, so a source deleted from OBS leaves its row behind. Rows
   with no excludes are swept automatically; a row that carries the user's
   VOD/mute decisions is only FLAGGED (it may belong to another scene collection
   that isn't loaded), with an inline ✕ Remove. Warn, not danger: nothing is
   broken — the row is just stale. Same recipe as .sec-item-title .badge--warn. */
.audio-missing {
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent);
}
/* The matrix row header becomes a flex line so the name, the badge and the
   Remove button sit on one row (and wrap on narrow viewports). */
.audio-rowhead { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.audio-rowhead > .grow { flex: 1; min-width: 0; font-weight: inherit; }
/* Per-destination "SRT soon" honesty marker on the matrix column headers: the
   destination columns are inert on the single-track RTMP relay. Muted, small,
   non-uppercase so it reads as a status note, not a teachable label. */
.sec-soon {
  display: inline-block; margin-left: var(--space-1); padding: 1px 6px;
  font-size: 9.5px; font-weight: 600; letter-spacing: .02em; text-transform: none;
  color: var(--text-dim); background: var(--surface-inset);
  border: 1px solid var(--border); border-radius: var(--r-sm); vertical-align: middle;
  cursor: help;
}
/* Info note: the honest "what applies today" callout under the matrix intro. */
.sec-note--info {
  padding: var(--space-2) var(--space-3); border: 1px solid var(--border);
  border-left: 2px solid var(--accent); border-radius: var(--r-sm);
  background: var(--surface-inset);
}
/* …and the same callout when the sentence is asking the reader to go and change
   something. Identical box, warn edge, readable body: severity lives in the rule
   and in the words, never in the body colour (WCAG 1.4.1). Added 2026-08-31 for
   the vision panel's self-diagnosis line, which is the one line on that surface
   that ever asks for an action. PADDING ONLY, never a block margin — the note
   sits in a gap container like every other child (DESIGN-GUIDELINES §1.4.1). */
.sec-note--warn {
  padding: var(--space-2) var(--space-3); border: 1px solid var(--border);
  border-left: 2px solid var(--warn); border-radius: var(--r-sm);
  background: var(--surface-inset);
}
.table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.table th, .table td { text-align: left; padding: 8px var(--space-2); border-bottom: 1px solid var(--border); }
.table th { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
.table tr:last-child td { border-bottom: 0; }
.table .num { text-align: right; }

/* ── stat (DESIGN-GUIDELINES §3.6: label + mono value for KPIs) ──
   The headline "Earnings this period" figure is PERSISTENT data, not a transient
   status, so it uses .stat (not .pill — §3.3 reserves .pill for LIVE/ERROR state).
   Value sized via the type scale (20px heading), not a hero gradient (§3.9). */
.stat { display: inline-flex; flex-direction: column; gap: 2px; }
.stat > .stat-label { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
.stat > .stat-value { font-family: var(--font-mono); font-size: 20px; font-weight: 700; color: var(--text); }
/* period-over-period delta (§3.9 trend context). Direction is colour-coded but
   the text ("+12% vs prior period") always carries the meaning (not colour-only). */
.stat > .stat-delta { font-size: 11.5px; font-weight: 600; }
.stat > .stat-delta.trend-up { color: var(--ok-soft); }
.stat > .stat-delta.trend-down { color: var(--danger); }

/* ── grid span (revenue/analytics focal card spans 2 of 3 cols) ── */
.sec-grid .span-2 { grid-column: span 2; }
@media (max-width: 980px) { .sec-grid .span-2 { grid-column: auto; } }

/* ── full-width action button ── */
.btn.block { width: 100%; justify-content: center; }

/* ── error region: cause + Retry affordance (DESIGN-GUIDELINES §5 error state).
   The error text and its Retry button sit on one row; Retry re-invokes the
   matching refresh fn. .sec-retry is a compact ghost button. ── */
.sec-errrow { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.sec-retry { border: 1px solid var(--border); background: var(--surface-inset); color: var(--text); font: inherit; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-sm); cursor: pointer; }
.sec-retry:hover { border-color: var(--border-accent); }

/* ── inline link-style button ("why estimated?") ── */
.linkbtn { border: 0; background: transparent; padding: 0; font: inherit; color: var(--accent-2); cursor: pointer; text-decoration: underline; }
.linkbtn:hover { color: var(--accent-soft); }

/* ── lightweight modal (CSV import / "why estimated?") ── */
.sec-modal { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: var(--space-4); background: rgba(0, 0, 0, .55); }
.sec-modal.hide { display: none; }
.sec-modal-box { width: min(520px, 100%); max-height: 90vh; overflow-y: auto; background: var(--surface); border: 1px solid var(--border-accent); border-radius: var(--r); box-shadow: var(--shadow-card); padding: var(--space-4); }
.sec-modal-box .card-head { margin-bottom: var(--space-3); }
.sec-modal-box .card-head { display: flex; align-items: center; justify-content: space-between; }

/* ── Edit-Stream-Info modal (Quick action 'edit-info', F1) ──
   Rebuilt 2026-08-02 as an "inherit & customize" per-channel list: SHARED values
   on top, then one collapsible override ROW per connected channel. A modal-local
   refined palette lives entirely in --si-* custom properties SCOPED to
   [data-si-modal], so this modern look is fully contained and never touches global
   tokens or other .sec-modal users (add-device wizard, CSV import). The .si-* /
   data-si-* names are the JS contract (sec-stream.js); don't rename. Platform
   colours appear only as small badges. NO em dashes in copy. Every value below is
   derived from a global token, so the modal re-skins with the rest of the app. */
.sec-modal[data-si-modal] {
  /* Every --si-* is an alias of (or a color-mix derived from) the global refined
     palette in omni-ui.css :root, so there is a single source of truth and the
     modal follows a re-skin. The platform tints are anchored to the canonical
     --p-* brand tokens (lightened toward --text for AA on the dark badge), never
     to the app accent: re-theming must not recolor a platform badge. */
  --si-accent: var(--accent);
  --si-accent-2: var(--accent-2);
  --si-accent-wash: var(--accent-wash);
  --si-accent-line: var(--border-accent);
  --si-field: var(--surface-inset);
  --si-surface: var(--surface);
  --si-surface-2: var(--surface-2);
  /* solid (not the translucent global --surface-3): it stacks on tinted rows */
  --si-surface-3: var(--surface-2);
  --si-body-inset: var(--inset-2);
  --si-box-grad-a: var(--surface-inset);
  --si-box-grad-b: var(--inset-2);
  --si-accent-glow: var(--accent-glow);
  --si-border: var(--border);
  --si-border-strong: var(--border-strong);
  --si-text: var(--text);
  --si-dim: var(--text-dim);
  --si-faint: var(--text-faint);
  --si-ink: var(--on-strong);
  --si-on-strong: var(--on-strong);
  --si-on-yt: var(--text);
  --si-tw: color-mix(in srgb, var(--p-twitch) 62%, var(--text));
  --si-kick: var(--p-kick);
  --si-yt: color-mix(in srgb, var(--p-youtube) 72%, var(--text));
  --si-r-lg: var(--r-lg);
  --si-r: var(--r);
  --si-r-sm: var(--r-sm);
  --si-shadow: 0 24px 60px -12px rgba(0, 0, 0, .7), 0 4px 12px rgba(0, 0, 0, .4);
}

/* The refined box: single-column list, narrower than the shared 520px box and
   scoped so other .sec-modal users keep their width. Vertical scroll only. */
.sec-modal[data-si-modal] .sec-modal-box {
  width: min(460px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0;
  color: var(--si-text);
  background: linear-gradient(180deg, var(--si-box-grad-a), var(--si-box-grad-b));
  border: 1px solid var(--si-border-strong);
  border-radius: var(--si-r-lg);
  box-shadow: var(--si-shadow);
}

/* Header: title + connected-channels sub line + a small square close button. */
.sec-modal[data-si-modal] .card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  padding: 18px 20px 14px;
  border-bottom: 1px solid var(--si-border);
}
.sec-modal[data-si-modal] .card-head h2 {
  margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--si-text);
}
.sec-modal[data-si-modal] .si-sub { margin-top: 2px; font-size: 12.5px; color: var(--si-faint); }
.sec-modal[data-si-modal] [data-si-close] {
  flex: none; width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  font-size: 15px; line-height: 1; cursor: pointer;
  color: var(--si-dim);
  background: transparent;
  border: 1px solid var(--si-border);
  border-radius: 8px;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] [data-si-close]:hover { background: var(--si-surface-2); color: var(--si-text); }

/* Body: a vertical stack of fields + the per-channel section. */
.sec-modal[data-si-modal] .sec-body {
  display: flex; flex-direction: column; gap: 16px;
  padding: 18px 20px 6px;
}

/* Field rhythm (shared block + each row body). */
.sec-modal[data-si-modal] .si-field { display: block; margin: 0; }
.sec-modal[data-si-modal] .si-label {
  display: block; margin-bottom: 7px;
  font-size: 12.5px; font-weight: 550; letter-spacing: .005em; color: var(--si-dim);
}
.sec-modal[data-si-modal] .si-label .dim { color: var(--si-faint); font-weight: 400; }
.sec-modal[data-si-modal] .si-rowtop {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 7px;
}
.sec-modal[data-si-modal] .si-rowtop .si-label { margin-bottom: 0; }
.sec-modal[data-si-modal] .si-count {
  font-size: 11.5px; color: var(--si-faint); font-variant-numeric: tabular-nums;
}
.sec-modal[data-si-modal] .si-cat-current { margin-top: 7px; font-size: 12.5px; color: var(--si-faint); }

/* Inputs: dark field, hairline border, refined violet focus ring. */
.sec-modal[data-si-modal] .inp {
  width: 100%; box-sizing: border-box;
  padding: 10px 12px;
  font: inherit; font-size: 14px; color: var(--si-text);
  background: var(--si-field);
  border: 1px solid var(--si-border);
  border-radius: var(--si-r-sm);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .inp::placeholder { color: var(--si-faint); }
.sec-modal[data-si-modal] .inp:focus {
  outline: none;
  border-color: var(--si-accent-line);
  box-shadow: 0 0 0 3px var(--si-accent-wash);
}

/* Category picker: an in-flow popover-like list (never clips in the scroll box). */
.sec-modal[data-si-modal] .si-cat-results {
  list-style: none; margin: 7px 0 0; padding: 5px;
  max-height: 180px; overflow-y: auto;
  background: var(--si-surface);
  border: 1px solid var(--si-border-strong);
  border-radius: var(--si-r-sm);
  box-shadow: var(--si-shadow);
}
.sec-modal[data-si-modal] .si-cat-results:empty { display: none; }
.sec-modal[data-si-modal] .si-cat-result {
  padding: 8px 10px; border-radius: var(--si-r-sm); cursor: pointer; font-size: 14px;
}
.sec-modal[data-si-modal] .si-cat-result:hover,
.sec-modal[data-si-modal] .si-cat-result:focus { background: var(--si-surface-2); }

/* "Per channel" section: a hairline-ruled label + a one-line hint above the rows. */
.sec-modal[data-si-modal] .si-perchannel { display: flex; flex-direction: column; gap: 10px; }
.sec-modal[data-si-modal] .si-seclabel {
  display: flex; align-items: center; gap: 12px;
  margin: 0; color: var(--si-faint);
  font-size: 11.5px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
}
.sec-modal[data-si-modal] .si-seclabel::after { content: ""; flex: 1; height: 1px; background: var(--si-border); }
.sec-modal[data-si-modal] .si-hint { margin: -4px 0 0; font-size: 12.5px; color: var(--si-faint); }

/* Per-channel rows: a collapsible with a clickable head + hidden override body.
   (.hide comes from the global !important rule → disconnected rows stay hidden.) */
.sec-modal[data-si-modal] .si-chrows { display: flex; flex-direction: column; gap: 9px; }
.sec-modal[data-si-modal] .si-chrow {
  overflow: hidden;
  background: var(--si-surface);
  border: 1px solid var(--si-border);
  border-radius: var(--si-r);
}
.sec-modal[data-si-modal] .si-chrow-head {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 13px; cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .si-chrow-head:hover { background: var(--si-surface-2); }
.sec-modal[data-si-modal] .si-badge {
  flex: none; width: 26px; height: 26px;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700; color: var(--si-ink);
  border-radius: 7px; background: var(--si-surface-3);
}
.sec-modal[data-si-modal] .si-badge[data-si-badge="twitch"] { background: var(--si-tw); color: var(--si-ink); }
.sec-modal[data-si-modal] .si-badge[data-si-badge="kick"] { background: var(--si-kick); color: var(--si-ink); }
.sec-modal[data-si-modal] .si-badge[data-si-badge="youtube"] { background: var(--si-yt); color: var(--si-on-yt); }
.sec-modal[data-si-modal] .si-chrow-meta { min-width: 0; }
.sec-modal[data-si-modal] .si-chname { font-size: 14px; font-weight: 600; color: var(--si-text); }
.sec-modal[data-si-modal] .si-chstate { margin-top: 1px; font-size: 12.5px; color: var(--si-faint); }
.sec-modal[data-si-modal] .si-chstate.over { color: var(--si-accent-2); }
.sec-modal[data-si-modal] .si-capnote { color: var(--si-faint); }
.sec-modal[data-si-modal] .si-customize {
  margin-left: auto; flex: none;
  padding: 5px 11px;
  font: inherit; font-size: 12.5px; font-weight: 550; color: var(--si-dim);
  background: transparent;
  border: 1px solid var(--si-border);
  border-radius: 7px;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .si-customize:hover { border-color: var(--si-accent-line); color: var(--si-accent-2); }
.sec-modal[data-si-modal] .si-chbody {
  display: none; flex-direction: column; gap: 12px;
  padding: 12px 13px 14px;
  background: var(--si-body-inset);
  border-top: 1px solid var(--si-border);
}
.sec-modal[data-si-modal] .si-chrow.open .si-chbody { display: flex; }
.sec-modal[data-si-modal] .si-chrow.open .si-customize {
  color: var(--si-accent-2); border-color: var(--si-accent-line); background: var(--si-accent-wash);
}
.sec-modal[data-si-modal] .si-reset {
  align-self: flex-start; padding: 0;
  font: inherit; font-size: 12px; color: var(--si-faint);
  background: none; border: none; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.sec-modal[data-si-modal] .si-reset:hover { color: var(--si-dim); }

/* Footer: de-emphasized sync switch on the left, Cancel + primary Save on the right. */
.sec-modal[data-si-modal] .sec-actions {
  display: flex; align-items: center; gap: 12px;
  margin-top: 8px;
  padding: 14px 20px 18px;
  border-top: 1px solid var(--si-border);
}
.sec-modal[data-si-modal] .si-sync-wrap {
  margin-right: auto;
  position: relative;
  display: flex; align-items: center; gap: 7px;
}
.sec-modal[data-si-modal] .si-sync {
  display: flex; align-items: center; gap: 9px; cursor: pointer;
}
.sec-modal[data-si-modal] .si-sync span { font-size: 12px; color: var(--si-faint); }
/* (?) help: a real focusable control that reveals a tokenized tooltip on hover AND
   keyboard focus. The tooltip is a real element referenced by aria-describedby. */
.sec-modal[data-si-modal] .si-help {
  flex: none; width: 16px; height: 16px; padding: 0;
  display: grid; place-items: center;
  font: inherit; font-size: 10.5px; font-weight: 700; line-height: 1; color: var(--si-dim);
  background: var(--si-surface-2);
  border: 1px solid var(--si-border);
  border-radius: 50%;
  cursor: help;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .si-help:hover { color: var(--si-text); border-color: var(--si-accent-line); }
.sec-modal[data-si-modal] .si-help:focus-visible { outline: 2px solid var(--si-accent); outline-offset: 2px; }
.sec-modal[data-si-modal] .si-tip {
  position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 1;
  width: max-content; max-width: 260px;
  padding: 9px 11px;
  font-size: 12px; font-weight: 400; line-height: 1.4; color: var(--si-dim);
  background: var(--si-surface-2);
  border: 1px solid var(--si-border-strong);
  border-radius: var(--si-r-sm);
  box-shadow: var(--si-shadow);
  opacity: 0; visibility: hidden; transform: translateY(3px);
  transition: opacity var(--dur-fast) var(--ease), visibility var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
  pointer-events: none;
}
/* Revealed on hover of the (?) or when it (or the tooltip) has focus. */
.sec-modal[data-si-modal] .si-help:hover + .si-tip,
.sec-modal[data-si-modal] .si-help:focus-visible + .si-tip,
.sec-modal[data-si-modal] .si-tip:hover,
.sec-modal[data-si-modal] .si-sync-wrap:focus-within .si-tip {
  opacity: 1; visibility: visible; transform: translateY(0);
}
/* The preserved sync checkbox, restyled as a pill switch (state = :checked). */
.sec-modal[data-si-modal] .si-switch {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 34px; height: 20px; margin: 0;
  position: relative; cursor: pointer;
  background: var(--si-surface-3);
  border: 1px solid var(--si-border);
  border-radius: 11px;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .si-switch::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--si-dim);
  transition: left var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] .si-switch:checked { background: var(--si-accent-wash); border-color: var(--si-accent-line); }
.sec-modal[data-si-modal] .si-switch:checked::after { left: 16px; background: var(--si-accent-2); }
.sec-modal[data-si-modal] .si-switch:focus-visible { outline: 2px solid var(--si-accent); outline-offset: 2px; }

/* Buttons: ghost Cancel + violet-gradient primary Save. */
.sec-modal[data-si-modal] .sec-actions .btn {
  padding: 9px 16px;
  font: inherit; font-size: 13.5px; font-weight: 600;
  border-radius: 9px; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}
.sec-modal[data-si-modal] [data-si-cancel] {
  color: var(--si-dim); background: transparent; border: 1px solid var(--si-border);
}
.sec-modal[data-si-modal] [data-si-cancel]:hover { background: var(--si-surface-2); color: var(--si-text); }
.sec-modal[data-si-modal] [data-si-save] {
  color: var(--si-on-strong); border: none;
  background: linear-gradient(180deg, var(--si-accent-2), var(--si-accent));
  box-shadow: 0 2px 10px var(--si-accent-glow);
}
.sec-modal[data-si-modal] [data-si-save]:hover { filter: brightness(1.06); }
.sec-modal[data-si-modal] [data-si-save][disabled] { filter: none; opacity: .7; cursor: default; }

/* Per-platform save status (one row per platform after Save). */
.sec-modal[data-si-modal] .si-status { list-style: none; margin: 0; padding: 0; }
.sec-modal[data-si-modal] .si-status:empty { display: none; }
.sec-modal[data-si-modal] .si-status-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 5px 0; font-size: 13px;
  border-bottom: 1px solid var(--si-border);
}
.sec-modal[data-si-modal] .si-status-row:last-child { border-bottom: 0; }
.sec-modal[data-si-modal] .si-status-name { font-weight: 600; color: var(--si-text); }
.sec-modal[data-si-modal] .si-status-row.ok .si-status-val { color: var(--ok-soft); }
.sec-modal[data-si-modal] .si-status-row.warn .si-status-val { color: var(--warn); }
.sec-modal[data-si-modal] .sec-err { font-size: 13px; }
.sec-modal-box .sec-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-3); }

/* ── visually-hidden utility (screen-reader-only) ──
   Used for <caption> on data tables so the table has an accessible name without
   a visible duplicate of the card <h2>. Token-spaced; no hardcoded clip magic
   beyond the standard 1px inset-rect pattern. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}

/* ── no-JavaScript fallback ──
   The app shell (sidebar + every section) is built by shell.js, so with JS
   disabled (or if shell.js fails to load) the page would otherwise render blank.
   This token-styled <noscript> message keeps the page from ever being silently
   empty and points to a static help/login route. */
.noscript-fallback {
  margin: var(--space-6) auto; max-width: 520px; padding: var(--space-6);
  background: var(--surface); border: 1px solid var(--border-accent);
  border-radius: var(--r); box-shadow: var(--shadow-card); text-align: center;
}
.noscript-fallback h1 { margin: 0 0 var(--space-3); font-size: 17px; }
.noscript-fallback p { margin: 0 0 var(--space-4); color: var(--text-dim); font-size: 13px; }
.noscript-fallback a { color: var(--accent-2); }

/* ═══════════════════════════════════════════════════════════════════════
   Phase 3 — Chat (#sec-chat). The merged multi-platform moderated feed
   (sec-chat.js). Tokens ONLY (no hardcoded color). Mirrors the .sec-*
   component vocabulary. The JS class names are the contract — do not rename.
   ═══════════════════════════════════════════════════════════════════════ */

/* The chat card is a vertical stack: filter toolbar → (connect prompt) → feed →
   composer → statusline. The feed is the only flexible region (it scrolls). */
.chat-card { display: flex; flex-direction: column; gap: var(--space-3); }

/* ── filter toolbar: per-platform toggle row ── */
.chat-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--border); }
.chat-filter { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--text-dim); cursor: pointer; user-select: none; }
.chat-filter input { margin: 0; cursor: pointer; }
/* A filter that is OFF dims its own label so the toolbar reflects active state. */
.chat-filter:has(input:not(:checked)) { opacity: .55; }

/* ── connect-a-channel empty prompt (shown when no platforms connected) ── */
.chat-connect { padding: var(--space-3); border: 1px dashed var(--border-accent); border-radius: var(--r-sm); background: var(--accent-wash); color: var(--text-dim); font-size: 12px; }
.chat-connect a { color: var(--accent-2); }

/* ── merged feed: the scroll container. Caps its own height and scrolls
   internally so a long session never grows the page body. ── */
.chat-feed { display: flex; flex-direction: column; gap: var(--space-2); overflow-y: auto; -webkit-overflow-scrolling: touch; max-height: 52vh; min-height: 180px; padding-right: var(--space-1); }
.chat-feed:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; border-radius: var(--r-sm); }
.chat-empty { padding: var(--space-4); text-align: center; font-size: 12px; }

/* ── a single chat line: platform dot · body (author + text) · mod actions ── */
.chat-row { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-1) var(--space-2); border-radius: var(--r-sm); }
.chat-row:hover, .chat-row:focus-within { background: var(--surface-inset); }
.chat-row .pdot { margin-top: 4px; }
.chat-body { flex: 1; min-width: 0; font-size: 13px; line-height: 1.4; word-break: break-word; }
.chat-author { font-weight: 600; color: var(--text); margin-right: 6px; }
.chat-text { color: var(--text); }
.chat-deleted-label { margin-left: 6px; font-size: 11px; }

/* moderation actions — quiet until the row is hovered/focused, but always
   keyboard-reachable (kept in flow; revealed via opacity, not display). */
.chat-mod { flex: none; display: inline-flex; gap: var(--space-1); align-items: center; opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
.chat-row:hover .chat-mod, .chat-row:focus-within .chat-mod { opacity: 1; }
.chat-mod-btn { min-width: 26px; height: 26px; padding: 0 var(--space-1); font-size: 12px; line-height: 1; }
.chat-mod-btn--danger { color: var(--danger); }
/* The compact Stream-Manager "My chat" card renders .chat-msg rows (chat-render.js)
   instead of .chat-row, and gets the SAME moderation affordance. `margin-left:auto`
   pins the action row to the trailing edge of the flex row. */
.chat-msg .chat-mod { margin-left: auto; }
.chat-msg:hover .chat-mod, .chat-msg:focus-within .chat-mod { opacity: 1; }
.chat-msg.deleted .msg-text { opacity: .5; text-decoration: line-through; }

/* ── composer (send box) + status line ── */
.chat-composer { display: flex; gap: var(--space-2); padding-top: var(--space-3); border-top: 1px solid var(--border); align-items: center; }
.chat-composer .grow { flex: 1; min-width: 0; }
/* the platform picker sizes to its content, not the full composer width */
.chat-composer select { flex: 0 0 auto; width: auto; }
/* The generic `.sec-body form` flex-COLUMN rule (line ~54, added in 339d247 to
   space settings forms) outranks .chat-composer (0,1,1 vs 0,1,0) and stacked the
   send box vertically — and .chat-composer's own align-items:center then centered
   every control horizontally in the column. Re-assert the ROW at higher
   specificity; keep this next to the composer rules so the collision stays known. */
.sec-body form.chat-composer { flex-direction: row; gap: var(--space-2); }
.chat-statusline { margin: 0; font-size: 11.5px; min-height: 1em; }

/* ── interactive state styling (the headline filter + moderate value) ──
   A filtered-out platform's rows must actually disappear; a moderated row must
   visibly collapse; a system/event line must read distinctly from chat. ── */
.chat-row.filter-hidden { display: none; }
.chat-row.deleted { opacity: .55; }
.chat-row.deleted .chat-text { text-decoration: line-through; }
.chat-row.is-event { border-left: 2px solid var(--accent); background: var(--accent-wash); }
.chat-row.is-event .chat-author { color: var(--accent-2); }

/* ── Shared chat rich rendering (chat-render.js): badges, colored names, emotes. ──
   The full chat section enriches its .chat-row bodies in place; the compact My-chat
   card builds .chat-msg rows via buildMessageEl. Both reuse .msg-badges / .chat-emote. */
.chat-msg { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-1) var(--space-2); border-radius: var(--r-sm); line-height: 1.4; }
.chat-msg:hover { background: var(--surface-inset); }
.chat-msg .pdot { margin-top: 4px; }
/* Local-time HH:MM stamp — muted, tabular, scales with the feed font-size (.82em) so the
   chat card's A−/A+ control sizes it along with the messages. */
.chat-msg .msg-time { flex: none; color: var(--text-faint); font-size: .82em; font-variant-numeric: tabular-nums; margin-top: 3px; white-space: nowrap; }
.chat-msg .msg-body { min-width: 0; }
.chat-msg .msg-author { font-weight: 700; }
.chat-msg .msg-author::after { content: ':'; color: var(--text-dim); font-weight: 400; margin-left: 1px; }
.chat-msg .msg-text { color: var(--text); word-break: break-word; }
/* Event lines (redeem/sub/cheer/raid) — accent-tinted, the description in accent. */
.chat-msg.is-event { border-left: 2px solid var(--accent); background: var(--accent-wash); padding-left: var(--space-2); }
.msg-event { color: var(--accent-2); word-break: break-word; }
/* Marked messages (first-time chatter / highlighted / announcement) — the NORMAL
   chat body plus a small .msg-event-tag pill above it. Quieter than event lines:
   accent left-rule only, no row wash. Shared by BOTH surfaces (.chat-row full
   chat section rows + .chat-msg My-chat card rows). */
.chat-row.is-marked, .chat-msg.is-marked { border-left: 2px solid var(--accent); padding-left: var(--space-2); }
.msg-event-tag {
  display: block; width: fit-content;
  margin-bottom: 2px; padding: 0 6px;
  border-radius: var(--r-sm);
  background: var(--accent-wash); color: var(--accent-2);
  font-size: .75em; font-weight: 600; line-height: 1.6; white-space: nowrap;
}

/* bot-protection W3 T3.3 — mods-only suspicious-user treatment (chat-render.js
   buildMessageEl). Same left-rule idiom as is-marked, recoloured per level;
   the .msg-flag label rides above the line. Never reaches an on-stream overlay
   (server strips it) — this is the cockpit "My chat" card / Chat section. */
.chat-row.is-monitored, .chat-msg.is-monitored {
  border-left: 2px solid var(--warn); padding-left: var(--space-2);
  background: color-mix(in srgb, var(--warn) 6%, transparent);
}
.chat-row.is-restricted, .chat-msg.is-restricted {
  border-left: 2px solid var(--danger); padding-left: var(--space-2);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.chat-msg.is-restricted .msg-text { color: var(--text-dim); }
.msg-flag {
  display: flex; align-items: center; gap: 6px; width: fit-content;
  margin-bottom: 2px;
  font-size: .72em; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
}
.msg-flag::before { content: ""; width: 7px; height: 7px; border-radius: 50%; flex: none; }
.msg-flag--mon { color: var(--warn); }
.msg-flag--mon::before { background: var(--warn); }
.msg-flag--res { color: var(--danger); }
.msg-flag--res::before { background: var(--danger); }
/* ═══════════════════════════════════════════════════════════════════════
   Chat → HISTORY tab (dashboard redesign R1b)
   The channel-wide browser over the stored chat log. It deliberately does NOT
   reuse .chat-feed: that one is a bottom-pinned live tail, this one is a
   top-anchored newest-first page you scroll and extend with "Load more". What it
   DOES share is the reading vocabulary (author colour, deleted state, event tone)
   so a line reads the same on both tabs.
   ═══════════════════════════════════════════════════════════════════════ */
.chat-history { display: flex; flex-direction: column; gap: var(--space-3); }
.hist-controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--border); }
.hist-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; min-width: 0; }
.hist-field.grow { flex: 1 1 12rem; }
.hist-day { display: inline-flex; align-items: center; gap: var(--space-1); }
.hist-hint { margin: 0; font-size: 11.5px; }

/* The list: capped + internally scrolling, like the live feed, so a 10k-line day
   never grows the page body. */
.hist-list { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; -webkit-overflow-scrolling: touch; max-height: 56vh; min-height: 180px; padding-right: var(--space-1); }
.hist-row { display: flex; align-items: baseline; gap: var(--space-2); padding: 3px var(--space-2); border-radius: var(--r-sm); font-size: 13px; line-height: 1.4; }
.hist-row:hover { background: var(--surface-inset); }
.hist-when { flex: none; color: var(--text-faint); font-size: .85em; font-variant-numeric: tabular-nums; }
/* The author is a real <button> (it opens the viewer card), so strip the button
   chrome and keep only the name. Tab order + Enter/Space come for free. */
.hist-author { flex: none; border: 0; background: transparent; padding: 0; font: inherit; font-weight: 700; color: var(--text); cursor: pointer; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hist-author:hover { text-decoration: underline; }
.hist-author::after { content: ':'; color: var(--text-dim); font-weight: 400; }
.hist-row--mod .hist-author::after { content: ''; }
.hist-text { color: var(--text); min-width: 0; word-break: break-word; }
.hist-row--deleted .hist-text { opacity: .55; text-decoration: line-through; }
.hist-deleted { font-size: 11px; }
/* A moderation row must not read like a chat line — accent rule + italic verb. */
.hist-row--mod { border-left: 2px solid var(--accent); background: var(--accent-wash); padding-left: var(--space-2); }
.hist-mod { color: var(--accent-2); font-style: italic; }

.hist-foot { display: flex; align-items: center; gap: var(--space-3); }
.hist-status { font-size: 11.5px; }
.hist-state { padding: var(--space-4); text-align: center; font-size: 12px; color: var(--text-dim); }
.hist-state p { margin: 0 0 var(--space-2); }
.hist-state--action { border: 1px dashed var(--border-accent); border-radius: var(--r-sm); background: var(--accent-wash); }

/* The opt-in/retention config that moved off the Bot page: a disclosure under the
   browser, not a card above it. */
.hist-config { border-top: 1px solid var(--border); padding-top: var(--space-3); font-size: 12px; }
.hist-config > summary { cursor: pointer; font-weight: 600; color: var(--text-dim); }
.hist-cfg-body { display: flex; flex-direction: column; gap: var(--space-2); padding-top: var(--space-3); }
.hist-cfg-body p { margin: 0; line-height: 1.5; }
.hist-cfg-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.hist-cfg-list { display: flex; flex-direction: column; gap: var(--space-1); }
.hist-cfg-row { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; }
.hist-cfg-row .grow { flex: 1; min-width: 0; }

/* Chat card head tools: the A−/A+ text-size buttons, the pop-out, the Open-chat link. */
.chat-head-tools { display: inline-flex; align-items: center; gap: var(--space-1); }
.chat-head-tools [data-chat-font] { min-width: 2rem; padding-left: var(--space-2); padding-right: var(--space-2); font-variant-numeric: tabular-nums; }
/* Preview card head tools: the pop-out button next to the Live/Offline pill. */
.preview-head-tools { display: inline-flex; align-items: center; gap: var(--space-2); }
/* The "detach this card into its own window" control (cockpit-popouts) — the SAME
   square glyph button on both the preview and the chat card, so one affordance
   reads the same everywhere. The glyph (U+1F5D7) is a symbol, not an emoji: pin a
   symbol-capable fallback stack so it never renders as a colour emoji or a tofu box. */
.card-popout {
  min-width: 2rem; padding-left: var(--space-2); padding-right: var(--space-2);
  font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", system-ui, sans-serif;
  line-height: 1;
}
/* badges + emotes (used by BOTH surfaces) */
.msg-badges { display: inline-flex; align-items: center; gap: 2px; margin-right: 4px; vertical-align: middle; }
.msg-badge { display: inline-flex; align-items: center; line-height: 0; }
.msg-badge svg { display: block; }
/* Real platform badge art (author.badgeIcons url entries) — same 14px box as
   the inline SVGs so mixed strips align. */
.msg-badge-img { display: block; width: 14px; height: 14px; border-radius: 2px; }
.chat-emote { display: inline-block; height: 1.5em; width: auto; vertical-align: middle; margin: -2px 1px; }

/* ── Compose-side emote picker (parity Phase E, chat-emote-picker.js): the 😀
   button anchors a .menu-based panel (frame/shadow from shell.css .menu) that
   opens UPWARD above the composer. Token-only. ── */
.emote-anchor { position: relative; flex: 0 0 auto; display: inline-flex; }
.emote-btn { font-size: 15px; }
.emote-picker {
  top: auto; bottom: calc(100% + var(--space-2));
  /* max-width must NOT reference 100%: the containing block is the 34px
     .emote-anchor (position:relative), so min(80vw, 100%) clamped the panel to
     icon-button width (review 2026-07-14). 80vw alone caps small screens. */
  width: 320px; max-width: 80vw;
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-2);
}
.emote-picker .emote-search { width: 100%; }
.emote-recent-wrap { display: flex; flex-direction: column; gap: var(--space-1); border-bottom: 1px solid var(--border); padding-bottom: var(--space-2); }
.emote-recent-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: .06em; }
.emote-recent, .emote-grid { display: flex; flex-wrap: wrap; gap: var(--space-1); align-content: flex-start; }
.emote-grid { max-height: 220px; overflow-y: auto; }
.emote-cell { border: 0; background: transparent; border-radius: var(--r-sm); padding: var(--space-1); cursor: pointer; line-height: 0; }
.emote-cell:focus { background: var(--surface-inset); }
@media (hover: hover) { .emote-cell:hover { background: var(--surface-inset); } }
.emote-cell img { width: 28px; height: 28px; object-fit: contain; display: block; }
/* the picker's honest empty line reuses .chat-empty metrics but stays compact */
.emote-picker .emote-empty { padding: var(--space-3); width: 100%; }

/* Moderation context menu (chat-actions.js) — opened by right-click on a chat
   row or by the row's "More" button. Items are real <button>s so the menu is
   fully keyboard operable (Tab/arrows + Enter, Escape to close). */
.ctx-menu { position: fixed; z-index: 80; background: var(--surface-raised); border: 1px solid var(--border); border-radius: var(--r-sm); box-shadow: var(--shadow-card); padding: 4px 0; min-width: 176px; }
.ctx-item { display: block; width: 100%; text-align: left; background: none; border: 0; font: inherit; padding: 6px 14px; color: var(--text); cursor: pointer; white-space: nowrap; font-size: 12px; }
.ctx-item:hover { background: var(--surface-inset); }
.ctx-item:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: -2px; }
.ctx-item-danger { color: var(--danger); }
.ctx-item-danger:hover { background: color-mix(in srgb, var(--danger) 15%, transparent); }
.ctx-separator { height: 1px; background: var(--border); margin: 4px 0; }

/* 🔴 .admin-embed REMOVED 2026-08-20 (owner ruling: "Wait, why is there an admin
   card on the 'Community' page?"). It sized the preview iframe of the standalone
   admin dashboard that sec-community.js used to append for role='admin' accounts;
   with that card gone the rule had no element left to style. The admin console is
   its own app. */

/* ═══════════════════════════════════════════════════════════════════════
   P2 — Settings hub (#sec-settings, sec-settings.js). A tabbed container: the
   shared .subtabs strip drives the FIVE WAI-ARIA tabs (its .subtabs button /
   .subtabs button.on rules already style + indicate the active tab), and each
   .settings-panel holds a lazily-rendered body. Tokens ONLY (no hardcoded
   hex/px).
   The 2026-08-16 redesign cut the tab set 8 → 5 (Account / Security /
   Connections / Developer / Billing), which retired two rules that lived here:
   `.settings-embed` (the Billing tab's 70vh iframe of /app/billing — now a real
   summary read from that page's own endpoint) and the whole `.theme-preview*`
   family (the Appearance tab's overlay-theme editor — Alerts & Overlays owns
   themes). Connections reuses the Destinations platform-card classes
   (.dest-grid / .dest-logo / .sec-item--platform / .pill) rather than growing a
   second card look. ═══════════════════════════════════════════════════════ */

/* Each tab's panel body. Panels are toggled hidden by OmniTabs (role=tabpanel);
   the [hidden] rule guarantees an inactive panel collapses even if a UA stylesheet
   would otherwise show it. */
.settings-panel { display: block; }
.settings-panel[hidden] { display: none; }

/* WIDTH (settings audit 2026-08-16, remodelled 2026-08-17). Settings is a FORM
   page, not a dashboard: its panels are label-over-control stacks and one-line
   list rows. The 2026-08-16 audit found the un-capped column stretching an email
   field and a session row the full width of a wide monitor — a 2000px line for a
   30-character value — and answered it with a pane cap
   (`#sec-settings .page-header, .subtabs, .settings-panel { max-width: 60rem }`,
   widened to 90rem the next day).

   THAT CAP IS DELETED (owner, 2026-08-17: "applying a max width to the full page
   is the wrong way to fix the issue") and MUST NOT COME BACK — sec-settings.test.js
   pins its absence. The audit's observation was right, its instrument was wrong:
   the thing that must not be 2000px wide is the INPUT, not the panel. So the cap
   moved onto the control itself (`--w-control`, top of this file) and the panels
   now use the width they are given by ADDING TRACKS:
     · Account   — identity + password ride `.sec-grid.cols-2`.
     · Security  — 2FA + sessions, then the paired-device cards, ride the same
                   auto-fit grid, so a wide monitor gets 3-4 device cards a row
                   instead of 2 stretched ones.
     · Connections — `.dest-grid` (Destinations block, above) is auto-fit, so cards add
                   columns rather than growing to 1200px apiece.
     · Billing / Developer — a single card that fills the pane; everything
                   inside it that could smear is a `.field > .input` or a
                   `.sec-note`, both capped at the element level.
   NOTE — the other half of the audit's item (unequal cards must not stretch) needs
   nothing here: `.sec-grid` already declares `align-items: start` globally, and
   sections-spacing/settings pins it so it stays that way. */

/* Stacked label-over-input form (change-email, 2FA disable/verify). Replaces the
   inline flex-direction:column style attribute with a token-spaced class. */
.settings-form { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-3); }

/* A panel that stacks several cards/rows down the page (Account: identity+password
   pair, then the handle card, then the danger zone). A container GAP, never a
   margin on the shared .card class — the SPACING PRIMITIVES contract. */
.settings-stack { display: flex; flex-direction: column; gap: var(--space-4); }

/* THE DANGER ZONE (Account). Account deletion is the one irreversible control in
   Settings, so its card is tinted rather than merely placed last: at a glance the
   eye must be able to tell it apart from the three ordinary cards above it. The
   tint is derived FROM the semantic token via color-mix (the .bot-chip precedent)
   — never a local hex — so it tracks a palette change. */
.settings-danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
.settings-danger .card-head h2 { color: var(--danger); }

/* QR holder (device pairing). The canvas the generator appends carries .settings-qr
   below; the holder only reserves the row and centres it. */
.settings-qr-holder { display: flex; justify-content: center; margin-bottom: var(--space-3); }

/* 2FA setup QR + the device-pairing QR canvas — capped to a sensible scan size via
   a relative max-width so neither overflows its well on narrow viewports. */
.settings-qr { display: block; width: 100%; max-width: 12rem; height: auto; border-radius: var(--r-sm); background: var(--surface); }

/* Inline row-rename editor (paired devices). REPLACES window.prompt: an inline
   field + Save/Cancel on one wrapping row, revealed in place of nothing and
   dismissed without touching the rest of the list. Same shape sec-sources.js uses
   for its device rows. */
.settings-inline-edit { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }

/* A small section sub-heading inside a settings card. */
.sec-subhead { font-size: var(--fs-card-title); font-weight: var(--fw-semibold); color: var(--text-dim); margin: var(--space-4) 0 var(--space-2); }

/* ═══════════════════════════════════════════════════════════════════════
   IA §3 — Stream Manager cockpit (#sec-stream, sec-stream.js). The lean live
   cockpit: a full-width stat bar over a multi-panel grid (preview, quick
   actions, activity, my-chat, collaboration, destination health) + the bonding
   link-health panel. Tokens ONLY (no hardcoded hex/px colour, no inline style);
   every panel ships all four states with honest empty copy. ═══════════════ */

/* ── the cockpit's BOT STATUS CHIP (dashboard redesign R2b) — a header-strip
   link, not a stat chip: the stat bar means LIVE STREAM metrics, and a service
   status among them would dilute that row. Tone tints the border + label only,
   so it stays quiet until something is actually wrong. ── */
.bot-chip { text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
.bot-chip--ok { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.bot-chip--warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); color: var(--warn); }
.bot-chip--off { color: var(--text-faint); }

/* ── (1) STAT BAR — compact chips, full width, scrolls locally on narrow ── */
.stat-bar {
  display: flex; gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--space-4);
}
.stat-chip {
  display: flex; flex-direction: column; gap: var(--space-1);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--space-2) var(--space-4);
  min-width: 7rem; flex: 1 1 7rem;
}
.stat-label { font-size: 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.stat-value { font-family: var(--font-mono); font-size: 18px; font-weight: 700; color: var(--text); }
/* the live session chip gets the accent treatment so uptime reads as "on air" */
.stat-chip.is-live { border-color: var(--border-accent); }
.stat-chip.is-live .stat-value { color: var(--accent-soft); }
/* destination/bitrate severity tint on the value (paired with the chip border) */
.stat-chip.stat--ok .stat-value { color: var(--ok-soft); }
.stat-chip.stat--warn { border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
.stat-chip.stat--warn .stat-value { color: var(--warn); }
.stat-chip.stat--danger { border-color: color-mix(in srgb, var(--danger) 40%, transparent); }
.stat-chip.stat--danger .stat-value { color: var(--danger); }
/* followers chip: a small session-gain line under the total + an (expandable)
   per-platform breakdown shown on the chip's title/hover. */
.stat-sub { font-size: 10.5px; font-weight: 600; color: var(--text-dim); }
.stat-sub.gain { color: var(--ok-soft); }
.stat-chip .stat-breakdown { font-size: 10px; color: var(--text-faint); line-height: 1.5; }
.stat-chip .stat-breakdown .miss { color: var(--warn); }

/* Cockpit grid (WIDE = two columns): column 1 is .cockpit-rest — the USER-DESIGNED
   card grid (Preview / Activity / Music / TTS / Dest health / Collaboration / Live
   settings / OBS settings / Bonding) — which scrolls with the page. My Chat is column
   2, pinned STICKY and viewport-tall, so the stat bar + quick actions + left cards
   scroll away behind it while the chat keeps the full height. Chat is NOT part of the
   designable grid in V1 (its width drag + sticky full height are already user-owned);
   it is 1st in source order and CSS places it — right column on wide, ORDERED under
   the top card on narrow (the position it has always had). */
.cockpit-grid {
  display: grid;
  /* Chat width is USER-ADJUSTABLE: the drag handle (wireCockpitChat) writes
     --cockpit-chat-w (px, clamped + persisted). 26rem is the pre-JS default. */
  grid-template-columns: minmax(0, 1fr) var(--cockpit-chat-w, 26rem);
  gap: var(--space-4);
  align-items: start;
}
/* THE DESIGNABLE GRID (cockpit layout L1). Column count comes from the user's layout
   doc via --cockpit-cols (1..4; 2 is the default doc + the pre-JS fallback), and each
   card carries --card-order / --card-span written by sec-stream.js. Cards in the same
   row stretch to the row height, so a half/half pair reads as one band — which is how
   the old hardcoded .cockpit-pair (Music | TTS, owner 2026-08-14) is reproduced. */
.cockpit-grid > .cockpit-rest {
  grid-column: 1; grid-row: 1; min-width: 0;
  display: grid;
  grid-template-columns: repeat(var(--cockpit-cols, 2), minmax(0, 1fr));
  gap: var(--space-4);
}
.cockpit-rest > * {
  min-width: 0;
  grid-column: span var(--card-span, 1);
  order: var(--card-order, 0);
}
/* THE NARROW CLAMP (L3, two rungs). L1 collapsed every design to ONE column below
   1280, which flattened a 3- or 4-column arrangement far more than the width
   demanded. The honest cap is: below 1280 render at most TWO columns, below 1024
   render one. (Below 900 the single-column stack takes over entirely, further down.)
   Spans are capped with the columns — a span-3 card in a 2-column grid would spill
   an implicit third column and blow the row out.

   WHY ATTRIBUTES AND NOT min(): capping in pure CSS wants `repeat(min(var(--cockpit-cols),2), …)`
   and `grid-column: span min(var(--card-span),2)`, i.e. math functions in an <integer>
   position — not dependable across browsers, and the failure mode is a silently
   invalid grid. But `min(n, 2)` over a 1..4 range is just "2, unless the design asked
   for 1", which an attribute selector says exactly. sec-stream.js's paintCockpitLayout
   mirrors --cockpit-cols / --card-span into data-cols / data-span for precisely this.
   The media query stays the thing that reacts to width: no resize listener, no
   first-paint flash, no JS in the loop at all. */
@media (max-width: 1280px) {
  .cockpit-grid > .cockpit-rest { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cockpit-rest > * { grid-column: span 2; }                    /* span 2, 3, 4 → the row */
  .cockpit-rest > [data-span='1'] { grid-column: span 1; }      /* a half-width card stays half */
  /* A design that asked for ONE column keeps it — capping is min(), not "always 2". */
  .cockpit-grid > .cockpit-rest[data-cols='1'] { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 1024px) {
  .cockpit-grid > .cockpit-rest,
  .cockpit-grid > .cockpit-rest[data-cols='1'] { grid-template-columns: minmax(0, 1fr); }
  .cockpit-rest > *,
  .cockpit-rest > [data-span='1'] { grid-column: 1; }
}
/* ...EXCEPT while editing. An editor that collapses the thing you are arranging is
   not an editor: in edit mode the designed columns render literally, however tight,
   so a span stepper and a side-by-side drop mean what they show. It sits AFTER both
   caps (same specificity, later wins) and is fenced above the phone stack, so it can
   never fight the <=900 single-column rules if you resize mid-edit. */
@media (min-width: 901px) {
  .cockpit-grid.is-editing > .cockpit-rest,
  .cockpit-grid.is-editing > .cockpit-rest[data-cols='1'] {
    grid-template-columns: repeat(var(--cockpit-cols, 2), minmax(0, 1fr));
  }
  .cockpit-grid.is-editing > .cockpit-rest > * { grid-column: span var(--card-span, 1); }
}
.cockpit-grid > .cockpit-chat {
  grid-column: 2; grid-row: 1;
  position: sticky; top: var(--space-4);
  align-self: start;
  display: flex; flex-direction: column;
  /* Height is set by JS (wireCockpitChat) from the chat's CURRENT top to the viewport
     bottom, so the input is never cut off at scroll-top and it grows as it sticks. This
     calc is only the pre-JS fallback. */
  height: calc(100dvh - 56px - var(--space-4) * 2);
}
.cockpit-chat > .sec-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.cockpit-chat .chat-feed.mini { flex: 1 1 auto; min-height: 0; max-height: none; } /* fill the tall card */

/* Drag handle on the chat's left edge (sits in the inter-column gap). Wide only. */
.cockpit-chat-resize {
  position: absolute; top: 0; bottom: 0; left: calc(-1 * var(--space-3));
  width: var(--space-4); cursor: col-resize; z-index: 3; touch-action: none;
}
.cockpit-chat-resize::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
  width: 2px; transform: translateX(-50%);
  background: var(--border-accent); opacity: 0;
  transition: opacity var(--dur-fast, .12s) var(--ease, ease);
}
.cockpit-chat-resize:hover::before, .cockpit-chat-resize:focus-visible::before { opacity: 1; }
.cockpit-chat-resize:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: -2px; border-radius: var(--r-sm); }

/* NARROW (one column): the single-column stack ALWAYS wins — the layout doc is
   ignored here (a designed multi-column arrangement is meaningless at this width).
   .cockpit-rest goes `display: contents` so its cards become items of the one-column
   .cockpit-grid directly; that is what lets the chat slot BETWEEN them. It sits right
   under the top card at ~50vh, no longer sticky, exactly as before.
   The order arithmetic: sec-stream.js writes --card-order as docOrder * 10, so order 5
   is always "after the first card, before the second" without the doc knowing that the
   chat exists. */
@media (max-width: 900px) {
  .cockpit-grid { grid-template-columns: 1fr; }
  .cockpit-grid > .cockpit-rest { display: contents; }
  .cockpit-rest > *,
  .cockpit-rest > [data-span='1'] { grid-column: 1; }
  .cockpit-grid > .cockpit-chat {
    grid-column: 1; grid-row: auto; position: static; height: auto; top: auto;
    order: 5;
  }
  .cockpit-chat .chat-feed.mini { flex: 0 1 auto; min-height: 8rem; max-height: 50vh; }
  .cockpit-chat-resize { display: none; } /* no width drag in single-column mode */
  /* No layout editor here: the narrow stack is fixed (order + full width), so
     there is nothing a designed multi-column doc could say. Hiding the door is
     the honest version of "not available" — better than an editor whose every
     control is a no-op at this width. */
  .cockpit-edit-toggle { display: none; }
}

/* ══ COCKPIT LAYOUT — EDIT MODE (L2) ═════════════════════════════════════════
   LOCKED BY DEFAULT: every selector below is scoped to .cockpit-grid.is-editing
   or to chrome that sec-stream.js only creates on entering edit mode, so a user
   who never presses "Edit layout" carries none of this — no handles, no hints.
   The chrome is an OVERLAY: the head strip is absolutely positioned over the top
   of each card, so turning edit mode on never reflows a card's content. */

/* The toolbar, above the grid and sticky so Done is reachable from anywhere in a
   long cockpit. Real buttons throughout (it is a role="toolbar"). */
.cockpit-editbar {
  position: sticky; top: 0; z-index: 6;
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  margin-bottom: var(--space-4); padding: var(--space-2) var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-accent); border-radius: var(--r);
  box-shadow: var(--shadow-card);
}
.cockpit-editbar-title {
  margin-right: auto; font-size: 13px; font-weight: 700;
  display: inline-flex; align-items: center; gap: var(--space-2);
}
.cockpit-editbar-title::before {
  content: ''; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); box-shadow: var(--glow-accent);
}
.cockpit-editbar .sec-err { flex: 1 0 100%; margin: var(--space-1) 0 0; }
/* The MIN-WIDTH refusal line (2026-08-15): shown when the − stepper is asked to
   take a card below the width its internals need. Its own row under the toolbar
   controls, like the error line above — but quiet, because nothing failed. */
.cockpit-editbar .cockpit-editbar-hint { flex: 1 0 100%; margin: var(--space-1) 0 0; }
.cockpit-cols { display: inline-flex; align-items: center; gap: var(--space-2); }
.cockpit-cols-label {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-faint);
}
.cockpit-cols-seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.cockpit-col-btn {
  font-family: var(--font-display); font-size: 12px; font-weight: 600;
  padding: 7px 12px; border: 0; background: transparent; color: var(--text-dim); cursor: pointer;
}
.cockpit-col-btn + .cockpit-col-btn { border-left: 1px solid var(--border); }
.cockpit-col-btn[aria-pressed='true'] { background: var(--accent-wash); color: var(--accent-2); }

/* FAINT COLUMN TRACKS. One hairline per column boundary, drawn as a tiled
   background on an overlay that is inset by half the grid gap — so the lines land
   in the gutters, not on the cards. The tile is one column + one gap wide, which
   is exactly the overlay's width divided by the column count. */
.cockpit-grid.is-editing > .cockpit-rest { position: relative; }
.cockpit-grid.is-editing > .cockpit-rest::before {
  content: ''; position: absolute; inset: calc(var(--space-4) / -2);
  pointer-events: none; border-radius: var(--r);
  background-image: linear-gradient(90deg, var(--border-accent) 0 1px, transparent 1px);
  background-size: calc(100% / var(--cockpit-cols, 2)) 100%;
  opacity: .55;
}

/* The live card stays rendered and keeps updating — the cockpit is still useful
   while you rearrange it — but goes INERT, so a drag can never trip a control
   inside a card. Only the edit chrome takes pointer events. */
.cockpit-grid.is-editing > .cockpit-rest > [data-card-id] {
  position: relative;
  pointer-events: none;
  user-select: none;
  outline: 1px dashed var(--border-accent); outline-offset: -1px;
}

/* The overlay head strip: grab handle + card name + span stepper. Absolutely
   positioned so the card underneath does not shift by a pixel, and the ONE thing
   inside an inert card that still takes pointer events. */
.cockpit-card-edit {
  pointer-events: auto;
  position: absolute; top: 0; left: 0; right: 0; height: 32px; z-index: 2;
  display: flex; align-items: center; gap: var(--space-2);
  padding: 0 var(--space-3);
  background: var(--surface-raised);
  border-bottom: 1px dashed var(--border-accent);
  border-radius: var(--r) var(--r) 0 0;
  cursor: grab; touch-action: none;
}
.cockpit-card-edit:active { cursor: grabbing; }
.cockpit-grip { color: var(--text-faint); font-size: 13px; letter-spacing: 2px; line-height: 1; cursor: grab; }
.cockpit-grip:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; border-radius: var(--r-chip); }
.cockpit-card-edit-t { font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Stepper hit targets are deliberately below --touch-min: this whole editor is
   pointer-only (the entry button is hidden at <=900px, where touch lives), and
   the keyboard path on the grip (+ / -) is the non-pointer equivalent.
   .cockpit-card-tools is the ONE region carved out of the head's grab surface, so
   every control in it (per-card reset, span stepper) stays clickable by construction. */
.cockpit-card-tools { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); }
.cockpit-spanner { display: inline-flex; align-items: center; gap: var(--space-1); }
/* PER-CARD RESET (L3) — the same 24px hit target as the stepper it sits beside. */
.cockpit-card-reset {
  width: 24px; height: 24px; padding: 0; display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: var(--r-chip);
  background: var(--surface-inset); color: var(--text-faint);
  font-size: 12px; line-height: 1; cursor: pointer;
}
.cockpit-card-reset:hover { color: var(--text-dim); border-color: var(--border-strong); }
.cockpit-span-btn {
  width: 24px; height: 24px; padding: 0; display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: var(--r-chip);
  background: var(--surface-inset); color: var(--text-dim);
  font-family: var(--font-display); font-size: 12px; line-height: 1; cursor: pointer;
}
.cockpit-span-btn[aria-disabled='true'] { opacity: .4; cursor: not-allowed; }
.cockpit-span-w {
  font-family: var(--font-mono); font-size: 10px; color: var(--text-faint);
  padding: 0 var(--space-1); white-space: nowrap;
}

/* Drag: the source card ghosts in place, a drop indicator occupies the slot the
   release would land in, and a title chip tracks the cursor (the mockup's floating
   card, without cloning a live card's video/sockets just to tilt it 2 degrees). */
.cockpit-rest > .is-drag-ghost { opacity: .4; border-style: dashed; }
.cockpit-drop {
  min-height: 5rem; display: grid; place-items: center; pointer-events: none;
  border: 2px dashed var(--accent); border-radius: var(--r);
  background: var(--accent-wash); color: var(--accent-2);
  font-size: 12px; font-weight: 600;
}
/* THE DEGENERATE GRID (L3). Built by paintCockpitLayout only when the cockpit has
   nothing to show — no cards, or every card hidden. It is a grid child like any
   other, spanning the full row whatever the column count, and it carries the one
   way back: reset the layout to the default. */
.cockpit-empty {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2);
}
.cockpit-drag-chip {
  position: fixed; left: var(--drag-x, 0); top: var(--drag-y, 0); z-index: 60;
  transform: translate(var(--space-3), var(--space-3)) rotate(-2deg);
  pointer-events: none;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--r-sm); box-shadow: var(--shadow-modal);
  font-size: 12px; font-weight: 600; white-space: nowrap;
}

/* Quick Actions — a BARE button row right under the metrics (no card wrapper). It
   mirrors the stat bar EXACTLY: same flex layout, gap, and per-item flex basis, so
   each button lines up under each metric chip. The tiles keep their own border/bg so
   they read as buttons; the strip itself has no chrome. */
.qa-strip .qa-grid { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.qa-strip .qa-tile { flex: 1 1 7rem; min-width: 7rem; flex-direction: row; justify-content: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); }
.qa-strip .qa-icon { font-size: 14px; }

/* ── Quick-action TOAST (clip URL / marker / BRB / go-live feedback). Stacked
   bottom-right, token-driven, auto-dismiss. textContent only (XSS-safe). ── */
.sec-toast-wrap {
  position: fixed; right: var(--space-4); bottom: var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-2);
  z-index: 60; max-width: min(92vw, 24rem); pointer-events: none;
}
.sec-toast {
  pointer-events: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--border-accent);
  border-radius: var(--r-sm); padding: var(--space-3) var(--space-4);
  box-shadow: var(--shadow-2, 0 4px 16px rgba(0,0,0,.4));
  font-size: 13px; color: var(--text); line-height: 1.4;
}
.sec-toast.ok { border-left-color: var(--ok-soft); }
.sec-toast.err { border-left-color: var(--danger); }
/* A go-live that SUCCEEDED but carries a caveat (e.g. the YouTube auto-provision
   pre-flight failed non-fatally, so YouTube may need a manual Go Live). Neither
   a success nor a failure — its own tone. */
.sec-toast.warn { border-left-color: var(--warn); }
.sec-toast a { color: var(--accent-soft); word-break: break-all; }

/* ── (2) STREAM PREVIEW — desktop live frame (sampler over IPC), or an honest
   desktop-only placeholder on the web. Never a fake <video>. ── */
.preview-stage {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center; text-align: center;
  aspect-ratio: 16 / 9; width: 100%;
  background: var(--surface-inset); border: 1px solid var(--border);
  /* NO border-radius on the stage: it clips the live <video> child, and a ROUNDED
     clip forces Chromium to re-rasterize the playing video ON THE MAIN THREAD every
     frame (no hardware-overlay fast path) → app-wide input lag while a preview is
     shown. A square overflow:hidden clip is free. (Rounding can be restored later via
     a GPU-friendly route if desired.) */
  padding: var(--space-4);
}
/* The live program frame (vcam <video> / sampler). Fills the stage edge-to-edge.
   NO border-radius — same reason as the stage: a rounded <video> can't use the cheap
   video compositing path and repaints on the main thread every frame. */
.preview-frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background: var(--bg);
  /* Promote the playing <video> to its OWN compositing layer so its per-frame
     updates don't re-rasterize the dense Stream-Manager card grid around it (the
     residual lag after the rounded-corner fix). Isolates the video's paint. */
  transform: translateZ(0);
  will-change: transform;
  contain: paint;
}
.preview-empty { max-width: 28rem; }
.preview-empty-title { font-family: var(--font-display); font-weight: 600; font-size: 14px; margin-bottom: var(--space-2); }
/* ── (4b) MUSIC — the cockpit's song player (B6). Playback moved out of OBS
   and into the control surface, so this card holds a REAL third-party embed. The
   stage carries YouTube's Required Minimum Functionality floor (at least 200x200,
   more than half visible, unmodified, never overlaid): no background image, no
   overlay child, no transform. Tokens only. ── */
.music-head-tools { display: inline-flex; align-items: center; gap: var(--space-2); }
/* TWO COLUMNS: art/video | music. The stage column is intrinsic (it is a fixed
   200x200 box) and music takes the slack. TTS used to be a third column here; it
   is its own card now (.cockpit-tts, directly below). */
.cockpit-music .music-cols {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-4); align-items: start;
}
.cockpit-music .music-col { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.cockpit-music .music-stage {
  position: relative; overflow: hidden;
  /* EXACTLY 200x200 — the RMF floor, at every viewport width. Not a percentage
     and not an aspect ratio: either can drop the embed under the floor. */
  width: 200px; height: 200px; min-width: 200px; min-height: 200px;
  background: var(--bg); border: 1px solid var(--border);
}
/* The embed fills the stage. NO border-radius (same reason as .preview-stage: a
   rounded clip costs the video its cheap compositing path). */
.music-stage iframe, .music-stage > div { display: block; width: 100%; height: 100%; border: 0; }
/* Shown INSTEAD of the stage while playback lives in the pop-out window. Same box
   as the stage so the handover does not reflow the row. */
.cockpit-music .music-popped {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: var(--space-2);
  width: 200px; height: 200px; overflow: auto;
  background: var(--surface-inset); border: 1px solid var(--border);
  padding: var(--space-3);
}
/* ALBUM ART (owner 2026-08-15): the same box as the stage, shown INSTEAD of it
   while the streamer's own player is the source. SMTC gives us a cover and never
   video, so this is the honest fill for a box that otherwise sits black through a
   whole set. It is a SIBLING of the stage, never a background or a child of it —
   the RMF rules above apply to the embed, and the embed is not here when this is. */
.cockpit-music .music-artstage {
  position: relative; display: flex; align-items: flex-end;
  width: 200px; height: 200px; min-width: 200px; overflow: hidden;
  background: var(--surface-inset); border: 1px solid var(--border);
}
.cockpit-music .music-art-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
}
.cockpit-music .music-art-meta {
  position: relative; display: flex; flex-direction: column; gap: 2px;
  width: 100%; padding: var(--space-2); min-width: 0;
}
/* Over a cover the words need their own ground, or a bright album art eats them. */
.cockpit-music .music-art-meta--over {
  background: linear-gradient(to top, rgba(0, 0, 0, .82), rgba(0, 0, 0, 0));
  padding-top: var(--space-4);
}
.cockpit-music .music-art-meta .t {
  font-weight: 600; font-size: 12px; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cockpit-music .music-art-meta .s {
  font-size: 11px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cockpit-music .music-art-meta--over .t { color: #fff; }
.cockpit-music .music-art-meta--over .s { color: rgba(255, 255, 255, .78); }
/* The transport reuses the Songs pane's three-group rhythm (#sec-songs
   .songs-transport), which is scoped to that section — repeat it here for the
   card rather than widening that selector.
   IT WRAPS (owner 2026-08-15, from a screenshot at ~half width where "Take over
   now" was clipped off the card's right edge): the row is one transport plus a
   volume group, and at the card's minimum span they have to be allowed onto two
   lines rather than overflowing the card box. Nothing here may set a width that
   the card cannot honour. */
.cockpit-music .songs-transport {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2); row-gap: var(--space-2); min-width: 0;
}
.cockpit-music .songs-btns { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.cockpit-music .songs-vol { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; flex: 1 1 8rem; }
.cockpit-music .songs-vol input[type="range"] { min-width: 0; flex: 1 1 auto; }
/* DENSITY (the same pass): the card carries a stage, a now-playing block, a
   progress bar and a transport in two of four columns, so its own rhythm tightens
   to the TTS card's rather than the roomier default. */
.cockpit-music .sec-body { gap: var(--space-2); }
.cockpit-music .sec-row { padding-block: var(--space-1); }
.cockpit-music .music-cols { gap: var(--space-3); }
.cockpit-music .songs-vol > .lbl, .cockpit-music .music-col > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
/* NARROW: the two columns stack. The stage keeps its 200x200 (RMF floor). */
@media (max-width: 900px) {
  .cockpit-music .music-cols { grid-template-columns: 1fr; gap: var(--space-3); }
}

/* ── PROGRESS BAR (owner 2026-08-15: "add a seek bar/status bar in the Music card
   and Now Playing site"). ONE rule set for BOTH surfaces — the cockpit's Music
   card and the Songs page's Now playing card render the same structure, and two
   stylesheets for one component is how they drift. Tokens only.
   The bar sits INSIDE the now-playing region, so it must never widen it:
   min-width:0 all the way down, and the readout is the only fixed-ish part. ── */
.np-prog { display: flex; align-items: center; gap: var(--space-2); min-width: 0; margin-top: var(--space-1); }
.np-prog-track {
  display: block; position: relative; flex: 1 1 auto; min-width: 0;
  height: 4px; border-radius: 2px; overflow: hidden;
  background: var(--surface-inset); border: 0; padding: 0;
}
.np-prog-fill { display: block; height: 100%; background: var(--accent); transition: width .9s linear; }
/* SEEKABLE (the cockpit's local-media bar only, and only where the media app
   said the playhead may move). A real <button>, so it is keyboard reachable and
   announces itself; the hit area is taller than the 4px it paints. */
.np-prog-track--seek { cursor: pointer; height: 4px; }
.np-prog-track--seek::after { content: ""; position: absolute; inset: -7px 0; }
.np-prog-track--seek:hover, .np-prog-track--seek:focus-visible { background: var(--accent-wash); }
.np-prog-time {
  font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* A live stream has real elapsed time and no length to show it against, so the
   readout stands alone rather than under an indeterminate bar claiming one. */
.np-prog--live .np-prog-time { flex: 0 0 auto; }
.np-prog-live {
  font-size: 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-dim); border: 1px solid var(--border); border-radius: 999px;
  padding: 0 var(--space-2);
}

/* ── (4c) TEXT TO SPEECH — the cockpit's TTS transport (T2, 2026-08-14). NOT a
   player: the audio and the queue both live in the OBS browser source, so this
   card only paints the overlay's reports and pushes verbs back. It reuses the
   music card's rhythm (a caption-less list of .sec-row items, then one transport
   row of button + volume groups) so the two read as one stack. Tokens only. ── */
/* The queue is the head of a list that can be long: bound it and let it scroll
   inside the card rather than growing the cockpit column without limit. */
.cockpit-tts .tts-queue { margin-top: var(--space-2); max-height: 13rem; overflow-y: auto; }
.cockpit-tts .tts-queue .sec-row .s { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cockpit-tts .tts-transport { align-items: center; row-gap: var(--space-2); margin-top: var(--space-3); }
.cockpit-tts .tts-btns { display: inline-flex; align-items: center; gap: var(--space-2); }
.cockpit-tts .tts-vol { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
/* Same caption treatment as the music card's volume label (and the .sec-row > .lbl
   rule), so a bare .lbl here reads as intentional rather than unstyled body text. */
.cockpit-tts .tts-vol > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
/* The +N chip never shrinks under the row's text column. */
.cockpit-tts .tts-queue .chip { flex: none; font-variant-numeric: tabular-nums; }

/* ── THE "NOW PLAYING" ROW (2026-09-01) ─────────────────────────────────────
   Duration + progress for the clip that is speaking, painted at the TOP of the
   queue card on BOTH surfaces that show it (the cockpit's own card and Alerts &
   Overlays ▸ TTS ▸ Operate), which is why it is scoped to the class rather than
   to .cockpit-tts. DISPLAY ONLY: no pointer affordance, no cursor change, no
   hover state — there is no seek here and nothing to seek, because the audio is
   in the OBS browser source.

   🔴 A NATIVE <progress>, NOT A DIV. The fraction is DATA, and a drawn fill
   would need a per-render inline width — which sec-stream.test.js's "no inline
   style" audit forbids. Attributes carry it instead (the same trade #sec-ads'
   ledger bar makes), it reads as a progress bar to a screen reader for free, and
   a <progress> with NO value attribute is INDETERMINATE natively, which is
   exactly what an unknown clip length must look like. Both vendor
   pseudo-elements are written: Chromium is the runtime, the -moz- line is one
   rule rather than an argument. */
.tts-np {
  display: flex; align-items: center; gap: var(--space-2);
  padding-block: var(--space-2); min-width: 0;
}
/* Same caption treatment the transport's Volume label gets, so a bare .lbl here
   reads as intentional rather than as unstyled body text. */
.tts-np > .lbl {
  flex: none; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
.tts-np-bar {
  -webkit-appearance: none; appearance: none;
  flex: 1 1 auto; min-width: 0; display: block; height: 6px; border: 0;
  background: var(--surface-2); border-radius: var(--r-pill); overflow: hidden;
}
.tts-np-bar::-webkit-progress-bar { background: var(--surface-2); }
.tts-np-bar::-webkit-progress-value { background: var(--accent); }
.tts-np-bar::-moz-progress-bar { background: var(--accent); }
/* A paused clip is not a stalled one, and the row says so quietly rather than
   changing colour: the transport row below already carries the pressed Pause. */
.tts-np.is-paused { opacity: .7; }
.tts-np-time {
  flex: none; font-variant-numeric: tabular-nums;
  font-size: 12px; color: var(--text-dim);
}

/* ── THE "YOUR CUSTOM VOICE" CARD (owner report + batch, 2026-08-27) ─────────
   Four small skins over the shell's own primitives. Everything structural — the
   card, the stack rhythm, the `.well`, the `.seg`, the `.field`, the `.btn` —
   is the shell's; these only fix what the card asked of them and could not get.

   🔴 WHY `.seg` NEEDED FIXING AT ALL. It used to sit as a direct child of a
   `.field`, which is a flex COLUMN, and the stretch belt at the top of this file
   deliberately does NOT pin `.field`'s children back to their intrinsic width
   (a field's children are meant to stretch). So an `inline-flex` pill that must
   shrink-wrap two buttons was pulled to the full width of the card and read as
   two bars glued together. The card puts it in a `.sec-actions` row now; this
   belt is the second half of that fix, so a future re-nesting cannot bring the
   bars back. */
.cv-pick > .sec-actions > .seg,
.stack > .sec-actions > .seg { flex: 0 0 auto; }

/* The clip picker reads as a TARGET, not a loose button beside grey text. */
.cv-pick { background: var(--surface-inset); }

/* The tips list: `.sec-note` type, but a `<ul>` still inherits the UA's 40px
   indent and its own bullet metrics, which is what made these read as a
   different (larger, wronger) list than every other tip list in the pane. */
.cv-tips { display: flex; flex-direction: column; gap: var(--space-2); }
.cv-tips > ul { margin: 0; padding-left: 1.15rem; }
.cv-tips > ul > li { margin: 0; padding: 0; line-height: 1.65; }
.cv-tips > ul > li + li { margin-top: var(--space-2); }

/* ── ITEM 6: THE TELEPROMPTER ────────────────────────────────────────────────
   Owner: "the script is far too small". It was 12px caption type, and it is the
   one paragraph on this pane meant to be read ALOUD, at a mic, at arm's length.
   A real reading size, a generous leading, and a prose measure so the eye can
   find the next line without hunting. */
.cv-scriptwell { background: var(--surface-inset); }
.cv-script {
  margin: 0; max-width: var(--w-prose);
  font-size: 17px; line-height: 1.9; color: var(--text);
}
/* THE PACING HIGHLIGHT. Unread words sit back, the current word lifts, read
   words stay at full strength behind it — so the paragraph reads as a position,
   not as a blinking cursor. The transition is what makes it a glide rather than
   a flicker at 140 words a minute. */
.cv-script > .cv-word {
  color: var(--text-dim); border-radius: var(--r-sm);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.cv-script > .cv-word.is-read { color: var(--text); }
.cv-script > .cv-word.is-on {
  color: var(--text); background: var(--accent-wash);
  box-shadow: 0 0 0 2px var(--accent-wash);
}
/* The pace select sits inline with the Record button, its caption in the same
   uppercase key the row labels elsewhere use. */
.cv-pace { display: inline-flex; align-items: center; gap: var(--space-2); }
.cv-pace > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
/* 🔴 REDUCED MOTION: the aid becomes a STATIC PROGRESS UNDERLINE. The highlight
   still advances (it is information, not decoration) but nothing glides and
   nothing glows: read words carry a rule beneath them and the current word is
   marked by weight alone. */
@media (prefers-reduced-motion: reduce) {
  .cv-script > .cv-word { transition: none; }
  .cv-script > .cv-word.is-on { background: transparent; box-shadow: none; font-weight: 700; }
  .cv-script > .cv-word.is-read { text-decoration: underline; text-decoration-color: var(--border-accent); }
}

/* ── (4d) PROTECTION — the cockpit panic / lockdown card (bot-protection T3.2).
   ONE card, THREE states (quiet armed / suggesting / locked down), same size in
   all three so nothing reflows when a raid starts — .card--raised is toggled on
   the two live states (shell.css owns that class). It reuses the shared .card /
   .card-head / .sec-body / .stack / .sec-actions / .btn / .pill / .dot / .lbl
   primitives; only the state BANNER, the "in force now" / all-clear list and the
   hint are new, and they follow the .cockpit-tts rhythm (a small stack of tokened
   blocks). Spacing is the container's gap (the .stack the body paints into), never
   a child margin. Tokens only. ── */
.cockpit-panic { display: flex; flex-direction: column; }
.cockpit-panic .prot-banner {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--r-sm);
  background: var(--surface-inset); border: 1px solid var(--border);
}
.cockpit-panic .prot-banner--warn {
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-inset));
  border-color: color-mix(in srgb, var(--warn) 38%, transparent);
}
.cockpit-panic .prot-banner--danger {
  background: color-mix(in srgb, var(--danger) 14%, var(--surface-inset));
  border-color: color-mix(in srgb, var(--danger) 42%, transparent);
}
.cockpit-panic .prot-banner-t {
  display: flex; align-items: center; gap: var(--space-2);
  font-weight: 700; font-size: 13.5px;
}
.cockpit-panic .prot-banner--warn .prot-banner-t { color: var(--warn); }
.cockpit-panic .prot-banner--danger .prot-banner-t { color: var(--danger); }
.cockpit-panic .prot-banner-s { font-size: 12px; color: var(--text-dim); }
.cockpit-panic .prot-applied { display: flex; flex-direction: column; gap: var(--space-2); }
.cockpit-panic .prot-applied-row {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: 12px; color: var(--text-dim);
}
.cockpit-panic .prot-applied-row b { color: var(--text); font-weight: 600; }
.cockpit-panic .prot-hint { font-size: 11px; color: var(--text-faint); }

/* ── (4e) AD MANAGER — the cockpit card (ad-manager epic, Task V0.15). Built to
   the OWNER-APPROVED G-UI mock, revision 2 (2026-08-23) — the DE-PILLED
   treatment. The verdict was "it seems like a lot of pills", so exactly ONE pill
   survives per card: the machine-state chip in the head, which is the glanceable
   state. Everything else is typographic hierarchy, the way the shipped Protection
   and TTS cards already carry secondary information — the posture is a quiet mono
   caption, the ETA and the snooze bank are a stat line (label + tabular value),
   the detector is a dot plus text, the "why not" line is plain secondary text,
   and the running break's countdown is mono text in the banner's title row.
   It reuses the shared .card / .card-head / .sec-body / .sec-actions / .btn /
   .pill / .dot / .well primitives; only the banner, the rolling-hour meter, the
   stat line, the confirm and the recent list are new, and they follow the
   .cockpit-panic rhythm (a small stack of tokened blocks). Spacing is the
   container's gap, never a child margin. Tokens only. ── */
.cockpit-ads { display: flex; flex-direction: column; }
/* `.pill--accent` is this card's own tone: the two ACTIVE states (in match, ad
   running) are neither ok nor a warning, they are "something is happening". The
   shared shell.css pill set has ok / warn / danger only, and widening a global
   primitive for one card would be the bigger change. */
.cockpit-ads .pill--accent { background: var(--accent-wash); color: var(--accent-2); }
.cockpit-ads .ads-posture {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-faint);
}
.cockpit-ads .ads-banner {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--r-sm);
  background: var(--surface-inset); border: 1px solid var(--border);
}
.cockpit-ads .ads-banner--warn {
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-inset));
  border-color: color-mix(in srgb, var(--warn) 38%, transparent);
}
.cockpit-ads .ads-banner--accent {
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-inset));
  border-color: var(--border-accent);
}
.cockpit-ads .ads-banner--danger {
  background: color-mix(in srgb, var(--danger) 14%, var(--surface-inset));
  border-color: color-mix(in srgb, var(--danger) 42%, transparent);
}
.cockpit-ads .ads-banner-t {
  display: flex; align-items: baseline; gap: var(--space-2);
  font-weight: 700; font-size: 13.5px;
}
/* The running break's countdown: mono TEXT in the title row, not a second pill. */
.cockpit-ads .ads-banner-t .bx {
  margin-left: auto; font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  font-variant-numeric: tabular-nums; color: var(--text-dim);
}
.cockpit-ads .ads-banner--warn .ads-banner-t { color: var(--warn); }
.cockpit-ads .ads-banner--accent .ads-banner-t { color: var(--accent-2); }
.cockpit-ads .ads-banner--danger .ads-banner-t { color: var(--danger); }
.cockpit-ads .ads-banner-s { font-size: 12px; color: var(--text-dim); }
/* The rolling-hour meter. `--ads-fill` / `--ads-due` are DATA, written on the
   track by the repaint (the model's two percentages) — the card emits no other
   inline style, and never on the first paint. */
.cockpit-ads .ads-meter { display: flex; flex-direction: column; gap: 6px; }
.cockpit-ads .ads-meter-top { display: flex; align-items: baseline; gap: var(--space-2); }
.cockpit-ads .ads-meter-label { font-size: 12.5px; font-weight: 600; }
.cockpit-ads .ads-meter-track {
  position: relative; height: 8px; border-radius: var(--r-sm); /* --r-xs never existed; resolved to square since authored */
  background: var(--surface-2); border: 1px solid var(--border); overflow: visible;
}
.cockpit-ads .ads-meter-fill {
  position: absolute; inset: 0 auto 0 0; width: var(--ads-fill, 0%);
  border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--accent), var(--accent-soft));
}
.cockpit-ads .ads-meter-fill.is-full {
  background: linear-gradient(90deg, var(--ok), var(--ok-soft));
}
/* P-1: the PENDING segment. A break that fired and has not been booked into the
   hour yet (the reconciler's ~2 minute echo grace). It starts where the banked
   fill ends and is deliberately SOFTER than it: the seconds are real but not yet
   confirmed, and a segment that matched the banked fill would be a claim, not a
   note. */
.cockpit-ads .ads-meter-pending {
  position: absolute; top: 0; bottom: 0;
  left: var(--ads-fill, 0%); width: var(--ads-pending, 0%);
  border-radius: var(--r-pill);
  background: var(--accent-soft); opacity: .45;
}
.cockpit-ads .ads-meter-due {
  position: absolute; top: -4px; bottom: -4px; left: var(--ads-due, 0%);
  width: 2px; background: var(--text-dim); border-radius: 2px;
}
.cockpit-ads .ads-meter-pend-note { color: var(--text-dim); }
.cockpit-ads .ads-meter-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-faint);
}
/* The stat line and the detector reading: the two things that used to be pills. */
.cockpit-ads .ads-stats {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-4); font-size: 12px; color: var(--text-dim);
}
.cockpit-ads .ads-stat { display: inline-flex; align-items: baseline; gap: 6px; }
.cockpit-ads .ads-stat .k { color: var(--text-faint); }
.cockpit-ads .ads-stat .v {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--text);
}
/* The DETECTION line (owner request, 2026-08-26): what the pipeline believes is
   on screen, and what the ad manager is doing about it. It replaces the old
   `.ads-detector` chip that lived in the stat row and could only speak when the
   reading carried a game slug. The stance wraps onto its own row (`flex-basis:
   100%`) so a long sentence never squeezes the reading beside it. */
.cockpit-ads .ads-detect {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 2px var(--space-2); font-size: 12px; color: var(--text-dim);
}
.cockpit-ads .ads-detect-k {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-faint);
}
.cockpit-ads .ads-detect-v {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; color: var(--text);
}
/* W-OFFLINE (2026-08-26): a reading the model has DATED ("Last seen: Fortnite ·
   12 min ago") steps back to the dim ink the label uses and drops its weight —
   the live reading is the only one that earns the card's foreground. The dot is
   already withheld by the model on exactly these readings. */
.cockpit-ads .ads-detect-v.is-aged { font-weight: 400; color: var(--text-dim); }
.cockpit-ads .ads-detect-s { flex: 1 1 100%; color: var(--text-dim); }
/* The CONDITIONAL stance (owner request, 2026-08-31), which only ever renders off
   air and only ever under the stance above it. It steps back one ink to the faint
   token the hint and the last-run line already wear: the stance is a statement
   about this stream, and this is a statement about a stream that is not running,
   so the pair must not read as two equally live claims. */
.cockpit-ads .ads-detect-w { color: var(--text-faint); }
.cockpit-ads .ads-why { font-size: 12px; color: var(--text-dim); }
/* RULING 32's last-manual-run line. QUIETER than the why-line above it, and
   deliberately so: the why-line is about right now and this is history, so the
   pair must not read as two equal claims about the same instant. */
.cockpit-ads .ads-why--last { font-size: 11px; color: var(--text-faint); }
.cockpit-ads .ads-hint { font-size: 11px; color: var(--text-faint); }
/* The Suggest prompt's expiry, pushed to the end of its own action row. */
.cockpit-ads .ads-hint--end { margin-left: auto; }
/* The Run-now confirm (G-STRINGS row 26): the one control on this card that
   spends the creator's audience, so it gets the danger ground. */
.cockpit-ads .ads-confirm {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--danger) 10%, var(--surface-inset));
  border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
}
.cockpit-ads .ads-confirm-t { font-weight: 700; font-size: 12.5px; color: var(--text); }
.cockpit-ads .ads-recent-label {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim);
}
.cockpit-ads .ads-recent { display: flex; flex-direction: column; gap: 6px; }
.cockpit-ads .ads-recent-row {
  display: flex; gap: var(--space-2); align-items: baseline;
  font-size: 11.5px; color: var(--text-dim);
}
.cockpit-ads .ads-recent-row .rw {
  font-family: var(--font-mono); font-size: 10.5px; color: var(--text-faint);
  flex: none; width: 3.2rem; font-variant-numeric: tabular-nums;
}

/* The stage while the preview lives in its OWN window (cockpit-popouts D5) — the
   inline decoder is suspended, so the stage says where the picture went instead
   of showing a frozen last frame. Sits above the <video>/<img> layers. */
.preview-popped { position: relative; z-index: 1; max-width: 28rem; }
.preview-info { margin-top: var(--space-3); }
.preview-info .grow { flex: 1; min-width: 0; }
.preview-info .grow .t { font-weight: 600; display: block; }
.preview-info .grow .s { font-size: 11.5px; display: block; }

/* ── (6) COLLABORATION — inline guest-invite link row (P13) ── */
.collab-invite { display: flex; gap: var(--space-2); align-items: center; margin-top: var(--space-2); }
.collab-invite .inp { flex: 1 1 auto; min-width: 0; }

/* ── (3) QUICK ACTIONS — roving-tabindex tile grid ── */
.qa-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: var(--space-2); }
.qa-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); padding: var(--space-3) var(--space-2);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm); color: var(--text); font: inherit; cursor: pointer;
  text-align: center; transition: border-color var(--dur-fast) var(--ease);
}
.qa-tile:hover { border-color: var(--border-accent); }
.qa-tile:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
.qa-tile[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
.qa-tile.is-live { border-color: color-mix(in srgb, var(--danger) 50%, transparent); }
.qa-tile.is-live .qa-icon { color: var(--danger); }
/* Task 2: a studio-cloud go-live pending on the pod — pulse until live/cancelled. */
.qa-tile.is-waiting { border-color: var(--border-accent); animation: qa-waiting-pulse 1.6s ease-in-out infinite; }
.qa-tile.is-waiting .qa-icon { color: var(--accent-soft); }
@keyframes qa-waiting-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.qa-icon { font-size: 18px; line-height: 1; }
.qa-label { font-size: 12px; font-weight: 600; }
.qa-soon { font-size: 9px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-faint); }

/* ── (4) ACTIVITY FEED — newest on top, scrolls internally ── */
.activity-feed { display: flex; flex-direction: column; gap: var(--space-1); overflow-y: auto; -webkit-overflow-scrolling: touch; max-height: 40vh; min-height: 8rem; }
.activity-row { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-1) var(--space-2); border-radius: var(--r-sm); font-size: 13px; }
.activity-row .pdot { margin-top: 4px; }
.activity-body { flex: 1; min-width: 0; line-height: 1.4; word-break: break-word; }
.activity-label { font-weight: 600; }
.activity-time { flex: none; margin-left: var(--space-2); font-size: 11px; font-variant-numeric: tabular-nums; white-space: nowrap; margin-top: 2px; }
.activity-actor { font-weight: 600; color: var(--accent-2); margin-left: 4px; }
.activity-detail { margin-left: 4px; font-size: 12px; }

/* ── (5) MY CHAT — compact variant of the chat feed ── */
.chat-feed.mini { max-height: 36vh; min-height: 8rem; }

/* ── responsive: the multi-column cockpit collapses to one column on mobile,
   the stat bar scrolls locally so the page body never scrolls horizontally ── */
@media (max-width: 560px) {
  .stat-bar { flex-wrap: nowrap; overflow-x: auto; }
  .stat-chip { flex: 0 0 auto; }
}

/* ═══════════════════════════════════════════════════════════════════════
   T5 — Topbar notification bell + drawer (notifications.js). A shell-chrome
   enhancer, not a section: it injects a bell button (with an unread badge)
   into the .topbar and an anchored popover drawer. Tokens ONLY (no hardcoded
   color). The .menu base (shell.css) supplies the floating-popover frame;
   these classes layer the bell badge + the row list on top. The JS class
   names are the contract — do not rename.
   ═══════════════════════════════════════════════════════════════════════ */

/* anchor: position:relative so the absolutely-positioned drawer pins to the
   bell, not the topbar (mirrors .chan-switch-anchor). */
.notif-anchor { position: relative; display: inline-flex; }

/* the bell button reuses .icon-btn; the badge floats on its top-right corner. */
.notif-bell { position: relative; }
.notif-bell-icon { line-height: 1; }
.notif-badge {
  position: absolute; top: -2px; right: -2px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; line-height: 1;
  color: var(--text); background: var(--danger);
  border-radius: var(--r-pill);
}
.notif-badge.hide { display: none; }

/* the drawer extends the shared .menu popover (frame + shadow come from there);
   the bell sits on the topbar's RIGHT, so right:0 (the .menu default) anchors
   it correctly — no left override needed. */
.notif-drawer { width: 320px; max-width: calc(100vw - var(--space-6)); }
.notif-head {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.notif-title { font-weight: 600; color: var(--text); font-size: 13px; }
.notif-mark { font-size: 11.5px; }

.notif-list {
  display: flex; flex-direction: column;
  max-height: 60vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: var(--space-1);
}
.notif-list .sec-empty {
  padding: var(--space-3); margin: 0; font-size: 12.5px;
}

.notif-row {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-sm);
  font-size: 13px;
}
.notif-row + .notif-row { border-top: 1px solid var(--border); }
.notif-row.is-unread { background: var(--accent-wash); }

/* category chip — small uppercase pill; color comes from tokens per bucket so
   meaning is carried by the WORD (the chip text) as well as the hue. */
.notif-cat {
  flex: none; margin-top: 1px;
  font-size: 9px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase;
  padding: 2px 6px; border-radius: var(--r-pill);
  color: var(--text); background: var(--surface-inset);
}
.notif-cat--tip { background: var(--ok); }
.notif-cat--sub { background: var(--accent); }
.notif-cat--raid { background: var(--warn); color: var(--bg); }
.notif-cat--follow { background: var(--surface-2); color: var(--accent-2); }

.notif-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.notif-line { line-height: 1.35; word-break: break-word; }
.notif-time { font-size: 11px; }

@media (max-width: 560px) {
  .notif-drawer { width: calc(100vw - var(--space-4)); }
}

/* ═══════════════════════════════════════════════════════════════════════
   S3-sections — mobile content reflow. ADDITIVE + mobile-scoped: every rule
   below sits inside @media(max-width:560px) or @media(hover:none) so the
   desktop layout is byte-unchanged. Uses the S1 foundation tokens
   (--touch-min 44px, --fs-title, --space-*). Touch-first: comfortable hit
   targets, no row crushing, primary actions reachable, hover-only affordances
   neutralised on touch. ═══════════════════════════════════════════════════ */
@media (max-width: 560px) {
  /* (1) Inputs — comfortable height + 16px font so iOS does not zoom on focus.
     16px is an intentional iOS-zoom guard, not a design token override. */
  .input, .select, .textarea, .cs-input { min-height: var(--touch-min); font-size: 16px; }
  .textarea { min-height: 88px; }

  /* (2) Row wrapping — stack controls instead of cramping them. */
  .between { flex-wrap: wrap; }
  .sec-copyrow, .cs-copyrow { flex-wrap: wrap; }
  .sec-copyrow .input, .cs-copyrow .cs-input { flex: 1 1 100%; }
  .sec-row { flex-wrap: wrap; }
  /* WRAPPING IS NOT ENOUGH FOR A FIELD STACK. `.field` is a column flex box with
     the default `min-width:auto`, so its base size is the INTRINSIC width of the
     control inside it — and `.input{width:100%}` resolves against that shrink-to-
     fit box, never widening it. A `datetime-local` measures ~240px of un-shrinkable
     UA chrome, so two of them in one row overflowed a phone card outright (the
     Schedule editor's Starts/Title row, and the social composer's Send-at row).
     Same shape as the `.sec-copyrow .input` rule two lines up: give the field a
     full-width basis and the row stacks instead of overflowing. */
  .sec-row > .field, .sec-row > .sec-check { flex: 1 1 100%; }
  .sec-check { min-height: var(--touch-min); }
  .sec-row .sec-health { margin-left: auto; }
  .preview-info { flex-wrap: wrap; }
  .preview-info .btn { width: 100%; }

  /* (3) Buttons — every tappable control clears the 44px minimum and centres
     its label/icon vertically once it is taller than its text line. */
  .btn, .btn--sm, .seg button, .sec-retry, .subtabs button, .notif-mark {
    min-height: var(--touch-min); display: inline-flex; align-items: center;
  }
  .icon-btn { width: var(--touch-min); height: var(--touch-min); }
  .chip button { min-width: var(--touch-min); min-height: var(--touch-min); }

  /* (4) Primary action reachability — stack the inline action row and let the
     primary CTA span full width so it is a thumb-sized target at the edge. */
  .sec-actions { flex-direction: column; }
  .sec-actions .btn--primary { width: 100%; justify-content: center; }

  /* (5) List rows — let the meta wrap and drop row actions onto their own line
     so they are full-size targets instead of pinched beside the title. */
  .sec-item { flex-wrap: wrap; }
  .sec-item-actions { flex: 1 1 100%; }
  .cs-row { flex-wrap: wrap; }
  .cs-row-actions { flex: 1 1 100%; }

  /* (6) Tables — keep a usable min width so the table scrolls inside its own
     overflow-x wrapper (.sec-tablewrap) instead of crushing columns and the
     body. */
  .sec-tablewrap > .table { min-width: 30rem; }

  /* (7) Toggle — enlarge the track + recompute the knob travel for touch. */
  .toggle { width: 48px; height: 28px; }
  .toggle::after { width: 22px; height: 22px; }
  .toggle.on::after { transform: translateX(20px); }

  /* (9) Tighter content gutter + fluid heading on small screens. */
  .content { padding: var(--space-4); }
  .page-header h1 { font-size: var(--fs-title); }
}

/* ── Mobile: the simple removable rows (bot variables/quotes) stack too — the
   main column takes the full width and the action buttons drop to their own
   full-width line, so the text is never pinched beside them. Matches the
   command/timer rework above. Desktop is unchanged.

   The moderator rows used to be pinned here too, under `#sec-moderators`. That
   id died with the dashboard redesign (R3 folded the destination into
   Moderation > Team), so the rules had been inert for months; the grant card now
   wraps on its own at every width (see .mod-grant-head, no media query). ── */
@media (max-width: 640px) {
  #sec-bot .sec-item:not(.sec-item--dest) { flex-wrap: wrap; }
  #sec-bot .sec-item:not(.sec-item--dest) > .sec-item-actions { flex: 1 1 100%; margin-left: 0; }
}

/* (8) Hover-only chat-mod actions are a critical touch bug: on a device with no
   hover the moderate buttons would never reveal. Pin them visible and give them
   a real target. The desktop hover-reveal is preserved by scoping it to
   @media(hover:hover). */
@media (hover: hover) {
  .chat-row .chat-mod, .chat-msg .chat-mod { opacity: 0; }
  .chat-row:hover .chat-mod, .chat-row:focus-within .chat-mod { opacity: 1; }
  .chat-msg:hover .chat-mod, .chat-msg:focus-within .chat-mod { opacity: 1; }
}
@media (hover: none) {
  .chat-mod { opacity: 1; }
  .chat-mod-btn { min-width: var(--touch-min); min-height: var(--touch-min); }
}
/* Coarse-pointer safety net regardless of width: the shell switches to the
   touch layout (off-canvas drawer + tab-bar) at 860px, but the 560px content
   block above only sizes inputs/sub-buttons up to 560px — leaving the 561–860px
   touch band (large phones landscape, tablets in portrait) with 13px inputs
   (iOS zoom-on-focus) and sub-44px controls. Apply the touch floor to every
   coarse-pointer width so it tracks the shell breakpoint, not just <=560px. */
@media (hover: none) and (pointer: coarse) {
  .input, .select, .textarea, .cs-input { min-height: var(--touch-min); font-size: 16px; }
  .btn, .btn--sm, .seg button, .subtabs button, .sec-retry, .notif-mark, .chip button {
    min-height: var(--touch-min);
  }
}

/* ── Studio tab strip (DD-6: Scene Designer as a tab, not a modal) ──
   A simple [Live | Scene Designer] strip switching the two Studio panes. Tokens
   only. The Designer pane hosts the embedded scene-editor (.se-embedded). */
.studio-tabs {
  display: flex;
  gap: var(--space-1, 4px);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-4);
}
.studio-tab {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-dim);
  font: inherit;
  font-weight: 600;
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  margin-bottom: -1px;
}
.studio-tab:hover { color: var(--text); }
.studio-tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.studio-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
.studio-pane { display: none; }
.studio-pane.is-active { display: block; }
/* Host for the embedded designer — give it room; the editor sizes itself. */
.studio-designer-host { display: block; width: 100%; }

/* ── Unified input list rows (Sources consolidation) ──────────────────────────
   One flat list row per input (NO card-in-card). unifiedRowHtml() emits these:
   a device row is a single .input-row (icon · name/meta · routing · rank · acts);
   an ingest row is .input-row.input-row--ingest with a .input-row-head plus an
   .input-eps endpoint sub-list. Layout uses flex so it reflows on narrow widths.
   The container is .input-list (data-unified-list): a token-gapped flex column so
   rows — and the empty-state/loading <p> children — stack with consistent spacing. */
.input-list { display: flex; flex-direction: column; gap: var(--space-2); }
.input-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-inset);
}
.input-row--ingest { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.input-row-head { display: flex; align-items: center; gap: var(--space-3); }
.input-ico { font-size: 18px; flex: none; line-height: 1; }
.input-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.input-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.input-meta { display: inline-flex; align-items: center; gap: var(--space-1); font-size: 11.5px; color: var(--text-dim); }
.input-goes { flex: none; }
.input-rank { flex: none; display: inline-flex; align-items: center; gap: var(--space-1); font-size: 12px; color: var(--text-dim); }
.input-actions { flex: none; display: inline-flex; align-items: center; gap: var(--space-1); }
/* Endpoint sub-list inside an ingest row. */
.input-eps { display: flex; flex-direction: column; gap: var(--space-1); padding-left: calc(18px + var(--space-3)); }
.input-ep { display: flex; align-items: center; gap: var(--space-2); font-size: 12px; }
.input-ep-slot {
  flex: none; min-width: 1.4em; text-align: center; font-family: var(--font-mono);
  font-size: 11px; color: var(--text-dim);
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: 0 4px;
}
.input-ep-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Status dot variants for the unified row (base .dot lives in shell.css). The
   meaning is in the row text; the dot color is a redundant cue, not the only one. */
.dot--on { background: var(--ok); box-shadow: 0 0 8px color-mix(in srgb, var(--ok) 60%, transparent); }
.dot--off { background: var(--danger); }
.dot--unknown { background: var(--text-faint); }
@media (max-width: 560px) {
  .input-row { flex-wrap: wrap; }
  .input-goes, .input-rank, .input-actions { flex: 1 1 auto; }
}

/* ── Add-Device wizard (reuses .sec-modal / .sec-modal-box) ────────────────────
   wizardStep1Html() emits .wiz-step > .wiz-opts > .wiz-opt(label) each wrapping a
   radio + .wiz-opt-body(.wiz-opt-title/.wiz-opt-sub); wizardStep2EncoderHtml()
   emits a bare .wiz-step. Card-like option rows so the type picker reads clearly. */
.wiz-step { display: flex; flex-direction: column; gap: var(--space-3); }
.wiz-opts { display: flex; flex-direction: column; gap: var(--space-2); }
.wiz-opt {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); cursor: pointer;
}
.wiz-opt input { margin-top: 3px; flex: none; }
.wiz-opt-body { display: flex; flex-direction: column; gap: 2px; }
.wiz-opt-title { font-weight: 600; }
.wiz-opt-sub { font-size: 12px; color: var(--text-dim); }

/* ── Alerts & Widgets rebuild (defaults+UX Phase 2) ────────────────────────────
   Reuses the shell card/badge/toggle/sec-item idiom; only the page-specific bits
   live here, all token-driven (no hardcoded colors). */
/* Headed alert groups + the grouped-editor section headings. Each group is a
   stack (heading + row list); the space BETWEEN groups belongs to their parent
   `.stack`, not to a `margin-bottom` on the group (SPACING PRIMITIVES). */
.aw-group { display: flex; flex-direction: column; gap: var(--space-2); }
.aw-group-title, .aw-editor-h {
  margin: 0; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim);
}
/* Template token-insert chips (the consumer's real interpolation vars). */
.aw-tokens { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }
.aw-token-btn {
  border: 1px solid var(--border); background: var(--surface-inset); color: var(--text-dim);
  border-radius: var(--r-pill); padding: 2px 8px; font-size: 11px; cursor: pointer;
}
.aw-token-btn:hover { color: var(--text); border-color: var(--border-accent); }
/* Widget-card inline-editable name (writes settings.label). */
.aw-name-input { flex: 1 1 180px; min-width: 0; max-width: 320px; font-weight: 600; }
/* Add-widget gallery card icon. */
.aw-gallery-icon { font-size: 20px; line-height: 1; }
/* Theme preview cards: token swatches + the active marker + the JSON disclosure. */
.aw-swatches { display: flex; gap: var(--space-1); align-items: center; }
.aw-swatch {
  width: 18px; height: 18px; border-radius: var(--r-sm);
  border: 1px solid var(--border); display: inline-block;
}
.aw-theme-card.is-active { border-color: var(--accent); }
.aw-theme-json summary { cursor: pointer; font-size: 12px; color: var(--text-dim); }
.aw-theme-json[open] summary { margin-bottom: var(--space-2); }
.aw-theme-json .textarea { width: 100%; min-height: 120px; }
/* "Add to scene" pick menus (widget cards + the alert box): a compact MENU panel
   (the shell's .menu/.menu-item idiom — flat hover-highlighted rows), NOT a stack
   of bordered buttons (which read as inputs — user feedback 2026-07-08 ×2). */
[data-scene-menu]:not([hidden]), [data-alert-scenemenu]:not([hidden]) {
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  padding: var(--space-1); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-md);
  min-width: 200px; max-width: 320px; width: max-content;
}
/* `hidden` must actually CLOSE the menu: these panels sit on .sec-actions, whose
   base `display:flex` (author origin) beats the UA's [hidden]{display:none} — so a
   pick "closed" the dropdown chrome (the :not([hidden]) rule above stops applying)
   while the flat scene buttons stayed rendered. Same belt as .menu[hidden] /
   .settings-panel[hidden]. */
[data-scene-menu][hidden], [data-alert-scenemenu][hidden] { display: none; }
/* Strip the .btn chrome inside the menus → .menu-item metrics. */
[data-scene-menu] .btn, [data-alert-scenemenu] .btn {
  justify-content: flex-start; text-align: left; width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0; background: transparent; box-shadow: none;
  border-radius: var(--r-sm); color: var(--text-dim); font-size: 13px;
}
[data-scene-menu] .btn:focus, [data-alert-scenemenu] .btn:focus {
  background: var(--surface-inset); color: var(--text);
}
@media (hover: hover) {
  [data-scene-menu] .btn:hover:not(:disabled), [data-alert-scenemenu] .btn:hover:not(:disabled) {
    background: var(--surface-inset); color: var(--text);
  }
}
[data-scene-menu] .btn:disabled, [data-alert-scenemenu] .btn:disabled { opacity: .5; cursor: default; }
[data-scene-menu] .sec-empty, [data-alert-scenemenu] .sec-empty { padding: var(--space-2); }
/* The external-OBS URL disclosure: give the revealed copy row space under the
   summary line instead of sitting flush against it. */
.sec-item-detail details > .sec-copyrow { margin-top: var(--space-2); }
/* Asset library (parity Phase A): the URL input + Choose… row and the folded-in
   library panel. Token-driven; previews stay small (thumbnail / compact audio). */
.aw-asset-field { margin-top: 6px; }
.aw-asset-field .between { gap: var(--space-2); }
.aw-asset-field .between .input { flex: 1; min-width: 0; }
.aw-asset-panel { margin-top: var(--space-2); }
.aw-asset-row { align-items: center; }
.aw-asset-preview { margin-top: var(--space-1); }
.aw-asset-thumb {
  width: 48px; height: 48px; object-fit: cover; display: block;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-inset);
}
.aw-asset-audio { display: block; width: 100%; max-width: 260px; height: 32px; }
/* The alert-box "add to scene" well. This rule was the 2026-07-08 one-well patch
   for "the .well base has padding but no child gaps, so these sat flush" — the
   same defect that then re-shipped on Tips and Public Profile. The gap now comes
   from `.well` itself (SPACING PRIMITIVES) and the margin-bottom from its
   container, so only the class name is left as a hook. */
.aw-scenewell { display: flex; flex-direction: column; gap: var(--space-3); }
.aw-scenewell details > .sec-copyrow { margin-top: var(--space-2); }

/* Gift bomb behavior row: this .between holds the label (.sec-item-main, flex:1 1 0
   so basis 0) beside a <select class="input">. The shared .input sets width:100% =
   flex-basis 100%, so the select claimed the whole row and starved the label to 0
   width, wrapping its text one character per line (vertical). Give the label a real
   basis so it can't collapse, size the select to its own content (not 100%), and let
   the row wrap on narrow screens instead of cramming both onto one line. Scoped to
   this row only. */
.aw-giftbomb-row { flex-wrap: wrap; }
.aw-giftbomb-row > .sec-item-main { flex: 1 1 14rem; }
.aw-giftbomb-row [data-alert-giftbomb] { flex: 0 1 auto; width: auto; min-width: 12rem; }

/* Widget appearance editor (preview & customize 2026-07-21) */
.aw-appearance { margin-top: var(--space-3); }
.aw-appearance > summary { cursor: pointer; }
.aw-ap-row { align-items: center; gap: var(--space-2); }
.aw-ap-color {
  width: 44px; height: 30px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-inset); cursor: pointer;
}
.aw-ap-range { flex: 1; max-width: 10rem; accent-color: var(--accent); }

/* Both the alert and widget previews now render in the shared detached pop-out
   (preview-popout P2b/P2c), so the former inline .aw-preview / .aw-preview-stage
   / .aw-preview-frame stage rules were removed as dead. */

/* Alert style preset picker (design-presets 2026-07-22). Three selectable
   cards above the per-type editors; the active one gets an accent ring. The
   swatch chips preview the per-category accents the preset paints on stream. */
/* A stack (heading + note + card grid), not a block with margins on each child:
   inside the `.ao-globals` well those margins ADDED to the well's gap and the
   preset block sat 28px from its neighbours while the rows under it sat at 12. */
.aw-preset-section { display: flex; flex-direction: column; gap: var(--space-2); }
.aw-preset-grid {
  display: grid; gap: var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.aw-preset-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1);
  text-align: left; cursor: pointer; font: inherit; color: var(--text);
  padding: var(--space-3); background: var(--surface-inset);
  border: 1px solid var(--border); border-radius: var(--r-sm);
}
.aw-preset-card:hover:not(.is-active) { border-color: var(--border-accent); }
.aw-preset-card.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.aw-preset-card:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
.aw-preset-name { font-weight: 600; }
.aw-preset-desc { margin: 0; }
/* No margin-top: .aw-preset-card is a gap column and owns the row rhythm. */
.aw-preset-swatches { display: flex; gap: var(--space-1); }
.aw-preset-swatch {
  width: 14px; height: 14px; border-radius: var(--r-full, 50%);
  border: 1px solid var(--border); display: inline-block;
}
/* Global "Alert style" (brand look) editor section — the global appearance layer
   under the preset picker (brand-customization P4.3). Tokens only. */
/* Same shape as .aw-preset-section: a stack, and the space to its neighbours
   comes from the `.ao-globals` well's gap. Its button and the note under it used
   to sit ~2px apart (block flow, no gap) — the "buttons touching" report. */
.aw-style-section { display: flex; flex-direction: column; gap: var(--space-2); }

/* Embedded live preview inside the "Alert style" modal (alert-style-modal Task 2):
   a display-only ?preview=1 overlay iframe that plays a sample alert with the
   current global look. Notification-sized, neutral surface, no chrome. Tokens only. */
.aw-style-preview { margin-top: var(--space-3); }
.aw-style-preview-frame {
  display: block; width: 100%; height: 200px;
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--bg); overflow: hidden;
}

/* Brand appearance editor (brand-customization 2026-07-23). One editor renders
   the four lever bundles for the global brand style; the same markup nests inside
   each per-type "Customize this type" override. Reuses .aw-ap-row/.aw-ap-color/
   .aw-ap-range from the widget-appearance work; adds the size + treatment groups.
   Tokens only. */
.aw-ap-editor { display: flex; flex-direction: column; gap: var(--space-3); }
/* S / M / L / XL size toggle group */
.aw-size-row { display: flex; gap: var(--space-1); flex-wrap: wrap; }
.aw-size-btn {
  min-width: 40px; padding: var(--space-1) var(--space-2);
  font: inherit; font-weight: 600; color: var(--text); cursor: pointer;
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.aw-size-btn:hover:not(.is-active) { border-color: var(--border-accent); }
.aw-size-btn.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.aw-size-btn:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
/* Accent treatment radio group */
.aw-treat-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.aw-treat-opt {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: 13px; color: var(--text); cursor: pointer;
}
.aw-treat-opt input { accent-color: var(--accent); }
/* Per-type "Customize this type" override disclosure inside each alert row
   (brand-customization P5.1). The shared .aw-ap-editor renders inside; this only
   sets the disclosure spacing + the summary affordance. Tokens only. */
.aw-type-custom { margin-top: var(--space-2); }
.aw-type-custom > summary { cursor: pointer; }
.aw-type-custom[open] > summary { margin-bottom: var(--space-2); }
.aw-type-custom > summary label { display: inline-flex; align-items: center; gap: var(--space-1); }

/* ═══════════════════════════════════════════════════════════════════════
   Bot (#sec-bot) — refined design pass (plan 2026-08-03, approved mockup match).
   The bot reuses the shared idioms (.sec-item--dest / .sec-item-summary /
   .sec-item-detail / .toggle / .subtabs / .card / .btn / .badge), but the
   approved mockup calls for a distinctly refined dark look: a violet accent,
   deep neutral surfaces, hairline borders, a segmented 3-tab bar, a pill toggle
   switch, single-line command rows (mono name + truncated response + trailing
   pills + hover actions), a guided editor, and moderation/mode cards in a grid.
   To get that look WITHOUT regressing any other section that shares these class
   names, the refined palette lives entirely in --bot-* custom properties SCOPED
   to #sec-bot (the exact pattern the stream-info modal uses with --si-*), and
   EVERY selector below is prefixed #sec-bot. Hardcoded hex appears ONLY in the
   --bot-* var block; every rule references those vars. Cards/rows/chips wrap +
   stack on narrow; the body never scrolls horizontally. A11y (role=switch,
   aria-checked/expanded, visible focus rings) is preserved throughout.
   ═══════════════════════════════════════════════════════════════════════ */
/* #sec-import reuses the bot-import panel (window.OmniBotImport.mount), whose
   layout rules below reference the --bot-* tokens; alias them here too so the
   reused panel reads identically on the Settings > Import page. */
#sec-bot,
#sec-import {
  /* Aliased to the global refined tokens (omni-ui.css :root) so there is ONE
     source of truth. Values are unchanged vs the former hardcoded palette. */
  --bot-accent: var(--accent);
  --bot-accent-2: var(--accent-2);
  --bot-accent-wash: var(--accent-wash);
  --bot-accent-line: var(--border-accent);
  --bot-accent-glow: var(--accent-glow);
  --bot-on-strong: var(--on-strong);
  --bot-surface: var(--surface);
  --bot-surface-2: var(--surface-2);
  --bot-surface-3: var(--surface-3);
  --bot-field: var(--surface-inset);
  --bot-body-inset: var(--inset-2);
  --bot-border: var(--border);
  --bot-border-strong: var(--border-strong);
  --bot-text: var(--text);
  --bot-dim: var(--text-dim);
  --bot-faint: var(--text-faint);
  --bot-danger: var(--danger);
  --bot-r: var(--r);
  --bot-r-sm: var(--r-sm);
  --bot-r-chip: var(--r-chip);
  --bot-r-pill: var(--r-pill);
}

/* ── page header + the "Active on" chip strip ── */
#sec-bot .page-header h1 { letter-spacing: -.02em; }
#sec-bot .page-header .sub { color: var(--bot-faint); }
#sec-bot .bot-active-on {
  align-items: center; flex-wrap: wrap; gap: var(--space-2);
  background: var(--bot-surface); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r); padding: 6px 10px;
}
#sec-bot .bot-active-on > .dim { color: var(--bot-faint); font-size: 12px; }
#sec-bot .bot-active-chips { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
#sec-bot .bot-active-chips .badge {
  text-transform: none; letter-spacing: 0; font-weight: 600; font-size: 12px;
  padding: 3px 9px; border-radius: var(--bot-r-pill);
  background: var(--bot-surface-2); border: 1px solid var(--bot-border); color: var(--bot-text);
}
#sec-bot .bot-active-chips .dim { color: var(--bot-faint); font-size: 12px; }

/* ── buttons (refined; decorative hover under hover-capable pointers only) ── */
#sec-bot .btn {
  background: var(--bot-surface-2); border: 1px solid var(--bot-border);
  color: var(--bot-dim); border-radius: var(--bot-r-sm);
}
#sec-bot .btn--primary {
  background: linear-gradient(180deg, var(--bot-accent-2), var(--bot-accent));
  border-color: transparent; color: var(--bot-on-strong);
  box-shadow: 0 2px 10px var(--bot-accent-glow);
}
#sec-bot .btn--danger { color: var(--bot-danger); border-color: var(--bot-border); }
@media (hover: hover) {
  #sec-bot .btn:hover { background: var(--bot-surface); color: var(--bot-text); border-color: var(--bot-border-strong); }
  #sec-bot .btn--primary:hover { filter: brightness(1.06); color: var(--bot-on-strong); border-color: transparent; }
  #sec-bot .btn--danger:hover { color: var(--bot-danger); border-color: var(--bot-danger); background: var(--bot-surface-2); }
}

/* ── cards + fields ── */
#sec-bot .card { background: var(--bot-surface); border-color: var(--bot-border); border-radius: var(--bot-r); }
#sec-bot .card-head { border-bottom-color: var(--bot-border); }
#sec-bot .card-head h2 { color: var(--bot-text); }
#sec-bot .sec-note { color: var(--bot-dim); }
#sec-bot .sec-legend > summary { color: var(--bot-dim); }
#sec-bot .field > .lbl { color: var(--bot-dim); }
#sec-bot .field > .help { color: var(--bot-faint); }
#sec-bot .input, #sec-bot .select, #sec-bot .textarea {
  background: var(--bot-field); border: 1px solid var(--bot-border); color: var(--bot-text);
  border-radius: var(--bot-r-sm);
}
#sec-bot .input::placeholder, #sec-bot .textarea::placeholder { color: var(--bot-faint); }
#sec-bot .input:focus-visible, #sec-bot .select:focus-visible, #sec-bot .textarea:focus-visible {
  outline: none; border-color: var(--bot-accent-line); box-shadow: 0 0 0 3px var(--bot-accent-wash);
}
/* Inline editor checkbox row (e.g. a timer's "Only post when live"): a compact
   labeled control that reads on the field background, accent-tinted when on. */
#sec-bot .ef-check {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 8px; padding: 8px 10px;
  background: var(--bot-field); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r-sm); color: var(--bot-text);
  font-size: 13px; cursor: pointer; user-select: none;
}
#sec-bot .ef-check > input[type="checkbox"] {
  flex: 0 0 auto; width: 16px; height: 16px; margin: 0;
  accent-color: var(--bot-accent); cursor: pointer;
}
#sec-bot .ef-check > input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--bot-accent); outline-offset: 2px;
}

/* ── the nine-row SUB-NAV (dashboard redesign R2b) ──
   The 3-tab fold was a 460px segmented pill box; nine deep-linked sub-pages need
   a scrollable underline strip instead (the approved mock). The strip scrolls
   horizontally ON ITSELF at narrow widths so the page body never does. */
#sec-bot .subtabs {
  gap: 2px; padding: 0; max-width: none;
  background: transparent; border: 0;
  border-bottom: 1px solid var(--bot-border); border-radius: 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin;
}
#sec-bot .subtabs button {
  flex: none; white-space: nowrap;
  border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  padding: 9px 13px; color: var(--bot-faint); font-weight: 600;
}
#sec-bot .subtabs button.on {
  color: var(--bot-text); background: transparent;
  border-bottom-color: var(--bot-accent);
}
#sec-bot .subtabs button:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: -2px; }
@media (hover: hover) {
  #sec-bot .subtabs button:hover { color: var(--bot-dim); }
}

/* ── list items (all rows) + the pill toggle switch ── */
#sec-bot .sec-item { background: var(--bot-surface); border-color: var(--bot-border); border-radius: var(--bot-r-sm); }
#sec-bot .sec-item--dest { border-radius: var(--bot-r); }
#sec-bot .sec-item--dest.is-expanded { border-color: var(--bot-accent-line); }
#sec-bot .sec-item-summary { align-items: center; gap: var(--space-3); }
#sec-bot .sec-item-chevron { color: var(--bot-faint); }
#sec-bot .sec-item-meta { color: var(--bot-dim); }
#sec-bot .toggle {
  width: 34px; height: 20px; border-radius: 11px;
  background: var(--bot-surface-3); border: 1px solid var(--bot-border);
}
#sec-bot .toggle::after { width: 14px; height: 14px; background: var(--bot-dim); }
#sec-bot .toggle.on { background: var(--bot-accent-wash); border-color: var(--bot-accent-line); }
#sec-bot .toggle.on::after { transform: translateX(14px); background: var(--bot-accent-2); }
#sec-bot .toggle:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: 2px; }

/* ── command + timer rows: mono name (left) · truncated response (middle) ·
   descriptor pills (right) · Edit/Remove reveal on hover/focus (pointer only) ── */
#sec-bot [data-pane="commands"] .sec-item-summary > .sec-item-main,
#sec-bot [data-pane="automation"] .sec-item-summary > .sec-item-main {
  flex-direction: row; align-items: baseline; gap: var(--space-3); min-width: 0;
}
#sec-bot .bot-row-name { flex: none; }
#sec-bot .bot-row-name .badge {
  background: transparent; border: 0; padding: 0; color: var(--bot-text);
  font-family: var(--font-mono); font-size: 13px; font-weight: 600;
  text-transform: none; letter-spacing: 0;
}
#sec-bot [data-pane="commands"] .bot-row-resp,
#sec-bot [data-pane="automation"] .bot-row-resp {
  flex: 1; min-width: 0; display: block;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--bot-dim);
}
#sec-bot .bot-row-pills { flex: none; display: inline-flex; align-items: center; gap: var(--space-2); }
#sec-bot .bot-row-pills .badge {
  background: var(--bot-surface-2); border: 1px solid var(--bot-border); color: var(--bot-dim);
  text-transform: none; letter-spacing: 0; font-weight: 600; font-size: 11px;
  border-radius: var(--bot-r-pill); padding: 2px 9px;
}
@media (hover: hover) {
  #sec-bot [data-pane="commands"] .sec-item-actions-row > .btn,
  #sec-bot [data-pane="automation"] .sec-item-actions-row > .btn {
    opacity: 0; transition: opacity var(--dur-fast) var(--ease);
  }
  #sec-bot [data-pane="commands"] .sec-item--dest:hover .sec-item-actions-row > .btn,
  #sec-bot [data-pane="automation"] .sec-item--dest:hover .sec-item-actions-row > .btn,
  #sec-bot [data-pane="commands"] .sec-item-summary:focus-within .sec-item-actions-row > .btn,
  #sec-bot [data-pane="automation"] .sec-item-summary:focus-within .sec-item-actions-row > .btn,
  #sec-bot [data-pane="commands"] .sec-item--dest.is-expanded .sec-item-actions-row > .btn,
  #sec-bot [data-pane="automation"] .sec-item--dest.is-expanded .sec-item-actions-row > .btn { opacity: 1; }
}
/* ── Mobile: stack each command/timer row (toggle + name on top, description on
   its own full-width line, actions on their own line). The summary becomes a
   2-col grid so the actions container can span both columns onto row 2. Desktop
   (>640px) keeps the single-row flex layout above, unchanged. ── */
@media (max-width: 640px) {
  #sec-bot [data-pane="commands"] .sec-item-summary,
  #sec-bot [data-pane="automation"] .sec-item-summary {
    display: grid; grid-template-columns: auto 1fr; gap: var(--space-2) var(--space-3); align-items: start;
  }
  #sec-bot [data-pane="commands"] .sec-item-summary > .toggle,
  #sec-bot [data-pane="automation"] .sec-item-summary > .toggle { grid-column: 1; grid-row: 1; align-self: center; }
  #sec-bot [data-pane="commands"] .sec-item-summary > .sec-item-main,
  #sec-bot [data-pane="automation"] .sec-item-summary > .sec-item-main {
    grid-column: 2; grid-row: 1; flex-direction: column; align-items: flex-start; gap: var(--space-1);
  }
  #sec-bot [data-pane="commands"] .bot-row-resp,
  #sec-bot [data-pane="automation"] .bot-row-resp { white-space: normal; }
  #sec-bot [data-pane="commands"] .sec-item-actions-row,
  #sec-bot [data-pane="automation"] .sec-item-actions-row { grid-column: 1 / -1; }
  #sec-bot [data-pane="commands"] .sec-item-actions-row > .btn,
  #sec-bot [data-pane="automation"] .sec-item-actions-row > .btn { flex: 1 1 auto; justify-content: center; }
  #sec-bot [data-pane="commands"] .sec-item-actions-row > .sec-item-chevron,
  #sec-bot [data-pane="automation"] .sec-item-actions-row > .sec-item-chevron { flex: 0 0 auto; align-self: center; }
}

/* ── Commands pane redesign (approved mock): one toolbar (filter + New command),
   group tab pills with counts, and grouped JOINED row lists (This channel /
   Standard / Features). Tokens only (the --bot-* block above). ── */
#sec-bot .bot-cmd-toolbar { display: flex; align-items: center; gap: var(--space-2); }
#sec-bot .bot-cmd-filter { flex: 1; min-width: 0; }
#sec-bot .bot-cmd-toolbar > .btn--primary { flex: none; white-space: nowrap; }

/* The collapsible New-command panel (the existing create form, revealed by the
   toolbar button). */
#sec-bot .bot-cmd-new {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); border: 1px solid var(--bot-accent-line);
  border-radius: var(--bot-r); background: var(--bot-body-inset);
}
#sec-bot .bot-cmd-new[hidden] { display: none; }

/* Group tab pills. */
#sec-bot .bot-cmd-tabs { display: flex; flex-wrap: wrap; gap: var(--space-1); }
#sec-bot .bot-cmd-tab {
  font-size: 12px; font-weight: 600; color: var(--bot-dim);
  padding: 5px 12px; border-radius: var(--bot-r-pill);
  border: 1px solid var(--bot-border); background: var(--bot-surface-2); cursor: pointer;
}
#sec-bot .bot-cmd-tab.on { color: var(--bot-accent-2); border-color: var(--bot-accent-line); background: var(--bot-accent-wash); }
#sec-bot .bot-cmd-tab .n { color: var(--bot-faint); font-family: var(--font-mono); font-size: 11px; margin-left: 2px; }
#sec-bot .bot-cmd-tab:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: 2px; }
@media (hover: hover) { #sec-bot .bot-cmd-tab:hover { color: var(--bot-text); } }

/* Grouped sections: header + hint over one joined row list per group. */
#sec-bot .bot-cmd-groups { display: flex; flex-direction: column; gap: var(--space-5); }
#sec-bot .bot-cmd-group-head {
  font-size: 13px; font-weight: 600; margin: 0 0 var(--space-2); color: var(--bot-dim);
  display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap;
}
#sec-bot .bot-cmd-group-head .hint { font-weight: 400; font-size: 11.5px; color: var(--bot-faint); }
#sec-bot .bot-cmd-rows {
  display: flex; flex-direction: column;
  background: var(--bot-surface); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r); overflow: hidden;
}
#sec-bot .bot-cmd-rows > .sec-item {
  border: 0; border-top: 1px solid var(--bot-border); border-radius: 0;
  background: transparent; margin: 0;
}
#sec-bot .bot-cmd-rows > .sec-item:first-child { border-top: 0; }
@media (hover: hover) { #sec-bot .bot-cmd-rows > .sec-item--dest:hover { background: var(--bot-surface-2); } }
#sec-bot .bot-cmd-rows > .sec-item--dest.is-expanded { background: var(--bot-body-inset); }

/* Row anatomy (mock proportions): the mono !name column is fixed-width so the
   response one-liners align down the list; aliases sit small under the name. */
#sec-bot .bot-cmd-rows .sec-item-title.bot-row-name {
  display: block; flex: 0 0 176px; min-width: 0; overflow: hidden;
}
#sec-bot .bot-cmd-rows .bot-row-name .badge { color: var(--bot-accent-2); font-weight: 700; }
#sec-bot .bot-cmd-aliases {
  display: block; font-family: var(--font-mono); font-size: 10px; color: var(--bot-faint);
  margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Right-side descriptor chips (cooldown / level / keyword / group tag). */
#sec-bot .bot-cmd-chip {
  background: transparent; border: 1px solid var(--bot-border); color: var(--bot-faint);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .04em;
  text-transform: none; padding: 2px 7px; border-radius: var(--bot-r-chip);
}
#sec-bot .bot-group-tag { text-transform: uppercase; }
/* Disabled rows read muted (the toggle still shows the true state). */
#sec-bot .bot-row-off .bot-row-name, #sec-bot .bot-row-off .bot-row-resp { opacity: .45; }
/* Feature + built-in rows: the toggle and the While-live select ARE operable
   (2026-08-09), but the row itself never expands, so the summary is not a
   pointer target. */
#sec-bot .bot-feature-row .sec-item-summary { cursor: default; }
#sec-bot .bot-feature-row [data-feature-enable] { cursor: pointer; }
/* The While-live control sits with the descriptor chips: compact and quiet, and
   never wide enough to squeeze the response line — but it shows a VALUE, not a
   hint, so it has to read AT REST.
   It shipped as `background:transparent` + `color:var(--bot-faint)` with
   `:hover { color: var(--text) }`, which made hover the ONLY legible state: at
   rest it was 11px --text-faint (#9b918a) on the row ground, the exact pairing
   this sheet already rejects a few hundred lines up (~3.4:1, fails WCAG 1.4.3
   AA for 11px content text), and touch + keyboard users never get hover at all.
   The transparent ground compounded it — a select with no author background
   hands its browser-drawn popup back to the UA canvas instead of the dark sheet
   every other select gets. Same class of bug as .btn--primary treating the
   translucent --accent-soft wash as a solid fill.
   It now sits on the shared inset field ground with --bot-dim (≈10.8:1); hover
   is EMPHASIS on top of a legible resting state, not the only readable state,
   and it is gated behind (hover: hover) like the rest of this pane so it never
   sticks on touch. */
#sec-bot .bot-livegate {
  height: 26px; padding: 0 var(--space-2); max-width: 168px;
  font-size: 11px; color: var(--bot-dim);
  background: var(--bot-field); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r-chip);
}
@media (hover: hover) {
  #sec-bot .bot-livegate:hover { color: var(--bot-text); border-color: var(--bot-border-strong); }
}
/* Inside the expanded editor it is a full-size field like its siblings — an
   explicit --bot-text (not `inherit`) so it matches the .select next to it. */
#sec-bot .sec-item-detail .bot-livegate { height: auto; max-width: none; font-size: inherit; color: var(--bot-text); }
@media (max-width: 640px) {
  #sec-bot .bot-cmd-toolbar { flex-wrap: wrap; }
  /* .sec-item-main stacks to a column here (rules above), so the fixed name
     column must relax or 176px becomes a HEIGHT basis. */
  #sec-bot .bot-cmd-rows .sec-item-title.bot-row-name { flex: none; width: auto; }
}

/* ── the guided command editor (inline detail) ── */
#sec-bot .sec-item-detail {
  margin-top: var(--space-1); padding: var(--space-3);
  background: var(--bot-body-inset); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r-sm);
}

/* Variable picker (guided command builder): a subtle inset panel of click-to-
   insert token chips styled as accent-wash pills. */
#sec-bot .bot-var-picker {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-2); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r-sm); background: var(--bot-surface);
}
#sec-bot .bot-var-picker > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--bot-faint);
}
#sec-bot .bot-var-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
#sec-bot .bot-var-chips .btn {
  border-radius: var(--bot-r-chip); font-family: var(--font-mono); font-size: 12px;
  padding: 3px 8px; color: var(--bot-accent-2);
  background: var(--bot-accent-wash); border: 1px solid var(--bot-accent-line);
}
#sec-bot .bot-var-more { margin: 0; }
#sec-bot .bot-var-more > summary { font-size: 11px; color: var(--bot-faint); }
#sec-bot .bot-var-more .bot-var-chips { margin-top: var(--space-2); }
@media (hover: hover) {
  #sec-bot .bot-var-chips .btn:hover { background: var(--bot-accent-line); color: var(--bot-text); border-color: var(--bot-accent-line); }
}

/* Live "PyreBot: …" preview — an accent-marked inset line under the builder
   (reads like a quoted chat line). Wraps so a long response never overflows. */
#sec-bot .bot-preview {
  display: flex; align-items: baseline; gap: var(--space-1); flex-wrap: wrap;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--bot-border); border-left: 2px solid var(--bot-accent);
  border-radius: var(--bot-r-sm); background: var(--bot-surface);
}
#sec-bot .bot-preview .dim { color: var(--bot-faint); }
#sec-bot .bot-preview .mono { color: var(--bot-accent-2); font-weight: 700; }
#sec-bot .bot-preview-body { color: var(--bot-text); word-break: break-word; min-width: 0; }

/* ── Moderation: filter cards + chat-mode cards in a responsive grid ──
   The filter rows stay the SAME expandable rule rows (toggle + Configure + Save +
   Remove all wired unchanged) — only presented as cards: friendly name over a
   plain-language summary, the switch top-right, actions below. auto-fit collapses
   to one column on narrow (no horizontal body scroll). */
#sec-bot [data-pane="moderation"] .sec-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--space-3);
}
#sec-bot [data-pane="moderation"] .sec-item {
  background: var(--bot-surface); border-color: var(--bot-border);
  border-radius: var(--bot-r); padding: var(--space-4);
}
#sec-bot [data-pane="moderation"] .sec-item-summary { flex-wrap: wrap; align-items: flex-start; gap: var(--space-2) var(--space-3); }
#sec-bot [data-pane="moderation"] .sec-item-summary > .sec-item-main { flex: 1 1 auto; }
#sec-bot [data-pane="moderation"] .sec-item-summary > .toggle { margin-left: auto; }
/* The filter card keeps its "actions below" shape: the Configure/Remove group
   drops onto its own full-width line under the name + toggle. */
#sec-bot [data-pane="moderation"] .sec-item-summary > .sec-item-actions-row { flex: 1 1 100%; }
#sec-bot [data-pane="moderation"] .sec-item-chevron { display: none; }
#sec-bot [data-pane="moderation"] .sec-item-title .badge {
  text-transform: none; letter-spacing: 0; background: transparent; border: 0; padding: 0;
  color: var(--bot-text); font-size: 14px; font-weight: 650;
}
#sec-bot [data-pane="moderation"] .sec-item-title .badge ~ .badge {
  background: var(--bot-surface-2); border: 1px solid var(--bot-border); color: var(--bot-dim);
  font-size: 11px; font-weight: 600; padding: 2px 9px; border-radius: var(--bot-r-pill);
}
#sec-bot [data-pane="moderation"] .sec-item-meta { color: var(--bot-dim); font-size: 12px; }

/* Editable chat-mode cards: a per-platform grid of switch + label pairs plus a
   slow-mode field. auto-fit collapses to one column on narrow. The platform-name
   title stays an accent chip (more specific than the filter-title rule above). */
#sec-bot .bot-mode-toggles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-3); margin-top: var(--space-2);
}
#sec-bot .bot-mode-toggle { display: flex; align-items: center; gap: var(--space-2); }
#sec-bot .bot-mode-toggle .lbl { font-size: 12px; color: var(--bot-text); cursor: pointer; }
#sec-bot .bot-mode-slow .input { max-width: 8rem; }
#sec-bot .bot-mode-card .sec-item-title .badge {
  text-transform: none; letter-spacing: 0; font-family: var(--font-mono);
  font-size: 13px; padding: 2px 8px; border-radius: var(--bot-r-chip);
  color: var(--bot-accent-2); background: var(--bot-accent-wash); border: 1px solid var(--bot-accent-line);
}

/* Banned words list editor (Phase 2): the blocklist filter card points here with
   a hint; the card itself is an add-form (input + Add on one row) over a chip
   list of the current words. Reuses the .chip component for each removable word. */
#sec-bot .bot-wordlist-hint { margin: 0; }
#sec-bot .bot-wordadd { display: flex; align-items: flex-end; gap: var(--space-2); flex-wrap: wrap; }
#sec-bot .bot-wordadd > .field { flex: 1 1 220px; min-width: 0; }
#sec-bot .bot-wordlist.chips { margin-top: var(--space-3); }
#sec-bot .bot-word .bot-word-label { color: var(--bot-text); }

/* ── Command import panel (bot-import.js). Reuses the bot surface + component
   tokens so it reads as one system with the rest of the section. The SAME panel is
   mounted on the Settings > Import page (sec-import.js), so every rule is scoped to
   BOTH #sec-bot and #sec-import — ONE rule set, no duplication. ── */
#sec-bot .bot-import-sources,
#sec-import .bot-import-sources { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
#sec-bot .bot-import-src.on,
#sec-import .bot-import-src.on {
  background: var(--bot-accent-wash); border-color: var(--bot-accent-line); color: var(--bot-text);
}
#sec-bot .bot-import-note,
#sec-import .bot-import-note { color: var(--bot-dim); }
/* The import card is a full-width (.span-2) card. A single left-aligned column at a
   readable width reads cleanly here; the old horizontal flex row (built for the
   narrow two-card layout) stranded the fields off to one side in the wide card. The
   preview list below sits OUTSIDE this form, so it still uses the card's full width. */
#sec-bot .bot-import-form,
#sec-import .bot-import-form {
  display: flex; flex-direction: column; align-items: stretch; flex-wrap: nowrap;
  gap: var(--space-4); margin-top: var(--space-2); max-width: 620px; margin-right: auto;
}
#sec-bot .bot-import-channel,
#sec-import .bot-import-channel { width: 100%; min-width: 0; }
/* [hidden] belt: the source affordances (per-source fields + Connect) toggle via the
   `hidden` attribute in bot-import.js, but `.field { display:flex }` and `.btn {
   display:inline-flex }` (author origin, class specificity) beat the UA
   `[hidden]{display:none}` rule — so a hidden field/button stayed visible (the SE JWT
   input showed for every source). These explicit rules restore the intended hide.
   Same belt idiom as .settings-panel[hidden] / .menu[hidden] elsewhere. */
#sec-bot .bot-import-channel[hidden],
#sec-bot .bot-import-jwt[hidden],
#sec-bot .bot-import-streamlabs[hidden],
#sec-bot [data-import-connect][hidden],
#sec-import .bot-import-channel[hidden],
#sec-import .bot-import-jwt[hidden],
#sec-import .bot-import-streamlabs[hidden],
#sec-import [data-import-connect][hidden] { display: none; }
#sec-bot .bot-import-jwt,
#sec-import .bot-import-jwt { width: 100%; }
#sec-bot .bot-import-token-link,
#sec-import .bot-import-token-link { align-self: flex-start; color: var(--bot-text); font-weight: 600; text-decoration: underline; }
#sec-bot .bot-import-steps,
#sec-import .bot-import-steps { margin: var(--space-1) 0 0; padding-left: 1.4em; display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--bot-dim); }
#sec-bot .bot-import-actions,
#sec-import .bot-import-actions { display: flex; gap: var(--space-2); }
#sec-bot .bot-import-summary-row,
#sec-import .bot-import-summary-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex-wrap: wrap; margin-top: var(--space-4);
}
#sec-bot .bot-import-summary,
#sec-import .bot-import-summary { margin: 0; }
#sec-bot .bot-import-list,
#sec-import .bot-import-list { margin-top: var(--space-3); }
#sec-bot .bot-import-row,
#sec-import .bot-import-row { display: flex; align-items: flex-start; gap: var(--space-3); }
#sec-bot .bot-import-check,
#sec-import .bot-import-check { margin-top: 3px; flex: none; }
#sec-bot .bot-import-row-head,
#sec-import .bot-import-row-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
#sec-bot .bot-import-resp,
#sec-import .bot-import-resp { margin-top: 2px; }
#sec-bot .bot-import-result,
#sec-import .bot-import-result { margin-top: var(--space-4); }

/* Settings > Import page (sec-import.js): the points-import panel (points-import.js)
   reuses the base component look; align its mode-picker + token link with the
   command panel so the two cards read as one system. Tokens only. */
#sec-import .points-import-modes { display: flex; flex-direction: column; gap: var(--space-2); }
#sec-import .points-import-mode-opt { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 2px var(--space-2); }
#sec-import .points-import-mode-opt > input { grid-row: 1 / span 2; align-self: center; }
#sec-import .points-import-mode-opt > .help { grid-column: 2; }
#sec-import .points-import-token-link { display: inline-block; margin-top: var(--space-1); font-weight: 600; text-decoration: underline; }
#sec-import .points-import-steps { margin: var(--space-1) 0 0; padding-left: 1.4em; display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--text-dim); }
#sec-import .points-import-preview,
#sec-import .points-import-result { margin-top: var(--space-4); }

/* ═══════════════════════════════════════════════════════════════════════
   Bot — OBSERVABILITY-FIRST rebuild (dashboard redesign R2b, approved mockup
   2026-08-14). Everything below is NEW surface: the per-channel status strip,
   the activity log, the quick-controls card, the searchable tables that replaced
   the stacked create-forms, the rail modal that replaced the inline row
   expanders, and the test console. It reuses the --bot-* token block declared at
   the top of the Bot section (no new palette, no hardcoded hex) and every
   selector stays scoped to #sec-bot.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── status strip: one chip per own channel ── */
#sec-bot .bot-strip {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin: var(--space-3) 0;
}
#sec-bot .bot-chan-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 7px 11px; font-size: 12.5px;
  background: var(--bot-surface); border: 1px solid var(--bot-border);
  border-radius: var(--bot-r-sm); color: var(--bot-text);
}
#sec-bot .bot-chan-chip--empty { display: block; color: var(--bot-faint); }
#sec-bot .bot-chan-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--bot-faint); }
#sec-bot .bot-chan-dot--ok { background: var(--ok, var(--bot-accent-2)); }
#sec-bot .bot-chan-dot--warn { background: var(--warn, var(--bot-accent)); }
#sec-bot .bot-chan-state { color: var(--bot-faint); font-size: 11.5px; }
#sec-bot .bot-chan-fix {
  font: inherit; font-size: 11px; font-weight: 700; cursor: pointer;
  padding: 4px 9px; border: 0; border-radius: var(--bot-r-chip);
  background: var(--bot-accent-wash); color: var(--bot-accent-2);
}
#sec-bot .bot-chan-fix:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: 2px; }
#sec-bot .bot-chan-fix[disabled] { opacity: .55; cursor: default; }
@media (hover: hover) { #sec-bot .bot-chan-fix:hover { background: var(--bot-accent-line); color: var(--bot-text); } }
/* The header Mute button reads as ARMED while the bot is silenced — the one
   control on this page whose state must be obvious without reading the label. */
#sec-bot .btn.bot-muted { background: var(--bot-accent-wash); border-color: var(--bot-accent-line); color: var(--bot-accent-2); }

/* ── OVERVIEW: activity log beside quick controls ── */
#sec-bot .bot-overview {
  display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: var(--space-4);
}
@media (max-width: 900px) { #sec-bot .bot-overview { grid-template-columns: 1fr; } }
#sec-bot .bot-log-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
#sec-bot .bot-log-chip {
  font: inherit; font-size: 11px; font-weight: 600; cursor: pointer;
  padding: 4px 11px; border-radius: var(--bot-r-pill);
  border: 1px solid var(--bot-border); background: transparent; color: var(--bot-dim);
}
#sec-bot .bot-log-chip.on { background: var(--bot-accent-wash); color: var(--bot-accent-2); border-color: var(--bot-accent-line); }
#sec-bot .bot-log-chip:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: 2px; }
#sec-bot .bot-log-list { display: flex; flex-direction: column; max-height: 60vh; overflow-y: auto; }
#sec-bot .bot-log-row {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: 7px 4px; border-bottom: 1px solid var(--bot-border); font-size: 12.5px;
}
#sec-bot .bot-log-row:last-child { border-bottom: 0; }
#sec-bot .bot-log-t { flex: none; width: 3.6rem; font-size: 11px; color: var(--bot-faint); }
#sec-bot .bot-log-tag {
  flex: none; font-size: 9.5px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; padding: 2px 7px; border-radius: var(--bot-r-pill);
  background: var(--bot-surface-2); color: var(--bot-dim); border: 1px solid var(--bot-border);
}
#sec-bot .bot-log-tag--command { background: var(--bot-accent-wash); color: var(--bot-accent-2); border-color: transparent; }
#sec-bot .bot-log-tag--audit { background: var(--bot-surface-2); color: var(--bot-dim); }
#sec-bot .bot-log-m { color: var(--bot-dim); min-width: 0; word-break: break-word; }
#sec-bot .bot-log-keep { margin-left: auto; flex: none; font-size: 9.5px; letter-spacing: .05em; color: var(--bot-faint); }
#sec-bot .bot-log-live {
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--bot-accent-2); padding: 2px 8px; border-radius: var(--bot-r-pill);
  background: var(--bot-accent-wash);
}
#sec-bot .bot-log-more { margin-top: var(--space-3); display: flex; justify-content: center; }
#sec-bot .bot-qc-block { display: flex; align-items: flex-end; gap: var(--space-2); }
#sec-bot .bot-qc-block > .input { flex: 1; min-width: 0; }
#sec-bot .bot-qc-head { font-size: 13px; margin: var(--space-4) 0 var(--space-2); color: var(--bot-text); }
#sec-bot .bot-qc-copy { font-size: 12px; color: var(--bot-dim); line-height: 1.5; margin: 0 0 var(--space-2); }
#sec-bot .bot-qc-copy b { color: var(--bot-text); }
#sec-bot .bot-qc-mute { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }

/* ── pane toolbars + TABLES ── */
#sec-bot .bot-pane-toolbar {
  display: flex; align-items: center; gap: var(--space-2);
  flex-wrap: wrap; margin-bottom: var(--space-3);
}
#sec-bot .bot-pane-search { flex: 1 1 12rem; min-width: 0; }
#sec-bot .bot-pane-toolbar-actions { display: inline-flex; gap: var(--space-2); flex: none; }
#sec-bot .bot-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
#sec-bot .bot-table th {
  text-align: left; padding: 7px 9px; font-size: 10px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--bot-faint);
  border-bottom: 1px solid var(--bot-border); white-space: nowrap;
}
#sec-bot .bot-table td {
  padding: 8px 9px; border-bottom: 1px solid var(--bot-border);
  color: var(--bot-dim); vertical-align: middle;
}
#sec-bot .bot-table tbody tr:last-child td { border-bottom: 0; }
#sec-bot .bot-tr { cursor: pointer; }
#sec-bot .bot-tr:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: -2px; }
@media (hover: hover) { #sec-bot .bot-tr:hover > td { background: var(--bot-surface-2); } }
#sec-bot .bot-tr-off > td { opacity: .5; }
#sec-bot .bot-tr-off .toggle { opacity: 1; }
#sec-bot .bot-tr-empty > td { border-bottom: 0; }
#sec-bot .bot-td-toggle, #sec-bot .bot-th-toggle { width: 44px; }
#sec-bot .bot-td-name { color: var(--bot-text); font-weight: 600; white-space: nowrap; }
#sec-bot .bot-td-name .mono { color: var(--bot-accent-2); }
#sec-bot .bot-td-resp {
  max-width: 22rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#sec-bot .bot-td-wrap { white-space: normal; max-width: none; }
#sec-bot .bot-td-cd, #sec-bot .bot-td-last { font-size: 11px; color: var(--bot-faint); white-space: nowrap; }
#sec-bot .bot-td-actions { text-align: right; white-space: nowrap; }
#sec-bot .bot-td-actions > .btn + .btn { margin-left: var(--space-1); }
#sec-bot .bot-th-actions { width: 8.5rem; }
#sec-bot .bot-pane-host { min-height: 4rem; }

/* ── the two OVERRIDE controls on a feature/built-in row (built-in command
   overrides C5, 2026-09-12). The Cooldown cell used to hold a literal em dash
   and the Min. level cell unchangeable text; both are real controls now, so the
   cell has to stop being `white-space: nowrap` faint text. A verb can have a
   CHANNEL window, a PER-VIEWER one, or both (only !pyramid has both today), so
   the cell stacks whatever that verb actually has, each field captioned - an
   unlabelled pair of numbers is two numbers nobody can tell apart. ── */
#sec-bot .bot-td-cd .bot-cd-cell { display: flex; flex-direction: column; gap: var(--space-1); }
#sec-bot .bot-cd-row { display: flex; align-items: center; gap: var(--space-1); }
#sec-bot .bot-cd-lab { flex: 0 0 3.6rem; font-size: 11px; color: var(--bot-faint); }
#sec-bot .bot-cd-in { width: 4.5rem; padding: 4px 6px; font-size: 11px; }
#sec-bot .bot-td-level { white-space: normal; }
#sec-bot .bot-td-level .bot-cmd-aliases { display: block; }
/* CUSTOMIZE OUTCOMES (coinflip v2). A native <details> in the description cell:
   it opens with no script and costs the grid no seventh column that would be
   empty on every other row. The bot portal's twin lives in public/bot/shell.css
   under .feat-coin; the two are hand-synced copies like the rest of this pane. */
#sec-bot .bot-td-resp .bot-coin { margin-top: var(--space-1); }
#sec-bot .bot-coin > summary { cursor: pointer; font-size: 11px; color: var(--bot-faint); }
#sec-bot .bot-coin > .dim { margin: var(--space-1) 0; font-size: 11px; }
#sec-bot .bot-coin .bot-cd-row { margin-top: 4px; }
#sec-bot .bot-coin-in { width: 9rem; padding: 4px 6px; font-size: 11px; }
/* CHANNEL ALIASES (built-in command overrides C7). They sit in the NAME cell,
   beside the shipped `also: ...` line, because that is where a creator already
   reads what else a command answers to. The add box is small and always present:
   a reveal toggle would be one more piece of state on a row that already has
   four controls. */
#sec-bot .bot-alias-chip { display: inline-flex; align-items: center; gap: 3px; margin-right: var(--space-1); }
#sec-bot .bot-alias-x { background: none; border: 0; color: var(--bot-faint); font: inherit; line-height: 1; padding: 0 1px; cursor: pointer; }
#sec-bot .bot-alias-x:hover { color: var(--bot-ink, inherit); }
#sec-bot .bot-alias-x[aria-disabled="true"] { opacity: .5; cursor: default; }
#sec-bot .bot-alias-in { width: 5.5rem; padding: 3px 6px; font-size: 11px; margin-top: var(--space-1); }

/* ══ THE BUILT-IN COMMANDS CARD (commands-unified U2, 2026-09-13) ═══════════
   The Features sub-page's per-group tables are gone: one card under the
   streamer's own commands, a live filter, and a collapsible group per family.
   A ROW IS A FLOW, not a six-column table row — every control a built-in has is
   small, and as columns each was a fixed width the longest verb in the product
   (`!remind <user|me> <message> [in <time>]`) had to wrap inside forever.
   Hand-synced with public/bot/shell.css's `.bi-*` block, as the rest of this
   pane is; the class names are deliberately identical so the two read alike. */
#sec-bot .bi-grp { border: 1px solid var(--bot-border); border-radius: var(--bot-r-sm); margin-bottom: var(--space-2); }
#sec-bot .bi-grp > summary {
  cursor: pointer; padding: var(--space-2) var(--space-3);
  display: flex; gap: var(--space-2); align-items: center; list-style: none;
  font-weight: 600; font-size: 12.5px; color: var(--bot-text);
}
#sec-bot .bi-grp > summary::-webkit-details-marker { display: none; }
#sec-bot .bi-grp > summary::before { content: '\25B8'; color: var(--bot-faint); transition: transform .12s; }
#sec-bot .bi-grp[open] > summary::before { transform: rotate(90deg); }
#sec-bot .bi-grp-name { color: var(--bot-text); }
#sec-bot .bi-grp-n { color: var(--bot-faint); font-weight: 500; font-size: 11px; }
/* Every hint the summary carries ("3 off", "Managed by Ads settings") pushes to
   the right edge; the FIRST takes the auto margin so a group with both keeps
   them together rather than spreading one to each end. */
#sec-bot .bi-grp-hint { color: var(--bot-faint); font-weight: 500; font-size: 11px; }
#sec-bot .bi-grp-hint:first-of-type { margin-left: auto; }
#sec-bot .bi-row {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-2) var(--space-3); border-top: 1px solid var(--bot-border);
}
/* An OFF row dims in place rather than moving or hiding: a streamer looking for
   the switch they flipped must find it where they left it. */
#sec-bot .bi-row-off { opacity: .55; }
#sec-bot .bi-row-off .toggle { opacity: 1; }
#sec-bot .bi-name { flex: 0 0 auto; min-width: 9rem; display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
#sec-bot .bi-name .mono { color: var(--bot-accent-2); font-weight: 600; }
#sec-bot .bi-desc { flex: 1 1 14rem; min-width: 11rem; font-size: 12px; color: var(--bot-faint); }
#sec-bot .bi-desc .bot-cmd-aliases { margin-left: var(--space-1); }
#sec-bot .bi-aliases { flex: 0 0 auto; display: flex; align-items: center; flex-wrap: wrap; }
#sec-bot .bi-ctl { flex: 0 0 auto; }
#sec-bot .bi-ctl-lvl { width: 9.5rem; }
#sec-bot .bi-ctl-cd { width: 10.5rem; }
#sec-bot .bi-ctl-gate { width: 10rem; }
#sec-bot .bi-ctl select { width: 100%; }
#sec-bot .bi-ctl .bot-cd-cell { display: flex; flex-direction: column; gap: var(--space-1); }
#sec-bot .bi-ctl .bot-cmd-aliases { display: block; font-size: 11px; }
/* The switch is last in the row and stays last: it is the control a streamer
   reaches for most, so it wants one predictable place. */
#sec-bot .bi-sw { flex: 0 0 auto; margin-left: auto; }

/* ── the RAIL MODAL (one dialog, every editor) ── */
#sec-bot .bot-rail-box { width: min(760px, 100%); }
#sec-bot .bot-rail { display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: var(--space-4); }
@media (max-width: 640px) { #sec-bot .bot-rail { grid-template-columns: 1fr; } }
#sec-bot .bot-rail-nav { display: flex; flex-direction: column; gap: 2px; }
@media (max-width: 640px) {
  #sec-bot .bot-rail-nav { flex-direction: row; flex-wrap: wrap; border-bottom: 1px solid var(--bot-border); padding-bottom: var(--space-2); }
}
#sec-bot .bot-rail-tab {
  font: inherit; font-size: 12.5px; font-weight: 600; text-align: left; cursor: pointer;
  padding: 8px 11px; border: 0; border-radius: var(--bot-r-sm);
  background: transparent; color: var(--bot-dim);
}
#sec-bot .bot-rail-tab.on { background: var(--bot-accent-wash); color: var(--bot-accent-2); }
#sec-bot .bot-rail-tab:focus-visible { outline: 2px solid var(--bot-accent-line); outline-offset: 2px; }
#sec-bot .bot-rail-pane { display: flex; flex-direction: column; gap: var(--space-3); }
#sec-bot .bot-rail-pane[hidden] { display: none; }
#sec-bot .bot-rail-pane .bot-livegate { height: auto; max-width: none; font-size: inherit; color: var(--bot-text); }
#sec-bot .bot-lbl-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
#sec-bot .bot-charcount { font-size: 10px; color: var(--bot-faint); font-weight: 400; }
#sec-bot .bot-code { min-height: 9rem; font-family: var(--font-mono); font-size: 12px; }
#sec-bot .bot-varref { max-height: 60vh; overflow-y: auto; }

/* ── TEST CONSOLE ── */
#sec-bot .bot-tc { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-4); }
@media (max-width: 900px) { #sec-bot .bot-tc { grid-template-columns: 1fr; } }
#sec-bot .bot-tc-roles { border: 0; padding: 0; margin: var(--space-3) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-3); }
#sec-bot .bot-tc-roles > legend {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--bot-faint); padding: 0;
}
#sec-bot .bot-tc-role { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--bot-dim); cursor: pointer; }
#sec-bot .bot-tc-role > input { accent-color: var(--bot-accent); }
#sec-bot .bot-tc-level { font-size: 12px; color: var(--bot-faint); margin-bottom: var(--space-2); }
#sec-bot .bot-tc-level b { color: var(--bot-text); }
#sec-bot .bot-tc-trace {
  border: 1px solid var(--bot-border); border-radius: var(--bot-r-sm);
  background: var(--bot-body-inset); padding: var(--space-3); font-size: 12.5px;
}
#sec-bot .bot-tc-row { display: flex; gap: var(--space-2); padding: 5px 0; align-items: baseline; }
#sec-bot .bot-tc-ico { flex: none; width: 1rem; text-align: center; }
#sec-bot .bot-tc-ok { color: var(--ok, var(--bot-accent-2)); }
#sec-bot .bot-tc-skip { color: var(--bot-faint); }
#sec-bot .bot-tc-fire { color: var(--bot-accent-2); }
#sec-bot .bot-tc-blocked { color: var(--bot-danger); }
#sec-bot .bot-tc-d { color: var(--bot-dim); min-width: 0; word-break: break-word; }
#sec-bot .bot-tc-reply {
  margin-top: var(--space-3); padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--bot-accent); background: var(--bot-accent-wash);
  border-radius: 0 var(--bot-r-sm) var(--bot-r-sm) 0; color: var(--bot-text); font-size: 12.5px;
}
#sec-bot .bot-tc-reply--none { border-left-color: var(--bot-border); background: var(--bot-surface-2); color: var(--bot-faint); }
#sec-bot .bot-tc-note { font-size: 11.5px; margin-top: var(--space-2); }
#sec-bot .bot-tc-note code { font-size: 11px; color: var(--bot-accent-2); }

/* ── Songs & Media (#sec-songs) spacing pass ──────────────────────────────────
   The section reuses the shared components as-is; everything here is SPACING
   and grouping, scoped to this section so no other page's geometry moves.
   Tokens only (--space-*), no new colors.

   ── 🔴 WHICH RULES BELOW CARRY `#sec-songs`, AND WHY (2026-08-18) ──
   A rule whose subject is a SHARED class (`.card`, `.sec-row`, `.sec-actions`,
   `.rc-item`, `.sec-list`) keeps the page scope: it re-tunes a component every
   other page also renders, so it must not escape this section. That is the
   handful directly below.

   A rule whose subject is a `.songs-*` class does NOT, because those classes
   belong to the COMPONENT, not to the page — and the component has three mounts:
     1. `#sec-songs`            the Songs & Media Settings tab (setup page, the
                                guided flow, the how-viewers-pay wizard);
     2. `#sec-alerts`           Alerts & Overlays' **Text to speech** tab, which
                                mounts the SAME cost wizard at `[data-tts-pay-host]`
                                (sec-tts.js is a panel module now, redesign R4b);
     3. the Pyre.Bot portal     via its own scoped copy, `public/bot/songs-flow.css`.
   Mount 2 was rendering the wizard's markup with NO layout at all — every rule it
   needed was locked behind an id it does not sit under. Dropping the id from the
   `.songs-*` rules is the fix, and it cannot leak: a `.songs-*` selector only
   matches markup the songs/wizard modules emit. `songs/geometry.test.js` pins
   that the wizard shell stays reachable from outside `#sec-songs`. */

/* Two .sec-body blocks in ONE card (the queue list + the add-a-track row; the
   source list + its OBS note) sat flush against each other: `.card > .sec-body`
   zeroes the standalone padding and only the FIRST body gets the card-head's
   padding-top, so the second started on the pixel the first ended. Same for a
   trailing .sec-actions (the settings Save row, each blocklist add form). Give
   the follow-on block the card-head's own divider + rhythm. */
#sec-songs .card > .sec-body + .sec-body,
#sec-songs .card > .sec-body + .sec-actions {
  padding-top: var(--space-4); border-top: 1px solid var(--border);
}
#sec-songs .card > .sec-body + .sec-actions { align-items: center; }
/* A .sec-actions add form (the three blocklist cards) holds a full-width .input
   beside its button: `width:100%` makes the input claim the whole line and the
   button wrap under it. Let the pair share one row, the way .sec-copyrow does. */
#sec-songs .sec-actions > .input.grow { flex: 1 1 12rem; width: auto; min-width: 8rem; }
/* Every row in this section aligns to the CARD's padding edge instead of the
   .sec-row's own inline padding, so switch rows, reward rows and the .field
   captions below them share one left edge instead of stepping 8px in and out. */
#sec-songs .sec-row { padding-left: 0; padding-right: 0; }
/* Row subtitles here are full sentences, not two-word hints. */
#sec-songs .sec-row .grow .s { line-height: 1.5; }
/* Stacked notes that explain the same control read as one block. */
.songs-notes { display: flex; flex-direction: column; gap: var(--space-2); }

/* ── THE SETUP-FIRST SETTINGS PAGE (music redesign M1-6, uncapped 2026-08-18) ──
   Four rules, one ruling: MORE CARDS, NOT WIDER CARDS — which is a rule about
   the CARDS, and M1-6 mistook it for a rule about the page. The column carried
   `max-width: var(--w-col)` and threw the rest of a wide monitor away; it does
   not any more (owner report 2026-08-18). The column FILLS the content region,
   the summary spans it, a field-stack card still caps at the width of the
   control it holds, and the card grid adds a column rather than stretching one —
   so the reclaimed width becomes cards per row, which is what the ruling asked
   for in the first place. Every gap is on a CONTAINER — the spacing ratchet's
   budget for inline margins is zero and its shared-class budget only ever goes
   down. Pinned by sections/songs/geometry.test.js. */
.songs-col { display: flex; flex-direction: column; gap: var(--space-4); }
.songs-col > [hidden] { display: none; }
.songs-sum { max-width: none; display: flex; flex-direction: column; }
/* The FIELD-STACK cards the per-section edit views are built from. Declared
   with the tokens they belong to rather than beside the views that arrive
   later, so the geometry ruling lands as one readable block. */
.songs-card { max-width: var(--w-card); }
.songs-grid { display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr)); }
/* A summary row's left-hand caption: a fixed measure so six answers line up in
   one column instead of stepping in and out with the length of their label. */
.songs-setup-k { flex: none; width: 9.5rem; color: var(--text-dim); font-size: 12px; }
/* The summary card's head carries two actions beside its title. */
.songs-sum-acts { display: inline-flex; align-items: center; gap: var(--space-2); }

/* ── THE GUIDED FLOW (music redesign M2-1, uncapped 2026-08-18) ──
   The wizard shell: a head, the five-step strip, one step's body, a footer.
   EVERY GAP IS ON A CONTAINER here too, so a step body can add or drop a strip
   without any of them carrying spacing of its own.
   IT CARRIES NO WIDTH CAP. The owner's 2026-08-18 report named the wizard
   specifically ("it looks bad especially on the wizard, when there's available
   width"): a wizard is the ONE surface a creator is asked to work straight
   through, so a 62rem box in a 1600px column is where dead space is most
   obvious. The option grid below adds tracks instead, and every long-form line
   inside carries `--w-prose` so nothing smears at the width the wizard now
   takes. */
.songs-wz {
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-5);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
}
.songs-wz-head { display: flex; flex-direction: column; gap: var(--space-2); }
.songs-wz-head > h2 { margin: 0; font-size: 17px; }
.songs-wz-head > .s { color: var(--text-dim); font-size: 12.5px; line-height: 1.6; max-width: var(--w-prose); }
.songs-wz-body { display: flex; flex-direction: column; gap: var(--space-4); }
.songs-wz-foot {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding-top: var(--space-4); border-top: 1px solid var(--border);
}
.songs-wz-foot > .s { color: var(--text-faint); font-size: 12px; }
.songs-wz-foot > .grow { flex: 1 1 auto; }

/* The step strip. A SKIPPED step is legible, not hidden: it keeps its slot and
   its dash so "step 3 of 5" means the same thing to everybody. */
.songs-steps { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.songs-step {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: 12px; color: var(--text-faint);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-pill);
}
.songs-step > .n {
  display: inline-grid; place-items: center; width: 18px; height: 18px;
  font-size: 11px; font-weight: 700; border-radius: var(--r-pill);
  background: var(--surface-2); color: var(--text-dim);
}
.songs-step.is-on { color: var(--text); border-color: var(--border-accent); }
.songs-step.is-on > .n { background: var(--accent); color: var(--on-strong); }
.songs-step.is-done { color: var(--text-dim); }
.songs-step.is-done > .n { color: var(--ok); }
.songs-step.is-skip { opacity: .6; }

/* THE PAYOUT JOURNEY reuses the two shapes above (in-Pyre onboarding W2-1): the
   Tips pane's payout takeover mounts `.songs-steps` / `.songs-step` and
   `.songs-wz-foot` rather than minting a second wizard language, and composes the
   rest of itself from `.stack` / `.well` / `.sec-grid` / `.sec-item-title`. This
   is the ONE rule it needs of its own. Same caption treatment as the cockpit TTS
   card's volume label (and the `.sec-row > .lbl` rule), so the five bare `.lbl`s
   inside the takeover — the "Payout setup" kicker, "What you will need", and the
   two status read-back tiles' keys — read as intentional captions rather than
   unstyled body text. */
[data-payout-wz] .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim);
}

/* A question head and an advisory strip: the two shapes every step is built
   from. The strip's tone is a border and a glyph, never a background wash that
   would need its own text color. */
.songs-q { display: flex; flex-direction: column; gap: var(--space-2); }
.songs-q > .q { font-size: 16px; font-weight: 600; }
.songs-q > .qs { margin: 0; color: var(--text-dim); font-size: 12.5px; line-height: 1.6; max-width: var(--w-prose); }
.songs-strip {
  display: flex; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong); border-radius: var(--r-sm);
}
.songs-strip > p { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose); }
.songs-strip.is-acc { border-left-color: var(--accent); }
.songs-strip.is-ok { border-left-color: var(--ok); }
.songs-strip.is-warn { border-left-color: var(--warn); }

/* THE OPTION CARDS (M2-2). More cards, not wider cards, one level down: the
   options auto-fit at a 20rem min inside the wizard rather than stacking full
   width, so three answers are comparable at a glance instead of scrolled
   through. The control is part of the card, and which control it is carries
   meaning: a checkbox is one of a set, a radio replaces the set. */
.songs-opts { display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
.songs-opts > .opt {
  position: relative;
  display: grid; grid-template-columns: auto auto 1fr; align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm); cursor: pointer;
}
.songs-opts > .opt.is-sel { border-color: var(--border-accent); background: var(--accent-wash); }
.songs-opts > .opt > h4,
.songs-opts > .opt > .why,
.songs-opts > .opt > .pick,
.songs-opts > .opt > .songs-facts { grid-column: 1 / -1; margin: 0; }
.songs-opts > .opt > h4 { font-size: 14px; }
/* The two long-form lines on an option card. Capped at the measure every other
   secondary line in this file uses (uncapping, 2026-08-18): the cards are grid
   tracks now and a track on an ultrawide is wider than a sentence wants to be. */
.songs-opts > .opt > .why,
.songs-opts > .opt > .pick {
  font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose);
}
.songs-opts > .opt > .pick { color: var(--text-faint); }
.songs-glyph { font-size: 18px; line-height: 1; }
.songs-ribbon {
  position: absolute; top: var(--space-2); right: var(--space-2);
  padding: 2px 8px; font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--on-strong); background: var(--accent); border-radius: var(--r-pill);
}
/* The two controls. A checkbox reads as a box, a radio as a ring: the shape is
   the affordance that says "one of these replaces the others". */
.songs-opts .box2,
.songs-opts .radio {
  width: 18px; height: 18px; flex: none;
  border: 1px solid var(--border-strong); background: var(--surface);
}
.songs-opts .box2 { border-radius: var(--r-chip); }
.songs-opts .radio { border-radius: var(--r-pill); }
/* 🔴 THE TICK IS ON THE CARD, NOT ON THE BOX (fixed 2026-08-23). `aria-checked`
   is written onto the `.opt` container — the box is `aria-hidden` chrome — so
   `.box2[aria-checked="true"]` matched nothing and a ticked option never filled
   its box in either shell. Same fix in public/bot/songs-flow.css. */
.songs-opts > .opt[aria-checked="true"] > .box2,
.songs-opts > .opt[aria-checked="true"] > .radio { background: var(--accent); border-color: var(--accent); }
/* A card that is a STATE READ-OUT, not a control (the TTS skin — see
   cost-wizard.js `optionCardHtml`'s readOnly note). It carries no widget role,
   so it must not offer a widget's pointer either. */
.songs-opts > .opt.is-static { cursor: default; }
/* The fact chips: what this answer costs and what it buys, at a glance. */
.songs-facts { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.songs-facts > span {
  padding: 2px 8px; font-size: 11px; color: var(--text-faint);
  background: var(--surface-2); border-radius: var(--r-pill);
}
.songs-facts > .is-pos { color: var(--ok); }
.songs-facts > .is-neg { color: var(--warn); }

/* ── THE COST WIZARD'S ONE NEW IDIOM (cost wizard W2-9): AN OPTION CARD THAT
   UNFOLDS ITS OWN CONFIGURATION WHEN TICKED. The music flow put each step's
   settings on its own screen; here the methods COEXIST, so the settings move
   INSIDE the card that owns them and the price sits against the thing that
   charges it.
   Consequence: a configured card needs far more room than a bare one. `is-stack`
   used to answer that with `1fr` — one column, always, at any width — which is
   how the pay wizard ended up as a single narrow ribbon on a wide monitor.
   IT IS AN AUTO-FIT AT A BIGGER MINIMUM NOW (2026-08-18): the same idiom as
   `.songs-opts` / `.songs-grid` / `.songs-fgrid` / `.songs-tgrid`, only with the
   track minimum a mint row actually needs. 40rem is that number, measured rather
   than picked: 12rem name + the 8rem cost box and its unit + two verbs + three
   gaps + the card's own 2x16px padding is ~40rem before the row starts wrapping.
   So below a genuinely wide window it is still exactly one column and nothing
   changes on a laptop or a phone; above it the ways a viewer can pay sit side by
   side and are comparable at a glance, which is the whole point of putting them
   on one step.
   🔴 MIRRORED IN public/bot/songs-flow.css under `.songs-flow` — the wizard is a
   BOT_SHARED_SRCS file, so both stylesheets change together or the portal gets
   the markup without the layout. */
.songs-opts.is-stack {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 40rem), 1fr));
}
.songs-opts > .opt > .songs-cfg,
.songs-opts > .opt > .songs-strip,
.songs-opts > .opt > .songs-fold { grid-column: 1 / -1; }
.songs-opts > .opt > .songs-cfg {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  cursor: default;
}
.songs-cfg-h { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.songs-cfg-h > .grow { flex: 1 1 auto; }
/* A MINT ROW: what the reward is, what it costs, and the one or two verbs that
   apply. Wraps rather than truncates, because the cost box and the buttons are
   the point of the row. */
.songs-mintrow {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.songs-mintrow > .grow { flex: 1 1 12rem; display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.songs-mintrow > .grow > b { font-weight: 600; font-size: 12.5px; }
.songs-mintrow > .grow > .s { font-size: 11.5px; line-height: 1.6; color: var(--text-faint); max-width: var(--w-prose); }
.songs-mintrow > .s { font-size: 11.5px; line-height: 1.6; color: var(--text-faint); max-width: var(--w-prose); }
.songs-mintrow.is-live { border-color: var(--border-accent); }
.songs-costcell { display: inline-flex; align-items: center; gap: var(--space-2); }
.songs-costcell > .input { width: 8rem; }
.songs-costcell > .u { font-size: 11.5px; color: var(--text-faint); }
/* The Free card is an ABSENCE, and its outline says so before the copy does. */
.songs-opts > .opt.is-free { border-style: dashed; background: transparent; }
/* The refusal table on step 2. Rows, not a <table>: each cell is prose. */
.songs-px { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
.songs-px-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.songs-px-head > .grow { flex: 1 1 auto; }
.songs-pxrow { display: flex; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
.songs-pxrow:last-child { border-bottom: 0; }
.songs-pxrow > .k { flex: 0 0 10rem; font-size: 11.5px; font-weight: 600; color: var(--text-faint); }
.songs-pxrow > .val { min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose); }
.songs-pxrow > .val > .mini { font-size: 11.5px; color: var(--text-faint); }
.songs-pxrow > .val > .mini.is-warn { color: var(--warn); }
.songs-pxrow.is-off > .val { color: var(--text-faint); }
/* MOBILE FIRST, and deliberately not a `max-width` query: geometry.test.js
   reads every `.songs-*` rule for a literal max-width, and a media PRELUDE is
   indistinguishable from a declaration once the stylesheet is split on `}`.
   Stacked is the base; the label column arrives when there is room for it. */
.songs-pxrow { flex-direction: column; gap: var(--space-1); }
.songs-pxrow > .k { flex: none; }
@media (min-width: 44rem) {
  .songs-pxrow { flex-direction: row; gap: var(--space-3); }
  .songs-pxrow > .k { flex: 0 0 10rem; }
}

/* THE TIER ROWS (cost-wizard W3-2). The TTS skin's step 2: one block per tier,
   a head that names it, and a cell grid of its prices. A grid rather than a
   table because every cell is a labelled control and a table would make the
   labels a header row the mobile layout could not keep beside them.
   TWO STYLESHEETS, ONE SOURCE MODULE: the twin lives in
   `public/bot/songs-flow.css` under `.songs-flow`, and geometry.test.js pins
   that both carry these selectors. Gap on the container, never a margin. */
.songs-tiers { display: flex; flex-direction: column; gap: var(--space-3); }
.songs-tier {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.songs-tier.is-base { border-color: var(--border-accent); }
.songs-tier > .th { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.songs-tier > .th > .grow { flex: 1 1 auto; }
.songs-tier > .th > .ord { font-size: 11.5px; color: var(--text-faint); }
.songs-tgrid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.songs-tgrid > .cell { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.songs-tgrid > .cell > .cl { font-size: 11.5px; font-weight: 600; color: var(--text-faint); }
.songs-tgrid > .cell > .cv { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.songs-tgrid > .cell > .cv > .help { flex: 1 1 100%; font-size: 11.5px; line-height: 1.6; color: var(--text-faint); }
.songs-tgrid > .cell > .cv .input { min-width: 0; }

/* THE DISCLOSURE FOLD (M2-3). What the redesign demotes the ~990 words of
   constant prose INTO: present, one click deep, and closed by default. The
   summary is the affordance, so it carries the pointer and the whole hit area. */
.songs-fold {
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.songs-fold > summary {
  padding: var(--space-3) var(--space-4);
  font-size: 12.5px; font-weight: 600; color: var(--text-dim); cursor: pointer;
}
.songs-fold-body {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: 0 var(--space-4) var(--space-4);
}
/* THE PER-SECTION EDIT VIEWS (M2-6). A way back, the card, a save bar. The
   card caps at --w-card and the Advanced fold's numeric fields auto-fit at a
   15rem min, so six of them sit two or three up rather than in a column of
   narrow boxes in a wide card. */
.songs-view-head { display: flex; align-items: center; gap: var(--space-2); }
.songs-view-head > .grow { flex: 1 1 auto; }
.songs-savebar {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4); border-top: 1px solid var(--border);
}
.songs-savebar > .grow { flex: 1 1 auto; }
.songs-fgrid { display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
/* The legacy blocklist field: present for the rows that have one, invisible for
   everyone else (see legacyBlocklistHtml). */
.songs-legacy.hide { display: none; }

/* A MIGRATION NOTICE (M4-2 / M4-4). Something that changed underneath the
   creator, on the surface it changed. Inset rather than loud: the non-blocking
   one is news about a source that still works, and the blocking one earns its
   emphasis from the accent border rather than from a color nobody asked for.
   A gap container, so no child ever needs a margin (the spacing ratchet). */
.songs-notice {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.songs-notice.is-blocking { border-color: var(--border-accent); }

/* THE SPOTIFY CHECKLIST (M2-4). Six boxes the creator works through once, in
   the mock's shape: a tick column and a text column, with the CURRENT box lit.
   The shipped wizard's copy and machinery are unchanged underneath. */
.songs-chk { display: flex; flex-direction: column; gap: var(--space-3); }
.songs-chk > .ck {
  display: grid; grid-template-columns: auto 1fr; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.songs-chk > .ck.is-now { border-color: var(--border-accent); }
.songs-chk > .ck > .box {
  display: inline-grid; place-items: center; width: 20px; height: 20px;
  font-size: 11px; border-radius: var(--r-pill);
  color: transparent; background: var(--surface-2); border: 1px solid var(--border-strong);
}
.songs-chk > .ck.is-done > .box { color: var(--on-strong); background: var(--ok); border-color: var(--ok); }
/* 🔴 THE BOX IS A BUTTON (owner report 2026-08-18: "I can't do anything in the
   wizard"). It shipped as an aria-hidden span with no handler on either surface,
   so the checklist could be read and never worked through. The button carries
   the SAME geometry as the span did; only the reset and the hit area are new. */
.songs-chk > .ck > button.box {
  appearance: none; padding: 0; position: relative;
  font: inherit; font-size: 11px; cursor: pointer;
}
/* A 20px tick is not a touch target. The hit area grows with a transparent
   ::after rather than with width, so the checklist's geometry is untouched. */
.songs-chk > .ck > button.box::after {
  content: ""; position: absolute; inset: -12px; border-radius: var(--r-pill);
}
.songs-chk > .ck > button.box:focus-visible {
  outline: 2px solid var(--border-accent); outline-offset: 2px;
}
.songs-chk > .ck > .txt { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.songs-chk > .ck > .txt > p { margin: 0; font-size: 12.5px; line-height: 1.6; max-width: var(--w-prose); }
/* The one link in the flow: step 1's developer-dashboard address, wrapped where
   it already stands (setup-flow.js `linkAddresses`). */
.songs-chk > .ck > .txt a { color: var(--accent); }
/* Every small print line in the wizard, capped at the same measure as the
   paragraphs above it (uncapping, 2026-08-18) — the wizard fills the content
   region now, and an 11.5px line is the first thing to become unreadable at
   1600px. */
.songs-chk .mini,
.songs-wz .mini {
  font-size: 11.5px; line-height: 1.6; color: var(--text-faint); max-width: var(--w-prose);
}
/* `.songs-copyrow` is the SHIPPED Spotify wizard's row, further down this file.
   The flow reuses it rather than declaring a second one: it is the same control
   doing the same job, and two rules would be two behaviors one reword apart. */

/* The strip's eyebrow: "You told us", quoting step 1 back at the creator. */
.songs-strip .said {
  display: inline-block; margin-right: var(--space-2);
  padding: 1px 7px; font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--on-strong); background: var(--accent-soft); border-radius: var(--r-pill);
}

/* The now-playing transport: three groups (transport / volume / the one
   destructive verb), not eight loose controls at a single gap. The volume label
   and its slider travel together; a bare .lbl in a flex row has no .field or
   .sec-row rule to style it, so it gets the same caption treatment here. */
.songs-transport { align-items: center; row-gap: var(--space-2); }
.songs-btns { display: inline-flex; align-items: center; gap: var(--space-2); }
.songs-vol { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
.songs-vol > .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
.songs-clear { margin-left: auto; }

/* Queue rows carry FIVE controls (the ▲▼ pair, Top, the Block menu, Remove)
   beside a drag handle, a position chip, the title and a duration. Two moves
   buy the title back its width: the arrows become one tight pair (they ARE one
   control) with compact padding, and both chips stop padding like buttons. */
#sec-songs .rc-item { gap: var(--space-2) var(--space-3); }
#sec-songs .rc-item > .chip { padding: 4px 10px; white-space: nowrap; font-variant-numeric: tabular-nums; }
#sec-songs .rc-item-actions { align-items: center; row-gap: var(--space-2); }
.songs-move { display: inline-flex; align-items: center; gap: var(--space-1); }
.songs-move .btn { padding-left: var(--space-2); padding-right: var(--space-2); }

/* The one-time consent gate on the local-media card (owner 2026-08-15: the
   acknowledgement was "way too hard to spot" as a toggle). It reads as a panel
   rather than a note: its own ground, an accent edge, and the area it guards
   dimmed and inert behind it (a disabled <fieldset>, so the browser itself
   refuses focus and clicks). Tokens only.

   Its bottom margin is GONE (music redesign M1-6): it sits directly in a
   .sec-body, which is already a flex column with a space-4 gap, so the margin
   was a second invisible layer on top of it. A margin sitting next to a gap is
   exactly the drift the spacing ratchet exists to stop. */
.songs-gate {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-left: 3px solid var(--accent); border-radius: var(--r-sm);
}
.songs-gate-title { margin: 0; font-size: 15px; font-weight: 600; }
.songs-gate-copy { margin: 0; color: var(--text-dim); font-size: 12.5px; line-height: 1.6; }
.songs-locked { border: 0; padding: 0; margin: 0; min-width: 0; }
.songs-locked[disabled] { opacity: .45; }

/* THE SPOTIFY BYOK WIZARD (2026-08-17). Six numbered steps plus the redirect URI
   in a read-only copy row. An inset well rather than a bare list, because it is
   setup homework sitting inside a settings card and it has to read as a block
   the streamer works THROUGH once, not as more settings. Tokens only.

   ITS BOTTOM MARGIN STAYS, unlike .songs-gate's above (music redesign M1-6).
   The gate sits in a .sec-body, which is already a gap container, so its margin
   was a double. This block sits inside `fieldset.songs-locked`, which is not one
   — deleting the margin here would collapse the wizard against the Client ID
   field below it, in the middle of the shipped Spotify BYOK card. Turning that
   fieldset into a gap container is a change to the whole card's rhythm and
   belongs with the phase that rebuilds it (M2), not with a geometry pass. */
.songs-wizard {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); margin-bottom: var(--space-4);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.songs-wizard-steps {
  margin: 0; padding-left: var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-2);
  color: var(--text-dim); font-size: 12.5px; line-height: 1.6;
}
.songs-wizard .field { margin: 0; }
/* The URI must be readable in full and copyable in one press: the input grows,
   the button does not. `min-width: 0` keeps the input from forcing the row wider
   than the card on a narrow viewport. */
.songs-copyrow { display: flex; gap: var(--space-2); align-items: center; }
.songs-copyrow .input { flex: 1 1 auto; min-width: 0; font-family: var(--font-mono, monospace); }
.songs-copyrow .btn { flex: 0 0 auto; }

/* THE LINK BACK (S2-3, Spotify Developer Policy). Displayed metadata must link
   back to Spotify, and the two CLICKABLE now-playing surfaces are this card and
   the cockpit's Music card — the overlay has none, because a browser source
   cannot be clicked. It reads as a quiet inline link under the track rather than
   a button: it is a citation, not an action the streamer is being asked to take.
   Tokens only. */
.songs-np-listen,
.cockpit-music .music-listen {
  display: inline-block; margin-top: var(--space-2);
  color: var(--text-dim); font-size: 12px; text-decoration: underline;
  text-underline-offset: 2px;
}
.songs-np-listen:hover,
.cockpit-music .music-listen:hover { color: var(--text); }

/* Browser sources: each source is a stacked well (address + copy, Add to scene,
   the menu it reveals, the result note) rather than a one-line row, so the two
   need real separation from each other. */
#sec-songs [data-songs-srclist] .sec-list { gap: var(--space-3); }
.songs-src-head { display: flex; flex-direction: column; gap: var(--space-1); }
.songs-src-head .t { font-weight: 600; }
.songs-src-head .s { color: var(--text-dim); font-size: 11.5px; line-height: 1.5; }
.songs-src .sec-actions { align-items: center; }

/* ── THE HEARING MATRIX (music redesign M3-8) ──
   One component, three mounts: the settings page's hero card, the flow's closing
   panel, and the Pyre.Bot portal. Five fixed rows, each a label column and a
   value column; a two-studio creator sees the two halves side by side inside the
   value column rather than a merged sentence that is true of neither.
   EVERY GAP IS ON A CONTAINER, so a row can gain or lose an env block without
   any child carrying spacing of its own. */
.songs-mxwrap { display: flex; flex-direction: column; gap: var(--space-4); }
.songs-mx {
  display: flex; flex-direction: column;
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  overflow: hidden;
}
.songs-mx.is-inner { border: 0; background: transparent; }
.songs-mx .mx-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2); border-bottom: 1px solid var(--border);
}
.songs-mx .mx-head > b { font-size: 13.5px; }
.songs-mx .mxrow {
  display: grid; grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
}
.songs-mx .mxrow:first-child { border-top: 0; }
/* THE KEY ROWS are the two a creator opens this panel to read: what they hear,
   and the one setting. An accent edge, never a background wash that would need
   its own text color. */
.songs-mx .mxrow.is-key { border-left: 3px solid var(--border-accent); }
.songs-mx .mxrow > .k {
  display: flex; align-items: baseline; gap: var(--space-2);
  font-size: 12.5px; font-weight: 600; color: var(--text-dim);
}
.songs-mx .mxrow > .val {
  display: flex; flex-direction: column; gap: var(--space-2);
  font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose);
}
/* One environment's answer inside a row. The label is the whole affordance that
   says "this half is about that studio". */
.songs-mx .env { display: flex; flex-direction: column; gap: var(--space-1); }
.songs-mx .env > .lab {
  font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint);
}
.songs-mx .env > p { margin: 0; }
.songs-mx .mini { font-size: 11.5px; line-height: 1.6; color: var(--text-faint); }

/* ── THE CONFIRMATION PANEL (music redesign M3-1) ──
   Where the music plays used to be a question with a paragraph of help. It is a
   STATEMENT now, because step 1 already answered it, and nothing on it is a
   control. The eyebrow says where the answer came from. */
.songs-verdict {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-5);
  background: var(--surface-inset); border: 1px solid var(--border-accent); border-radius: var(--r-sm);
}
.songs-verdict > .eyebrow {
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint);
}
.songs-verdict > h4 { margin: 0; font-size: 16px; line-height: 1.5; }
.songs-verdict > .lead { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose); }
.songs-verdict .env { display: flex; flex-direction: column; gap: var(--space-1); }
.songs-verdict .env > .lab {
  font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint);
}
.songs-verdict .env > p { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--text-dim); max-width: var(--w-prose); }
/* The three-step by-hand checklist reuses the Spotify checklist's shape, so the
   custom path reads as the same system rather than a second design. */
.songs-verdict .mini { font-size: 11.5px; line-height: 1.6; color: var(--text-faint); }

/* ── Coming-soon placeholder page (shell.js comingSoonPanel). A category whose
   nav group is flagged `comingSoon` renders this reserved-space card instead of
   its real section — reversible: un-flag the group to restore the real page.
   Tokens only. ── */
.soon-wrap { display: grid; place-items: center; padding: 60px 20px; }
.soon-card {
  position: relative; max-width: 520px; width: 100%; text-align: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 44px 36px; box-shadow: var(--shadow-card);
  overflow: hidden;
}
/* Soft radial accent glow behind the card contents (decorative). */
.soon-card::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 80% at 50% -10%, var(--accent-wash), transparent 60%);
}
.soon-card > * { position: relative; }
.soon-ic {
  width: 64px; height: 64px; margin: 0 auto var(--space-4); display: grid;
  place-items: center; font-size: 30px; border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--border-strong);
}
.soon-card .soon-pill { display: inline-block; margin-bottom: var(--space-3); }
.soon-card h2 { font-size: 22px; margin: 0 0 var(--space-2); }
.soon-card p { color: var(--text-dim); max-width: 400px; margin: 0 auto 22px; font-size: 14px; line-height: 1.55; }
.soon-roadmap { color: var(--text-faint); font-size: 12px; }

/* ── #sec-public-profile (2026-08-15) ───────────────────────────────────────
   Three additions only; everything else on the page uses the existing card /
   field / sec-item / badge vocabulary. Tokens only, no hardcoded hex or px
   colours (DESIGN-GUIDELINES §3). */

/* The banner preview, at the SAME 1920x310 ratio the public page renders it, so
   what the creator crops to here is what visitors see. `aspect-ratio` reserves
   the box, so revealing an upload never reflows the card under it. */
#sec-public-profile .pp-banner {
  width: 100%; max-width: 560px; aspect-ratio: 1920 / 310; object-fit: cover;
  border-radius: var(--r-md); border: 1px solid var(--border);
  background: var(--surface-inset); display: block;
}
#sec-public-profile .pp-banner-empty {
  width: 100%; max-width: 560px; aspect-ratio: 1920 / 310;
  border-radius: var(--r-md); border: 1px dashed var(--border-strong);
  background: var(--surface-inset); display: grid; place-items: center; font-size: 13px;
}

/* Brand marks beside a field label and inside an auto chip. Geometry-only paths
   filled with `currentColor`, so a mark inherits the label's colour instead of
   shipping a baked-in brand hex that would fight one of the two colour schemes. */
#sec-public-profile .pp-mark-wrap {
  display: inline-flex; align-items: center; vertical-align: -3px;
  margin-right: var(--space-2); color: var(--text-dim);
}
#sec-public-profile .pp-mark-wrap .pp-mark { display: block; }

/* ── PER-MOD FEATURE ACCESS (cost-wizard plan W4-5, 2026-08-18) ──────────────
   The three tri-state toggles on a moderator row (Moderation > Team) and the
   matching block on the default-access card. Rendered by the SHARED
   public/stream/sections/mod-team.js, so public/bot/team-panel.css carries the
   same rules re-scoped for the portal and the two change together.

   THE GRANT CARD IS A TWO-BAND STACK (layout fix, 2026-08-18). W4-5 added the
   trio as a fourth child of the `.sec-item` flex ROW and asked it to break the
   line with `flex: 1 1 100%`. `.sec-item` does not wrap, so instead of breaking
   the line the trio claimed it: `.sec-item-main` (flex-basis 0) resolved to zero
   width and the moderator's name rendered one letter per line. The card now
   stacks an identity band over the trio, exactly the way `.sec-item--dest`
   stacks its summary over its detail, and only the identity band is horizontal.

   Spacing is GAP on the container, never a margin on a child (DESIGN-GUIDELINES
   §1.4.1) — public/stream/sections-spacing.test.js ratchets those to zero. */
.sec-item.mod-grant { flex-direction: column; align-items: stretch; gap: var(--space-3); }
/* Band 1: who / level / Revoke. It WRAPS, so a narrow card drops the level
   select and the actions onto their own lines instead of pinching the name. */
.mod-grant-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3); }
/* A real basis (not `flex: 1`'s zero) is what keeps the identity column from
   being shrinkable to nothing: below it the row wraps rather than crushes. */
.mod-grant-head > .sec-item-main { flex: 1 1 12rem; }
.mod-grant-head > .mod-grant-level { flex: 0 0 auto; min-width: 9rem; }
.mod-grant-head > .sec-item-actions { flex: 0 0 auto; }
/* Band 2: the trio, as many columns as fit. `min(…, 100%)` keeps a single
   column from overflowing a card narrower than the track (same guard as
   .sec-grid above). */
.mod-grant-ov, .mod-default-ov {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--space-3);
}
.mod-grant-ov { padding-top: var(--space-3); border-top: 1px solid var(--border); }
.mod-default-ov > .sec-note { grid-column: 1 / -1; }
.mod-grant-ov > .mod-ov, .mod-default-ov > .mod-ov { min-width: 0; }
/* The help line under each select carries the owner's shorthand and a sentence;
   it wraps rather than stretching the column. */
.mod-ov > .help { display: block; overflow-wrap: anywhere; }

/* ══════════════════════════════════════════════════════════════════════════
   SCHEDULE & SOCIAL (schedule-social S5). Every selector is scoped to the
   destination or to a `.sched-` class of its own, so nothing here can reach
   another page. Four small shapes and no page-level width cap: "more cards,
   not wider cards" is a rule about the CARDS, and `--w-col` was deleted
   2026-08-18. Every gap is on a CONTAINER (SPACING PRIMITIVES, top of file).
   ══════════════════════════════════════════════════════════════════════════ */

/* THE PER-ROW PRESENCE SQUARES. P is the creator's Pyre page, T is Twitch, Y is
   YouTube. Three states, and the state is never color-ONLY: an off square is
   dimmed AND its title says so, a drifting one is amber AND the row carries the
   "Out of step with …" pill beside it (DESIGN-GUIDELINES §5). Letters rather
   than brand marks because a logo cannot read as a STATE at 18px. */
.sched-appears { display: inline-flex; align-items: center; gap: var(--space-1); flex: none; }
.sched-pp {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--r-chip);
  border: 1px solid var(--border); background: var(--surface-inset);
  font-family: var(--font-mono); font-size: 9px; font-weight: 600;
  color: var(--text-faint);
}
.sched-pp.is-on { color: var(--text); border-color: var(--border-strong); }
.sched-pp.is-on.is-twitch { color: var(--p-twitch); border-color: color-mix(in srgb, var(--p-twitch) 55%, transparent); }
.sched-pp.is-on.is-youtube { color: var(--p-youtube); border-color: color-mix(in srgb, var(--p-youtube) 45%, transparent); }
.sched-pp.is-on.is-pyre { color: var(--accent-2); border-color: var(--border-accent); }
.sched-pp.is-drift { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, transparent); }

/* THE FOUR TWITCH WRITE LIMITS, stated before the opt-in. A numbered list is the
   shape: they are four distinct constraints, not one paragraph, and a creator
   has to be able to point at the one that bit them. */
.sched-limits {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding-left: var(--space-5); list-style: decimal;
}
.sched-limits > li { color: var(--text-dim); font-size: 12.5px; line-height: 1.6; max-width: var(--w-prose); }
.sched-limits > li::marker { font-family: var(--font-mono); font-size: 11px; color: var(--warn); }

/* A ROW-LEVEL EXPLANATION: the drift callout, the partner refusal, the delete
   confirmation. An inset block with a title, a sentence and its actions —
   deliberately NOT a modal (progressive over modal-first, §3.9) and NOT a side
   stripe (§3.9 bans a colored edge > 1px; the tone is carried by the pill inside
   it and by the row's own squares). */
.sched-say { display: flex; flex-direction: column; gap: var(--space-2); }
.sched-say > .sec-note { max-width: var(--w-prose); }

/* A slot row keeps the squares, the time, the title and its actions on ONE line
   until the card is genuinely too narrow, then wraps rather than crushing the
   title to one word per line (the recurring .sec-item failure). */
#sec-schedule .sec-item { flex-wrap: wrap; align-items: center; }
#sec-schedule .sched-when {
  flex: none; min-width: 11rem; font-family: var(--font-mono); font-size: 12px;
}
#sec-schedule .sched-title { flex: 1 1 10rem; min-width: 0; overflow-wrap: anywhere; color: var(--text-dim); }

/* ── NARROW WIDTHS (mobile-fix 2026-08-19) ───────────────────────────────────
   The mock was drawn desktop-first and this block shipped with no narrow step at
   all. Nothing here is a redesign: it is the same "collapse to one column, wrap
   by gap, never scroll the page sideways" contract the older panes keep, applied
   to the four shapes this destination invented. Every collection above
   (`.sec-grid.cols-3` summary, `.sec-grid.cols-2` sync/social) already carries
   the `minmax(min(…, 100%), 1fr)` guard, so those collapse on their own.
   ─────────────────────────────────────────────────────────────────────────── */
@media (max-width: 560px) {
  /* THE TIME COLUMN. `min-width: 11rem` (176px) with `flex: none` is a DESKTOP
     alignment device — it lines the times up down the list. On a phone card
     (~300px of usable row) it cannot shrink and cannot be aligned with anything,
     so it becomes 60% of the row plus an overflow below ~360px. Let it take its
     content width and shrink; the row is already `flex-wrap: wrap`. */
  #sec-schedule .sched-when { flex: 0 1 auto; min-width: 0; }
  /* And the title takes a line of its own rather than a 10rem sliver beside it —
     it is the one part of a slot row a creator actually reads. */
  #sec-schedule .sched-title { flex: 1 1 100%; }
  /* The four Twitch write limits: a decimal list still needs room for its own
     marker, but not a desktop indent. */
  .sched-limits { padding-left: var(--space-4); }
}

/* ══════════════════════════════════════════════════════════════════════════
   CLIPS (clips plan C5, mock rev 3). Every selector is scoped to the
   destination or to a `.clip-` class of its own, so nothing here reaches
   another page. Every gap is on a CONTAINER (SPACING PRIMITIVES, top of file);
   this block declares NO block margins.
   ══════════════════════════════════════════════════════════════════════════ */

/* THE LIBRARY GRID. auto-fill (not auto-fit) so a lone clip keeps a card's
   width instead of stretching a 16:9 thumbnail across the whole page. */
.clip-grid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  align-items: start;
}
/* 🔴 NO `overflow: hidden` HERE (mobile-fix 2026-08-19). It was carried to clip
   the thumbnail's square corners against the card's radius, but an ancestor with
   a non-visible overflow clips ABSOLUTELY POSITIONED descendants too, regardless
   of z-index — and that swallowed the whole download menu (`.clip-menu` opens
   `top: calc(100% + …)`, i.e. below the card's last row), which is the only route
   to the free 9:16 portrait file. The corners are clipped by the thumbnail
   itself, which already carries its own `overflow: hidden`. */
.clip-card {
  display: grid; gap: 0;
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r); min-width: 0;
}

/* 🔴 THE THUMBNAIL IS A BUTTON, NOT A PLAYER (theater-mode ruling). It carries
   the 16:9 box the embed will take, so opening the lightbox does not reflow the
   grid, and it is a real <button> so it keeps its role, its focus ring and its
   Enter/Space activation for free. */
.clip-thumb {
  position: relative; display: block; width: 100%; padding: 0; border: 0;
  aspect-ratio: 16 / 9; background: var(--surface-2); cursor: pointer;
  /* It is the card's TOP edge, so it owns the top two corners now that the card
     no longer clips (see the note on .clip-card). */
  border-radius: calc(var(--r) - 1px) calc(var(--r) - 1px) 0 0;
  overflow: hidden;
}
.clip-thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The placeholder: a clip Twitch has not settled yet has NO thumbnail URL, and
   a broken <img> glyph would read as a broken product. */
.clip-thumb--empty { display: grid; place-items: center; }
.clip-thumb-glyph, .clip-thumb-play {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: var(--r-pill); color: var(--text); font-size: 16px;
}
.clip-thumb-glyph { background: var(--surface-3); }
.clip-thumb-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  background: color-mix(in srgb, var(--p-twitch) 85%, transparent);
}
.clip-thumb-dur {
  position: absolute; right: var(--space-2); bottom: var(--space-2);
  padding: 1px 6px; border-radius: var(--r-chip);
  background: var(--inset-2); color: var(--text); font-size: 10px;
}
.clip-meta { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); min-width: 0; }
.clip-title { font-weight: 600; font-size: 12.5px; overflow-wrap: anywhere; }
.clip-stats { display: flex; gap: var(--space-3); color: var(--text-faint); font-size: 10.5px; }
.clip-acts { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.clip-acts > .sec-err { flex: 1 1 100%; }
.clip-chip--warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }

/* THE CREATE ROW: one action, one live chip, one honest note (the C9 reshape
   removed the offset picker, so there is nothing else to put here). */
.clip-createrow { align-items: center; }
.clip-live-dot { width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--ok); }
.clip-live { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }

/* THE TWO PAGE-LEVEL STATES. A banner is the amber "one more permission" line
   with its CTA; a callout is a titled block (not connected / nothing here yet).
   Tone comes from the border + a token wash, never from a color-only cue. */
.clip-banner {
  display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center;
  padding: var(--space-3) var(--space-4); border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}
.clip-banner > .sec-note { flex: 1 1 20rem; }
.clip-callout {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4); border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface-inset);
}
.clip-callout--ok { border-color: color-mix(in srgb, var(--ok) 35%, transparent); }

/* THE DOWNLOAD MENU. Anchored to its own button, so the two renderings of the
   action row (grid card and theater) place it identically. */
.clip-menu-wrap { position: relative; }
.clip-menu {
  position: absolute; left: 0; top: calc(100% + var(--space-1)); z-index: 20;
  display: grid; min-width: 15rem;
  background: var(--surface-2); border: 1px solid var(--border-strong);
  border-radius: var(--r-sm); box-shadow: var(--shadow-card); overflow: hidden;
}
.clip-menu.hide { display: none; }
.clip-menu-item {
  display: grid; gap: 2px; text-align: left; padding: var(--space-2) var(--space-3);
  border: 0; border-bottom: 1px solid var(--border);
  background: transparent; color: var(--text); font: inherit; font-size: 12px; cursor: pointer;
}
.clip-menu-item > span { color: var(--text-faint); font-size: 11px; }
.clip-menu-foot { padding: var(--space-2) var(--space-3); color: var(--text-faint); font-size: 11px; }
.clip-menu-reauth { display: grid; gap: var(--space-2); padding: var(--space-3); }

/* THE POST COMPOSER (C5-2). Progressive disclosure INSIDE the action row, not a
   second modal stacked over the theater (§3.9 prefers progressive over
   modal-first). It spans its row so the message field has real width. */
.clip-composer-slot { flex: 1 1 100%; }
.clip-composer {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border: 1px solid var(--border);
  border-radius: var(--r-sm); background: var(--surface-inset);
}
.clip-composer textarea { width: 100%; resize: vertical; }
.clip-composer-targets {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  border: 0; padding: 0;
}
.clip-composer-targets > .lbl { flex: 1 1 100%; padding: 0; }

/* ══ 🔴 THEATER MODE ══════════════════════════════════════════════════════
   The lightbox reuses the shell's .sec-modal backdrop and only widens the box:
   a 16:9 player capped at 90vw / 85vh, centered over a dimmed page. The fade is
   a single opacity animation, and it is disabled under prefers-reduced-motion
   (belt: shell.css already kills every animation under that query, and this
   local rule keeps the promise testable inside this block).
   ═════════════════════════════════════════════════════════════════════════ */
.sec-modal[data-clip-theater] { animation: clip-theater-in var(--dur) var(--ease); }
.sec-modal[data-clip-theater] .clip-theater-box {
  width: min(90vw, 64rem); max-width: 90vw; max-height: 85vh;
  display: flex; flex-direction: column; gap: var(--space-2);
  background: var(--surface); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-modal); overflow: auto;
}
.clip-theater-head { display: flex; justify-content: flex-end; }
.clip-theater-stage {
  aspect-ratio: 16 / 9; width: 100%; max-height: 70vh;
  display: grid; place-items: center;
  background: var(--inset-2); border-radius: var(--r-sm); overflow: hidden;
}
.clip-theater-frame { width: 100%; height: 100%; border: 0; display: block; }
.clip-theater-foot { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.clip-theater-title { flex: 1 1 12rem; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
/* The action row is the LAST thing in a box that is `overflow: auto` and capped
   at 85vh, so a menu opening downward from it lands outside the visible box and
   has to be scrolled to. In the theater it opens UPWARD instead, over the player,
   where there is always room. (The grid card opens downward as before — nothing
   clips it there since .clip-card dropped its overflow.) */
.clip-theater-foot .clip-menu { top: auto; bottom: calc(100% + var(--space-1)); }
@keyframes clip-theater-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .sec-modal[data-clip-theater] { animation: none; }
}

/* ── NARROW WIDTHS (mobile-fix 2026-08-19) ─────────────────────────────────── */
@media (max-width: 560px) {
  /* THE THEATER FITS. The box is `min(90vw, 64rem)` inside a backdrop padded by
     --space-4 on each side, so it needs `0.9vw <= vw - 32` — false below 320px,
     where the lightbox overflows its own backdrop. Dropping the backdrop gutter
     on a phone moves that break-even to 240px. This is the same two-line pattern
     public-site/creator.css already uses for the public clips theater. */
  .sec-modal[data-clip-theater] { padding: var(--space-3); }
  /* THE DOWNLOAD MENU. `min-width: 15rem` (240px) is a floor, so it cannot shrink
     to a phone card — it opened wider than its own card and hung off the right
     edge. On a phone the wrap takes the whole action row, which makes `100%` the
     card width and the menu exactly as wide as the card, never wider. */
  .clip-menu-wrap { flex: 1 1 100%; }
  .clip-menu { min-width: 0; width: 100%; }
}

/* ── COMMUNITY: the viewer CRM (community plan CM6) ─────────────────────────
   The roster, its eight segment chips, the two filter groups and the two data
   tables. Tokens only. Every rule is scoped to the pane's own `cm-` prefix or to
   #sec-community, so nothing here can reach another section's markup.

   MOBILE IS PART OF DONE. Three mechanisms carry it, and none of them is a
   media query:
     • both control rows WRAP (eight chips, then the two segmented groups and
       the search box), so a phone stacks them instead of overflowing;
     • the tables live in `.sec-tablewrap`, the repo's own overflow-x wrapper, so
       a wide table scrolls INSIDE ITS CARD and never widens the shell;
     • `min-width: 0` on the pane and the results region, because a grid item's
        automatic minimum size is its MIN-CONTENT width — the exact mechanism
        that let the reconnect strip widen the whole app (commit 8c0d36ce).
   The 560px block below only adds what wrapping cannot: touch-sized chips and a
   table min-width so columns scroll rather than crush. */
#sec-community { min-width: 0; }
.cm-segs { display: flex; flex-wrap: wrap; gap: var(--space-2); min-width: 0; }
.cm-seg {
  padding: 7px var(--space-3); border: 1px solid var(--border); border-radius: var(--r-pill);
  background: transparent; color: var(--text-dim); font: inherit; font-size: 12px;
  font-weight: 600; cursor: pointer; white-space: nowrap;
}
.cm-seg:hover { color: var(--text); border-color: var(--border-strong); }
.cm-seg.on { background: var(--accent); border-color: var(--accent); color: var(--on-strong); }
.cm-seg:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }

.cm-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
/* The two segmented groups do not wrap internally (that is the control's shape),
   so they scroll inside themselves on the narrowest phones instead of pushing
   the card wider than the screen. */
.cm-filters > .seg { max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.cm-search { flex: 1 1 14rem; min-width: 0; max-width: 22rem; }
.cm-results { min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); }

/* The roster + subscriber tables. `.table` owns the type and the rules; these
   add only what the CRM's own columns need. */
.cm-table td, .cm-table th { white-space: nowrap; }
.cm-table .cm-td-name { white-space: normal; overflow-wrap: anywhere; color: var(--text); font-weight: 600; }
.cm-table .cm-td-act { text-align: right; }
.cm-name { margin-inline-end: var(--space-1); }
.cm-unknown { color: var(--text-faint); font-weight: 400; }

/* The platform dot. A class per platform so the colour is a token, never a hex
   literal in markup (DESIGN-GUIDELINES §3). An unknown platform stays neutral
   rather than borrowing another platform's colour. */
.cm-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 3px;
  margin-inline-end: 7px; vertical-align: 1px; background: var(--text-faint);
}
.cm-dot--twitch { background: var(--p-twitch); }
.cm-dot--youtube { background: var(--p-youtube); }
.cm-dot--kick { background: var(--p-kick); }

/* 🔴 THE CREATOR'S STAR IS NOT A PLATFORM ROLE (§A13). It is a private mark, so
   it is drawn as a mark: a warn-toned glyph, never a chip that could be mistaken
   for the MOD/VIP/SUB badges beside it. */
.cm-star { color: var(--warn); font-size: 12px; }
.cm-tag {
  display: inline-block; font-size: 10px; padding: 1px 7px; border-radius: var(--r-pill);
  background: var(--accent-wash); color: var(--accent-2); border: 1px solid var(--border-accent);
  margin-inline-start: 4px; vertical-align: 1px;
}
.cm-rolechip {
  display: inline-block; font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px;
  border-radius: 5px; border: 1px solid var(--border); color: var(--text-faint);
}
.cm-rolechip + .cm-rolechip { margin-inline-start: 4px; }
.cm-chip-new, .cm-chip-out, .cm-tier, .cm-gift {
  display: inline-block; font-family: var(--font-mono); font-size: 10px; font-weight: 600;
  padding: 2px 8px; border-radius: var(--r-chip); border: 1px solid var(--border);
  color: var(--text-dim); white-space: nowrap;
}
.cm-chip-new { color: var(--accent-2); border-color: var(--border-accent); margin-inline-start: 6px; }
.cm-chip-out { color: var(--text-faint); margin-inline-start: 6px; }
.cm-tier { color: var(--accent-2); border-color: var(--border-accent); }

/* The honest states. A callout, not a toast: these are facts about the data that
   stay true until the data changes. */
.cm-callout {
  border-inline-start: 3px solid var(--warn); background: var(--surface-inset);
  border-radius: 0 var(--r-sm) var(--r-sm) 0; padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2); min-width: 0;
}
.cm-callout--ok { border-inline-start-color: var(--accent); }
.cm-callout b { font-size: 12.5px; }
.cm-state { color: var(--text-dim); }
.cm-pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }

@media (max-width: 560px) {
  /* Touch: the chips are the pane's primary navigation, so they get the shared
     44px floor like every other tappable control (mobile block rule 3). */
  .cm-seg { min-height: var(--touch-min); display: inline-flex; align-items: center; }
  /* The search box takes its own line once the two filter groups have wrapped. */
  .cm-search { flex: 1 1 100%; max-width: none; }
  /* Keep a usable min width so the table SCROLLS inside .sec-tablewrap instead
     of crushing seven columns into a phone. Wider than the shared 30rem floor
     because this table has more columns than the revenue breakdown does. */
  .sec-tablewrap > .cm-table { min-width: 34rem; }
}

/* ── COMMUNITY: the per-viewer drawer (community plan CM6-2) ────────────────
   The shell's rail-modal idiom, borrowed from #sec-moderation's filter editor:
   a `.sec-modal` backdrop, one box, a section list on the left and one pane per
   section on the right. The box is WIDER than the shared 520px default because
   the drawer carries a section list beside its content.

   NARROW WIDTHS. Below 640px the rail stacks: the section list becomes a
   wrapping row of tabs above the panes, which is exactly what
   #sec-moderation's own rail does at the same breakpoint. Below 560px the
   backdrop drops its gutter so the box fits a 320px phone, the same two-line
   pattern the clips theater uses. */
.cm-drawer-box { width: min(46rem, 100%); }
.cm-rail { display: flex; gap: var(--space-4); align-items: flex-start; min-width: 0; }
.cm-rail-nav { flex: none; width: 10.5rem; display: flex; flex-direction: column; gap: 2px; }
.cm-rail-tab {
  text-align: left; border: 0; background: transparent; color: var(--text-dim);
  font: inherit; font-size: 12.5px; font-weight: 600; padding: 8px var(--space-3);
  border-radius: var(--r-sm); cursor: pointer;
}
.cm-rail-tab.on { background: var(--surface-2); color: var(--text); }
.cm-rail-tab:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 2px; }
.cm-rail-panes { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.cm-rail-pane { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

.cm-kv { display: grid; grid-template-columns: auto 1fr; gap: var(--space-1) var(--space-4); font-size: 12px; min-width: 0; }
.cm-kv dt { color: var(--text-faint); }
.cm-kv dd { margin: 0; color: var(--text-dim); font-family: var(--font-mono); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.cm-src { margin-inline-start: var(--space-2); color: var(--text-faint); font-family: var(--font-display); }
.cm-erase { color: var(--text-faint); }

.cm-panel { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.cm-panel-title { font-size: 12px; font-weight: 600; color: var(--text); }
.cm-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.cm-li { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; font-size: 12px; min-width: 0; }
.cm-li-main { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; color: var(--text-dim); }
.cm-li-meta { color: var(--text-faint); font-family: var(--font-mono); font-size: 11px; }

.cm-linkrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.cm-linkmain { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; }
.cm-merged tfoot td { font-weight: 600; color: var(--text); border-top: 1px solid var(--border); }

@media (max-width: 640px) {
  /* The rail becomes a wrapping tab strip above the panes, matching the
     moderation editor's behaviour at the same width. */
  .cm-rail { flex-direction: column; }
  .cm-rail-nav { width: 100%; flex-direction: row; flex-wrap: wrap; }
}
@media (max-width: 560px) {
  /* The box is `min(46rem, 100%)` inside a backdrop padded by --space-4 each
     side; dropping the gutter is what lets it fit a 320px phone. */
  .sec-modal.cm-drawer { padding: var(--space-3); }
  .cm-rail-tab { min-height: var(--touch-min); display: inline-flex; align-items: center; }
  .cm-linkrow > .btn { flex: 1 1 100%; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
  .sec-modal.cm-drawer { animation: none; transition: none; }
}

/* ── COMMUNITY: the creator's own marks + the link flow (CM6-3) ─────────────
   The Notes card is the first place in Pyre that stores a creator's OPINION
   about a viewer, and it is drawn as a private card: its own head with a
   "Private to you" chip, an explicit Save, and the star kept visually apart
   from the Twitch role switches below it (§A13). */
.cm-panel-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.cm-panel-head > .cm-panel-title { flex: 1 1 8rem; min-width: 0; }
.cm-note { width: 100%; min-height: 5rem; resize: vertical; }
.cm-marks { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.cm-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); min-width: 0; }
.cm-tagchip {
  display: inline-flex; align-items: center; gap: var(--space-1); font-size: 10px;
  padding: 2px 8px; border-radius: var(--r-pill); background: var(--accent-wash);
  color: var(--accent-2); border: 1px solid var(--border-accent);
}
.cm-tagchip button { border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; padding: 0; line-height: 1; }
.cm-tagchip button:hover { color: var(--danger); }
.cm-tagin { flex: 0 1 9rem; min-width: 0; }
.cm-linkq { flex: 1 1 12rem; min-width: 0; }
.cm-linkresults { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.cm-err { color: var(--warn); }

@media (max-width: 560px) {
  /* Every mark control takes its own full-width line rather than being pinched
     into a four-item row on a 320px screen. */
  .cm-marks > * { flex: 1 1 100%; }
  .cm-tagin, .cm-linkq { flex: 1 1 100%; }
  .cm-marks .btn { justify-content: center; }
  .cm-tagchip button { min-width: var(--touch-min); min-height: var(--touch-min); justify-content: center; }
}

/* ── COMMUNITY: the Twitch role card (community plan CM6-5) ─────────────────
   The page's first PLATFORM-MUTATING control, so it is drawn as its own card
   with a chip that names the platform it changes. It sits BELOW the notes card
   on purpose: the creator's star is a private mark and these switches are real
   Twitch roles, and §A13 says the two must never share a control or read as one
   group of settings. */
.cm-roles { border-top: 1px solid var(--border); padding-top: var(--space-3); }
.cm-chip-plat {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono);
  font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-chip);
  border: 1px solid var(--border-accent); color: var(--accent-2); white-space: nowrap;
}
@media (max-width: 560px) {
  /* Each switch takes its own line: two checkbox labels side by side on a phone
     put their text under the wrong box. */
  .cm-roles .cm-marks > .sec-check { flex: 1 1 100%; }
}

/* ══════════════════════════════════════════════════════════════════════════
   DELAY (#sec-delay) — plan 2026-08-19-stream-delay, DL6.

   The page is length + card + chat + one honest state block, so it needs very
   little of its own: the preset row (a shared `.seg` that WRAPS), the build
   progress bar, and the callout the three unavailable states and the jump-cut
   confirm are drawn in. Everything else is a shared component class.

   🔴 The confirm is a CARD IN THE FLOW, at every width — never a hover
   affordance and never a native confirm(). It is the one dialog in this arc
   that has to be read (DL6-4).
   ═══════════════════════════════════════════════════════════════════════════ */
.dly-pick { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); }
.dly-presets { flex-wrap: wrap; }
.dly-custom { flex: 0 1 14rem; min-width: 0; }
.dly-custom > .input { font-family: var(--font-mono); }
.dly-grid { align-items: start; }

/* The state block above the controls: building / live / draining / waiting /
   offline. A `.well`, so the gap contract owns its rhythm. */
.dly-state { min-width: 0; }
.dly-state-title { font-size: 13px; }
.dly-state > .sec-row { border-bottom: 0; padding-inline: 0; }

/* The build progress bar. The percentage is DATA, so it rides a custom property
   rather than a hardcoded width. */
.dly-bar { height: 8px; border-radius: var(--r-pill); background: var(--surface-inset); overflow: hidden; }
.dly-bar > i { display: block; height: 100%; width: var(--dly-pct, 0%); background: var(--accent); border-radius: var(--r-pill); }

/* The callout the honest states are drawn in (the `.cm-callout` idiom). */
.dly-callout {
  border-inline-start: 3px solid var(--warn); background: var(--surface-inset);
  border-radius: 0 var(--r-sm) var(--r-sm) 0; padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2); min-width: 0;
}
.dly-callout b { font-size: 12.5px; }
.dly-callout p { color: var(--text-dim); max-width: 70ch; }
.dly-callout--danger { border-inline-start-color: var(--danger); }
/* 🔴 The exclusivity state is a SALES SURFACE, not an error: it is tinted with
   the brand accent, not with a warning colour (owner gate 7's honesty rail). */
.dly-callout--pitch { border-inline-start-color: var(--accent); }
.dly-slot:empty { display: none; }

/* The card previews (what viewers see). 16:9 so the streamer judges the art at
   the shape it is actually spliced at. */
.dly-preview {
  aspect-ratio: 16 / 9; border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-inset); display: grid; place-items: center; text-align: center;
  padding: var(--space-3); overflow: hidden;
}
.dly-preview-line { color: var(--text-dim); font-size: 12px; }
.dly-preview-cd { font-family: var(--font-mono); font-size: 26px; font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
  /* Touch: every pill is a real target, and the row wraps rather than scrolls —
     a horizontally scrolling preset row hides the longer delays behind a
     gesture nobody discovers (DL6-4). */
  .dly-presets { display: flex; width: 100%; }
  .dly-presets > button { min-height: var(--touch-min); flex: 1 1 auto; justify-content: center; }
  /* The approved 390px row is FOUR pills. The ones outside it fold away here and
     stay reachable through the free field directly below (DL6-4). */
  .dly-presets > [data-dly-narrow="0"] { display: none; }
  /* Every control in the pane clears the touch floor, not just the pills. */
  .dly-state .btn, .dly-callout .btn, .dly-pitch .btn, .dly-grid .btn { min-height: var(--touch-min); }
  /* The card actions stack rather than pinching three buttons into one line. */
  .dly-grid .sec-actions > .btn { flex: 1 1 100%; justify-content: center; }
  /* The length field and the apply control take their own full-width lines, so
     the primary action is thumb-reachable rather than pinched beside a field. */
  .dly-custom { flex: 1 1 100%; }
  .dly-custom > .input { max-width: none; }
  .dly-pick > * { flex: 1 1 100%; }
  /* The confirm's two buttons stack full width: side by side at 390px they are
     both too small and too close for the one decision that cannot be undone. */
  .dly-confirm .sec-actions > .btn { flex: 1 1 100%; justify-content: center; min-height: var(--touch-min); }
}

/* The cockpit's delay chip + tile (DL6-3). The chip is a stat-chip that also
   carries a build bar and a one-click action, so it needs a row of its own
   inside the column the other chips are. */
.stat-delay { min-width: 0; }
.stat-delay .stat-value { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
.dly-chip-bar { display: block; width: 64px; max-width: 100%; height: 6px; border-radius: var(--r-pill); background: var(--surface-inset); overflow: hidden; }
.dly-chip-bar > i { display: block; height: 100%; width: var(--dly-pct, 0%); background: var(--accent); border-radius: var(--r-pill); }
.stat-delay .btn { align-self: flex-start; }
/* The tile is display:flex, so it needs the belt or [hidden] never closes it —
   and with no Pyre studio in the path the tile must really be GONE, not dimmed. */
.qa-tile[hidden] { display: none; }

@media (max-width: 560px) {
  /* The chip must not push the stat bar into a horizontal scroll: the bar wraps
     already, so the chip only has to stay inside its own column (DL6-4). */
  .stat-delay { flex: 1 1 100%; }
  .stat-delay .stat-value { flex-wrap: wrap; }
  .dly-chip-bar { width: 100%; }
  .stat-delay .btn { min-height: var(--touch-min); }
}

/* ── #sec-ads: the MONETIZE > Ads SETUP destination (ad-manager V0.16) ────────
   The LIVE surface is the cockpit's .cockpit-ads card; everything here is setup.

   🔴 THE DE-PILLING RULE, OWNER-RULED TWICE. Revision 2 of the G-UI mock demoted
   every pill on these frames to typographic hierarchy, and the owner's second
   verdict extended it to this page and replaced the per-game TABLE with collapsed
   per-game cards: "It's still a lot of Pills. I think it'd be better for each game
   to have its own collapsed cards, with various games states." So there is not one
   .pill, .chip or badge in this block. The only chrome-shaped controls are the
   shared .seg segmented control and the shared .toggle switch; everything else is
   a caption, a definition list or a muted word. A test pins it (sec-ads.test.js,
   "THE PILL AUDIT"), so a future card cannot quietly re-introduce one.

   Tokens only, no hardcoded colour or radius, and every gap comes from a
   container rather than a child margin (DESIGN-GUIDELINES §1.4.1, §3). */

/* The card-head right side: a quiet mono caption where the mock's revision 1 had
   a badge, plus the label + switch pair the shared components already own. */
#sec-ads .ads-tools { display: inline-flex; align-items: center; gap: var(--space-2); }
#sec-ads .ads-tools .lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-dim); white-space: nowrap;
}
#sec-ads .ads-note-mono {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em;
  color: var(--text-dim); white-space: nowrap;
}

/* ── the cockpit strip (ads page overhaul, 2026-08-31) ──────────────────────
   Three columns at most — stance, ledger, this machine — the stance widest
   because it holds three sentences.

   🔴 FLEX, NOT A FIXED THREE-COLUMN GRID, AND THAT IS THE WHOLE POINT. The strip
   honestly renders ONE, TWO or THREE columns depending on what exists: there is
   no ledger meter off air (`adsView` builds none when `live === false`) and no
   machine column in a browser tab (no desktop bridge, W-DETECT's rule). A
   `grid-template-columns: 1.35fr 1fr 1fr` would hold the missing tracks open and
   leave a ghost gap where a readout the creator cannot have would go. Flex
   distributes whatever is there across the full row, so two columns are two
   columns rather than two thirds of three.

   The stance column keeps its extra weight through `flex-grow`, which is the
   same 1.35 : 1 : 1 the mock draws when all three are present. */
#sec-ads .ads-cockpit-grid { display: flex; flex-wrap: wrap; gap: var(--space-3); }
#sec-ads .ads-cockpit-col {
  flex: 1 1 240px; min-width: 0;
  display: flex; flex-direction: column; gap: var(--space-2);
  background: var(--surface-inset);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: var(--space-3);
}
#sec-ads .ads-cockpit-col[data-ads-col-stance] { flex-grow: 1.35; flex-basis: 300px; }
#sec-ads .ads-stance-off {
  font-size: var(--fs-micro); letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-faint); font-weight: 600;
}
#sec-ads .ads-stance-line { font-size: var(--fs-section); font-weight: 600; }
#sec-ads .ads-stance-now { font-size: var(--fs-small); color: var(--text-dim); }
/* The CONDITIONAL stance is the one line that is about the future, so it is the
   one line that takes the accent. It is `accent-2`, not `accent`: this is a
   reading, not a control, and the control tone belongs to things you can press. */
#sec-ads .ads-stance-would { font-size: var(--fs-small); color: var(--accent-2); }
/* A labelled fact in the ledger column: the key on the left, the value on the
   right, both on one baseline. */
#sec-ads .ads-stat {
  display: flex; align-items: baseline; gap: var(--space-2);
  font-size: var(--fs-small); color: var(--text-dim);
}
#sec-ads .ads-stat .k { flex: 1; min-width: 0; color: var(--text-faint); }
#sec-ads .ads-stat .v { flex: none; color: var(--text); font-variant-numeric: tabular-nums; }

/* 🔴 THE LEDGER BAR IS A NATIVE <progress>, AND THE REASON IS A TEST. The mock
   draws a filled meter and it is this column's most glanceable element, but a
   drawn fill normally needs a per-render width — an inline `style` attribute,
   which sec-ads.test.js's "the page carries no inline style" audit forbids
   anywhere on this page. `<progress value max>` carries the fraction in
   ATTRIBUTES, so the shape is data and every pixel of the paint is here. It also
   reads as a progress bar to a screen reader for free.
   Both vendor pseudo-elements are written: Chromium is the real runtime, and the
   `-moz-` line is one rule rather than a fallback worth arguing about. */
#sec-ads .ads-bar {
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%; height: 6px; border: 0;
  background: var(--surface-2); border-radius: var(--r-sm); overflow: hidden;
}
#sec-ads .ads-bar::-webkit-progress-bar { background: var(--surface-2); }
#sec-ads .ads-bar::-webkit-progress-value {
  background: var(--accent-2); transition: width var(--dur) var(--ease);
}
#sec-ads .ads-bar::-moz-progress-bar { background: var(--accent-2); }
/* A met target is the good outcome, so it wears the OK tone rather than the
   accent — the same signal the cockpit card's own full meter carries. */
#sec-ads .ads-bar.is-full::-webkit-progress-value { background: var(--ok); }
#sec-ads .ads-bar.is-full::-moz-progress-bar { background: var(--ok); }

/* One narrow rule for the strip: below the shell's own two-column breakpoint the
   columns stop competing and stack, which is also what the mock's 390px frame
   draws. */
@media (max-width: 980px) {
  #sec-ads .ads-cockpit-col,
  #sec-ads .ads-cockpit-col[data-ads-col-stance] { flex-basis: 100%; }
}

/* ── the DISARMED strip: one row, no boxes (ads polish, 2026-09-01) ─────────
   🔴 THE COLUMNS DO NOT RENDER WHEN THE MASTER SWITCH IS OFF, so there is no
   rule here that empties them — `cockpitStripHtml` returns this row instead of
   the grid. The bug the owner photographed was `align-items: stretch` growing
   two bordered, empty columns to the height of a third that had rows in it, and
   an empty box tall enough to look deliberate is worse than no box.

   The mark, the sentence and the switch's twin, on one wrapping line. The
   sentence takes the flexible share so the button never wraps alone under a
   full-width paragraph, and the button is the row's only accented thing because
   it is the row's only action. */
#sec-ads .ads-arm-off {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
}
#sec-ads .ads-arm-off > .sec-note { flex: 1 1 24rem; min-width: 0; }
#sec-ads .ads-arm-off > .btn { flex: none; }

/* The ad-minutes stepper. A number with two buttons, not a select: the range is
   0..10 and the 0 end is a real setting the copy has to explain in place. */
#sec-ads .ads-stepper {
  display: inline-flex; align-items: center; flex: none;
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
}
#sec-ads .ads-stepper button {
  min-width: 34px; min-height: 32px; padding: 0 var(--space-2);
  background: transparent; border: 0; color: var(--text-dim);
  font: inherit; font-size: 15px; line-height: 1; cursor: pointer;
}
#sec-ads .ads-stepper button:hover:not([disabled]) { color: var(--text); }
#sec-ads .ads-stepper button[disabled] { opacity: .4; cursor: not-allowed; }
#sec-ads .ads-sv {
  min-width: 2.5ch; padding: 0 var(--space-1); text-align: center;
  font-weight: 600; font-variant-numeric: tabular-nums;
}

/* The two key/value lists (the postures, and the five {reason} words). A <dl>
   rather than the global .legend, whose row shape is a single inline chip line. */
#sec-ads .ads-legend { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
#sec-ads .ads-legend > div { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); }
#sec-ads .ads-legend dt {
  flex: 0 0 7rem; font-size: 11.5px; font-weight: 600; color: var(--text);
}
#sec-ads .ads-legend dd {
  margin: 0; flex: 1 1 14rem; min-width: 0;
  font-size: 11.5px; color: var(--text-dim); max-width: var(--w-prose);
}

/* A rendered chat line: the announce preview, and the refusal a mod reads. It is
   deliberately NOT styled like the real chat pane — it is a quotation. */
#sec-ads .ads-chat {
  display: flex; gap: var(--space-2); align-items: baseline;
  padding: var(--space-2) var(--space-3); border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border); font-size: 12.5px;
}
#sec-ads .ads-who { flex: none; font-weight: 700; color: var(--accent-2); }
#sec-ads .ads-said { min-width: 0; color: var(--text); word-break: break-word; }
/* A preview carrying a token Pyre will not fill in. The warn tint is the SIGNAL
   that the line is not what the creator thinks it is; the words carry the fact. */
#sec-ads .ads-chat.is-bad { border-color: var(--warn); }

/* The unknown-token save warning, sitting with the template field that produced
   it. NOT an error: the save LANDED, and the first word of the sentence says so.
   The warn tone is the same signal the tinted preview line carries, so the two
   halves of one fact read as one fact. Deliberately not chip-shaped. */
#sec-ads .ads-warn { color: var(--warn); }

/* Token names inside the hint: inline code, revision 2's own replacement for the
   chips it had drawn there. */
#sec-ads .ads-tok {
  font-family: var(--font-mono); font-size: 11px;
  padding: 1px 5px; border-radius: 4px;
  background: var(--surface-2); color: var(--text);
}

/* ── the game TILE grid (ads page overhaul, 2026-08-31) ───────────────────
   Nine collapsed cards stacked vertically was nine rows of chrome before the
   first answer. `auto-fill` at 214px puts every game, its tier, its health and
   its on-state inside one screen on a desktop and two-up at 390px.

   ⚠️ NO CHIP RADIUS IN THIS BLOCK OR ANYWHERE AFTER THE `#sec-ads` BANNER.
   `sec-ads.test.js`'s CSS-side pill audit reads this sheet from that banner on
   and forbids the shell's pill-radius token by name, because a chip radius on a
   caption is a pill by another name. Every radius below is `--r-sm`. (Do not
   write the token here either: the audit scans the whole slice, comments
   included, and it is right to — a commented-out pill is one paste from a live
   one.) */
#sec-ads .ads-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(214px, 1fr));
  gap: var(--space-3);
}
#sec-ads .ads-tile {
  position: relative; text-align: left;
  background: var(--surface-inset);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-2);
  cursor: pointer; color: var(--text);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
/* ── the game POSTER (ads polish, 2026-09-01) ──────────────────────────────
   Twitch Helix box art, behind the words.

   🔴 IT IS AN `<img>` AND NOT A `background-image`, AND A TEST IS THE REASON: a
   per-tile backdrop needs a per-tile URL, which on this page means an inline
   `style` attribute, which the "no inline style" audit forbids outright. The
   `src` carries the data and every pixel of the treatment is here — the same
   trade that made the ledger bar a native `<progress>`.

   🔴 THE TEXT IS THE TILE. The poster is dimmed hard and then covered by a
   gradient that goes opaque under the foot, because a tile a creator cannot read
   at a glance has stopped being a tile. Full colour when the game is on, drained
   when it is off, so the poster carries the same on/off signal the tile's own
   dimming does instead of arguing with it.

   The stacking is explicit rather than lucky: the picture is the only positioned
   child at `z-index:0`, and everything else is lifted to 1. Without that a later
   in-flow sibling would paint under a positioned element. */
#sec-ads .ads-tile-art {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
  opacity: .16; filter: saturate(1.1);
  pointer-events: none; border-radius: var(--r-sm);
}
/* `::after`, not `::before`: both live at z-index 0, so DOM order decides, and a
   ::before would paint UNDER the picture it is there to cover. Scoped by `:has`
   so the fallback tile — which is transparent and dashed by design, and never
   gets a poster — does not gain a filled bottom half. */
#sec-ads .ads-tile:has(> .ads-tile-art)::after {
  content: ""; position: absolute; inset: 0; z-index: 0; border-radius: var(--r-sm);
  background: linear-gradient(180deg, transparent 0%, var(--surface-inset) 82%);
  pointer-events: none;
}
#sec-ads .ads-tile > :not(.ads-tile-art) { position: relative; z-index: 1; }
/* A switched-off game keeps its poster and loses its colour: the tile is already
   at .62 opacity, and a full-colour picture behind a faded name reads as the
   tile being broken rather than as the game being off. */
#sec-ads .ads-tile.is-off .ads-tile-art { filter: grayscale(1); opacity: .1; }
#sec-ads .ads-tile.is-sel .ads-tile-art { opacity: .22; }
#sec-ads .ads-tile:hover .ads-tile-art { opacity: .24; }
#sec-ads .ads-tile:hover { border-color: var(--border-strong); background: var(--surface-raised); }
#sec-ads .ads-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#sec-ads .ads-tile.is-sel {
  border-color: var(--border-accent); background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px var(--accent-wash);
}
/* A switched-off game is DIMMED, not hidden: the creator has to be able to find
   it to switch it back on, and a missing tile reads as a missing game. */
#sec-ads .ads-tile.is-off { opacity: .62; }
/* The fallback tile is dashed and quiet — it is a statement about what Pyre does
   with an unknown game rather than a game of your own. */
#sec-ads .ads-tile.is-fallback { border-style: dashed; cursor: default; background: transparent; }
#sec-ads .ads-tile.is-fallback:hover { background: transparent; border-color: var(--border); }
#sec-ads .ads-tile-top { display: flex; align-items: flex-start; gap: var(--space-2); }
#sec-ads .ads-tile-name {
  font-size: var(--fs-body); font-weight: 600;
  line-height: 1.2; flex: 1; min-width: 0;
}
#sec-ads .ads-tile-read {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-small); color: var(--text-dim); min-height: 18px;
}
#sec-ads .ads-tile-foot {
  display: flex; align-items: center; gap: var(--space-2);
  padding-top: var(--space-2); border-top: 1px solid var(--border);
}
#sec-ads .ads-tile-sum {
  flex: 1; min-width: 0; font-size: var(--fs-micro); color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Tier + health MARKS, never pills (owner ruling 19). `.ads-mark` is the shipped
   micro-label treatment; `.mark` is the tile grid's own, and the tone modifiers
   below are shared by both so one word cannot read two ways. */
#sec-ads .mark {
  font-size: var(--fs-micro); letter-spacing: .06em; text-transform: uppercase;
  font-weight: 600; color: var(--text-faint); white-space: nowrap;
}
#sec-ads .mark--tier {
  font-family: var(--font-mono); letter-spacing: .02em; text-transform: none;
  color: var(--text-dim); flex: none;
}
/* ð´ THESE FOUR ARE EMITTED BY CONCATENATION (`'mark--' + health.state`), so a
   grep for the literal class name in sec-ads.js finds nothing. They are NOT
   orphans, and `sec-ads.test.js` proves it the only way a string search cannot:
   by asserting that the modifier set here is exactly the health vocabulary
   `HEALTH_CHIP_WORDS` can produce. A sixth health word would fail there rather
   than shipping as an unstyled mark. */
#sec-ads .mark--verified { color: var(--ok); }
#sec-ads .mark--degraded,
#sec-ads .mark--stale_pack,
#sec-ads .mark--unsupported { color: var(--warn); }
/* `unverified` is deliberately UNTINTED — it is the resting state of every vision
   game nobody has confirmed yet, and a colour on it would read as a fault — but
   it is DECLARED rather than left to fall through, so the coverage pin can prove
   every reachable health word has a rule instead of guessing at silence. */
#sec-ads .mark--unverified { color: var(--text-faint); }

/* ── the detail SHELF (ads page overhaul, 2026-08-31) ───────────────────
   FULL WIDTH under the grid, not a master/detail split: the verification panel
   draws an annotated 16:9 frame, and a 380px detail column is narrower than the
   picture it has to show. */
#sec-ads .ads-shelf {
  background: var(--surface-raised);
  border: 1px solid var(--border-accent);
  border-radius: var(--r-sm);
  overflow: hidden;
}
#sec-ads .ads-shelf-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
#sec-ads .ads-shelf-head h3 { font-size: var(--fs-section); font-weight: 600; }
#sec-ads .ads-shelf-close { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
#sec-ads .ads-shelf-body {
  padding: var(--space-3) var(--space-4) var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3);
}
/* MOMENT CHIPS: a grid, so four moments are one band rather than four rows.
   ⚠️ THE CLASS IS `ads-mchip`, NOT `*-chip`. The mock calls them chips in prose;
   the pill audit bans the token in a class name, and it is right to — a name that
   reads as a pill is one stylesheet edit from being one. */
#sec-ads .ads-moments {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--space-2);
}
#sec-ads .ads-mchip {
  background: var(--surface-inset);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-2) var(--space-3);
  display: flex; flex-direction: column; gap: 4px;
}
#sec-ads .ads-mchip.is-off { opacity: .62; }
/* The toggle sits in the SAME row as the name — the whole point of the chip, and
   the thing a full-width row could not give (owner rev 3). */
#sec-ads .ads-mchip-top {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
}
#sec-ads .ads-mchip-t { font-weight: 600; font-size: var(--fs-small); }
#sec-ads .ads-mchip-s { color: var(--text-faint); font-size: var(--fs-micro); line-height: 1.4; }
/* The disclosure. Quiet by construction: a summary that reads as a link, not a
   button, because opening it changes nothing. */
#sec-ads .ads-quiet {
  background: var(--surface-inset);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-2) var(--space-3);
}
#sec-ads .ads-quiet > summary {
  cursor: pointer; font-size: var(--fs-small); color: var(--text-faint); list-style: none;
}
#sec-ads .ads-quiet > summary::-webkit-details-marker { display: none; }
#sec-ads .ads-quiet > summary::before { content: "\203A\A0"; color: var(--text-faint); }
#sec-ads .ads-quiet[open] > summary::before { content: "\2304\A0"; }
#sec-ads .ads-adv { padding-top: var(--space-2); }
#sec-ads .ads-adv .sec-row .grow { max-width: 56ch; }

/* ── the one settings card (ads page overhaul, 2026-08-31) ────────────────
   Four labelled wells in one card, clearly separated from the per-game area, so
   nothing about a game is filed under a global heading. Two up on a desktop and
   one below the shell's own breakpoint. */
#sec-ads .ads-set-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3);
  align-items: start;
}
@media (max-width: 980px) { #sec-ads .ads-set-grid { grid-template-columns: 1fr; } }
/* Generous rows inside the disclosure: one moment per line, its caption under
   its name. They are the ONLY `.sec-row`s left in a shelf — the moments
   themselves are chips now. */
#sec-ads .ads-adv .sec-row { padding: var(--space-3) 0; gap: var(--space-4); }
#sec-ads .ads-adv .sec-row:last-child { border-bottom: 0; }
/* ⛔ `.ads-cell` AND `.ads-dead` ARE DELETED (2026-08-31). They were the moment
   row's control cluster and the dead row's mark-plus-reason pair, and neither
   markup exists: a moment is a chip whose control sits in its own top row, and a
   dead moment is not drawn at all.
   The budget caption ("up to 45s") keeps its treatment: it is the machine
   explaining itself, and it still renders inside the Advanced disclosure. */
#sec-ads .ads-why {
  font-size: 11px; color: var(--text-dim); text-align: right; max-width: 16rem;
}
#sec-ads .ads-mark {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-dim);
}
/* ⛔ THE DEAD-CELL ROW TREATMENT IS GONE (2026-08-31, owner rev 3). A moment the
   game does not have is not drawn at all now, so there is no greyed row to tint:
   the two maps still decide which chips EXIST, and their sentences live on the
   wiki's game table. */
/* The lobby row's risk line (W-CLEAR CL-2, spec D6). It is CONTENT — the one
   thing about that class a creator has to be able to decide against — so it
   keeps the readable `.dim` token the `.s` rule already gives it rather than
   fading to `.faint`, and earns its separation from the description with a rule
   instead of with a paler colour.
   PADDING, NEVER A CHILD MARGIN — `schedule/guards.test.js` and
   `sec-delay.test.js` both scan every line of this file after their own banner
   for `margin-top|bottom|block`, and this rule sits after both. The rule they
   enforce is the layout one this file already lives by: spacing belongs to the
   container (or, here, to the element's own box), so a row that grows a third
   line cannot push its neighbours around. */
#sec-ads .ads-adv .ads-risk {
  padding: 4px 0 0 var(--space-2);
  border-left: 2px solid var(--border);
}

/* ── the verification panel (vision plan S3) ────────────────────────────────
   The one live surface on this settings page: a strip of what the vision
   detector is reading, opened per game card so a creator can score a pack
   against their own capture before trusting it. Tokens only; the two bar
   percentages are the ONLY inline style anywhere on it (written by the page as
   `--ads-fill` / `--ads-needs`, the cockpit meter's own data idiom). */
#sec-ads .ads-verify-slot {
  padding: var(--space-2) 0 var(--space-3);
}
#sec-ads .ads-verify { gap: var(--space-4); }
/* 🔴 THE PANEL READS LEFT, IN DOCUMENT FLOW, ALWAYS (2026-08-31).
   `#sec-ads .ads-why` is `text-align: right; max-width: 16rem`, which is correct
   for what it was written for — the caption hugging the right edge of a game
   state ROW. This panel reused the class for prose (the crop caption, the pack's
   "Watches the lobby corner…" sentence, the frame-size readout), and a run of
   right-aligned 16rem fragments stacked down a well is what the owner was looking
   at when they said the UI feels broken. Everything on the panel that is a
   SENTENCE is `.sec-note` now; this rule is the belt for anything that is not. */
#sec-ads .ads-verify .ads-why { text-align: left; max-width: var(--w-prose); }
/* The ROI thumbnail: a small, crisp crop of what the detector is looking at. It
   is a convenience beside the bars, so it is bounded and never allowed to push
   the panel wide. */
#sec-ads .ads-vthumb {
  max-width: 100%; width: auto; max-height: 160px;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  image-rendering: pixelated; align-self: flex-start;
}
/* ── the score meters ──────────────────────────────────────────────────────
   One meter per signal: the name and its score on one row, the track under both,
   the machine detail faint at the bottom.

   ⚠️ REBUILT 2026-08-31. The old row was a 12px label, an 8px `--surface-2`
   track on a `--surface-inset` well (barely a shade apart) and a right-aligned
   run-on of two numbers with the signal id at the same weight as the human name.
   Mid match most scores are near zero, so what was actually on screen was an
   invisible groove with a 2px dim notch on it — the owner read the whole strip as
   "nearly invisible hairline ticks". The track is the readout, so it now has the
   height, the contrast and the notch of one. */
#sec-ads .ads-vbar { display: flex; flex-direction: column; gap: 6px; }
#sec-ads .ads-vbar-top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2);
}
/* The score, right, in the page's mono numerals so a column of them lines up and
   a moving digit does not shuffle the ones beside it. */
#sec-ads .ads-vbar-val {
  font-family: var(--font-mono); font-size: 12px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--text); flex: none;
}
#sec-ads .ads-vbar-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2); font-size: 10.5px; color: var(--text-faint);
}
#sec-ads .ads-vbar-need { font-variant-numeric: tabular-nums; }
#sec-ads .ads-vbar-foot > .ads-mark { color: var(--text-faint); }
#sec-ads .ads-vbar-track {
  /* ⚠️ `--r-sm`, NOT `--r-xs`. There is no `--r-xs` token in this app — the four
     rules that named it (this track's ancestor among them) have been resolving to
     `initial`, i.e. SQUARE, since they were written. That is half of why the
     meters read as grooves rather than as bars. The chip radius is banned inside
     `#sec-ads` (`sec-ads.test.js`, the pill audit, which scans this block line by
     line), and at 10px tall `--r-sm` is fully rounded anyway. */
  position: relative; height: 10px; border-radius: var(--r-sm);
  /* The groove is the PAGE background inside an inset well, not `--surface-2`
     which sits a hair above it: an empty meter has to read as an empty channel
     rather than as a slightly different shade of the box it is in. */
  background: var(--bg); border: 1px solid var(--border-strong);
  overflow: visible;
}
/* A score that is over zero is VISIBLE at any width. Without the floor a 1%
   reading and a 0% reading paint the same nothing, which is how a live detector
   came to look like a dead one. */
#sec-ads .ads-vbar-fill {
  position: absolute; inset: 1px auto 1px 1px; width: var(--ads-fill, 0%);
  min-width: 6px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--accent), var(--accent-soft));
}
/* …and a REAL zero keeps the floor from lying about it. */
#sec-ads .ads-vbar-track.is-empty .ads-vbar-fill { min-width: 0; }
/* Once the score is over the threshold the bar reads as a hit, not a near miss. */
#sec-ads .ads-vbar-track.is-over .ads-vbar-fill {
  background: linear-gradient(90deg, var(--ok), var(--ok-soft));
}
/* The threshold notch, at the calibrated line the score has to clear. It stands
   PROUD of the track top and bottom so it survives a fill running underneath it. */
#sec-ads .ads-vbar-needs {
  position: absolute; top: -3px; bottom: -3px; left: var(--ads-needs, 0%);
  width: 2px; background: var(--text); opacity: .75; border-radius: 2px;
}
/* The confirmation line ("Match end detected 3s after the banner"): the answer
   to "play one match", so it reads as an affirmative rather than as small print. */
#sec-ads .ads-vconfirm { color: var(--ok); font-weight: 600; }
/* The two labelled groups under the evidence grid: the meters, and the
   calibration rows. Both are columns of like things, so they get the tighter
   rhythm `.stack--tight` gives and a separating rule from the block above rather
   than a bigger hole. The rule is on the BOX, never a child margin. */
#sec-ads .ads-vbars, #sec-ads .ads-vcals {
  padding-top: var(--space-3); border-top: 1px solid var(--border);
}
/* ── the calibration strip (V-CAL4) ────────────────────────────────────────
   One row per signal while the wide search runs: the signal id on the page's
   mono micro-label, the state word, and the transform the search measured. The
   row is a single line that WRAPS rather than a grid, because the honesty line
   is the one part whose length is not knowable (per-axis scale plus two
   offsets) and a column sized for it would leave a gutter on every locked row
   that landed exactly where the pack said. */
#sec-ads .ads-vcal {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2);
}
#sec-ads .ads-vstate { font-size: 12px; font-weight: 600; color: var(--text-dim); }
/* Still searching: the dim default, spelled rather than inherited so the three
   states are all findable from the markup that emits them. */
#sec-ads .ads-vstate.is-searching { color: var(--text-dim); }
/* A lock reads as an affirmative, on the same green the confirmation line uses. */
#sec-ads .ads-vstate.is-found { color: var(--ok); }
/* The SOFTER lock: the position is measured and the scale is the pack's own, so
   it is a find rather than a full one and must not wear the same green. */
#sec-ads .ads-vstate.is-advisory { color: var(--warn); }
/* A spot the CREATOR placed that Pyre could not recognize there. It is the warn
   colour and not a failure red on purpose: the round-robin has not given up on
   it, the creator can move it again, and nothing is broken. */
#sec-ads .ads-vstate.is-unrecognized { color: var(--warn); }
/* "Your layout changed. Verify detection again." — it renders beside the button
   on a CLOSED card, so it carries its own colour rather than relying on being
   inside the panel's well. */
#sec-ads .ads-vreverify { color: var(--warn); font-weight: 600; }
#sec-ads .ads-vflags {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
}

/* ── the annotated headline (2026-08-31) ───────────────────────────────────
   The whole scene with every sampled rectangle drawn over it. The boxes are
   BUTTONS positioned in per-cent of the picture, written by the page as
   --rx/--ry/--rw/--rh (the --ads-fill data idiom, one rung up): the browser lays
   the image out at whatever width the column gives it and the fractions follow,
   so nothing here needs to know the rendered size.

   ── THE EVIDENCE GRID ──
   Frame left, close-up right, meters full width underneath. Two thirds / one
   third rather than the shared `.cols-2` halves, because these are not peers:
   the scene is the thing being read and the detail is the annotation on it. It
   collapses to one column before either half gets too narrow to be a picture. */
#sec-ads .ads-vgrid { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
@media (max-width: 900px) {
  #sec-ads .ads-vgrid { grid-template-columns: minmax(0, 1fr); }
}
#sec-ads .ads-vscene-col { min-width: 0; }
/* 🔴 THE PICTURE FILLS ITS COLUMN (2026-08-31, owner: "the annotation window is
   too small, I can't read any of it"). It used to be an `inline-block` shrunk to
   a ~360px bitmap's own size, floated at the start of a stack. It is a BLOCK at
   100% now, and the producer's long edge went 360 → 640 in the same commit so
   the source has the pixels to be enlarged into. The `max-width` is the source's
   own long edge plus a little: past that a creator gets a soft picture rather
   than a bigger one, and the boxes are already as legible as they will get. */
#sec-ads .ads-vscene {
  position: relative; display: block; width: 100%; max-width: 720px;
  line-height: 0;
  border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden;
  background: var(--bg);
}
#sec-ads .ads-vscene-img { display: block; width: 100%; height: auto; }
#sec-ads .ads-vbox {
  position: absolute; left: var(--rx, 0%); top: var(--ry, 0%);
  width: var(--rw, 0%); height: var(--rh, 0%);
  padding: 0; margin: 0; background: transparent; cursor: pointer;
  border: 1.5px solid var(--text-dim); border-radius: 2px;
  /* The smallest ROI in a pack can be a few per cent of the picture, which is
     under a comfortable target. The box itself stays the size of the rectangle
     it describes (it is a measurement, not a control) and the BAR ROW below is
     the full-width target for the same spot. */
  min-width: 8px; min-height: 8px;
}
/* Quiet: an outline and nothing else. Most boxes are quiet most of the time and
   a picture of seven competing highlights says nothing. */
#sec-ads .ads-vbox.is-quiet { border-color: color-mix(in srgb, var(--text-dim) 70%, transparent); }
/* Over its line right now. */
#sec-ads .ads-vbox.is-over {
  border-color: var(--warn); box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn) 35%, transparent);
}
/* Confirmed, or a position the calibration has locked. */
#sec-ads .ads-vbox.is-good {
  border-color: var(--ok); box-shadow: 0 0 0 1px color-mix(in srgb, var(--ok) 35%, transparent);
}
/* The one Pyre is checking on this frame: a solid ring, so the round-robin is
   visible as movement rather than having to be explained. */
#sec-ads .ads-vbox.is-active {
  border-color: var(--accent); border-width: 2px;
  animation: ads-vbox-pulse 1.6s ease-in-out infinite;
}
/* The one the creator PINNED. It outranks everything above it visually because
   it is the only state they chose. */
#sec-ads .ads-vbox.is-picked { border-color: var(--accent); border-style: dashed; border-width: 2px; }
/* The one the detail is AUTO-FOLLOWING: the panel chose it, not the creator, so
   it is a quieter claim than the dashed pin above. It is what keeps the picture
   and the close-up beside it reading as one thing. */
#sec-ads .ads-vbox.is-shown { border-color: var(--accent-2); border-width: 2px; }
#sec-ads .ads-vbox:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@keyframes ads-vbox-pulse {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); }
  50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
}
/* Seven always-on tags over a 360px picture is not a reading of anything, so a
   box names itself when it is the one being checked, the one selected, or the
   one under the pointer or the focus ring. */
/* The gap is in the offset, not in a child margin (the pane guard's rule): the
   tag is absolutely positioned, so `calc(100% + 2px)` is the same 2px without
   asking a child to push its own siblings around. */
/* The tag SCALES with the picture. A fixed 10px was legible over a 360px
   thumbnail and is a speck over a 700px one, which is the same "I can't read any
   of it" defect one element down. `clamp` floors it at what it always was and
   lets it grow to a real caption on a wide card. */
#sec-ads .ads-vbox-tag {
  position: absolute; left: 0; bottom: calc(100% + 3px);
  padding: 2px 6px; border-radius: var(--r-sm);
  font-size: clamp(10px, 0.95vw, 13px);
  line-height: 1.4; font-weight: 600; white-space: nowrap;
  background: var(--surface-1); color: var(--text); border: 1px solid var(--border);
  opacity: 0; transition: opacity 120ms ease;
  pointer-events: none;
}
#sec-ads .ads-vbox.is-active .ads-vbox-tag,
#sec-ads .ads-vbox.is-picked .ads-vbox-tag,
#sec-ads .ads-vbox.is-shown .ads-vbox-tag,
#sec-ads .ads-vbox:hover .ads-vbox-tag,
#sec-ads .ads-vbox:focus-visible .ads-vbox-tag { opacity: 1; }
/* A box against the top edge would hang its tag outside the picture, and the
   scene clips. The PAGE decides which side (`regionModel`'s `tagBelow`), off
   the same per-cent it positions the box with, rather than this sheet guessing
   from a style attribute. */
#sec-ads .ads-vbox.is-tagbelow .ads-vbox-tag { bottom: auto; top: calc(100% + 2px); }

/* ── ADJUST SPOTS: the boxes become handles ────────────────────────────────
   🔴 THE MODE HAS TO LOOK LIKE A MODE. In every other state a box is a
   MEASUREMENT — an outline saying "Pyre read this rectangle" — and the one thing
   that must never happen is a creator dragging one without realising the picture
   has changed meaning. So the whole scene declares itself: the boxes gain a
   move cursor and four visible corner grips, the pulse on the round-robin's
   current box is stood down (it is noise while you are aiming), and the tags come
   on permanently so every handle names what it is before it is grabbed. */
#sec-ads .ads-vscene.is-adjust { cursor: default; }
/* A drag over a picture selects the picture otherwise, in every browser. */
#sec-ads .ads-vscene.is-adjust,
#sec-ads .ads-vscene.is-adjust .ads-vscene-img { user-select: none; -webkit-user-select: none; }
#sec-ads .ads-vbox.is-adjust {
  cursor: move; border-style: solid; border-width: 2px;
  /* The handle needs a body to grab, not just an edge: a 2px border on a small
     ROI is not a target. The fill is barely there so the game underneath still
     reads, which is the whole reason a creator can tell where to put it. */
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  /* `touch-action: none` is what makes a drag on a touch screen a drag rather
     than a page scroll. Without it the pointer events fire and are then
     cancelled by the browser's own panning, which reads as a box that will not
     move on exactly the devices where aiming is hardest. */
  touch-action: none;
}
/* Named while the mode is on, not only on hover: you cannot aim at a handle you
   have to hover to identify. */
#sec-ads .ads-vscene.is-adjust .ads-vbox-tag { opacity: 1; }
/* Dropped, and Pyre has not started reading here yet. DASHED and dimmed, because
   this is the one box on the picture that is NOT a measurement — it is where the
   creator put it, waiting to be picked up on the detector's next poll. It must
   never pass for a rectangle that was sampled. */
#sec-ads .ads-vbox.is-pending {
  border-style: dashed; border-color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 8%, transparent);
}
/* The four corner grips. `aria-hidden` spans, never controls — the keyboard road
   to the same two gestures is on the box itself (arrows move, shift+arrows move
   further), and four extra tab stops per box would be twenty unaimable ones on
   one picture. */
#sec-ads .ads-vbox-grip {
  position: absolute; width: 10px; height: 10px;
  background: var(--surface-1); border: 1.5px solid var(--accent);
  border-radius: 2px;
}
#sec-ads .ads-vbox-grip.is-nw { left: -5px; top: -5px; cursor: nwse-resize; }
#sec-ads .ads-vbox-grip.is-ne { right: -5px; top: -5px; cursor: nesw-resize; }
#sec-ads .ads-vbox-grip.is-sw { left: -5px; bottom: -5px; cursor: nesw-resize; }
#sec-ads .ads-vbox-grip.is-se { right: -5px; bottom: -5px; cursor: nwse-resize; }
/* The toggle sits on its own row under the calibration rows, right-aligned with
   the panel's other action rows rather than floating at the start of a stack. */
#sec-ads .ads-vadjust { justify-content: flex-start; }

@media (prefers-reduced-motion: reduce) {
  #sec-ads .ads-vbox.is-active { animation: none; }
  #sec-ads .ads-vbox-tag { transition: none; }
}
/* The bar list doubles as the picture's legend, so its rows are the same click
   target as the boxes. A button carrying a stacked layout needs the browser's
   own button chrome stood down first. */
#sec-ads button.ads-vbar {
  width: 100%; text-align: left; padding: var(--space-1) var(--space-2);
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  color: inherit; font: inherit; cursor: pointer;
}
#sec-ads button.ads-vbar:hover { background: var(--surface-2); }
#sec-ads button.ads-vbar[aria-pressed="true"] {
  border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent);
}
/* The row for the spot the detail is AUTO-FOLLOWING. Quieter than the pin above,
   the same relationship its box on the picture has. */
#sec-ads button.ads-vbar.is-shown {
  border-color: color-mix(in srgb, var(--accent-2) 55%, transparent);
}
#sec-ads button.ads-vbar:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The spot's human name leads the row; the id is demoted to the faint mono line
   under the meter, where a support screenshot still catches it and where it no
   longer competes with the words a person wrote. */
#sec-ads .ads-vbar-name {
  font-size: 12.5px; font-weight: 600; color: var(--text);
  min-width: 0; overflow-wrap: anywhere;
}
#sec-ads .ads-vcal-name { font-size: 12.5px; font-weight: 600; color: var(--text); }
/* The calibration row's honesty line ("1.02x · 8px down"): a MEASUREMENT, so it
   is mono and left in flow rather than borrowing the right-aligned caption
   treatment `.ads-why` was written for. */
#sec-ads .ads-vcal-line {
  font-family: var(--font-mono); font-size: 11px;
  font-variant-numeric: tabular-nums; color: var(--text-dim);
}
/* ── the close-up (2026-08-31) ─────────────────────────────────────────────
   It is a `.well` beside the picture now, not a bare rule-and-indent under it.
   Framing it is half of what stops the pair reading as two unrelated fragments;
   the heading naming the spot and its score is the other half. */
#sec-ads .ads-vdetail { min-width: 0; align-self: start; }
#sec-ads .ads-vdetail-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2); flex-wrap: wrap;
}
#sec-ads .ads-vdetail-name {
  font-size: 13px; font-weight: 600; color: var(--text);
  min-width: 0; overflow-wrap: anywhere;
}
/* "strongest now" / "pinned": a MODE, so it is the mono micro-label every other
   machine note on this page wears, not a coloured pill claiming a status. */
#sec-ads .ads-vdetail-tag { flex: none; }
#sec-ads .ads-vdetail .ads-vthumb { max-height: 200px; }

/* ── 390px (ads page overhaul, 2026-08-31) ───────────────────────────
   The mock's two phone frames are the SAME renderers at one breakpoint, not a
   second layout. Four rules do the whole job:
     · the cockpit's columns and the settings grid stack (handled at 980px above,
       which every narrow width also passes);
     · tiles go TWO-UP rather than one, because a tile is a summary and one per
       row would be the accordion again in a different costume;
     · moment chips go one per row, because a chip's whole point is that its name
       and its switch share a line, and two-up at 390px breaks that line;
     · the shelf loses its side padding, so an annotated 16:9 frame gets the full
       width it needs to stay legible.
   The two survivors below are unchanged: a caption that is right-aligned in a
   wide row has nothing to align against here, and a stepper button has to be
   thumb-sized. */
@media (max-width: 560px) {
  #sec-ads .ads-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); }
  /* The summary WRAPS rather than ellipsing, because half of 390px is too narrow
     to say "Match end, pre-match countdown, lobby" in one line and an ellipsis
     there would hide the moment list entirely. The foot wraps with it, so the
     health and Off marks drop to their own line instead of sitting inside the
     wrapped sentence — verified by eye at the narrow breakpoint, where they read
     as interleaved without this. */
  #sec-ads .ads-tile-sum { white-space: normal; flex: 1 1 100%; }
  #sec-ads .ads-tile-foot { flex-wrap: wrap; }
  #sec-ads .ads-moments { grid-template-columns: 1fr; }
  #sec-ads .ads-shelf-body { padding: var(--space-3) var(--space-2) var(--space-3); }
  /* The disclosure's rows stack rather than squeezing the caption into a sliver. */
  #sec-ads .ads-adv .sec-row { flex-wrap: wrap; gap: var(--space-2); }
  #sec-ads .ads-why { text-align: left; max-width: none; }
  #sec-ads .ads-stepper button { min-height: var(--touch-min); }
}

/* ══════════════════════════════════════════════════════════════════════════
   ANALYTICS (#sec-analytics) — analytics plan AN5-1, from the approved AN4
   mock (rev 1). Everything that is not a continuous series stays on the
   primitives this sheet already has: .stat, .stat-bar, .availbadge, .table
   inside .sec-tablewrap, .bar for the buckets. Nothing new was invented that
   an existing class could carry.
   ══════════════════════════════════════════════════════════════════════════ */

/* 🔴 min-width:0 ON THE SECTION AND ON EVERY GRID CHILD. A grid item's
   automatic minimum is its MIN-CONTENT width, which is the exact mechanism
   that let a reconnect strip widen the whole app in 8c0d36ce. The stream
   table is the widest thing on this page and it must scroll inside its own
   wrapper, never push the shell sideways. */
#sec-analytics { min-width: 0; }
#sec-analytics .sec-grid > * { min-width: 0; }
/* The full-width row of a two-column grid (the curve, the follower card). */
#sec-analytics .an-span2 { grid-column: 1 / -1; }

/* ── the viewer curve ──────────────────────────────────────────────────── */
.an-curve { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.an-curve .sk { display: block; width: 100%; height: auto; }
.sk-grid { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: .55; }
/* non-scaling-stroke is what keeps the line 1.8px at 640 wide AND at 390. */
.sk-line { fill: none; stroke-width: 1.8; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; }
.sk-area { opacity: .13; }
.sk-dot { stroke: var(--bg); stroke-width: 1.5; }
.an-axis { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; color: var(--text-faint); letter-spacing: .04em; }
.an-curvekey { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: 11.5px; color: var(--text-dim); }
.an-curvekey b { color: var(--text); font-weight: 600; }
.an-curvewrap { position: relative; background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--space-3); }
/* The dropout sentence wears the dashed rule that IS the break in the line. */
.an-gapnote { font-size: 11px; color: var(--warn); display: inline-flex; align-items: center; gap: 6px; }
.an-gapnote::before { content: ""; width: 14px; height: 0; border-top: 2px dotted var(--warn); display: inline-block; }

/* ── the schedule verdict ──────────────────────────────────────────────── */
.an-verdict {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  background: var(--surface-inset); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--space-3);
}
.an-verdict .vlabel { font-family: var(--font-mono); font-size: 15px; font-weight: 700; letter-spacing: .01em; }
.an-verdict.v-ontime .vlabel { color: var(--ok-soft); }
.an-verdict.v-late .vlabel { color: var(--warn); }
.an-verdict.v-early .vlabel { color: var(--accent-2); }
/* 🔴 `unmatched` is DIM, never danger: a spontaneous stream has done nothing
   wrong and the colour must not scold either. */
.an-verdict.v-unmatched .vlabel { color: var(--text-dim); }
.an-verdict .vline { flex: 1 1 18rem; min-width: 0; font-size: 12.5px; color: var(--text-dim); }

/* ── the adherence timeline ────────────────────────────────────────────── */
.an-slots { display: flex; flex-direction: column; gap: var(--space-1); }
.an-slot { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2); border-radius: var(--r-sm); min-width: 0; }
.an-slot:hover { background: var(--surface-3); }
.an-slot .sday { flex: 0 0 8.5rem; font-size: 12px; font-weight: 600; }
.an-slot .sday span { display: block; font-weight: 400; font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); }
/* The centre line is the slot the creator announced; the bar runs RIGHT when
   they were late and LEFT when they were early, so the pattern reads before
   any number does. */
.an-track { flex: 1 1 8rem; min-width: 6rem; height: 22px; position: relative; background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-chip); overflow: hidden; }
.an-track::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--border-strong); }
/* The shaded band IS the 10-minute tolerance, drawn to scale: the track reaches
   60 minutes each way (schedule-view.js TRACK_HALF_MIN), so +/-10 minutes is
   16.67% of its width. The key under the timeline promises "inside the shaded
   band", so the band and the rule have to be the same thing. */
.an-track .tol { position: absolute; top: 0; bottom: 0; left: calc(50% - 8.335%); width: 16.67%; background: var(--surface-2); }
.an-track i { position: absolute; top: 4px; bottom: 4px; border-radius: 2px; display: block; }
.an-track i.late { left: 50%; background: var(--warn); }
.an-track i.early { right: 50%; background: var(--accent); }
.an-track i.ontime { background: var(--ok); }
.an-slot .soff { flex: 0 0 8.5rem; text-align: right; font-family: var(--font-mono); font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--text-dim); }
.an-slot.is-late .soff { color: var(--warn); }
.an-slot.is-ontime .soff { color: var(--ok-soft); }
.an-slot.is-early .soff { color: var(--accent-soft); }
.an-slot.is-unmatched .soff { color: var(--text-faint); }
.an-tolkey { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: 11px; color: var(--text-faint); }
.an-tolkey i { width: 10px; height: 10px; border-radius: 2px; display: inline-block; margin-right: 5px; vertical-align: -1px; }
.an-tolkey i.k-ontime { background: var(--ok); }
.an-tolkey i.k-late { background: var(--warn); }
.an-tolkey i.k-early { background: var(--accent); }

/* ── the quiet ad row (PART AD) ────────────────────────────────────────── */
.an-adrow {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
  padding: var(--space-2) var(--space-3); border: 1px solid var(--border);
  border-radius: var(--r-sm); background: var(--surface-inset);
}
.an-adrow .adfig { font-family: var(--font-mono); font-size: 12.5px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.an-adrow .adfig b { color: var(--text); font-weight: 700; }
.an-adrow .an-link { margin-left: auto; }
.an-adrow .adnote { flex: 1 1 100%; font-size: 11px; color: var(--text-faint); max-width: var(--w-prose); }

/* ── the honest states ─────────────────────────────────────────────────── */
.an-honest {
  background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2);
}
.an-honest .hline { font-size: 12.5px; color: var(--text-dim); max-width: var(--w-prose); }
.an-honest.is-warn { border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.an-honest.is-danger { border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
/* 🔴 THE DASH IS NOT A ZERO. It is the only mark this page uses where a number
   would otherwise go, and it is deliberately quiet rather than alarming. */
.an-dash { font-family: var(--font-mono); color: var(--text-faint); }

/* ── the correlation rows and their deep links ─────────────────────────── */
.an-corr {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) 0; border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.an-corr:last-child { border-bottom: 0; }
.an-corr .cright { display: inline-flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; justify-content: flex-end; }
.an-corr .cv { font-family: var(--font-mono); font-weight: 700; font-variant-numeric: tabular-nums; }
.an-link { font-size: 11.5px; font-weight: 600; color: var(--accent-soft); text-decoration: none; }
.an-link:hover { text-decoration: underline; }

/* ── the start-time buckets ────────────────────────────────────────────────
   Ordered buckets are five groups, not a continuous series, so they use the
   shared .bar meter rather than a curve: a line between them would imply
   minutes we did not measure (DESIGN-GUIDELINES §3.6). */
.an-buckets { display: flex; flex-direction: column; gap: var(--space-2); }
.an-bucket { display: flex; align-items: center; gap: var(--space-3); font-size: 12px; }
.an-bucket .bk { flex: 0 0 6.5rem; color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; }
.an-bucket .bar { flex: 1; min-width: 0; height: 8px; }
.an-bucket .bv { flex: 0 0 3.5rem; text-align: right; font-family: var(--font-mono); font-weight: 700; font-variant-numeric: tabular-nums; }

/* ── the first-run empty state ─────────────────────────────────────────── */
.an-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); padding: var(--space-8) var(--space-4); text-align: center; }
.an-empty h3 { font-size: 16px; font-weight: 600; }
.an-empty p { font-size: 13px; color: var(--text-dim); max-width: 48ch; }
.an-emptymark { width: 56px; height: 56px; border-radius: var(--r); border: 1px dashed var(--border-strong); display: grid; place-items: center; color: var(--text-faint); }

/* The stream table is clickable, so it says so. */
#sec-analytics .table tbody tr[data-an-session] { cursor: pointer; transition: background var(--dur-fast) var(--ease); }
#sec-analytics .table tbody tr[data-an-session]:hover { background: var(--surface-3); }
#sec-analytics .table tbody tr.is-open { background: var(--accent-wash); }

/* ══ MOBILE IS PART OF DONE (owner ruling 5, AN5-1 Step 4) ══════════════════
   Drawn in the mock at 390px, not promised: the table scrolls inside its own
   wrapper and the PAGE never scrolls sideways, the stat bar becomes a
   horizontal strip, every control row wraps, the curve keeps its shape, and
   every target clears 44px. */
@media (max-width: 560px) {
  /* Columns scroll rather than crush. The wrapper scrolls, not the page. */
  #sec-analytics .sec-tablewrap > .table { min-width: 34rem; }
  #sec-analytics .subtabs button { min-height: var(--touch-min); }
  #sec-analytics .seg button { min-height: var(--touch-min); padding: 8px 14px; }
  #sec-analytics .btn { min-height: var(--touch-min); }
  #sec-analytics .page-header .ph-actions { margin-left: 0; width: 100%; }
  /* The adherence row moves its track to its own line so the day, the offset
     and the verdict stay readable side by side. */
  .an-slot { flex-wrap: wrap; row-gap: var(--space-1); }
  .an-slot .sday { flex: 1 1 auto; }
  .an-slot .soff { flex: 0 0 auto; }
  .an-track { flex: 1 1 100%; order: 3; }
  .an-verdict { gap: var(--space-2); }
  .an-corr { flex-wrap: wrap; }
  .an-curvekey { gap: var(--space-2); }
}

/* ══════════════════════════════════════════════════════════════════════════
   HOME — the `Last stream` card (analytics plan AN6-1, from the same approved
   AN4 mock). It is a TEASER for #sec-analytics, so it borrows that page's
   primitives outright: .stat-label / .stat-value for the figures, .legend +
   .pdot for the platforms, .an-link for the way out, .an-empty for day one,
   .an-dash for a figure nobody has counted yet. The only new thing here is the
   wrapping stat row, because Home's cards are narrower than the page's.
   ══════════════════════════════════════════════════════════════════════════ */

/* 🔴 min-width:0 ON THE CARD AND ON THE ROW. A grid item's automatic minimum is
   its MIN-CONTENT width — the mechanism that let a reconnect strip widen the
   whole app in 8c0d36ce — and this card now holds an 80px SVG beside a monospace
   number. Without this it can push Home's grid wider than the viewport. */
.home-teaser { min-width: 0; }
.home-teaser .hs-row { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); min-width: 0; }
/* Each stat is free to shrink to its own content and to WRAP onto the next line
   rather than crushing its neighbours. `1 1 5.5rem` is what makes three of them
   sit in a row on a desktop card and stack cleanly at 320px. */
.home-teaser .hs { flex: 1 1 5.5rem; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* The peak stat carries the sparkline, so it asks for more of the row. */
.home-teaser .hs--peak { flex: 1 1 11rem; }
.home-teaser .hs .stat-value { font-size: 17px; }
.home-teaser .hs-spark { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.home-teaser .hs-spark .stat-value { flex: none; }
/* The teaser size the curve builder was written for: 80 by 22, fixed, so the
   sparkline reads as a glyph beside the number rather than a chart. */
.home-teaser .hs-sparkline { flex: none; display: block; }
.home-teaser .hs-sparkline .sk { width: 80px; height: 22px; display: block; }
.home-teaser .hs-foot {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--border);
}
/* Day one is every account on release day (the collector is forward-only), so
   the empty state is tighter here than on the page it links to — it is one card
   in a grid, not a whole section. */
.home-teaser .an-empty { padding: var(--space-5) var(--space-3); }
.home-teaser .an-empty h3 { font-size: 14px; }
.home-teaser .an-empty p { font-size: 12.5px; }

/* ══ MOBILE IS PART OF DONE (AN6-1 Step 4) ═════════════════════════════════
   Pinned at 320px, the narrowest phone this product supports: the stat row
   wraps, the sparkline keeps its 80px and never forces a scrollbar, and the
   card cannot widen Home's grid. */
@media (max-width: 560px) {
  .home-teaser .hs { flex: 1 1 8rem; }
  .home-teaser .hs--peak { flex: 1 1 100%; }
  .home-teaser .hs-foot { row-gap: var(--space-2); }
  .home-teaser .btn { min-height: var(--touch-min); }
}
