/* ============================================================================
   Influencer Detective — Design System v1 (see DESIGN_SYSTEM.md)
   Charcoal app theme. Dark mode only. Derived from the approved mockups.
   This file = tokens + reusable component classes + Tabulator re-theme.
   ============================================================================ */

/* ============================================================================
   THE CONTROL TOWER — single source of truth for the whole app's colors.
   Change a value HERE and it updates everywhere (Tailwind utilities like
   `bg-canvas` AND the component classes below both derive from these).
   Colors are stored as "R G B" channels so opacity modifiers work too
   (e.g. bg-canvas/85, var(--border-subtle) uses ink at 6% alpha).
   ============================================================================ */
:root {
  color-scheme: dark; /* makes native controls (select dropdown lists, scrollbars, date pickers) render dark */
  /* --- surfaces (elevation by lightness) --- */
  --c-canvas:11 11 15;     /* app background        #0B0B0F */
  --c-surface:20 20 23;    /* sidebar, panels, rows #141417 */
  --c-elevated:28 28 33;   /* cards, modals, hover  #1C1C21 */
  --c-hover:36 36 42;      /* hover state           #24242A */
  --c-inputbg:32 32 37;    /* form fields           #202025 */
  --c-edge:38 38 44;       /* solid border          #26262C */
  /* --- text --- */
  --c-ink:245 245 247;     /* primary  #F5F5F7 */
  --c-ink2:161 161 170;    /* secondary #A1A1AA */
  /* muted #8A8A93. Lifted 2026-09-14 (owner) from #6B6B73, which was 3.7:1 on the canvas and
     below the 4.5:1 small text needs; this is 5.7:1. It feeds --text-3 and Tailwind's text-ink3,
     i.e. every muted line in the app, plus a few muted strokes that borrow it (a slider knob, a
     dashed chip border, a tab underline), which simply read a little clearer. Its own commit, so it
     can be reverted alone if a screen looks wrong. */
  --c-ink3:138 138 147;
  /* Muted TEXT token the marketing pages (pricing, landing) read by name. Same value as --c-ink3
     above since the lift; kept as a separate name because the marketing mockups keep THEIR --ink3
     at the old grey for strokes, dividers and placeholders, and their templates port that block. */
  --ink-muted:#8A8A93;
  /* --- brand + status --- */
  --c-accent:139 92 246;   /* violet  #8B5CF6 */
  --c-ok:34 197 94;        /* green   #22C55E */
  --c-warn:251 191 36;     /* amber   #FBBF24 */
  --c-bad:239 68 68;       /* red     #EF4444 */
  --c-info:75 141 255;     /* blue    #4B8DFF */
  --c-special:168 85 247;  /* purple  #A855F7 */
  --c-clip:34 211 238;     /* cyan    #22D3EE */

  /* --- derived aliases (used by the component classes; do not edit, edit channels above) --- */
  --bg-base:rgb(var(--c-canvas)); --bg-surface:rgb(var(--c-surface)); --bg-elevated:rgb(var(--c-elevated));
  --bg-hover:rgb(var(--c-hover)); --bg-input:rgb(var(--c-inputbg)); --edge:rgb(var(--c-edge));
  --border-subtle:rgb(var(--c-ink) / .06); --border:rgb(var(--c-ink) / .10); --border-strong:rgb(var(--c-ink) / .16);
  --text-1:rgb(var(--c-ink)); --text-2:rgb(var(--c-ink2)); --text-3:rgb(var(--c-ink3));
  --accent:rgb(var(--c-accent)); --accent-soft:rgb(var(--c-accent) / .15);
  --green:rgb(var(--c-ok)); --amber:rgb(var(--c-warn)); --red:rgb(var(--c-bad));
  --blue:rgb(var(--c-info)); --purple:rgb(var(--c-special)); --cyan:rgb(var(--c-clip));

  /* --- brand gradient + non-color tokens --- */
  --grad:linear-gradient(95deg,#F72FE0 0%,#8B3DFF 50%,#4B6EF5 100%);
  --grad-glow:0 0 24px rgba(139,61,255,.45);
  --grad-green:linear-gradient(90deg,#16A34A 0%,#4ADE80 100%); /* in-progress bars — simple same-hue green */
  --r-sm:6px; --r-md:10px; --r-lg:16px; --r-pill:999px;
  --table-font:14px;
}

/* progress bars (running stages, campaign keyword progress) — lime→green gradient */
.bar-track { background:var(--bg-hover); border-radius:999px; overflow:hidden; }
.bar-fill { background:var(--grad-green); border-radius:999px; height:100%;
  transition:width .35s ease; box-shadow:0 0 12px rgba(74,222,128,.45); }
.bar-fill-indeterminate { position:relative; width:35% !important; border-radius:999px; height:100%;
  background:var(--grad-green); box-shadow:0 0 12px rgba(74,222,128,.45);
  animation:bar-slide 1.3s ease-in-out infinite; }
@keyframes bar-slide { 0%{margin-left:-35%} 100%{margin-left:100%} }

/* ---- base ---------------------------------------------------------------- */
body {
  font-family:'Inter',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  font-feature-settings:"cv02","cv03","cv04";
}
.font-mono,.mono-label,.tabular { font-family:'JetBrains Mono',ui-monospace,monospace; }
.tabular,.num { font-variant-numeric:tabular-nums; }

/* ---- typographic helpers ------------------------------------------------- */
.mono-label { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3); }
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; }
/* ITALIC gradient words that never lose their slanted edge (launch tests, 2026-09-17: the Spanish "sí" and
   the last "t" of "actually convert" were clipped on Log in / Sign up). Use `grad-text grad-italic`, never
   `grad-text italic`. WHY: background-clip:text paints the gradient only inside the span's own box, and
   Inter loads with no italic face (_ds_head.html asks for weights only), so the browser fakes the slant and
   a tall letter leans up to ~.24em past that box. box-decoration-break:clone gives EVERY line piece its own
   room (.3em right, .1em left), and the negative margins give the room back so the full stop and the words
   around it do not move. Values from designs/mockups/auth-pages-unfinished-migration.html #lt-auth-fixes
   (option B, BUILD NOTE; measured there in all seven languages). In the app, a pixel check (the gradient
   words vs the same words in plain white) on Log in and Sign up, English and Spanish, at 1440/1280/1024:
   79-198 cut pixels before, 0 after (one anti-aliasing pixel mid-letter at 1024 on a 1x screen). */
.grad-italic { font-style:italic; -webkit-box-decoration-break:clone; box-decoration-break:clone;
  padding-right:.3em; margin-right:-.3em; padding-left:.1em; margin-left:-.1em; }
.eyebrow { display:inline-flex; align-items:center; gap:8px; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--text-2);
  border:1px solid var(--border); border-radius:var(--r-pill); padding:6px 13px; background:var(--bg-surface); }
.eyebrow .ic { width:13px; height:13px; border-radius:4px; background:var(--grad); }

/* ---- buttons ------------------------------------------------------------- */
.btn { border:none; border-radius:var(--r-pill); padding:10px 18px; font-family:inherit; font-size:13px;
  font-weight:600; cursor:pointer; display:inline-flex; align-items:center; gap:7px; line-height:1;
  transition:background .15s,border-color .15s,transform .1s,box-shadow .15s; }
.btn:active { transform:translateY(1px); }
.btn-grad { background:var(--grad); color:#fff; font-weight:700; text-shadow:0 1px 3px rgba(0,0,0,.55);
  box-shadow:var(--grad-glow); }
/* Lift + glow beats the old brightness(1.08) filter, which was too subtle to notice on most buttons
   (owner feedback 2026-08-25, "gradient button hover is not present everywhere" - it was always there,
   just invisible). .btn-grad:active overrides .btn's own :active so a click still visibly presses down
   instead of the hover scale winning the whole time the mouse sits there (same specificity as .btn:active,
   later in source order - without this the press feedback silently disappears on every gradient button). */
.btn-grad:hover { transform:scale(1.035); box-shadow:0 0 34px rgba(139,61,255,.65); }
.btn-grad:active { transform:scale(.98); }
@media (prefers-reduced-motion:reduce) { .btn-grad:hover, .btn-grad:active { transform:none; } }
.btn-outline { background:transparent; color:var(--text-1); border:1px solid var(--border-strong); }
.btn-outline:hover { background:var(--bg-hover); }
.btn-ghost { background:transparent; color:var(--text-2); }
.btn-ghost:hover { background:var(--bg-hover); color:var(--text-1); }
.btn-danger { background:rgba(239,68,68,.12); color:#FCA5A5; border:1px solid rgba(239,68,68,.3); }
.btn-danger:hover { background:rgba(239,68,68,.2); }

/* ---- banners (warning / error / info notices) — 2026-08-25 ---------------
   Replaces 8 ad hoc raw-Tailwind alert boxes across project_detail.html, base.html and
   bulk_email.html (bg-amber-900/10 + a bare "&#9888;" character, each hand-rolled slightly
   differently). One component: an icon in a colored circle (real Lucide icon, not a text glyph -
   matches the app-wide Lucide migration everything else already went through) + a bold lead +
   muted rest. NO left accent bar (tried, owner said no - the icon circle alone carries enough
   hierarchy). .banner-info is for the two found instances that were misusing amber "warning" for a
   plain FYI note - color is supposed to mean something here, not just look consistent. */
.banner { display:flex; align-items:flex-start; gap:11px; padding:10px 13px; border-radius:var(--r-md);
  font-size:12.5px; border:1px solid transparent; }
/* Several banners are shown/hidden by toggling Tailwind's .hidden. .banner{display:flex} and
   .hidden{display:none} are BOTH one plain class, so whichever stylesheet lands later in the head
   wins - and that is often Tailwind's CDN-injected sheet, not app.css. Two classes beat one, so
   this settles it on purpose instead of by luck of source order (DESIGN_SYSTEM §17.3). */
.banner.hidden { display:none; }
.banner .ic-circle { flex:none; width:20px; height:20px; border-radius:50%; display:flex;
  align-items:center; justify-content:center; margin-top:1px; }
.banner .ic-circle svg { display:block; }
.banner .lead { font-weight:600; }
.banner .rest { color:var(--text-2); }
.banner .em { color:var(--text-1); }
.banner a.retry { text-decoration:underline; cursor:pointer; }
.banner-warn { background:rgba(251,191,36,.07); border-color:rgba(251,191,36,.18); }
.banner-warn .ic-circle { background:rgba(251,191,36,.18); }
.banner-warn .ic-circle svg { stroke:#FBD375; }
.banner-warn .lead { color:#FBD375; }
.banner-warn a.retry { color:#FBD375; }

/* A reply that MAY have asked us to stop (suppression.sort_inbound, 2026-09-27; owner-approved
   designs/mockups/unsubscribe-pause-review.html). AMBER while paused (owner: grey "might not get
   observed if you just skim"), the .banner-warn values with a stronger border; a quiet line once
   settled. Rendered by stopNoteHtml() in base.html, above the email in the creator panel. */
.stop-note { border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-elevated); padding:12px; margin:8px 0 10px; }
.stop-note.is-ask { background:rgba(251,191,36,.07); border-color:rgba(251,191,36,.38); padding:14px; }
.stop-note .sn-head { display:flex; align-items:center; gap:8px; font-size:13px; font-weight:700; color:#FBD375; margin:0 0 8px; }
.stop-note .sn-head svg { width:15px; height:15px; color:var(--amber); flex:none; }
.stop-note .sn-src { display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:rgba(251,211,117,.7); }
.stop-note .sn-src svg { width:13px; height:13px; flex:none; }
.stop-note .sn-find { margin:6px 0 0; font-size:14px; line-height:1.5; color:var(--text-1); font-weight:500; }
.stop-note .sn-sub { display:block; font-size:12.5px; color:var(--text-3); font-weight:400; margin-top:2px; }
.stop-note .sn-ask { font-size:12.5px; color:var(--text-2); margin:12px 0 8px; font-weight:600; }
.stop-note .sn-btns { display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap; }
.stop-note .sn-done { display:flex; align-items:center; gap:8px; flex-wrap:wrap; font-size:12.5px; color:var(--text-2); }
.stop-note .sn-done button { color:#A8C7FF; background:none; border:0; padding:0; font:inherit; cursor:pointer; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; }
.stop-mark { background:rgba(251,191,36,.15); color:#FDE68A; border-radius:4px; padding:1px 3px; font-weight:600; }

/* Drafts our AI check HELD BACK (draft_check.py, 2026-09-27; owner-approved
   designs/mockups/draft-hold-review.html A): the quiet line under Send All, the one-at-a-time window
   (#held-modal in project_detail.html) and the note above the draft in the creator panel. */
.held-line { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:12px 0 0; font-size:13px; color:var(--text-2); }
.held-line b { color:var(--text-1); }
.held-line svg { width:14px; height:14px; color:var(--text-3); flex:none; }
.held-count { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; color:var(--text-3); font-variant-numeric:tabular-nums; white-space:nowrap; }
.held-prog { height:3px; border-radius:999px; background:var(--bg-hover); overflow:hidden; margin:8px 0 12px; }
.held-prog i { display:block; height:100%; background:rgb(var(--c-ink) / .55); transition:width .25s; }
.held-who { display:flex; align-items:center; gap:8px; margin-bottom:10px; }
.held-av { width:26px; height:26px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#fff; background:linear-gradient(135deg,#8B3DFF,#3B82F6); flex:none; }
.held-src { display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
.held-src svg { width:13px; height:13px; flex:none; color:var(--text-2); }
.held-find { margin:6px 0 12px; font-size:15px; line-height:1.5; color:var(--text-1); font-weight:500; }
.held-mail { border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-surface); overflow:hidden; }
.held-mhead { padding:10px 14px; border-bottom:1px solid var(--border-subtle); font-size:12px; color:var(--text-3); line-height:1.6; overflow-wrap:anywhere; }
.held-mhead b { color:var(--text-2); font-weight:600; }
.held-mbody { padding:12px 14px; font-size:13.5px; line-height:1.6; color:var(--text-2); white-space:pre-line; overflow-wrap:anywhere; }
.held-mbody mark, .held-note mark { background:rgba(251,191,36,.15); color:#FDE68A; border-radius:4px; padding:1px 3px; font-weight:600; -webkit-box-decoration-break:clone; box-decoration-break:clone; }
.held-facts { font-size:12px; color:var(--text-3); line-height:1.5; margin:10px 0 0; }
.held-facts b { color:var(--text-2); font-weight:600; }
.held-ask { font-size:12.5px; color:var(--text-2); margin:14px 0 8px; font-weight:600; }
.held-btns { display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap; }
.held-btns .btn[aria-pressed="true"] { border-color:rgba(167,139,250,.6); color:#fff; background:rgba(139,61,255,.14); }
.held-foot { display:flex; align-items:center; gap:8px; margin-top:10px; }
.held-edit { margin-left:auto; font-size:12.5px; color:#A8C7FF; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; background:none; border:0; padding:0; cursor:pointer; font-weight:500; }
.held-ledger { border-top:1px solid var(--border-subtle); }
.held-lrow { display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid var(--border-subtle); font-size:13px; color:var(--text-1); }
.held-lrow .h { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.held-done-say { font-size:13px; color:var(--text-2); line-height:1.55; margin:12px 0 0; }
.held-note { border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-elevated); padding:12px; margin-bottom:10px; }
.held-note .held-q { margin:8px 0 0; font-size:13px; line-height:1.55; color:var(--text-2); }
.held-note .acts { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; font-size:12px; color:var(--text-3); }
@media (max-width:480px) { .held-btns .btn { flex:1 1 100%; justify-content:center; } }
@media (max-width:480px) { .stop-note .sn-btns .btn { flex:1 1 100%; justify-content:center; } }
.banner-error { background:rgba(239,68,68,.07); border-color:rgba(239,68,68,.18); }
.banner-error .ic-circle { background:rgba(239,68,68,.18); }
.banner-error .ic-circle svg { stroke:#FCA5A5; }
.banner-error .lead { color:#FCA5A5; }
.banner-error a.retry { color:#FCA5A5; }
.banner-info { background:var(--bg-hover); border-color:var(--border); }
.banner-info .ic-circle { background:rgb(var(--c-ink) / .10); }
.banner-info .ic-circle svg { stroke:var(--text-2); }
.banner-info .lead { color:var(--text-1); }

/* ---- inputs -------------------------------------------------------------- */
.ds-input,.ds-select,.ds-textarea {
  background:var(--bg-input); border:1px solid var(--border); border-radius:var(--r-md);
  padding:10px 13px; color:var(--text-1); font-family:inherit; font-size:14px; outline:none; }
.ds-input::placeholder,.ds-textarea::placeholder { color:var(--text-3); }
.ds-input:focus,.ds-select:focus,.ds-textarea:focus {
  border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
/* ---------------------------------------------------------------------------
   .ds-toggle — the shared iOS-style switch (DESIGN_SYSTEM §8.3). Reach for this
   rather than writing another one; before this existed the app had no toggle at
   all and settings used plain checkboxes.

   Markup:  <button class="ds-toggle" aria-pressed="true|false"><i></i></button>
   Flip `aria-pressed` in JS; the styling and the animation follow from it, and
   screen readers get the state for free.

   The knob moves with translateX, NOT by swapping left/right — you cannot
   animate between two different offset properties, which is why the first
   version snapped instead of sliding.
   --------------------------------------------------------------------------- */
.ds-toggle {
  position:relative; flex-shrink:0; width:40px; height:22px; padding:0; border:none;
  border-radius:var(--r-pill); background:var(--bg-hover); cursor:pointer;
  transition:background .22s cubic-bezier(.22,1,.36,1), box-shadow .18s;
}
.ds-toggle i {
  position:absolute; top:3px; left:3px; width:16px; height:16px; border-radius:50%;
  background:var(--text-3); display:block; box-shadow:0 1px 3px rgba(0,0,0,.4);
  transition:transform .22s cubic-bezier(.22,1,.36,1), background .22s;
}
.ds-toggle[aria-pressed="true"] { background:var(--grad); box-shadow:0 0 14px rgba(139,61,255,.35); }
.ds-toggle[aria-pressed="true"] i { transform:translateX(18px); background:#fff; }
.ds-toggle:hover { filter:brightness(1.12); }
.ds-toggle:active i { width:19px; }                 /* slight squash on press */
.ds-toggle:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
@media (prefers-reduced-motion:reduce) {
  .ds-toggle, .ds-toggle i { transition:none; }
}

/* violet-accented checkboxes/radios app-wide (row selects, toggles, column menu, settings) */
input[type="checkbox"], input[type="radio"] { accent-color:rgb(var(--c-accent)); }

/* .ds-form: wrap a dialog/form body to style every control consistently (focus rings, spacing).
   Higher specificity than Tailwind utilities, so it cleanly overrides inline bg/border classes. */
.ds-form label { display:block; font-size:13px; font-weight:500; color:var(--text-2); margin-bottom:5px; }
/* A label that WRAPS its own text field ("start [20]", "max/day [500]") is an inline phrase, not a
   caption above a field, so it stays on the line with its box. Tick-box labels are untouched. */
.ds-form label:has(> input:not([type="checkbox"]):not([type="radio"])) {
  display:inline-flex; align-items:center; gap:6px; margin-bottom:0; }
.ds-form input:not([type="checkbox"]):not([type="radio"]),
.ds-form select, .ds-form textarea {
  background:var(--bg-input); border:1px solid var(--border); border-radius:var(--r-md);
  color:var(--text-1); padding:9px 12px; font-size:14px; outline:none;
  transition:border-color .15s, box-shadow .15s; }
/* Full width is the DEFAULT, not a verdict: a field that carries its own Tailwind width (`w-14`,
   `w-24`, `w-auto`, `w-full`) keeps it. This rule used to set `width:100%` on every field at a
   specificity no width utility can beat, so a deliberately small box inside a .ds-form was silently
   stretched to the full row. Found 2026-09-23 (owner screenshot): the mailbox pacing row
   ("start [20] + [50] /week -> max/day [500]") exploded into full-width boxes on separate lines,
   Schedule search's "Custom" dropdown was crushed to 26px by its w-24 days box, and the Get-ideas
   count box. `[class^="w-"]`/`[class*=" w-"]` match a whole `w-` class, not `min-w-`/`max-w-`. */
.ds-form input:not([type="checkbox"]):not([type="radio"]):not([class^="w-"]):not([class*=" w-"]),
.ds-form select:not([class^="w-"]):not([class*=" w-"]),
.ds-form textarea:not([class^="w-"]):not([class*=" w-"]) { width:100%; }
/* ...and a SMALL box that keeps its own width gets tight padding: 12px a side leaves a w-12 number
   box ~22px for its digits, which cut "18" to "1" and "500" to "50" in the pacing row. */
.ds-form input:not([type="checkbox"]):not([type="radio"]):not(.w-full):is([class^="w-"], [class*=" w-"]) {
  padding:6px 8px; }
.ds-form input:focus, .ds-form select:focus, .ds-form textarea:focus {
  border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
.ds-form input::placeholder, .ds-form textarea::placeholder { color:var(--text-3); }

/* ===== DIALOGS (modals): THE one standard for every dialog in the app (2026-09-22) ==============
   DESIGN_SYSTEM.md §15.5 "Dialog standard" is the rule; this is its CSS. Markup:
     <div id="x-modal" class="ds-modal hidden flex bg-black/60 backdrop-blur-sm">   <- the overlay
       <div class="ds-modal-panel card-elevated w-full max-w-lg p-6">              <- the card
         <div>title + x</div>                   stays on screen
         <div class="ds-modal-body">...</div>   the ONLY part that scrolls
         <div>Cancel / primary</div>            stays on screen
   Why each line exists - every one of them was a real, owner-reported clip:
   - base.html MOVES every .ds-modal / .ds-portal to be a direct child of <body> on load. #vt-main
     (the page's content wrapper) has `view-transition-name` for the page fade, and that makes it its
     own stacking layer: a dialog declared inside a page could never rise above the top bar (z-30) or
     the sidebar (z-40) whatever z-index it had, so on a short screen the top bar sat on the dialog's
     title. That was the screenshot, and the four earlier "height" patches never touched it.
   - `.ds-modal-panel{margin:auto}` instead of relying on the overlay's items-center: a flex-centred
     box taller than the screen overflows equally above AND below, and the part above can never be
     scrolled to. Auto margins centre it when it fits and drop to 0 when it does not.
   - `max-height:100%` of the overlay (whose padding is the screen gutter), not `90vh`: vh ignores
     the gutter and, on phones, counts the area behind the browser's own toolbar.
   - the MIDDLE scrolls, never the whole card: a whole-card scroll takes the buttons off the bottom
     with the content, and on a laptop the customer saw four textareas and no Save button.
   - the z-index lives HERE (100: above the creator panel z-80 and the notification panel z-85,
     below the shared confirm z-120), so no overlay carries a Tailwind z-/p- class of its own.
     Tailwind's CDN sheet is injected AFTER app.css and wins a tie of equal specificity, so a
     leftover `z-50` or `py-6` would silently beat these rules. tests/test_dialog_standard.py
     refuses both. */
.ds-modal { position:fixed; inset:0; z-index:100; overflow-y:auto; overscroll-behavior:contain; padding:16px; }
@media (min-width:640px) { .ds-modal { padding:24px; } }
/* Stacked on top of an open dialog: the shared showConfirm / showPrompt. Two classes, so it beats
   the base rule whatever the source order. */
.ds-modal.ds-modal-top { z-index:120; }
.ds-modal-panel { margin:auto; max-height:100%; display:flex; flex-direction:column; min-height:0; }
/* Only the middle may give up height. A flex child with overflow:hidden (a progress bar, a clipped
   preview) has NO minimum height, so without this a capped panel squashes it to 0px instead of
   scrolling. A wrapper that CONTAINS the middle (a wizard step: body + its own buttons) must still
   shrink, which is what the :has() exclusion is for. */
.ds-modal-panel > :not(.ds-modal-body):not(:has(.ds-modal-body)) { flex-shrink:0; }
/* A short dialog with no marked middle scrolls as a whole ONLY as a last resort, so an unexpectedly
   long one still cannot spill past its own card. Anything with buttons gets a .ds-modal-body. */
.ds-modal-panel:not(:has(.ds-modal-body)) { overflow-y:auto; overscroll-behavior:contain; }
/* The negative margin + equal padding reach into the card's own padding (every padded card here is
   p-5 or p-6, 20-24px): a scroll box clips whatever sticks out of it, so without this room an input
   touching its edge would lose half its 3px focus ring and a chosen option would lose its 18px glow
   at the sides. Net change to the layout: none; the scrollbar simply sits nearer the card's edge.
   On a card with NO padding of its own, add `m-0` (Tailwind wins the tie) - see #be-detail-modal. */
.ds-modal-body { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  margin:-4px -20px; padding:4px 20px; }
/* The deal picker's content (#collab-assign-body) also opens inside the bulk bar's drop-down, a p-3
   box that scrolls itself. There the middle must be plain content: the 20px reach would stick out of
   the 12px padding and put a sideways scrollbar on the menu. */
#bulk-menu-deal .ds-modal-body { margin:0; padding:0; overflow:visible; }
/* THE CLOSE BUTTON of every WINDOW dialog (forms, lists, editors), top-right, one look everywhere.
   Owner's pick "A, quiet square", 2026-09-23 (designs/mockups/dialog-close-button.html, shot on the
   real dialogs): a plain ×, and a soft square under it on hover - the same shape as the run summary
   sheet's close, the one crafted × the app already had. QUESTION dialogs ("are you sure", the run
   window, pick-an-option) get NO × on purpose: Cancel sits beside the answer, and an × on a paid step
   reads as "did that cancel or not?". Escape and a click on the dark background still cancel them.
   Markup: <button data-modal-close class="ds-modal-x" aria-label="{{ t('common_close') }}" onclick="..."></button>
   The × is drawn by the mask below, so the button has no text and no glyph to get wrong (the old
   `&times;` text came out at three sizes and two greys). 32px square: a comfortable phone target. */
.ds-modal-x { width:32px; height:32px; flex:none; display:inline-grid; place-items:center; padding:0;
  border-radius:8px; color:var(--text-2); background:transparent; border:1px solid transparent; cursor:pointer;
  transition:background .15s, border-color .15s, color .15s; }
.ds-modal-x::before { content:""; width:14px; height:14px; background:currentColor;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E") center/contain no-repeat;
          mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6L6 18'/%3E%3C/svg%3E") center/contain no-repeat; }
.ds-modal-x:hover { background:var(--bg-hover); border-color:var(--border-subtle); color:var(--text-1); }
.ds-modal-x:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* ===== FORM CONTROL TOWER — global, adaptable form-control styling ============================
   ONE place styles every input/select/textarea + the native dropdown option list across the whole
   app. Colors derive from the token channels, so changing the tokens (or these rules) updates every
   field everywhere. Skips checkboxes/radios (accent-color above) and Tabulator (its own theme). ==== */
/* dark native <option> dropdown list (Chromium/Edge honor option bg/color) — kills the white default */
select option, select optgroup { background:var(--bg-elevated); color:var(--text-1); }
/* consistent violet focus ring on every text-like control + select, app-wide */
select:focus, textarea:focus,
input:focus:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]):not([type="submit"]):not([type="button"]) {
  border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); outline:none; }
/* live on hover, before focus (2026-08-23 "life" upgrade, ported from onboarding) — the big
   textarea/field a user is about to click did nothing until the click; waking up on arrival
   reads as a surface that responds, not a flat rectangle. Same one-step lighten as the rest of
   the app's elevation system (surface -> hover), so it stays inside the existing token language. */
select:hover, textarea:hover,
input:hover:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]):not([type="submit"]):not([type="button"]) {
  background:var(--bg-hover); border-color:var(--border-strong); }
select, textarea, input:not([type="checkbox"]):not([type="radio"]) {
  transition:background .15s, border-color .15s, box-shadow .15s; }

/* ---- native <select> popup restyle (progressive enhancement, 2026-08-25) --
   `appearance: base-select` (stable Chrome/Edge, not yet Safari/Firefox) lets the OPEN
   option-list popup be restyled with real CSS instead of the OS default — on Windows that
   default is a blue hover/selected highlight that clashes with this app's violet dark theme.
   Scoped to the bare `select` element (not a class) so every dropdown gets it automatically,
   present and future, with no opt-in needed from whoever writes that template. Gated behind
   @supports so browsers without the feature just keep today's native popup — this block only
   ADDS popup styling and never touches the closed box, so it layers on top of whatever each
   select's own classes already set (.ds-select, .ds-form select, Tailwind utility classes,
   the Tabulator header-filter select, etc) without changing it.
   Reference/live demo: designs/mockups/custom-select-dropdown.html. Rationale + decision
   record: DESIGN_SYSTEM.md (search "base-select"). */
@supports (appearance: base-select) {
  select, select::picker(select) { appearance: base-select; }
  select:open { border-color:var(--border-strong); } /* keyboard-opened (no hover) still gets the lit border */
  select::picker-icon { color:var(--text-3); transition:rotate .15s; }
  select:open::picker-icon { rotate:180deg; }

  select::picker(select) {
    background:var(--bg-elevated); border:1px solid var(--edge); border-radius:10px;
    padding:5px; margin-top:6px; box-shadow:0 20px 50px rgba(0,0,0,.5);
    opacity:0; transform:translateY(-4px) scale(.98); transform-origin:top center;
    transition:opacity .14s, transform .14s;
  }
  select:open::picker(select) { opacity:1; transform:translateY(0) scale(1); }
  @starting-style {
    select:open::picker(select) { opacity:0; transform:translateY(-4px) scale(.98); }
  }

  select option {
    padding:7px 10px; border-radius:6px; font-size:13px; color:var(--text-2);
    transition:background .1s;
  }
  select option:hover, select option:focus { background:var(--bg-hover); color:var(--text-1); }
  select option:checked { background:rgba(139,92,246,.14); color:var(--text-1); font-weight:600; }
  /* small violet dot instead of the browser's default checkmark glyph — matches the app's
     existing "small colored dot means status" idiom (same one .chip .dot already uses). */
  select option::checkmark {
    order:1; margin-left:auto; width:6px; height:6px; border-radius:50%;
    background:var(--accent); content:"";
  }
}

/* ---- a whole card gated by <fieldset disabled> --------------------------------
   Used where someone can SEE a panel but lacks the capability to change it (today: Project
   Prompts for a member without "Project settings"). The browser's own disabled styling washes
   text out to near-unreadable on a dark ground, which reads as broken rather than as read-only.
   This makes it look like the engineered-prompt textareas already do on that same card:
   dimmer surface, still-legible text, not-allowed cursor. Scoped to `fieldset[disabled]` so it
   can never touch an ordinary disabled control elsewhere. */
fieldset[disabled] textarea,
fieldset[disabled] input:not([type="checkbox"]):not([type="radio"]),
fieldset[disabled] select {
  /* rgb(... / a), NOT rgba(... / a): --c-canvas is a bare "11 11 15" channel triplet, and the
     legacy rgba() function does not accept the slash form. Written wrong first time; it silently
     dropped the whole declaration and the field kept the browser's washed-out disabled grey. */
  background:rgb(var(--c-canvas) / .7); color:var(--text-2); cursor:not-allowed; opacity:1;
  border-color:var(--edge);
}
fieldset[disabled] textarea:hover,
fieldset[disabled] input:hover,
fieldset[disabled] select:hover { background:rgb(var(--c-canvas) / .7); border-color:var(--edge); }
fieldset[disabled] .btn, fieldset[disabled] button { opacity:.45; cursor:not-allowed; }
/* The toggle is a styled <button>, so it is already covered above - but it must not keep the
   gradient "on" look at 45%, which reads as an active control that simply ignored the click. */
fieldset[disabled] .ds-toggle { filter:grayscale(1); }

/* ---- chips / badges (status taxonomy) ------------------------------------ */
.chip { display:inline-flex; align-items:center; gap:6px; border-radius:var(--r-pill);
  padding:3px 10px; font-size:12px; font-weight:600; line-height:1.3; }
.chip .dot { width:7px; height:7px; border-radius:50%; }
.chip-green  { background:rgba(34,197,94,.14);  color:#7EE6A8; }
.chip-amber  { background:rgba(251,191,36,.14); color:#FBD375; }
.chip-red    { background:rgba(239,68,68,.14);  color:#FCA5A5; }
.chip-blue   { background:rgba(75,141,255,.14); color:#A8C7FF; }
.chip-purple { background:rgba(168,85,247,.16); color:#D8B4FE; }
.chip-cyan   { background:rgba(34,211,238,.14); color:#9AE9F5; }
.chip-mute   { background:var(--bg-hover);      color:var(--text-3); }
.dot-green{background:var(--green)} .dot-amber{background:var(--amber)} .dot-red{background:var(--red)}
.dot-blue{background:var(--blue)} .dot-purple{background:var(--purple)}

/* ---- cards --------------------------------------------------------------- */
.card { background:var(--bg-surface); border:1px solid var(--border-subtle); border-radius:var(--r-lg); }
.card-elevated { background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--r-lg); }
.card-glow { position:relative; overflow:hidden; }
/* Cursor-tracked spotlight, chosen over a mousemove-free light sweep for dashboard project cards
   (owner pick 2026-08-25, designs/mockups/hover-magic-options.html §1). Brand-violet tint, not neutral
   white (owner call, 2026-08-25 follow-up) - unlike the nav/KPI cases, this is defensible: these cards
   are genuinely clickable links (no false-affordance risk) and hovered occasionally, not dozens of
   times a session, so spending the accent color here doesn't dilute it the way it would in the sidebar.
   --mx/--my are set by the delegated mousemove listener in base.html. */
.card-spot { overflow:hidden; }
.card-spot::before { content:""; position:absolute; inset:0; opacity:0; transition:opacity .2s; pointer-events:none;
  background:radial-gradient(160px circle at var(--mx,50%) var(--my,50%),rgba(139,92,246,.20),transparent 65%); }
.card-spot:hover::before { opacity:1; }

/* Ambient glow for stat cards that AREN'T clickable (campaign Creators/Videos/Transcribed) - no lift,
   no border-strong, on purpose: those two specifically mean "you can click this" elsewhere in the app
   (.card-spot above), and reusing them here would be a false affordance. Violet-tinted per owner call
   2026-08-25, see designs/mockups/hover-magic-options.html §6.
   Cursor-tracked (owner call 2026-08-26, same doc §3 "is THIS the glow you meant") - was a glow fixed
   at 50%/30% no matter where the mouse was, which read as a broken version of .card-spot's tracking
   glow right next to it on the same page. Same --mx/--my contract as .card-spot, set by the SAME
   delegated mousemove listener in base.html (now also matches .stat-glow) - deliberately still no
   lift/border-strong, since the glow alone doesn't promise a click does anything. */
.stat-glow { position:relative; overflow:hidden; }
.stat-glow::before { content:""; position:absolute; inset:0; opacity:0; transition:opacity .2s; pointer-events:none;
  background:radial-gradient(160px circle at var(--mx,50%) var(--my,50%),rgba(139,92,246,.20),transparent 65%); }
.stat-glow:hover::before { opacity:1; }
@media (prefers-reduced-motion:reduce) { .stat-glow::before { display:none; } }
@media (prefers-reduced-motion:reduce) { .card-spot::before { display:none; } }
@media (prefers-reduced-motion:reduce) { .steps-rail-row::before { display:none; } }
/* The violet light in a card's top-right corner. `circle closest-side`, NOT a bare `circle` (fixed
   2026-09-17): a bare circle is sized to the box's FARTHEST corner, and the old box was a tall strip (55%
   wide, 160% high, right -20%), so the light reached past the strip's left side and was cut off there while
   still lit: a hard vertical violet line, faint on the first project card and plain to see on the empty
   dashboard's hero on a phone. Now the light fades to nothing before any edge of its box, so nothing is
   cut. The box is wider (64%, right -24.5%) so the light keeps the SAME centre (92.5% across, 20% down)
   and about the same reach as before on the cards that use it (first project card ~121px, desktop hero
   ~372px, both within a few px of the old fade-out); only the cut edge is gone. */
.card-glow::after { content:""; position:absolute; right:-24.5%; top:-60%; width:64%; height:160%;
  background:radial-gradient(circle closest-side,rgba(139,61,255,.16),transparent); pointer-events:none; }

/* ---- dashboard project cards: drag-to-reorder (2026-08-25, picked from
   designs/mockups/dashboard-reorder-options.html option 2) -----------------
   Grip sits in normal flow BEFORE the "Project" label, in its own flex row — not
   position:absolute over it, which is what made an earlier pass collide with the label text.
   Reserving the space in-flow (even while invisible at opacity:0) means the label never shares
   pixels with the icon, hover or not. Owner/admin only — see routes/api.reorder_projects for why. */
/* Belt-and-suspenders for the draggable="false" attribute in the template - some WebKit builds
   still let content-level dragging start (e.g. a text selection inside the card) without this. */
.proj-card { -webkit-user-drag:none; user-select:none; }
.proj-card-head { display:flex; align-items:center; gap:7px; margin-bottom:9px; }
.proj-card-head .mono-label { margin-bottom:0; }
.proj-grip { opacity:0; transition:opacity .15s, background .15s; cursor:grab; flex:none;
  width:20px; height:20px; border-radius:6px; display:flex; align-items:center; justify-content:center;
  color:var(--text-3); margin:-3px 0; touch-action:none; }
.group:hover .proj-grip { opacity:1; }
.proj-grip:hover { background:var(--bg-hover); color:var(--text-1); }
.proj-grip:active { cursor:grabbing; }
/* Touch has no hover to reveal this on, so it'd be permanently invisible there (dragging still
   works by grabbing anywhere on the card - this is just the visual hint for where to grab). */
@media (hover:none) { .proj-grip { opacity:.55; } }
/* The card's OLD spot while it's being dragged: dimmed + dashed, so it reads as "this slot is
   empty, the card went with your cursor" rather than a second, separate copy sitting inert. */
.proj-card.proj-placeholder { opacity:.3; border-style:dashed; transform:none !important; }
/* The card that actually follows the cursor: a solid, lifted clone (shadow + slight scale/tilt),
   not the browser's native drag-image snapshot — a flat translucent bitmap read as "just moving a
   copy" when this was first tried with native draggable="" + dragstart/drop. */
/* !important on position: the clone also carries the card's own Tailwind "relative" utility
   class, which ties on specificity with this rule - !important is the same fix already used on
   .proj-placeholder's transform above, for the identical reason. Belt-and-suspenders only; the
   JS also sets ghost.style.position inline, which is what actually makes this deterministic. */
.proj-drag-ghost { position:fixed !important; margin:0; z-index:999; pointer-events:none;
  box-shadow:0 24px 48px rgba(0,0,0,.55), 0 0 0 1px var(--border-strong);
  transform:scale(1.04) rotate(-1.4deg); background:var(--bg-elevated); }

/* ---- home: the sort box and the fold-while-dragging (2026-09-23, mockup
   dashboard-reorder-options.html item 15 option A). The six-step cards are several hundred px tall,
   so while a drag is on (body.proj-folding, set by dashboard.html) every card folds to ONE line -
   grip, name, creator count, step - and the whole list fits on screen. Scoped to <body>, not the
   grid, because the ghost clone is appended to <body> and must fold too. ---- */
.home-sort { display:inline-flex; align-items:center; gap:9px; }
.home-sort .mono-label { margin:0; }
.home-sort select { width:auto; min-width:170px; padding:8px 34px 8px 12px; font-size:13px; white-space:nowrap; }
.hm-foldmeta { display:none; margin-left:auto; align-items:center; gap:12px; font-size:12.5px;
  color:var(--text-3); white-space:nowrap; font-variant-numeric:tabular-nums; }
body.proj-folding #projects-grid { row-gap:8px; }
body.proj-folding .hm-card { padding:11px 16px; }
body.proj-folding .hm-card .hv-spine, body.proj-folding .hm-card .hm-desc,
body.proj-folding .hm-card .hm-next, body.proj-folding .hm-card .hm-stepline,
body.proj-folding .hm-card .hv-hours, body.proj-folding .hm-card .hm-more,
body.proj-folding .hm-card .hm-menu, body.proj-folding .proj-card-head .mono-label,
body.proj-folding .proj-fold-hide { display:none; }
body.proj-folding .hm-card .hm-head { align-items:center; }
body.proj-folding .hm-card .hm-head > .min-w-0 { display:flex; align-items:center; gap:10px; }
body.proj-folding .proj-card-head { margin:0; }
body.proj-folding .proj-grip { opacity:1; }
body.proj-folding .hm-card .hm-name, body.proj-folding .hm-card.is-big .hm-name { font-size:15px; }
body.proj-folding .hm-card .hm-foldmeta { display:inline-flex; }
body.proj-folding .hm-card.card-glow::after { display:none; }
/* the plain fallback card (past FULL_CARD_LIMIT) folds to its grip and name */
body.proj-folding .proj-card:not(.hm-card) { padding:11px 16px; display:flex; align-items:center; gap:10px; }
body.proj-folding .proj-card:not(.hm-card) h3 { margin:0; font-size:15px; }
@media (max-width:640px) {
  .home-sort select { min-width:0; }
  body.proj-folding .hm-card .hm-foldstep { display:none; }
}

/* ---- edge-lit cards (2026-08-23 "life" upgrade, ported from onboarding's black-mode cards) ----
   Same job as .card/.card-elevated, but the border is a gradient that's bright at the top-left and
   fades out, instead of a flat hairline — reads as a lit surface instead of a flat rectangle. Fill
   stays the normal charcoal token (no blur, no big shadow — those stay reserved for the pure-black
   onboarding/auth screens per DESIGN_SYSTEM.md §2/§6, an explicit owner call after project-black.html
   was rejected as too harsh for the dense creators table). Deliberately a SEPARATE class from
   .card/.card-elevated rather than a redefinition of them: those two are also used by Dashboard
   (deferred), outreach_flow, billing, and floating menus, none of which were part of this pass. */
.card-edge {
  border:1px solid transparent; border-radius:var(--r-lg);
  background:
    linear-gradient(var(--bg-surface),var(--bg-surface)) padding-box,
    linear-gradient(150deg, rgb(var(--c-ink) / .18), rgb(var(--c-ink) / .045) 38%, rgb(var(--c-ink) / .02) 70%) border-box;
  box-shadow:0 1px 0 rgb(var(--c-ink) / .04) inset;
}
.card-edge-el {
  border:1px solid transparent; border-radius:var(--r-lg);
  background:
    linear-gradient(var(--bg-elevated),var(--bg-elevated)) padding-box,
    linear-gradient(150deg, rgb(var(--c-ink) / .18), rgb(var(--c-ink) / .045) 38%, rgb(var(--c-ink) / .02) 70%) border-box;
  box-shadow:0 1px 0 rgb(var(--c-ink) / .04) inset;
}
/* dividers instead of a second nested box (the other half of the same upgrade) — put on a block
   that sits INSIDE a .card-edge/.card-edge-el to separate it from what's above, without giving it
   its own background + border (which is how a card ends up with a darker box nested inside it). */
.card-inset-divider { border-top:1px solid var(--border-subtle); margin-top:11px; padding-top:11px; }

/* ---- sidebar nav --------------------------------------------------------- */
.ds-side { background:var(--bg-surface); border-right:1px solid var(--border-subtle); }
/* Brand mark. The gradient is listed AFTER the image on purpose: if the PNG ever fails to load this
   falls back to the plain gradient tile it used to be, rather than to an empty hole. Circle, not the
   old 8px square, because the mark itself is a disc. Same 28px and same glow, so nothing around it
   moves. DESIGN_SYSTEM §3 allows the gradient here: the logo is one of the three places it may be
   used at all (primary CTA, logo, active-nav bar). */
.ds-logo { --ds-logo-size:28px; width:var(--ds-logo-size); height:var(--ds-logo-size);
  flex:0 0 var(--ds-logo-size); border-radius:50%;
  background-image:url('/static/icons/brand-mark.png?v=3'), var(--grad);
  background-size:cover; background-position:center; background-repeat:no-repeat;
  box-shadow:0 0 18px rgba(139,61,255,.4); }
/* Size variants exist so the auth pages, the 404 and the legal shell can use THIS class instead of
   hand-rolling their own tile, which is what they used to do: four templates each carried their own
   `bg-brand` span with an inline glow, so the sidebar could be restyled and every logged-out page
   would silently keep the old look. One class, one mark. */
.ds-logo-md { --ds-logo-size:32px; box-shadow:0 0 18px rgba(139,61,255,.5); }
.ds-logo-lg { --ds-logo-size:36px; box-shadow:0 0 22px rgba(139,61,255,.5); }
.nav-item { display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:var(--r-md);
  color:var(--text-2); font-weight:500; cursor:pointer; position:relative; transition:background .15s,color .15s; }
/* Full-row violet wash on hover (owner call 2026-08-25, overriding the neutral pick I'd have made -
   see designs/mockups/hover-magic-options.html §4). Same tint recipe as the app's status chips
   (fill rgb(accent,.14) + lightened text) so it's not a one-off color. Icons use stroke="currentColor"
   in base.html, so they recolor for free with no extra rule. */
.nav-item:hover { background:rgba(139,92,246,.14); color:#C4B5FD; }
.nav-item.active { background:var(--bg-hover); color:var(--text-1); }
.nav-item.active::before { content:""; position:absolute; left:0; top:7px; bottom:7px; width:3px;
  border-radius:3px; background:var(--grad); }
/* The sidebar's menu is the only part that scrolls (base.html .side-nav comment). Thin scrollbar so it
   does not eat the 228px column. */
.side-nav { scrollbar-width:thin; scrollbar-color:var(--border-strong) transparent; }

/* ---- stage pipeline pills ------------------------------------------------ */
.pill-stage { display:inline-flex; align-items:center; gap:8px; background:var(--bg-surface);
  border:1px solid var(--border-subtle); border-radius:var(--r-pill); padding:7px 14px; font-size:13px;
  color:var(--text-2); cursor:pointer; transition:background .15s,color .15s; }
.pill-stage:hover { background:var(--bg-hover); color:var(--text-1); }
.pill-stage .n { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; color:var(--text-3); }
.pill-stage.done { color:var(--text-1); }
.pill-stage.done .n { color:var(--green); }
.pill-stage.active { color:var(--text-1); border:1px solid transparent;
  background:linear-gradient(var(--bg-surface),var(--bg-surface)) padding-box, var(--grad) border-box; }

/* stage RUN buttons — must read as an action (click to run), not a selectable chip.
   Violet-tinted at REST (not just on hover, 2026-08-25 revision) — every other clickable thing in
   this app (links, focus rings, the accent) uses violet as its "you can click this" cue; a neutral
   gray resting state gave this one nothing to say "action" until you hovered it. Still flat, still
   no shadow, still not the reserved one-gradient-per-view — just recruits color, not elevation. */
.btn-run { display:inline-flex; align-items:center; gap:9px; padding:8px 14px; border-radius:var(--r-md);
  background:rgb(var(--c-accent) / .10); border:1px solid rgb(var(--c-accent) / .45); color:var(--text-1); font-size:13px;
  font-weight:600; cursor:pointer; box-shadow:0 1px 0 rgba(0,0,0,.35);
  transition:background .15s,border-color .15s,transform .08s; }
/* a play-in-circle = the universal "run / start" affordance. Solid accent fill (not accent-soft) so
   the white glyph pops against it instead of reading as violet-on-violet. */
.btn-run::before { content:""; flex:none; width:18px; height:18px; border-radius:50%;
  background:var(--accent) center/8px no-repeat;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 2l7 4-7 4z' fill='%23fff'/%3E%3C/svg%3E"); }
.btn-run:hover { background:rgb(var(--c-accent) / .18); border-color:rgb(var(--c-accent) / .7); }
.btn-run:active { transform:translateY(1px); }
.btn-run .n { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; color:var(--text-3); }
/* display:inline-flex above beats the browser's own [hidden] rule, so a hidden run button would still
   show. Step 5 draws two (run / "say who the emails are from") and swaps them with [hidden]. */
.btn-run[hidden] { display:none; }

/* ============================================================================
   Steps panel — the six-step guided flow (project_detail.html), replaces the old flat row of
   stage-run buttons. NO GRADIENT anywhere in this block: DESIGN_SYSTEM's one-gradient-per-view
   rule is already spent elsewhere on this page (the primary "Run [next] ->" language / nav),
   so a second gradient moment inside this panel would break that rule at the page level even
   though each individual mockup only ever showed one. Dominance for the CURRENT step comes from
   scale, weight, spacing, colour and position instead — see the class comments below.
   ============================================================================ */
.steps-panel-head { padding:16px 20px; border-bottom:1px solid var(--border-subtle); }
.steps-panel-head p { color:var(--text-2); font-size:13px; margin-top:6px; max-width:70ch; }

/* ---- The rail + the docked pane (layout B) --------------------------------------------------
   A permanent left spine carrying all six steps' identity and state at a glance, in about two
   lines each, next to ONE step's full detail docked on the right. The rail is why this works
   without hover: every step's status is readable without touching anything, and only the full
   explanation costs a tap.

   Selecting a rail row swaps the pane. The "which step should I be doing" signal therefore lives
   on the RAIL, permanently, not in the pane: the current step keeps its accent pill and its
   "Do this next" chip no matter which pane you are reading. */
.steps-wrap { display:grid; grid-template-columns:268px 1fr; align-items:stretch; }
.steps-rail { border-right:1px solid var(--border-subtle); padding:8px; }
.steps-rail-row { position:relative; display:flex; align-items:flex-start; gap:10px; width:100%;
  padding:11px 12px; border-radius:var(--r-md); border:1px solid transparent; background:none;
  font-family:inherit; text-align:left; cursor:pointer; }
.steps-rail-row:hover { background:var(--bg-hover); border-color:var(--border-subtle); }
/* Same cursor-tracked spotlight recipe as .card-spot (app.css ~L237), reimplemented here rather than
   sharing the class: .card-spot relies on overflow:hidden to clip its glow, but this row's ::after
   connector line (below) deliberately extends 1px past the row's own bottom edge so six rows read as
   one continuous track - overflow:hidden would clip that too. No overflow needed here since the glow
   sits UNDER the row's own content and a slightly-smaller radius than the dashboard cards (130px vs
   160px, these rows are shorter) keeps it from visibly poking past rounded corners on its own.
   border-radius:inherit avoids painting a hard rectangle inside the row's rounded corners.
   --mx/--my come from the same delegated mousemove listener in base.html (extended to match this class
   too). Owner-approved 2026-08-25 upgrade of the flat .steps-rail-row:hover above, kept as-is. */
.steps-rail-row::before { content:""; position:absolute; inset:0; border-radius:inherit; opacity:0;
  transition:opacity .2s; pointer-events:none;
  background:radial-gradient(130px circle at var(--mx,50%) var(--my,50%),rgba(139,92,246,.20),transparent 65%); }
.steps-rail-row:hover::before { opacity:1; }
.steps-rail-row.is-sel { background:var(--bg-elevated); border-color:var(--border); }
/* The rail is a real tablist with a roving tabindex, so the focus ring is load-bearing: without it
   arrow-key navigation moves an invisible cursor. Same idiom as .ds-toggle / .ob-seg. */
.steps-rail-row:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* The connector: a hairline from under one number pill to the next, so six rows read as one
   ordered track rather than six unrelated links. */
.steps-rail-row::after { content:""; position:absolute; left:25px; top:39px; bottom:-1px; width:1px;
  background:var(--border-subtle); }
.steps-rail-row:last-child::after { display:none; }
.steps-rail-num { width:26px; height:26px; border-radius:8px; flex:none; display:grid; place-items:center;
  background:var(--bg-hover); color:var(--text-2); font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:11.5px; font-weight:600; margin-top:1px; }
.steps-rail-row.is-done .steps-rail-num { color:var(--green); }
/* The current step's marker. Solid accent, never the brand gradient: the page spends its one
   gradient moment outside this panel (DESIGN_SYSTEM "one gradient moment per view"). */
.steps-rail-row.is-current .steps-rail-num { background:var(--accent-soft); color:#C4B5FD;
  box-shadow:0 0 0 2px rgba(139,92,246,.35); }
.steps-rail-body { min-width:0; flex:1; }
.steps-rail-title { display:flex; align-items:center; gap:7px; flex-wrap:wrap; font-size:13.5px;
  font-weight:600; color:var(--text-1); }
.steps-rail-title .chip { font-size:10.5px; padding:2px 7px; }
/* display:block is structural, not decoration: without it this only sits on its own line by
   accident, because the title above happens to be a flex container. */
.steps-rail-mini { display:block; font-size:12px; color:var(--text-3); margin-top:2px; line-height:1.4; }

/* The pane. All six render server-side and are toggled with [hidden] (not innerHTML) so every run
   button stays in the DOM: the table's row-selection relabelling walks them all, including steps
   nobody has opened. min-height stops the card jumping when you move from a long step to a short
   one. */
.steps-panes { padding:20px 22px; min-height:330px; }
.steps-pane-eyebrow { display:flex; align-items:center; justify-content:space-between; gap:14px;
  flex-wrap:wrap; margin-bottom:9px; }
.steps-pane-head { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.steps-pane-title { font-size:19px; font-weight:800; letter-spacing:-.015em; color:var(--text-1); line-height:1.25; }
/* Deliberately smaller than the mockup's 40px figure: the outcome funnel directly above this panel
   already carries seven large numbers, and two competing number displays stacked is the reason the
   "obvious in two seconds" test failed. This still reads as the pane's headline fact. */
.steps-pane-stat { display:flex; align-items:baseline; gap:9px; margin-top:12px; flex-wrap:wrap; }
.steps-pane-num { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:27px; font-weight:700;
  letter-spacing:-.02em; color:var(--text-1); font-variant-numeric:tabular-nums; line-height:1; }
.steps-pane-numlabel { font-size:12.5px; color:var(--text-2); max-width:26ch; line-height:1.35; }
/* The numbered stages behind this step. Mono because every other stage number on this page (the
   pipeline strip, the retry-errors row) is mono, and the point is that they are the SAME numbers. */
.steps-pane-stages { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px;
  letter-spacing:.06em; color:var(--text-3); margin-top:6px; }
.steps-pane-instruction { font-size:13.5px; color:var(--text-2); line-height:1.6; margin-top:12px; max-width:66ch; }
.steps-gap { color:#FBD375; }
.steps-note { font-size:12.5px; color:var(--text-3); margin-top:6px; }
/* The two halves of a grouped step (step 2 = listen then judge, step 3 = channel then comments).
   Sits directly under the headline number because it IS that number, broken open. */
.steps-subqueues { font-size:12px; color:var(--text-3); margin-top:5px; }
.steps-subqueues b { color:var(--text-2); font-variant-numeric:tabular-nums; }
/* "N cannot enter this step". Collapsed by default: on a healthy project it is a small number and
   nobody needs it open, but on the flagship it is thousands and it is the answer to the question
   the panel otherwise leaves hanging. */
.steps-blocked { margin-top:10px; font-size:12.5px; }
.steps-blocked > summary { color:var(--text-3); cursor:pointer; list-style:none; padding:4px 0;
  border-bottom:1px dotted var(--border-subtle); display:inline-block; }
.steps-blocked > summary::-webkit-details-marker { display:none; }
.steps-blocked > summary::before { content:'\25B8'; display:inline-block; margin-right:6px;
  transition:transform .15s; color:var(--text-3); }
.steps-blocked[open] > summary::before { transform:rotate(90deg); }
.steps-blocked > summary:hover { color:var(--text-2); }
.steps-blocked > summary b { color:var(--text-2); font-variant-numeric:tabular-nums; }
.steps-blocked-list { margin:9px 0 0; padding:0; list-style:none; max-width:62ch; }
.steps-blocked-list li { display:flex; gap:10px; align-items:baseline; padding:3px 0;
  color:var(--text-3); line-height:1.45; }
/* A reason the customer can actually change is the point of the whole list, so it is the one that
   is not greyed out. The rest are facts about the creator and nothing can be done about them. */
.steps-blocked-list li.is-fixable { color:var(--text-2); }
.steps-blocked-n { font-family:'JetBrains Mono',ui-monospace,monospace; font-variant-numeric:tabular-nums;
  min-width:5.5ch; text-align:right; flex:none; color:var(--text-2); }
.steps-blocked-fix { margin-top:9px; font-size:12.5px; color:var(--text-2); max-width:62ch;
  line-height:1.5; }
.steps-blocked-fix .steps-quiet-link { margin-left:4px; }
.steps-actions { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-top:16px; }
.steps-quiet-link { font-size:12.5px; color:var(--text-3); text-decoration:underline;
  background:none; border:none; cursor:pointer; font-family:inherit; padding:0; }
.steps-quiet-link:hover { color:var(--text-2); }
.steps-caughtup { margin:0 0 16px; padding:14px 16px; background:var(--bg-elevated);
  border:1px solid var(--border-subtle); border-radius:var(--r-md); }
.steps-caughtup p { font-size:13px; color:var(--text-2); margin-top:5px; max-width:64ch; }

/* ---- THE FINISHED-SEARCH BAND (mockup section 16) -------------------------------------------
   One component, four states, above the six steps: the receipt on the first-ever run (.res-card),
   the one line on every later one (.res-line), and the same line in amber for "found nobody" and
   "refused before it started" (.res-line.is-warn). Ported 1:1 from the approved mockup; the green
   tint is the same rgba(34,197,94,...) pair the other "this went well" surfaces use, deliberately
   softer than a chip because it covers a whole row. */
/* The band is its OWN card, so .res-line and .res-card carry the border and the radius themselves.
   Wrapping either in a .card would draw a border inside a border and round the tint at a different
   radius from the box around it, which reads as a misaligned box rather than one row. */
.res-line { display:flex; align-items:center; gap:14px; flex-wrap:wrap; padding:12px 20px;
  background:rgba(34,197,94,.05); border:1px solid rgba(34,197,94,.22); border-radius:var(--r-md); }
.res-line.is-warn { background:rgba(251,191,36,.06); border-color:rgba(251,191,36,.22); }
.res-line .tick { color:#7EE6A8; flex:none; }
.res-line.is-warn .tick { color:#FBD375; }
.res-line .txt { flex:1; min-width:260px; font-size:13px; color:var(--text-2); line-height:1.5; }
.res-line .txt b { color:var(--text-1); font-weight:650; }
.res-line .x { color:var(--text-3); background:none; border:none; cursor:pointer; font-size:16px;
  line-height:1; padding:0 2px; }
.res-line .x:hover { color:var(--text-1); }
.res-card { margin:0; padding:16px 18px; border:1px solid rgba(34,197,94,.22);
  background:rgba(34,197,94,.05); border-radius:var(--r-md); }
.res-card h3 { font-size:17px; font-weight:750; letter-spacing:-.012em; margin-top:6px; }
.res-tiles { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-top:12px; max-width:540px; }
.res-tile { background:var(--bg-base); border:1px solid var(--border-subtle);
  border-radius:var(--r-sm); padding:9px 11px; }
.res-tile .n { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:20px; font-weight:700;
  color:var(--text-1); line-height:1; font-variant-numeric:tabular-nums; }
.res-tile .k { font-size:11px; color:var(--text-3); margin-top:5px; line-height:1.35; }
.res-card p { font-size:12.5px; color:var(--text-2); margin-top:12px; line-height:1.55; max-width:66ch; }
.res-card p b { color:var(--text-1); font-weight:600; }
.res-card .act { display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-top:14px; }
/* Which phrase found whom. The count is mono because it is a bare figure in a column of figures,
   which is exactly what mono is for (DESIGN_SYSTEM section 15.7). */
.res-yield { margin-top:12px; max-width:540px; font-size:12.5px; color:var(--text-2); }
.res-yield .lbl { font-size:10.5px; letter-spacing:.12em; text-transform:uppercase;
  color:var(--text-3); margin-bottom:2px; }
.res-yield .row { display:flex; justify-content:space-between; gap:12px; padding:5px 0;
  border-top:1px solid var(--border-subtle); }
.res-yield b { font-family:'JetBrains Mono',ui-monospace,monospace; font-weight:600;
  color:var(--text-1); white-space:nowrap; font-variant-numeric:tabular-nums; }
/* The count-and-price pill that sits beside an action, so the button says what it does and the
   pill says what it costs without the two competing. */
.cost-face { display:inline-flex; align-items:center; gap:7px; font-size:12.5px; color:var(--text-2);
  background:var(--bg-hover); border-radius:var(--r-pill); padding:4px 12px;
  font-variant-numeric:tabular-nums; }
@media (max-width:560px) { .res-tiles { grid-template-columns:1fr; } }

/* ---- The prerequisite checklist -------------------------------------------------------------
   ALWAYS the full list, satisfied rows included. A list that only appears when something is broken
   teaches nobody what a step needs and reassures nobody when it is fine. State is carried by a
   glyph AND a word, never colour alone. */
.steps-checks { margin-top:14px; border:1px solid var(--border); border-radius:var(--r-md);
  overflow:hidden; max-width:660px; }
.steps-checks-head { padding:8px 12px; background:var(--bg-hover); font-size:12.5px; color:var(--text-2); }
.steps-checks-head b { color:#FBD375; font-weight:600; }
.steps-checks-head.is-plain b { color:var(--text-1); }
.steps-check { display:flex; align-items:flex-start; gap:10px; padding:9px 12px;
  border-top:1px solid var(--border-subtle); font-size:13px; }
.steps-check:first-child { border-top:none; }
.steps-check-mark { flex:none; width:15px; text-align:center; line-height:1.5; font-size:12px; }
.steps-check.is-ok .steps-check-mark { color:var(--green); }
/* A missing INPUT is a warning: the step will run and bill in full, just worse. A missing WORK
   prerequisite is not, it just means an earlier step hasn't got there yet. */
.steps-check.is-missing-input .steps-check-mark { color:#FBD375; }
.steps-check.is-missing-work .steps-check-mark,
.steps-check.is-missing-optional .steps-check-mark { color:var(--text-3); }
.steps-check .lbl { flex:1; min-width:0; color:var(--text-2); }
.steps-check .lbl small { display:block; color:var(--text-3); font-size:11.5px; margin-top:2px; line-height:1.45; }
.steps-check-act { flex:none; }
/* The preview of what is actually saved. Two lines, then an ellipsis. The full text is one click
   away in the editor dialog, which is why there is no separate expander here: two controls for one
   job. line-clamp rather than a fade overlay on purpose - a fade cannot tell whether the text
   actually overflows, so it washed out short values that were already complete. */
.steps-check-preview { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; margin-top:3px; color:var(--text-3); font-size:11.5px; line-height:1.5; }

/* Side rows that are not steps of their own (YouTube channel hydration, sponsorship profile). */
.steps-need-row { display:flex; align-items:center; gap:10px; padding:9px 0; font-size:13px; }
.steps-need-row .lbl { flex:1; min-width:0; color:var(--text-2); }
.steps-need-row .lbl small { display:block; color:var(--text-3); font-size:11.5px; margin-top:1px; }

/* Below this width the app has already dropped its sidebar, and a 268px rail alongside a pane
   squeezes both. Stack instead: rail on top, full width, pane underneath. */
@media (max-width:1100px) {
  .steps-wrap { grid-template-columns:1fr; }
  .steps-rail { border-right:none; border-bottom:1px solid var(--border-subtle); }
  .steps-panes { padding:16px; min-height:0; }
}

/* "What happens in this step" - collapsed by default (open only on the current step), same
   triangle-marker idiom as the rest of the app's disclosure widgets. Tap to open, no hover. */
.steps-details { margin-top:14px; }
.steps-details summary {
  font-size:12px; color:var(--text-3); cursor:pointer; list-style:none;
  display:inline-flex; align-items:center; gap:6px; }
.steps-details summary::-webkit-details-marker { display:none; }
.steps-details summary::before { content:'\25B8'; color:var(--text-3); font-size:9px; }
.steps-details[open] summary::before { content:'\25BE'; }
.steps-details summary:hover { color:var(--text-2); }
.steps-detail-box { margin-top:9px; padding:12px 14px; background:var(--bg-base);
  border:1px solid var(--border-subtle); border-radius:var(--r-md); font-size:13px;
  color:var(--text-2); max-width:66ch; line-height:1.55; }
.steps-detail-box p + p { margin-top:8px; }
.steps-detail-box b { color:var(--text-1); font-weight:600; }
.steps-detail-inside { color:var(--text-3); margin-top:10px; }

/* ============================================================================
   Tabulator re-theme (the creators table) — charcoal tokens
   ============================================================================ */
.tabulator { border:none !important; font-size:var(--table-font); background:var(--bg-surface) !important;
  color:var(--text-1) !important;
  /* THE ACTUAL FIX for the header below never truly sticking (v88/89 both still tracked scroll
     1:1 and could land anywhere, including mid-table - verified live in the browser before this
     landed, not assumed): Tabulator's own stylesheet sets `overflow:hidden` here. That makes THIS
     element position:sticky's reference frame for its .tabulator-header child - but this element's
     own scrollTop never moves (the PAGE scrolls it as a normal block; nothing ever scrolls IT
     internally), so relative to that frame there is nothing to "stick" against and the header just
     sits at its natural in-flow spot, drifting 1:1 with the page. `overflow:clip` paints the exact
     same visual clip as `hidden` but does not count as a scroll container for sticky purposes, so
     it stops trapping the header and lets it find the real scrolling page instead - same fix, same
     reasoning already documented for the phone-width guard in DESIGN_SYSTEM.md §18.4 ("clip, not
     hidden, so position:sticky headers keep working"), just needed here too.
     Also strips the identity transform (matrix(1,0,0,1,0,0), a GPU-layer hint) Tabulator's own
     stylesheet puts here - not the actual cause once overflow was fixed, but a real no-op transform
     nonetheless, and a transform on any ancestor is its own separate way to break sticky. */
  overflow:clip !important; transform:none !important; }
.tabulator .tabulator-header { background:var(--bg-base) !important;
  border-bottom:1px solid var(--border-subtle) !important; color:var(--text-3) !important;
  /* Pinned directly under the bulk-select bar (project_detail.html #bulk-bar: sticky, top-14,
     z-20) instead of Tabulator's default position:relative. Without this the header scrolled up
     in normal flow and passed BEHIND the sticky bar, which visually swallowed it mid-scroll.
     --tbl-head-top is written by _syncTableHeadSticky() (project_detail.html) whenever the bar
     shows/hides, since its height (56px app header + the bar itself, which can wrap to two lines)
     is not a constant. z-15 keeps it under the bulk bar (20) and above plain rows (auto). */
  position:sticky !important; top:var(--tbl-head-top, 56px) !important; z-index:15; }
.tabulator .tabulator-header .tabulator-col { background:var(--bg-base) !important;
  border-right:1px solid var(--border-subtle) !important; }
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
  white-space:normal !important; line-height:1.2 !important; text-overflow:clip !important;
  overflow:visible !important;
  /* Break BETWEEN words only (e2e 2026-10-01, W3-17/W4-15/W2-10). This was word-break:break-word,
     which split a word that did not fit its column: "VIDEO S", "TRANSCRI BED", "SCOR E". Columns are
     now made wide enough for their longest header word (window.headerFitColumns, base.html), so a
     header wraps at a space or not at all. */
  word-break:normal !important; overflow-wrap:normal !important; hyphens:manual;
  color:var(--text-3) !important; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:10px; letter-spacing:.08em; text-transform:uppercase; }
.tabulator .tabulator-row { background:var(--bg-surface) !important;
  border-bottom:1px solid var(--border-subtle) !important; color:var(--text-1) !important; }
.tabulator .tabulator-row.tabulator-row-even { background:var(--bg-elevated) !important; }
.tabulator .tabulator-row:hover { background:var(--bg-hover) !important; }
/* Solid (not translucent) — a transparent accent tint composites against whatever sits behind the
   row (Tabulator's own default background peeks through there), washing the color out to near-white
   and making the text unreadable. A flat opaque color has no such dependency. */
.tabulator .tabulator-row.tabulator-selected { background:rgb(45 38 65) !important; color:var(--text-1) !important; }
.tabulator .tabulator-row.tabulator-selected .tabulator-cell { color:var(--text-1) !important; }
/* Native text-selection drag (e.g. shift-clicking across checkboxes) defaults to the browser's
   light selection color, which is unreadable on this dark theme — keep it legible everywhere. */
::selection { background:rgb(var(--c-accent) / .5); color:#fff; }
.tabulator .tabulator-cell { padding:8px 12px; border-color:var(--border-subtle) !important; }
.tabulator .tabulator-tableholder { background:var(--bg-surface) !important; overflow-x:scroll !important;
  /* A size container, so the empty-state sentence below can be exactly as wide as the part of the
     table you can SEE (100cqw). Inline-size only: the holder's width already comes from its parent,
     never from its rows, so this changes no table's width. */
  container-type:inline-size; }
/* THE EMPTY-STATE SENTENCE STAYS ON SCREEN (e2e 2026-10-01, W3-03). Tabulator makes its placeholder
   as wide as ALL the columns together and centres the sentence in that, so on a phone (30 columns,
   ~3,000px) "No creators in this project yet" sat 700px off the right edge and the table read as a
   blank dark box. Pinning the sentence to the left of the scrolling area at the visible width keeps it
   centred in what you see, at every width, on every creators grid (All Creators, project, campaign). */
.tabulator .tabulator-tableholder .tabulator-placeholder { justify-content:flex-start !important; }
.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents {
  position:sticky; left:0; width:100cqw; box-sizing:border-box; }
.tabulator .tabulator-cell a { text-decoration:none; }
.tabulator .tabulator-cell a:hover { text-decoration:underline; }
/* reserve consistent title height so 1-line and 2-line titles align their filter bars on one row */
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
  min-height:26px; display:flex; align-items:flex-start; }
.tabulator .tabulator-header .tabulator-header-filter { margin-top:4px; }
.tabulator .tabulator-header .tabulator-header-filter input,
.tabulator .tabulator-header .tabulator-header-filter select {
  background:var(--bg-input); color:var(--text-1); border:1px solid var(--border);
  border-radius:var(--r-sm); height:28px; box-sizing:border-box; padding:2px 8px; width:100%; }
.tabulator .tabulator-header .tabulator-header-filter input:focus,
.tabulator .tabulator-header .tabulator-header-filter select:focus {
  border-color:var(--accent); outline:none; }
/* HEADER INTERACTIVITY — sort arrows, hover, active-sort column (verified against the real
   Tabulator 6.2.5 DOM, not guessed from generic docs: the arrow is a CSS border-triangle with NO
   extra class for direction — the state lives entirely in the column's own `aria-sort` attribute
   (none|ascending|descending), which Tabulator's stock stylesheet already keys off; these rules
   just re-point the same selectors at the app's tokens instead of its light-theme greys #bbb/#666).
   Column-resize handles (`.tabulator-col-resize-handle`) are NOT currently enabled anywhere in this
   app (resizableColumns is never set) so that hover rule has no visible effect today — kept anyway
   so nothing needs revisiting if resizing is ever turned on. */
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-sorter .tabulator-arrow {
  border-bottom-color:var(--text-3) !important; }
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="ascending"] .tabulator-col-sorter .tabulator-arrow {
  border-bottom-color:var(--accent) !important; }
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="descending"] .tabulator-col-sorter .tabulator-arrow {
  border-top-color:var(--accent) !important; }
/* Sortable header hover — the column background had zero hover state at all, so a clickable header
   looked identical to a non-clickable one. */
.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover {
  background:var(--bg-hover) !important; cursor:pointer; }
/* Active-sort column: brighten the LABEL too, not just the tiny arrow glyph, so "what is this table
   sorted by" reads at a glance instead of requiring a squint at a 6px triangle. */
.tabulator .tabulator-header .tabulator-col[aria-sort="ascending"] .tabulator-col-title,
.tabulator .tabulator-header .tabulator-col[aria-sort="descending"] .tabulator-col-title {
  color:var(--text-1) !important; }
.tabulator .tabulator-col-resize-handle:hover { background:var(--accent-soft); }
/* PAGER (server-side creators table). Tabulator ships a light-theme footer with grey system
   buttons, which lands in a dark app as a bright strip along the bottom. Restyled from the same
   tokens as everything else rather than hand-picked colours, and shaped like .btn-outline /
   .btn-ghost so the page numbers read as the app's buttons. */
.tabulator .tabulator-footer {
  background:var(--bg-base) !important; border-top:1px solid var(--border-subtle) !important;
  color:var(--text-2); padding:8px 12px; }
.tabulator .tabulator-footer .tabulator-paginator { color:var(--text-2); font-size:12px; }
/* "1-500 of 24,737" */
.tabulator .tabulator-footer .tabulator-page-counter { color:var(--text-3); font-size:12px; }
.tabulator .tabulator-footer .tabulator-page {
  background:transparent !important; color:var(--text-2) !important;
  border:1px solid var(--border) !important; border-radius:var(--r-sm) !important;
  padding:4px 10px !important; margin:0 2px !important; font-size:12px; font-weight:600;
  min-width:30px; transition:background .15s, border-color .15s, color .15s; }
.tabulator .tabulator-footer .tabulator-page:hover:not(.active):not(:disabled) {
  background:var(--bg-hover) !important; color:var(--text-1) !important;
  border-color:var(--border-strong) !important; }
/* The current page is the one thing here that should carry the app's accent. */
.tabulator .tabulator-footer .tabulator-page.active {
  background:var(--accent-soft) !important; color:var(--text-1) !important;
  border-color:var(--accent) !important; }
/* First / Prev at the start of the set are genuinely unavailable - say so quietly rather than
   leaving them looking clickable. */
.tabulator .tabulator-footer .tabulator-page:disabled {
  opacity:.35; cursor:default; }
.tabulator .tabulator-cell .tabulator-editor {
  background:var(--bg-input); color:var(--text-1); border-color:var(--border); }

/* ---- scrollbar -----------------------------------------------------------
   Owner's pick 2026-10-09 (option C, designs/mockups/scrollbar.html): 12px rail, 8px soft brand-purple
   handle, full purple under the mouse. It used to be a 5px dark-grey handle that vanished on the canvas.
   The 2px gap around the handle is a TRANSPARENT border + background-clip, not a border painted in the
   canvas colour: the painted one drew a dark ring wherever a scroller sat on a lighter panel.
   Firefox ignores ::-webkit-scrollbar and only offers thin/auto width, so it gets the colours from
   scrollbar-color below. That rule is fenced to browsers WITHOUT ::-webkit-scrollbar on purpose:
   Chrome 121+ honours scrollbar-color too, and once an element has it Chrome DROPS every
   ::-webkit-scrollbar rule for it, which would throw away the width and the hover. (.side-nav sets its
   own thin grey scrollbar-color that way on purpose, see its rule.) */
::-webkit-scrollbar { width:12px; height:12px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:rgb(var(--c-accent) / .5); border-radius:999px;
  border:2px solid transparent; background-clip:padding-box; }
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { background-color:var(--accent); }
@supports not selector(::-webkit-scrollbar) {
  html { scrollbar-color:rgb(var(--c-accent) / .5) transparent; }
}
/* TOOLTIPS. Tabulator 6.2.5 ships `.tabulator-tooltip` with a border-radius, a font-size and a
   max-width - and NO background or text colour. It inherits, which in this app means our light
   text on the browser's white default: white on white, so it reads as a blank rectangle rather
   than a tooltip. Eight columns use one (Found by, Audio, Health, Deal, Opps...), so it shows up
   all over the creators table. Styled like .card-elevated so it reads as part of the app. */
.tabulator-tooltip {
  background:var(--bg-elevated) !important; color:var(--text-1) !important;
  border:1px solid var(--border-strong) !important; border-radius:var(--r-sm) !important;
  padding:6px 9px !important; font-size:12px; line-height:1.45;
  box-shadow:0 6px 20px rgba(0,0,0,.45) !important; max-width:min(420px, 90vw) !important;
  white-space:normal; }

/* The little square where a horizontal and a vertical scrollbar meet. The browser paints it WHITE
   by default, and the track/thumb rules above do not cover it - so on the creators table, which is
   the one place in the app with both scrollbars at once, a bright dot sat in the bottom-right
   corner. Invisible everywhere else, which is why it survived this long. */
::-webkit-scrollbar-corner { background:transparent; }

/* ============================================================================
   RESPONSIVE / MOBILE  (see DESIGN_SYSTEM.md §18)
   Thresholds: lg(1024) = desktop chrome present · sm/md = content reflow ·
   ≤640 = phone (overlay goes full-screen). All rules here are STRICTLY ADDITIVE
   — they only fire below a breakpoint, so the desktop layout is untouched.
   ============================================================================ */

/* --- shell: off-canvas sidebar drawer below lg ---------------------------- */
#sidebar-scrim { display:none; }              /* scrim only exists on mobile when open */
@media (max-width:1023px) {
  /* sidebar slides off-canvas; the main column reclaims the full width
     (the ml-0 lg:ml-[228px] on #app-main handles the margin side). */
  #app-sidebar { transform:translateX(-100%); transition:transform .2s ease;
    box-shadow:0 0 60px rgba(0,0,0,.5); }
  body.sidebar-open #app-sidebar { transform:translateX(0); }
  body.sidebar-open #sidebar-scrim { display:block; }
  body.sidebar-open { overflow:hidden; }       /* lock page scroll while the drawer is open */
}

/* --- notification panel: full-width sheet on small screens ---------------- */
/* The bell lives in the top bar (visible at every width — the sidebar is a drawer below lg, so a
   bell in there would be invisible on a phone). On a narrow screen a 400px panel hugging the right
   edge looks pasted on, so pin both edges and let it fill the width under the header instead. */
@media (max-width:640px) {
  #notif-panel { left:12px !important; right:12px !important; width:auto !important; }
}

/* --- creator overlay: full-screen sheet on phones ------------------------- */
@media (max-width:640px) {
  /* Pin BOTH edges (not width:100vw) so the sheet stays edge-to-edge even if some
     page has residual horizontal overflow that would otherwise shift a right-anchored
     panel. !important beats the inline style="width:480px" + any JS-set inline width. */
  #creator-panel { left:0 !important; right:0 !important; width:auto !important; }
  #panel-resize-handle { display:none !important; }   /* 6px drag handle is useless on touch */
}

/* --- safety guard: never let stray content scroll the whole page sideways --
   overflow-x:clip (not hidden) does NOT create a scroll container, so position:sticky
   headers keep working — unlike overflow:hidden. Modern Chromium/Edge (our target). */
@media (max-width:1023px) {
  html, body { overflow-x:clip; }
}

/* --- iOS input-zoom fix ---------------------------------------------------
   iOS Safari auto-zooms the viewport when you focus a control whose font-size is < 16px
   (and won't zoom back out) — jarring on every form. Force 16px on phones so focusing a
   field is calm. Keep Tabulator's compact header filters small (higher-specificity override). */
@media (max-width:640px) {
  input, select, textarea { font-size:16px; }
  .tabulator input, .tabulator select { font-size:13px; }
}

/* ==========================================================================
   ONBOARDING — the guided first-project flow (/onboarding, onboarding.html)
   Prefixed .ob-* so nothing here can collide with the rest of the app. The ONE
   gradient moment per screen stays the primary button, so the group tiers use
   violet at three intensities rather than three hues — tier is not a status,
   and the status palette means something else everywhere else (§3.6).
   ========================================================================== */

/* --- worked examples: announced as "Examples", industries as plain links ---
   NOT pills. A filled pill among mutually exclusive options reads as "pick one",
   which is precisely the impression a niche chooser must avoid. */
/* Block labels: say what a block IS before the reader has to work it out from its
   contents, and say whether it wants an answer. Used by Examples and by the
   follow-up question. */
.ob-exhead, .ob-blabel {
  display:flex; align-items:center; gap:8px;
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3); margin-bottom:7px;
}
/* whether this block wants input from them, or is just there to be read */
.ob-need {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:8px; letter-spacing:.09em;
  text-transform:uppercase; border-radius:var(--r-pill); padding:2px 7px; font-weight:600;
}
.ob-need.is-opt { color:var(--text-3); border:1px solid var(--border); }
.ob-need.is-req { color:#FCD980; background:rgb(var(--c-warn) / .12); border:1px solid rgb(var(--c-warn) / .3); }
/* the quote is THEIR words, so it gets announced as such */
.ob-yousaid { font-size:10px; color:var(--text-3); margin-bottom:4px; }
.ob-exlink {
  font-size:11.5px; color:var(--text-3); background:transparent; border:none; padding:0 0 2px;
  border-bottom:1.5px solid transparent; transition:color .15s, border-color .15s;
}
.ob-exlink:hover { color:var(--text-2); }
.ob-exlink.is-on { color:var(--text-1); font-weight:600; border-bottom-color:rgba(167,139,250,.8); }
/* No container and no left accent rule — just quiet text under the heading. */
/* One example per QUESTION now that the screen asks four things. The label is what makes it
   readable as an ANSWER to a specific question rather than a paragraph about a game studio. */
.ob-example { font-size:11.5px; color:var(--text-3); line-height:1.6; max-width:62ch; }
.ob-exrow { margin-bottom:5px; }
.ob-exrow:last-child { margin-bottom:0; }
.ob-exrow span {
  color:var(--text-2); font-weight:600; margin-right:7px;
}
.ob-exrow span::after { content:':'; }

/* --- reading state ------------------------------------------------------ */
.ob-dots { display:inline-flex; gap:5px; }
.ob-dots i { width:6px; height:6px; border-radius:50%; background:var(--accent); display:block; animation:ob-pulse 1.2s ease-in-out infinite; }
.ob-dots i:nth-child(2) { animation-delay:.15s; }
.ob-dots i:nth-child(3) { animation-delay:.3s; }
@keyframes ob-pulse { 0%,100% { opacity:.25; } 50% { opacity:1; } }

/* --- their own text, with what we picked up highlighted in place -------- */
.ob-theirtext {
  background:var(--bg-surface); border:1px solid var(--border-subtle); border-radius:var(--r-md);
  padding:14px 16px; font-size:12.5px; line-height:1.85; color:var(--text-2); white-space:pre-wrap;
  /* someone can paste a whole website page in here — cap it so the screen stays a screen */
  max-height:260px; overflow-y:auto; overscroll-behavior:contain;
}
/* override the browser default <mark> (yellow) — unreadable on a dark canvas */
.ob-theirtext mark.ob-hl {
  background:rgb(var(--c-accent) / .22); color:var(--text-1);
  border-radius:3px; padding:1px 3px; animation:ob-hlin .45s ease both;
}
@keyframes ob-hlin { from { background:transparent; color:var(--text-2); } }
.ob-skipped { display:flex; align-items:flex-start; gap:7px; font-size:11px; color:var(--text-3); margin-top:9px; line-height:1.5; }
.ob-skipped-i {
  width:14px; height:14px; border-radius:50%; background:rgba(255,255,255,.06); color:var(--text-3);
  font-size:9px; display:inline-flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2px;
}
.ob-notwired {
  font-size:12px; color:var(--text-3); line-height:1.6; padding:12px 14px;
  border:1px dashed var(--border); border-radius:var(--r-md);
}
/* long pastes: show the passages we actually used rather than the whole document */
.ob-theirtext.is-excerpted { max-height:none; }
.ob-excerpt { margin:0 0 10px; }
.ob-excerpt:last-of-type { margin-bottom:0; }
.ob-elide { color:var(--text-3); opacity:.6; padding:0 3px; }
.ob-showall {
  margin-top:10px; font-size:11px; color:#A78BFA; font-weight:600;
  background:transparent; border:none; padding:0;
}
.ob-showall:hover { text-decoration:underline; }
.ob-fulltext { margin-top:12px; padding-top:12px; border-top:1px solid var(--border-subtle); }
/* comment box under a yes/no answer */
.ob-infer-comment { margin-top:10px; }
/* a field we could not fill: dashed + amber edge so the gap reads as a gap, not as an empty form */
.ob-gap { border-style:dashed !important; border-color:rgb(var(--c-warn) / .45) !important; }

/* --- the inference. Owner picked variant B: the SAME card as its neighbours,
   distinguished by its label and slightly larger text rather than by paint. The
   earlier version had three violet things at once (tinted fill, violet border,
   glow) and read as shouting. --- */
.ob-infer { margin-top:12px; padding:14px 16px; border-radius:var(--r-md); }
.ob-infer-tag {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:8.5px; letter-spacing:.12em;
  text-transform:uppercase; color:#A78BFA; margin-bottom:7px;
}
.ob-infer p { font-size:13px; color:var(--text-1); line-height:1.6; font-weight:500; }
.ob-pill {
  font-size:11.5px; border-radius:var(--r-pill); padding:5px 13px;
  border:1px solid var(--border-strong); color:var(--text-2); background:transparent;
}
.ob-pill:hover { color:var(--text-1); }
.ob-pill.is-yes { background:rgb(var(--c-accent) / .2); border-color:rgb(var(--c-accent) / .5); color:#fff; font-weight:600; }

/* --- a real follow-up: prose that quotes them, never another form field - */
.ob-ask { margin-top:12px; padding:11px 13px; background:rgb(var(--c-accent) / .06); border:1px solid rgb(var(--c-accent) / .22); border-radius:var(--r-md); }
.ob-quoted { font-size:11.5px; color:#C4B5FD; font-style:italic; line-height:1.5; padding-left:9px; border-left:2px solid rgb(var(--c-accent) / .5); }
.ob-askq { font-size:12.5px; color:var(--text-1); line-height:1.55; margin-top:7px; }

/* --- build beats -------------------------------------------------------- */
.ob-beat { display:flex; align-items:center; gap:12px; padding:10px 0; }
.ob-beat span:last-child { font-size:13px; color:var(--text-3); transition:color .3s; }
.ob-beat-i {
  width:22px; height:22px; border-radius:50%; flex-shrink:0; display:inline-flex;
  align-items:center; justify-content:center; font-size:10px;
  border:1px solid var(--border); background:var(--bg-elevated); color:transparent; transition:all .3s;
}
.ob-beat.is-done .ob-beat-i { background:rgb(var(--c-ok) / .16); border-color:transparent; color:var(--green); }
.ob-beat.is-done span:last-child { color:var(--text-1); }

/* --- keyword group rows ------------------------------------------------- */
.ob-row { background:var(--bg-surface); border:1px solid var(--border-subtle); border-radius:var(--r-md); margin-bottom:7px; overflow:hidden; transition:opacity .2s; }
.ob-row.is-off { opacity:.42; }
.ob-rhead { display:grid; grid-template-columns:40px 104px 1fr auto 22px; align-items:center; gap:11px; padding:11px 13px; }
.ob-rrole { font-size:9.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; white-space:nowrap; }
.ob-t1 .ob-rrole { color:#A78BFA; }
.ob-t2 .ob-rrole { color:#8E82B8; }
.ob-t3 .ob-rrole { color:var(--text-3); }
.ob-rname { font-size:13.5px; font-weight:700; color:var(--text-1); }
.ob-rnum { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; color:var(--text-3); font-variant-numeric:tabular-nums; white-space:nowrap; }
.ob-caret { color:var(--text-3); font-size:10px; background:transparent; border:none; padding:2px 4px; }
.ob-caret:hover { color:var(--text-1); }
.ob-rbody { padding:11px 13px 13px; border-top:1px solid var(--border-subtle); }

/* --- keyword + criteria chips ------------------------------------------ */
.ob-kchip {
  display:inline-flex; align-items:center; gap:6px; background:var(--bg-elevated);
  border:1px solid var(--border); border-radius:var(--r-pill); padding:5px 7px 5px 12px;
  font-size:11.5px; font-weight:500; color:var(--text-1);
}
.ob-kchip .ob-x {
  width:15px; height:15px; border-radius:50%; background:rgba(255,255,255,.07); color:var(--text-3);
  font-size:9.5px; display:inline-flex; align-items:center; justify-content:center; border:none; padding:0;
}
.ob-kchip .ob-x:hover { background:rgb(var(--c-bad) / .2); color:#FCA5A5; }
.ob-kchip.is-more { border-style:dashed; color:var(--text-3); padding:5px 12px; }
.ob-kchip.is-add { border-style:dashed; color:var(--text-3); padding:5px 12px; }
.ob-kchip.is-add:hover { color:var(--text-1); border-color:var(--border-strong); }
/* "+N more" / "Show fewer" is a button now (onboarding.html obToggleMore), so it answers hover like
   its dashed neighbour "+ add your own" does. */
.ob-kchip.is-more:hover { color:var(--text-1); border-color:var(--border-strong); }
.ob-kchip.is-sug { background:transparent; border-style:dashed; color:var(--text-2); padding:5px 12px 5px 7px; }
.ob-kchip.is-sug:hover { color:var(--text-1); border-color:rgb(var(--c-accent) / .45); }
.ob-kchip.is-sug .ob-plus {
  width:15px; height:15px; border-radius:50%; background:rgb(var(--c-accent) / .18); color:#A78BFA;
  font-size:10px; display:inline-flex; align-items:center; justify-content:center; margin-right:5px;
}
.ob-kchip.is-mine { background:rgb(var(--c-accent) / .15); border-color:rgb(var(--c-accent) / .42); }
/* inline "add your own" field, sized like the chip it replaces */
.ob-kchip.is-editing { padding:3px 6px; border-color:rgb(var(--c-accent) / .5); }
.ob-kchip-input {
  background:transparent; border:none; outline:none; color:var(--text-1);
  font-size:11.5px; font-family:inherit; width:150px; padding:2px 4px;
}
.ob-kchip-input::placeholder { color:var(--text-3); }

/* --- 2026-10-03: your numbers, the clarifying question, what our AI reads (plan-screen.js) ----
   designs/mockups/setup-numbers.html, the owner's pick B. */
.ob-numrow { display:flex; align-items:center; flex-wrap:wrap; gap:6px; padding:8px 0; font-size:12px; color:var(--text-2);
  border-top:1px solid var(--border-subtle); }
.ob-numrow:first-of-type { border-top:0; }
.ob-num { font:inherit; font-weight:700; color:var(--text-1); background:transparent; border:0; border-bottom:1px dashed var(--border);
  padding:0 2px; width:7ch; text-align:center; font-variant-numeric:tabular-nums; }
.ob-num.w2 { width:3ch; }
.ob-num:focus { outline:none; border-bottom-color:var(--accent); }
.ob-num::placeholder { color:var(--text-3); font-weight:600; }
.ob-numsrc { margin-top:6px; font-size:11px; color:var(--text-3); line-height:1.5; }
.ob-clarify { margin-bottom:10px; padding:14px 16px; border:1px solid transparent; border-radius:var(--r-lg);
  background:linear-gradient(#141417,#141417) padding-box, var(--grad) border-box; }
.ob-clarify-q { margin-top:6px; font-size:14px; line-height:1.5; font-weight:600; color:var(--text-1); }
.ob-clarify-acts { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
.ob-clarify-said { margin-top:8px; font-size:12.5px; color:var(--text-2); }
.ob-air { padding:9px 0; border-top:1px solid var(--border-subtle); }
.ob-air:first-child { border-top:0; padding-top:2px; }
.ob-air-k { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; font-size:12px; color:var(--text-1); }
.ob-air-from { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--text-3); }
.ob-air-t { margin-top:4px; font-size:12px; line-height:1.55; color:var(--text-2); white-space:pre-line; }
.ob-air-t.clamp { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ob-air-all { font:inherit; font-size:11.5px; margin-top:3px; padding:0; background:none; border:0; cursor:pointer;
  color:var(--text-2); text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; }
.ob-air-all:hover { color:var(--text-1); }

/* --- standard checks + optional marker ---------------------------------- */
.ob-check { display:flex; align-items:flex-start; gap:8px; font-size:11.5px; color:var(--text-2); line-height:1.7; }
.ob-check-i {
  width:14px; height:14px; border-radius:50%; background:rgb(var(--c-ok) / .15); color:var(--green);
  font-size:8px; display:inline-flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:3px;
}
.ob-opt {
  display:inline-flex; font-size:9px; font-family:'JetBrains Mono',ui-monospace,monospace;
  letter-spacing:.09em; text-transform:uppercase; color:var(--text-3);
  border:1px solid var(--border); border-radius:var(--r-pill); padding:1px 7px; margin-left:6px; vertical-align:middle;
}

/* --- a disabled primary must LOOK disabled, or it reads as broken ------- */
.btn-grad:disabled, .btn-grad[disabled] { opacity:.4; cursor:not-allowed; box-shadow:none; }

/* --- the pay step: ONE recommended amount, alternatives one click away ----
   Not a pricing table. Three equal rows ask the buyer to compare when we have
   already decided for them, and with outcome claims (rightly) banned there is
   nothing to compare anyway. --- */
.ob-planline {
  display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;
  font-size:11.5px; color:var(--text-3); margin-bottom:14px;
}
.ob-planline-name { color:var(--text-1); font-weight:700; font-size:12.5px; }

.ob-hero {
  border:1px solid transparent; border-radius:14px; padding:18px 20px;
  background:
    linear-gradient(rgba(22,19,32,.96), rgba(16,14,24,.96)) padding-box,
    linear-gradient(150deg, rgba(199,164,255,.45), rgba(139,92,246,.16) 45%, rgba(255,255,255,.04)) border-box;
  box-shadow:0 12px 44px rgba(139,92,246,.18), 0 1px 0 rgba(255,255,255,.06) inset;
}
.ob-hero-top { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
/* HOURS LEAD (treatment C). The saving is the only headline; price sits under it as a
   confirmation detail, because by this screen the buyer has already decided to pay.
   No gradient on the figure: the gradient is the primary-action signal and belongs on the
   button. Deliberately NO second "here" bar - we cannot honestly size the buyer's own side
   of that comparison, and an unsized bar is decoration pretending to be evidence. */
.ob-hero-figs { display:flex; align-items:baseline; gap:9px; }
.ob-hero-hrs {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:40px; font-weight:700;
  line-height:1; letter-spacing:-.025em; color:var(--text-1);
}
.ob-hero-unit { font-size:13px; font-weight:600; color:var(--text-2); }
/* The worth line under the hours ("worth $900, at $20 an hour"). Since 2026-09-24 the only user of
   .ob-hero is the checkout dialog's hours box (window.planCards.buyAnchor, .ob-hero.is-compact); the
   onboarding pack hero it was built for, with its price/saving/credits lines (.ob-hero-cr, .ob-save,
   .ob-hero-price) and the .ob-hourswhy line under it, was replaced by the shared top-up rows. */
.ob-hero-worth { font-size:11.5px; color:var(--text-2); margin-top:9px; line-height:1.65;
                 font-variant-numeric:tabular-nums; }
.ob-hero-worth b { color:var(--text-1); font-weight:600; }

/* ===== THE PLAN CARD (.plc-*) - ONE card for /pricing, /billing and the onboarding pay step =====
   Markup: app/templates/_plan_card.html. Script: _plan_card_js.html. Built 2026-09-24 to replace three
   separate cards (pricing.html's own .plan, and the .plan-card + .plan-price pair billing.html and
   onboarding.html shared) that had drifted into saying different things. Owner-approved design:
   designs/mockups/pricing-cards-checklist.html, view "A4".

   Hierarchy, and why: THE PRICE is the biggest thing on the card (owner: it was "not 100% clear which
   value is the price" when the hours led). Then what the money buys (creators a month), then the value
   box (hours saved, and what they are worth), then credits small, as the meter. The value box is a
   tinted panel, NOT a comparison bar: two prices on one axis read "cheaper", the discount story.
   The recommended plan gets the one gradient edge (DESIGN_SYSTEM 19.5 recipe: OPAQUE interior, the
   gradient only on the border). Tokens only: this file's :root is loaded by the public pages too. */
.plc-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; align-items:stretch; }
@media (max-width:1100px) { .plc-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:560px)  { .plc-grid { grid-template-columns:1fr; } }
.plc {
  position:relative; display:flex; flex-direction:column; text-align:left; min-width:0;
  padding:20px 18px 18px; border-radius:14px;
  background:var(--bg-surface); border:1px solid var(--border);
  transition:border-color .25s ease, box-shadow .25s ease, transform .25s cubic-bezier(.2,.9,.3,1);
}
.plc.is-feat {
  border-color:transparent;
  background:linear-gradient(#131119,#131119) padding-box, var(--grad) border-box;
  box-shadow:0 18px 50px rgba(139,61,255,.20);
}
/* /pricing only: the card is a button that drives the big figure above it */
.plc[role="button"] { cursor:pointer; }
.plc[role="button"]:hover { border-color:var(--border-strong); transform:translateY(-2px); }
.plc.is-feat[role="button"]:hover { border-color:transparent; }
.plc[role="button"]:focus-visible { outline:2px solid rgba(201,166,255,.7); outline-offset:3px; }
.plc.is-on { box-shadow:0 0 0 1px rgba(201,166,255,.55), 0 0 34px rgba(139,61,255,.22); }
.plc.is-feat.is-on { box-shadow:0 0 0 1px rgba(201,166,255,.35), 0 18px 50px rgba(139,61,255,.32); }
.plc-tag {
  position:absolute; top:-10px; left:18px; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:#fff; white-space:nowrap;
  background:var(--grad); border-radius:var(--r-pill); padding:3px 10px; box-shadow:0 0 18px rgba(139,61,255,.45);
}
/* the customer's own plan on /billing: a quiet tag, the gradient stays the recommendation's */
.plc-tag.is-current { background:#10231A; color:#7EE6A8; border:1px solid rgba(126,230,168,.35); box-shadow:none; }
.plc-name { font-size:15px; font-weight:700; letter-spacing:-.01em; color:var(--text-1); }
.plc-suits { font-size:13.5px; line-height:1.45; color:var(--ink-muted); margin-top:2px; min-height:40px; }
.plc-price { display:flex; align-items:flex-start; gap:2px; margin-top:18px; line-height:1; }
.plc-cur { font-size:22px; font-weight:700; color:var(--text-2); margin-top:6px; }
.plc-amt { font-size:50px; font-weight:800; letter-spacing:-.04em; color:var(--text-1); font-variant-numeric:tabular-nums; }
.plc-per { align-self:flex-end; font-size:14px; font-weight:500; color:var(--ink-muted); margin:0 0 7px 8px; line-height:1.25; }
.plc-billed { font-size:13px; color:var(--ink-muted); margin-top:7px; font-variant-numeric:tabular-nums; }
/* A switchable grid (/pricing) carries the yearly line on every card and shows it only on yearly */
.plc-y-only { display:none; }
.plc-grid.is-yearly .plc-y-only { display:block; }
.plc-gets { margin-top:12px; font-size:15px; line-height:1.4; color:var(--text-1); }
.plc-gets b { font-weight:800; font-variant-numeric:tabular-nums; }
.plc-gets small { display:block; font-size:12.5px; color:var(--ink-muted); margin-top:2px; }
.plc-val { margin-top:16px; padding:12px 14px 11px; border-radius:10px;
  background:rgba(139,92,246,.07); border:1px solid rgba(167,139,250,.22); }
/* nowrap + 21px: "≈211 hours saved" must stay on one line in a quarter-width card, or the four
   boxes end up different heights */
.plc-vh { font-size:13px; color:var(--text-2); white-space:nowrap; line-height:1.2; }
.plc-vh b { font-size:21px; font-weight:800; letter-spacing:-.03em; color:var(--text-1); margin-right:6px; font-variant-numeric:tabular-nums; }
.plc-vw { font-size:12.5px; color:var(--ink-muted); margin-top:6px; }
.plc-vw b { font-size:14px; font-weight:700; color:#C9A6FF; font-variant-numeric:tabular-nums; }
.plc-vw small { display:block; font-size:12px; color:var(--ink-muted); margin-top:1px; }
.plc-cr { font-size:12.5px; color:var(--ink-muted); margin-top:14px; font-variant-numeric:tabular-nums; }
.plc-cr b { color:var(--text-2); font-weight:600; }
/* The page's own footer (a Buy button, a first-run line) sits at the bottom whatever the card height */
.plc-foot { margin-top:auto; padding-top:16px; }
/* At rest the Buy button is an outlined pill, so it reads as a button before anyone hovers (a bare
   .btn has no fill and no border, and on the old card it looked like plain text until hovered). */
.plc-foot .btn { transition:background .18s,border-color .18s,box-shadow .18s,color .18s;
  background:transparent; color:var(--text-1); border:1px solid var(--border-strong); justify-content:center; }
/* hovering anywhere on an in-app card lights its button (owner call 2026-08-27, kept from .plan-card) */
.plc:hover .plc-foot .btn:not([disabled]) { background:var(--grad); border-color:transparent; box-shadow:0 0 24px rgba(139,61,255,.45); color:#fff; }

/* What every plan has, said once under the cards (owner's pick, "A3 - shared list once") */
.plc-strip { margin-top:14px; display:grid; grid-template-columns:auto repeat(3,minmax(0,1fr)); gap:28px; align-items:start;
  padding:18px 22px; border:1px solid var(--border-subtle); border-radius:var(--r-lg); background:rgba(255,255,255,.015); }
.plc-strip-h { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--ink-muted); padding-top:3px; white-space:nowrap; }
.plc-strip-it { display:grid; grid-template-columns:30px minmax(0,1fr); gap:11px; }
.plc-strip-ic { width:30px; height:30px; border-radius:9px; display:flex; align-items:center; justify-content:center;
  background:rgba(139,92,246,.12); border:1px solid rgba(139,92,246,.25); color:#C9A6FF; }
.plc-strip-ic svg { width:15px; height:15px; }
.plc-strip-t { font-size:14px; font-weight:600; line-height:1.35; color:var(--text-1); }
.plc-strip-t small { display:block; font-size:12.5px; font-weight:400; line-height:1.45; color:var(--ink-muted); margin-top:2px; }
.plc-strip-t a { color:inherit; text-decoration:underline; text-decoration-color:rgba(201,166,255,.45); text-underline-offset:3px; }
@media (max-width:1100px) { .plc-strip { grid-template-columns:1fr 1fr; gap:18px; } .plc-strip-h { grid-column:1 / -1; } }
@media (max-width:560px)  { .plc-strip { grid-template-columns:1fr; } }
/* The refund line, opening the strip on all three pages (_plan_card.html refund_line, 2026-10-02) */
.plc-refund { display:flex; align-items:center; gap:10px; margin:14px 0 0; font-size:12.5px; line-height:1.45; color:var(--text-2); }
.plc-refund b { color:var(--text-1); font-weight:600; }
.plc-refund-ic { color:#4ADE80; flex:0 0 18px; }
.plc-refund-ic svg { width:18px; height:18px; display:block; }

/* The plan name while it is being typed and still empty shows the "Name your project" prompt
   (plan-screen.js obEditPlanName sets data-ph), so an empty editable heading never looks broken. */
#ob-plan-name[contenteditable="true"]:empty::before { content:attr(data-ph); color:var(--text-3); }

/* ===== THE PAY STEP'S BRIEF (.obb-*, onboarding #step-credits, drawn by obRenderBrief in
   js/onboarding/pay-and-run.js). Ported from the approved mockup designs/mockups/pay-step-start-small.html
   ("Top 1 · Case file", its .cf-* / .pq-* / .ps-ring rules), renamed .obb-* because .cf-* is the shared
   confirm dialog's namespace. The group rows reuse the plan screen's .ob-rrole / .ob-rname / .ob-rnum /
   .ob-kchip and its .ob-tN tier colours. ===== */
.obb-h1 { font-size:26px; font-weight:800; letter-spacing:-.012em; word-spacing:.04em; line-height:1.15; color:var(--text-1); margin:2px 0 6px; }
@media (max-width:560px) { .obb-h1 { font-size:22px; } }
.obb-file { border-radius:18px; border:1px solid transparent; overflow:hidden; margin:4px 0 6px;
  background:radial-gradient(70% 60% at 0% 0%, rgba(139,61,255,.18), transparent 70%) padding-box,linear-gradient(#111015,#111015) padding-box,linear-gradient(160deg,rgba(192,132,252,.45),rgba(255,255,255,.06) 40%,rgba(255,255,255,.03) 80%) border-box;
  box-shadow:0 24px 70px rgba(0,0,0,.4); }
.obb-top { display:flex; align-items:center; gap:14px; padding:18px 20px; border-bottom:1px solid var(--border-subtle); }
.obb-mark { flex:0 0 40px; width:40px; height:40px; border-radius:12px; display:grid; place-items:center; color:#E9D5FF;
  background:radial-gradient(circle at 30% 25%, rgba(192,132,252,.45), rgba(139,61,255,.12) 70%); box-shadow:inset 0 0 0 1px rgba(192,132,252,.4),0 0 24px rgba(139,61,255,.3); }
.obb-mark svg { width:19px; height:19px; }
.obb-tt { display:flex; flex-direction:column; gap:1px; min-width:0; }
.obb-eb { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.16em; text-transform:uppercase; color:#C4B5FD; }
.obb-name { font-size:20px; line-height:1.15; font-weight:800; color:var(--text-1); letter-spacing:-.01em; overflow-wrap:anywhere; }
.obb-meta { font-size:12px; color:var(--text-3); }
.obb-tr { margin-left:auto; display:flex; flex-direction:column; align-items:flex-end; gap:6px; flex:none; }
.obb-status { display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:600; color:#86EFAC; background:rgb(var(--c-ok) / .1);
  border:1px solid rgb(var(--c-ok) / .25); border-radius:999px; padding:3px 10px; white-space:nowrap; }
.obb-status::before { content:''; width:6px; height:6px; border-radius:50%; background:#4ADE80; box-shadow:0 0 8px rgba(74,222,128,.8); }
.obb-change { font:inherit; font-size:12.5px; color:var(--text-2); background:none; border:0; padding:0; cursor:pointer; white-space:nowrap;
  text-decoration:underline; text-underline-offset:3px; text-decoration-color:var(--border-strong); }
.obb-change:hover { color:var(--text-1); }
.obb-lbl { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--text-3); margin-bottom:9px; }
/* "You sell" / "Your buyers": their answers quoted side by side. Clamped, because the offer the AI read can
   run to ~900 characters and this is a read-back, not the place to reread a page of text. */
.obb-brief { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); border-bottom:1px solid var(--border-subtle); }
.obb-brief.is-one { grid-template-columns:minmax(0,1fr); }
.obb-brief > div { padding:16px 20px; min-width:0; }
.obb-brief > div + div { border-left:1px solid var(--border-subtle); }
.obb-brief .obb-lbl { margin-bottom:7px; }
.obb-brief q { quotes:'\201C' '\201D'; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:5; overflow:hidden;
  font-size:15.5px; font-weight:600; line-height:1.45; color:var(--text-1); overflow-wrap:anywhere; }
.obb-sec { padding:15px 20px; border-bottom:1px solid var(--border-subtle); }
.obb-chips { display:flex; flex-wrap:wrap; gap:6px; }
.obb-chips .ob-kchip { padding:4px 11px; max-width:100%; overflow-wrap:anywhere; }
.obb-rules { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:12px 22px; }
.obb-rl { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.12em; text-transform:uppercase; margin-bottom:8px; }
.obb-rl.ok { color:#86EFAC; } .obb-rl.no { color:#FCA5A5; }
.obb-chips .obb-rc { gap:7px; padding:4px 11px 4px 6px; }
.obb-rc i { display:grid; place-items:center; width:16px; height:16px; border-radius:50%; flex:0 0 16px; }
.obb-rc i svg { width:9px; height:9px; }
.obb-rc.ok { border-color:rgb(var(--c-ok) / .28); } .obb-rc.ok i { background:rgb(var(--c-ok) / .16); color:#4ADE80; }
.obb-rc.no { border-color:rgba(248,113,113,.28); } .obb-rc.no i { background:rgba(248,113,113,.16); color:#F87171; }
.obb-where { display:grid; gap:14px; }
.obb-gh { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:7px; }
.obb-ring { width:20px; height:20px; flex:none; fill:none; stroke:rgba(255,255,255,.12); stroke-width:1.2; }
.obb-ring .on { stroke-width:2.2; }
.ob-t1 .obb-ring .on-fill { fill:#A78BFA; stroke:#A78BFA; filter:drop-shadow(0 0 5px rgba(167,139,250,.75)); }
.ob-t2 .obb-ring .on { stroke:#8E82B8; filter:drop-shadow(0 0 4px rgba(142,130,184,.55)); }
.ob-t3 .obb-ring .on { stroke:#6E6A82; }
/* "What happens next": the method as a footer band, the only "us" part of the box, so it comes last. */
.obb-band { padding:16px 20px 18px; background:linear-gradient(180deg,rgba(255,255,255,.015),rgba(139,61,255,.05)); }
.obb-blbl { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--text-3); margin-bottom:11px; }
.obb-steps { list-style:none; margin:0; padding:0; display:grid; gap:10px; align-items:start;
  grid-template-columns:minmax(0,1fr) 16px minmax(0,1fr) 16px minmax(0,1fr) 16px minmax(0,1fr); }
.obb-steps.obb-n3 { grid-template-columns:minmax(0,1fr) 16px minmax(0,1fr) 16px minmax(0,1fr); }
.obb-step { display:flex; gap:9px; min-width:0; }
.obb-sn { flex:0 0 22px; width:22px; height:22px; border-radius:50%; display:grid; place-items:center; font-size:11px; font-weight:700; color:#C4B5FD;
  background:rgb(var(--c-accent) / .16); box-shadow:inset 0 0 0 1px rgb(var(--c-accent) / .35); }
.obb-step b { display:block; font-size:13px; color:var(--text-1); }
.obb-step div span { display:block; font-size:12px; color:var(--text-2); line-height:1.4; margin-top:1px; }
.obb-arr { color:var(--text-3); padding-top:3px; }
.obb-arr svg { width:14px; height:14px; }
/* The plan section's heading IS the question; the shared cards below are its answers. */
.obb-q { font-size:20px; font-weight:800; letter-spacing:-.012em; color:var(--text-1); margin:34px 0 4px; }
.obb-qsub { font-size:12.5px; color:var(--text-2); margin:0 0 16px; }
@media (max-width:760px) {
  .obb-top { flex-wrap:wrap; }
  .obb-tr { margin-left:0; align-items:center; flex-direction:row; flex-wrap:wrap; gap:8px 12px; width:100%; }
  .obb-brief, .obb-rules { grid-template-columns:minmax(0,1fr); }
  .obb-brief > div + div { border-left:0; border-top:1px solid var(--border-subtle); }
  .obb-steps, .obb-steps.obb-n3 { grid-template-columns:minmax(0,1fr); }
  .obb-arr { display:none; }
}
@media (prefers-reduced-motion:reduce) { .plc { transition:none; } }

/* ===== THE TOP-UP ROWS (.pkr-*) - ONE component for /billing "Top up" and the onboarding pay step =====
   Markup: pack_rows in app/templates/_plan_card.html. Owner's pick "B · Rows" (2026-09-24,
   designs/mockups/topup-cards-shared.html): the plan card's units on one line per pack, deliberately
   quieter than the plan cards above them (plans beat every pack per creator). One framed list, rows
   split by hairlines. No row is highlighted: the owner ruled out "Our pick", and a silent highlight
   would be the same claim with no words. Tokens only, like .plc-*. */
.pkr { border:1px solid var(--border); border-radius:14px; background:var(--bg-surface); overflow:hidden; }
.pkr-row { display:grid; grid-template-columns:minmax(0,1.25fr) minmax(0,.8fr) minmax(0,1fr) minmax(0,1.35fr) 150px;
  gap:18px; align-items:center; padding:14px 18px; transition:background .18s; }
.pkr-row + .pkr-row { border-top:1px solid var(--border-subtle); }
.pkr-row:hover { background:rgba(255,255,255,.018); }
.pkr-nm { font-size:14px; font-weight:700; letter-spacing:-.01em; color:var(--text-1); }
.pkr-suits { font-size:12.5px; color:var(--ink-muted); margin-top:1px; }
.pkr-cr { font-size:12px; color:var(--ink-muted); margin-top:2px; font-variant-numeric:tabular-nums; }
.pkr-cr b { color:var(--text-2); font-weight:600; }
.pkr-price { display:flex; align-items:baseline; gap:1px; line-height:1; }
.pkr-cur { font-size:15px; font-weight:700; color:var(--text-2); align-self:flex-start; margin-top:2px; }
.pkr-amt { font-size:28px; font-weight:800; letter-spacing:-.035em; color:var(--text-1); font-variant-numeric:tabular-nums; }
.pkr-per { font-size:12.5px; color:var(--ink-muted); margin-left:5px; }
.pkr-gets { font-size:13.5px; line-height:1.35; color:var(--text-1); }
.pkr-gets b { font-weight:800; font-variant-numeric:tabular-nums; }
.pkr-gets small { display:block; font-size:12px; color:var(--ink-muted); }
/* the plan card's value box (.plc-val), one size down; filled by window.planCards.fill */
.pkr-val { padding:8px 12px; border-radius:10px; background:rgba(139,92,246,.07); border:1px solid rgba(167,139,250,.22); }
.pkr-vh { font-size:12.5px; color:var(--text-2); white-space:nowrap; line-height:1.2; }
.pkr-vh b { font-size:17px; font-weight:800; letter-spacing:-.03em; color:var(--text-1); margin-right:5px; font-variant-numeric:tabular-nums; }
.pkr-vw { font-size:12px; color:var(--ink-muted); margin-top:3px; }
.pkr-vw b { font-size:13px; font-weight:700; color:#C9A6FF; font-variant-numeric:tabular-nums; }
.pkr-vw small { display:none; }   /* the rate is said once, under the rows (.pkr-rate) */
.pkr-buy { display:flex; flex-direction:column; align-items:stretch; gap:5px; }
.pkr-buy form { display:flex; flex-direction:column; }
/* Same at-rest outlined pill and hover-lights-the-button as the plan card's footer (.plc-foot .btn) */
.pkr-buy .btn { justify-content:center; background:transparent; color:var(--text-1); border:1px solid var(--border-strong);
  transition:background .18s,border-color .18s,box-shadow .18s,color .18s; }
.pkr-row:hover .pkr-buy .btn:not([disabled]) { background:var(--grad); border-color:transparent; box-shadow:0 0 24px rgba(139,61,255,.45); color:#fff; }
.pkr-ppc { font-size:11.5px; color:var(--text-3); text-align:center; }
.pkr-ppc span { font-family:'JetBrains Mono',ui-monospace,monospace; color:var(--text-2); }
.pkr-rate { font-size:11.5px; color:var(--ink-muted); margin-top:8px; }
.pkr-rate:empty { display:none; }
/* Narrower: name + price on top, creators + hours below, the buy area across the bottom */
@media (max-width:1100px) {
  .pkr-row { grid-template-columns:minmax(0,1fr) auto; gap:10px 16px; }
  .pkr-price { justify-content:flex-end; }
  .pkr-buy { grid-column:1 / -1; flex-direction:row; align-items:center; }
  .pkr-buy form { flex:1; }
  .pkr-ppc { white-space:nowrap; }
}
/* Phone, and onboarding's narrow card: keep "Standard top-up" on one line (it broke as "top- / up") */
@media (max-width:560px) {
  .pkr-row { padding:14px; }
  .pkr-nm { white-space:nowrap; }
  .pkr-amt { font-size:24px; }
}
@media (prefers-reduced-motion:reduce) { .pkr-row, .pkr-buy .btn { transition:none; } }

/* A textarea that grows with its content instead of scrolling. A short field tells the reader a
   short answer is expected, which is the opposite of what an optional question wants. Initialised
   by the [data-grow] hook (currently onboarding.html; lift it to base.html on the second user). */
.ds-grow { overflow:hidden; resize:none; }

/* Step 1's four questions, grouped into the two topics they actually belong to. Group name is a
   quiet rule-with-a-label, not a heading — it separates without adding weight. */
.ob-qgroup {
  display:flex; align-items:center; gap:10px;
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3); margin:18px 0 2px;
}
.ob-qgroup::after { content:''; flex:1; height:1px; background:rgba(255,255,255,.08); }
.ob-qgroup:first-child { margin-top:0; }
.ob-q { margin-top:12px; }
.ob-qlab { display:block; font-size:12.5px; font-weight:700; letter-spacing:-.01em; margin-bottom:5px; }
.ob-qhint { font-size:11px; color:var(--text-3); line-height:1.5; margin-bottom:6px; }

/* platform picker: multi-select, so these ARE toggles and a filled state is correct here
   (unlike the example links, where a filled state wrongly implied "pick one") */
.ob-plat {
  font-size:12px; font-weight:600; padding:7px 14px; border-radius:8px;
  color:var(--text-2); background:var(--bg-elevated); border:1px solid var(--border);
  transition:background .15s, border-color .15s, color .15s;
}
.ob-plat:hover { border-color:var(--border-strong); color:var(--text-1); }
.ob-plat.is-on {
  background:rgb(var(--c-accent) / .16); border-color:rgb(var(--c-accent) / .5); color:var(--text-1);
}
/* .ob-hero-rec / -suits / -split and the .ob-alt* pills: removed 2026-09-24 with onboarding's pack
   hero, replaced by the shared top-up rows (.pkr-*). */

/* collapsed reference material: available to anyone reasoning it through, invisible to everyone else */
.ob-details summary {
  font-size:11.5px; color:var(--text-2); cursor:pointer; list-style:none;
  display:inline-flex; align-items:center; gap:6px;
}
.ob-details summary::-webkit-details-marker { display:none; }
.ob-details summary::before { content:'▸'; color:var(--text-3); font-size:9px; }
.ob-details[open] summary::before { content:'▾'; }
.ob-details summary:hover { color:var(--text-1); }
.ob-price { display:flex; justify-content:space-between; align-items:baseline; gap:12px; font-size:12.5px; color:var(--text-2); padding:4px 0; }
.ob-price b { color:var(--text-1); font-family:'JetBrains Mono',ui-monospace,monospace; font-variant-numeric:tabular-nums; white-space:nowrap; }
/* ROI anchor. Always describes THEIR ALTERNATIVE (what doing it by hand costs them), never our
   output — that is the property that lets a number exist here at all, given every forecast about
   results is banned. */
.ob-anchor {
  display:flex; align-items:baseline; gap:11px;
  padding:11px 13px; border-radius:var(--r-md);
  background:rgba(255,255,255,.03); border:1px solid var(--border-subtle);
  font-size:11.5px; color:var(--text-3); line-height:1.6;
}
.ob-anchor-n {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:15px; font-weight:700;
  color:var(--text-1); white-space:nowrap; flex-shrink:0; font-variant-numeric:tabular-nums;
}

/* No left accent rule (owner). Just a quiet panel. */
/* "We kept what you wrote." Quiet by design: it explains a prefilled box, it is not news. */
.ob-resumed {
  display:inline-block; font-size:11.5px; color:var(--text-2);
  background:rgb(var(--c-accent) / .10); border:1px solid rgb(var(--c-accent) / .24);
  border-radius:var(--r-pill); padding:5px 12px;
}
.ob-reassure {
  padding:11px 13px; font-size:11.5px; color:var(--text-2); line-height:1.6;
  background:rgb(var(--c-ok) / .06); border:1px solid rgb(var(--c-ok) / .18);
  border-radius:var(--r-md);
}

/* ==========================================================================
   THE ONBOARDING CONVERSATION (2026-09-15) - one question per screen, the same
   shape as the guided setup's takeover on the project page.

   THE SPEAKER LINE, THE WAIT AND THE INFERENCE CARD ARE NOT HERE. They are
   .sg-ai / .sg-ai-mark / .sg-w / .sg-wait / .ob-dots / .sg-infer further up this
   file, shared with the project page and drawn by _ai_conversation.html. Only
   the things this page shapes differently - a card instead of a modal panel -
   are defined below, and every value is a token.
   ========================================================================== */
.ob-convo-eye {
  display:inline-flex; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px;
  letter-spacing:.12em; text-transform:uppercase; color:var(--text-3); margin-bottom:13px;
}
.ob-convo-eye.is-done { color:#4ADE80; }
.ob-convo-q { font-size:21px; font-weight:800; letter-spacing:-.02em; line-height:1.3; margin-bottom:13px; }
/* Question 1 only: what the answer is FOR, and why pasting is welcome. Accent-coloured because it
   is a promise the product keeps thirty seconds later, not a warning. */
.ob-convo-hint { font-size:12.5px; color:#C4B5FD; line-height:1.55; margin-bottom:13px; max-width:520px; }
.ob-convo-said-k {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.1em;
  text-transform:uppercase; color:#C4B5FD; margin-bottom:5px;
}
/* What they typed, handed back during the wait and on the read-failed screen. A pasted page is
   clamped rather than hidden: without this the question and the buttons went off the bottom of the
   screen, which is exactly what the project page's tour hit and fixed the same way. */
.ob-convo-answer {
  background:var(--bg-input); border:1px solid var(--border); border-radius:var(--r-md);
  padding:14px 16px; font-size:14px; color:var(--text-1); line-height:1.6; white-space:pre-wrap;
}
.ob-convo-answer.is-long { max-height:120px; overflow:hidden; }
/* THE ANSWER BOX HAS A CEILING. `.ds-grow` grows a textarea to fit its content and never stops, and
   question 1 openly invites a whole pasted website page - so on a phone the box became two thousand
   pixels tall with `overflow:hidden` and Continue somewhere far below the fold. The cap keeps the
   question, the box and the button on one screen; past it the box scrolls, so nothing they pasted is
   lost or unreachable. Id selector because `.ds-grow` sets overflow:hidden and has to be beaten. */
#ob-convo-box { max-height:46vh; overflow-y:auto; overscroll-behavior:contain; }
.ob-convo-read { font-size:13.5px; color:var(--text-1); line-height:1.65; max-width:490px; }
.ob-convo-fail { font-size:13px; color:var(--text-2); line-height:1.62; max-width:500px; margin-top:14px; }
.ob-convo-lead { font-size:13px; color:var(--text-2); line-height:1.6; margin-bottom:11px; }
/* An answer written FOR them, as one block. Bordered in the accent so it reads as a proposal to
   judge, never as something they typed. */
.ob-convo-prop {
  background:var(--bg-input); border:1px solid rgb(var(--c-accent) / .3); border-radius:var(--r-md);
  padding:14px 16px; font-size:13.5px; color:var(--text-1); line-height:1.65; white-space:pre-wrap;
}
.ob-convo-items { display:flex; flex-direction:column; gap:8px; }
.ob-convo-item {
  display:flex; align-items:flex-start; gap:10px; text-align:left; width:100%; cursor:pointer;
  padding:11px 13px; border-radius:var(--r-md); font-family:inherit; font-size:13px; line-height:1.55;
  background:transparent; color:var(--text-2); border:1px solid var(--border);
}
.ob-convo-item.is-on { background:rgb(var(--c-accent) / .10); color:var(--text-1); border-color:rgb(var(--c-accent) / .45); }
.ob-convo-item .tk {
  flex:0 0 15px; height:15px; margin-top:1px; border-radius:4px; display:inline-flex;
  align-items:center; justify-content:center; font-size:10px; color:#fff;
  border:1px solid rgba(255,255,255,.25);
}
.ob-convo-item.is-on .tk { background:var(--grad); border-color:transparent; }
.ob-convo-from { font-size:11.5px; color:var(--text-3); line-height:1.5; margin-top:9px; }
/* The size question's "Set:" line (onboarding conversation-state.js obSizeSaidHtml): the same green note
   the project page's setup draws for the same sentence (setup-gate-ending.js _sgAppliedHtml). */
.ob-size-set { display:flex; gap:8px; align-items:flex-start; margin-top:10px; padding:10px 12px; border-radius:9px;
  background:rgba(34,197,94,.07); border:1px solid rgba(34,197,94,.22); font-size:12px; color:var(--text-2); line-height:1.5; }
.ob-size-set-t { color:#4ADE80; flex:none; }
/* Plan screen: a search group's name opens it as the arrow does (plan-screen.js, e2e round 5). */
[data-ob-open] { cursor:pointer; }

/* The pay screen with SOME credits, fewer than the whole plan can need (owner pick C, 2026-10-11; mockup
   pay-step-start-small.html#shortc; payment-return.js obUpdateShortBalance). Two answers of EQUAL weight
   (a decision, never one loud button and one quiet link), big, with the gradient EDGE on both. */
.ob-short-facts { font-size:13.5px; color:var(--text-2); line-height:1.55; margin:0 0 18px; max-width:640px; }
.ob-short-eq { display:flex; gap:12px; flex-wrap:wrap; }
.ob-short-btn { flex:1 1 220px; display:inline-flex; align-items:center; justify-content:center; font:inherit; font-size:14.5px;
  font-weight:700; color:#fff; padding:14px 20px; border-radius:var(--r-pill); cursor:pointer;
  background:linear-gradient(#141418,#141418) padding-box, var(--grad) border-box; border:1.5px solid transparent;
  transition:box-shadow .2s, transform .15s; }
.ob-short-btn:hover { box-shadow:0 0 26px rgba(139,61,255,.35); transform:translateY(-1px); }
.ob-short-in { display:flex; flex-direction:column; align-items:center; line-height:1.25; }
.ob-short-in small { font-size:11.5px; font-weight:500; color:var(--text-3); margin-top:2px; }
.ob-short-gap { margin:28px 0 10px; }
.ob-short-box { margin:22px 0 20px; padding:15px 16px; border-radius:var(--r-md); border:1px solid var(--border); background:rgba(255,255,255,.02); }
.ob-short-box-h { font-size:13.5px; color:var(--text-2); line-height:1.5; margin:0 0 12px; }
@media (prefers-reduced-motion: reduce) { .ob-short-btn { transition:none; } .ob-short-btn:hover { transform:none; } }

/* "Any creators you don't want?" suggestions (owner pick D, 2026-10-11; mockup onboarding-setup-gate-flow.html
   section 25). A tick box read as "I want these", so each row LEADS with the action: "Skip", then the
   creators, and the whole row is the button. On = red, "Skipping". Shared by the onboarding conversation
   (conversation-screens.js) and the project page's setup (setup-gate-ai-tool.js). Rows start OFF. */
.ob-skip-row { align-items:flex-start; }
.ob-skip-tag { flex:none; display:inline-flex; align-items:center; gap:6px; margin-top:-1px; padding:3px 11px;
  border-radius:var(--r-pill); border:1px solid var(--border-strong); font-size:12px; font-weight:600; line-height:1.4; color:var(--text-2); }
.ob-skip-tag svg { display:none; }
.ob-skip-row.is-skip { background:rgba(239,68,68,.07); border-color:rgba(239,68,68,.32); color:var(--text-1); }
.ob-skip-row.is-skip .ob-skip-tag { background:rgba(239,68,68,.14); border-color:rgba(239,68,68,.35); color:#FCA5A5; }
.ob-skip-row.is-skip .ob-skip-tag svg { display:inline; }
/* Where to look. A setting, sitting quietly under the last answer - never styled as a question. */
.ob-convo-plat { display:flex; gap:7px; align-items:center; flex-wrap:wrap; margin-top:18px; }
.ob-convo-plat b {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.11em;
  text-transform:uppercase; color:var(--text-3); font-weight:500; margin-right:3px;
}
/* flex-wrap, because the primary button's label is not always short ("Build my plan") and three
   ghost buttons beside it crushed the row into two-line buttons at 390px. */
.ob-convo-acts { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:22px; flex-wrap:wrap; }
.ob-convo-actsL { display:flex; gap:8px; flex-wrap:wrap; }
.ob-convo-actsR { display:flex; align-items:center; gap:10px; }
.ob-convo-warn { font-size:12px; color:#FBBF24; margin-top:9px; line-height:1.5; }
/* THE SETUP FACTS on question 1's read-back (2026-09-26, option A; onboarding.html obFactsHtml, mockup
   designs/mockups/onboarding-project-name.html): brand name, own business or a client, the date they need
   creators by. Each is a "Where to look" row (.ob-convo-plat + .ob-plat); only the stacking, the read value
   and its quiet "change" pill are new. */
.ob-facts { margin-top:16px; }
.ob-facts .ob-convo-plat { margin-top:9px; min-height:32px; }
.ob-facts .ob-convo-plat:first-child { margin-top:0; }
.ob-facts .ob-convo-plat b { min-width:118px; white-space:nowrap; }
.ob-fact-v { font-size:14px; font-weight:700; color:var(--text-1); letter-spacing:-.005em; overflow-wrap:anywhere; }
.ob-fact-edit { font-size:11px; color:var(--text-3); border:1px solid var(--border); border-radius:999px; padding:2px 10px; background:none; cursor:pointer; }
.ob-fact-edit:hover { color:var(--text-1); border-color:var(--border-strong); }
.ob-fact-in { flex:1 1 220px; max-width:320px; }
.ob-fact-in input { width:100%; padding:7px 11px !important; font-size:13px !important; }
/* The "why" screen: the question is the only text, and the box is big enough to invite a real answer
   (a small field invites a small answer, owner 2026-09-16). */
.ob-why-box { min-height:150px; }
/* Its quiet "Not now" sits beside Back: centred on it, a size up from the example links it borrows from. */
.ob-convo-actsL .ob-exlink { align-self:center; font-size:12.5px; }
/* The plan's "Need creators by" line under its name. */
.ob-plan-date { display:inline-flex; align-items:center; gap:6px; font-size:11.5px; color:var(--text-2); margin:-2px 0 12px; }
.ob-plan-date svg { color:var(--text-3); }

/* QUESTION 1 AS A MESSAGE BOX (Concept B, owner pick 2026-09-17; onboarding.html obQ1AddRow/obQ1Card,
   mockup designs/mockups/onboarding-q1-sources.html). The COMPOSER carries the border and the focus ring,
   so the link and file cards read as part of the same message; its textarea is bare. The ring values are
   body.ob-page's own textarea focus, moved up one level. */
.ob-comp {
  position:relative; background:var(--bg-input); border:1px solid var(--border); border-radius:14px;
  transition:background .16s, border-color .16s, box-shadow .16s;
}
.ob-comp:hover { border-color:rgba(255,255,255,.18); }
.ob-comp:focus-within {
  border-color:var(--accent);
  box-shadow:0 0 0 3px rgb(var(--c-accent) / .18), 0 0 22px rgb(var(--c-accent) / .14);
}
.ob-comp textarea.ob-comp-box,
.ob-comp textarea.ob-comp-box:hover,
.ob-comp textarea.ob-comp-box:focus {
  display:block; width:100%; min-height:104px; background:transparent; border:0; box-shadow:none; outline:none;
  color:var(--text-1); font-family:inherit; font-size:13.5px; line-height:1.65; padding:13px 15px 4px;
}
.ob-comp .ob-src-list { padding:10px 12px 0; }
.ob-src-list { display:flex; flex-wrap:wrap; gap:8px; }
.ob-src-list.hidden, .ob-src-menu.hidden { display:none; }   /* beats display:flex whatever the load order */
/* "+" and the marks. align-items:flex-start with the marks nudged down: when the marks wrap onto
   several lines (a phone), "+" stays level with the first line instead of centring on the block. */
.ob-comp-row { position:relative; display:flex; align-items:flex-start; gap:6px; padding:6px 10px 10px; }
.ob-plus {
  flex:none; width:32px; height:32px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center;
  background:var(--bg-elevated); color:var(--text-1); border:1px solid var(--border-strong); cursor:pointer;
  transition:background .15s, border-color .15s;
}
.ob-plus:hover, .ob-plus[aria-expanded="true"] { background:var(--bg-hover); border-color:rgb(var(--c-accent) / .6); }
.ob-plus:focus-visible, .ob-mark-grp:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.ob-marks { display:flex; align-items:center; flex-wrap:wrap; gap:2px 4px; flex:1; min-width:0; padding-top:3px; }
/* A group of marks is a BUTTON (Files opens the picker, the rest open the link field), so it gets the
   hover of something pressable; it looks like a quiet line until then. */
.ob-mark-grp {
  display:inline-flex; align-items:center; gap:7px; padding:4px 7px; border:0; border-radius:8px; cursor:pointer;
  background:transparent; font-family:inherit; font-size:11.5px; line-height:18px; color:var(--text-3); white-space:nowrap;
  transition:background .15s, color .15s;
}
.ob-mark-grp:hover { background:var(--bg-hover); color:var(--text-1); }
.ob-mark-grp .mk { display:inline-flex; align-items:center; gap:5px; color:var(--text-2); }
.ob-mark-grp img { display:block; width:14px; height:14px; object-fit:contain; }
/* After the read: the cards, then the same row, under a divider. */
.ob-q1-after { position:relative; margin-top:18px; padding-top:16px; border-top:1px solid var(--border-subtle); border-radius:0; }
.ob-q1-after .ob-comp-row { padding:0; margin-top:12px; }
.ob-q1-after .ob-src-list.hidden + .ob-comp-row { margin-top:0; }
/* The menu "+" opens: upload, or paste a link. */
.ob-src-menu {
  position:absolute; left:4px; top:calc(100% + 2px); z-index:30; width:350px; max-width:calc(100% - 8px); padding:6px;
  background:var(--bg-elevated); border:1px solid var(--border-strong); border-radius:12px;
  box-shadow:0 18px 40px rgba(0,0,0,.55);
}
.ob-src-mi {
  display:flex; align-items:center; gap:11px; width:100%; text-align:left; padding:8px; border:0; border-radius:8px;
  background:transparent; color:var(--text-1); font-family:inherit; cursor:pointer;
}
.ob-src-mi:hover { background:var(--bg-hover); }
.ob-src-mi b { display:block; font-size:13px; font-weight:600; }
.ob-src-mi small { display:block; font-size:11.5px; color:var(--text-3); margin-top:1px; }
.ob-src-ic {
  flex:none; width:30px; height:30px; border-radius:8px; display:inline-flex; align-items:center; justify-content:center;
  background:var(--bg-hover); color:var(--text-2);
}
.ob-src-mlink { display:flex; align-items:center; gap:9px; margin-top:4px; padding:8px 4px 4px 8px; border-top:1px solid var(--border-subtle); }
.ob-src-mlink input {
  flex:1; min-width:0; background:var(--bg-input); border:1px solid var(--border); border-radius:8px;
  color:var(--text-1); font-family:inherit; font-size:13px; padding:7px 10px;
}
/* A SOURCE CARD: a link or a file, one look. The tile carries the service's own mark (thesvg,
   app/static/icons/brands), a globe for a plain website, a file for text. */
.ob-src {
  display:inline-flex; align-items:center; gap:10px; min-width:0; max-width:360px; padding:7px 8px;
  background:var(--bg-elevated); border:1px solid var(--border); border-radius:10px; font-size:12.5px; color:var(--text-1);
}
.ob-src.is-bad { border-color:rgb(var(--c-warn) / .35); }
.ob-src-tile {
  flex:none; width:30px; height:30px; border-radius:7px; display:inline-flex; align-items:center; justify-content:center;
  background:var(--edge); color:var(--text-2);
}
.ob-src-tile.is-web { color:#C4B5FD; }
.ob-src-tile img { display:block; width:18px; height:18px; object-fit:contain; }
.ob-src-nm { display:flex; flex-direction:column; flex:1 1 auto; min-width:0; line-height:1.3; }
.ob-src-nm b { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ob-src-nm small { font-size:11px; color:var(--text-3); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* The reason wraps: it is the one thing on the card they need to read in full. */
.ob-src-nm small.is-bad { color:#FBBF24; white-space:normal; }
.ob-src-read {
  flex:none; display:inline-flex; align-items:center; gap:5px; padding:0 6px;
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.1em; text-transform:uppercase; color:#4ADE80;
}
.ob-src-read i { width:6px; height:6px; border-radius:50%; background:var(--green); display:inline-block; }
.ob-src-rm {
  flex:none; width:24px; height:24px; border:0; border-radius:6px; display:inline-flex; align-items:center; justify-content:center;
  background:transparent; color:var(--text-3); cursor:pointer;
}
.ob-src-rm:hover { background:var(--bg-hover); color:var(--text-1); }
/* QUESTION 1'S COUNT (2026-10-01, owner pick: mockup onboarding-q1-sources.html round 3, option A
   "An itemised list"; onboarding.html obQ1List). On the ask screen the cards stack into one list, full
   width: their own words first, then each link and file, with what is read of each in a right-hand
   column (green when all of it, amber when part or none) and the reason on the line under the name. The
   total sits under the list, above their words. */
.ob-src-list.is-count { flex-direction:column; align-items:stretch; }
.ob-src-list.is-count .ob-src { display:flex; max-width:100%; }
.ob-src-nm small.is-part { color:#FBBF24; white-space:normal; }
.ob-src-cnt {
  flex:none; margin-left:auto; padding-left:12px; font-size:11.5px; color:var(--text-2); white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
.ob-src-cnt:empty { display:none; }   /* still being read: nothing to count yet */
.ob-src-cnt.is-all { color:#4ADE80; }
.ob-src-cnt.is-part { color:#FBBF24; }
/* The own-words row has no remove button; this keeps its count in the same column as the rows under it. */
.ob-src-rmsp { flex:none; width:24px; }
.ob-q1-tot {
  /* Inside the list (#ob-q1-srcs), so the list's own 12px side padding lines it up with the rows. */
  display:flex; align-items:baseline; flex-wrap:wrap; gap:3px 8px; margin:2px 0 4px; padding:10px 3px 0;
  border-top:1px solid var(--border-subtle); line-height:1.45;
}
.ob-q1-tot b { font-size:12.5px; font-weight:600; color:var(--text-1); font-variant-numeric:tabular-nums; }
.ob-q1-tot span { font-size:11.5px; color:var(--text-3); }
.ob-q1-tot.is-at b { color:#FBBF24; }
/* A file being dragged over question 1. pointer-events:none so the page keeps receiving the drag. */
.ob-drop {
  position:absolute; inset:-1px; z-index:20; border-radius:14px; pointer-events:none;
  display:flex; align-items:center; justify-content:center; gap:10px; padding:12px; text-align:center;
  background:linear-gradient(rgb(var(--c-surface) / .94), rgb(var(--c-surface) / .94)) padding-box, var(--grad) border-box;
  border:1.5px solid transparent; box-shadow:0 0 34px rgb(var(--c-accent) / .28);
  font-size:14px; font-weight:600; color:var(--text-1);
}
.ob-drop .mk { display:inline-flex; align-items:center; gap:6px; }
.ob-drop img { display:block; width:18px; height:18px; object-fit:contain; }

/* PHONE. Left to wrap, the row scattered: Back and "Change it" on one line, "I'll write my own" on
   the next, then the primary button stranded on the left of a third (seen at 390px, 2026-09-15).
   column-reverse puts the primary on its own full-width row directly under the content, where a
   thumb is, and leaves the secondary actions in one tidy row beneath it. The source order is
   unchanged, so nothing about reading order or tab order moves. */
@media (max-width:560px) {
  .ob-convo-acts { flex-direction:column-reverse; align-items:stretch; gap:14px; flex-wrap:nowrap; }
  /* Left-aligned, not space-between: with two buttons space-between pins one to each edge as if they
     were opposites, and with three it strands the last one alone under a gap. */
  .ob-convo-actsL { justify-content:flex-start; gap:4px 10px; }
  .ob-convo-actsR { justify-content:center; }
  .ob-convo-actsR .btn-grad { flex:1; justify-content:center; }
  /* The label gets its own line rather than pushing two of the three chips onto a second one. */
  .ob-convo-plat b { flex:0 0 100%; margin-right:0; margin-bottom:2px; }
  /* 16px or iOS zooms in on focus (the app-wide rule above loses to the composer's own size). */
  .ob-comp textarea.ob-comp-box, .ob-comp textarea.ob-comp-box:focus, .ob-src-mlink input,
  .ob-fact-in input, textarea.ob-why-box { font-size:16px !important; }
  .ob-src { max-width:100%; }
  /* Question 1's count on a phone: only the COUNT drops to its own line, under the name (the tile stays
     beside the name, or the icon floats alone above it). The remove button is pinned to the corner,
     or a flowed one lands orphaned on a line of its own. flex-basis 0 on the name: with auto its base
     width is the whole amber reason, and the row would drop the name below the tile instead. */
  .ob-src-list.is-count .ob-src { position:relative; flex-wrap:wrap; align-items:flex-start; padding-right:32px; }
  .ob-src-list.is-count .ob-src-tile { margin-top:1px; }
  .ob-src-list.is-count .ob-src-nm { flex:1 1 0; align-self:center; }
  .ob-src-list.is-count .ob-src-cnt { flex:1 1 100%; margin-left:40px; padding-left:0; padding-top:3px; white-space:normal; }
  .ob-src-list.is-count .ob-src-rm { position:absolute; top:7px; right:5px; }
  .ob-src-list.is-count .ob-src-rmsp { display:none; }
  .ob-q1-tot { flex-direction:column; align-items:flex-start; gap:2px; }
}

/* ==========================================================================
   ONBOARDING LOOK SWITCHES (operator-only, ?look= and ?chrome=off)
   Two INDEPENDENT switches on purpose: hold one constant and flip the other to
   find out what is actually causing a "feels grey" reaction. Overrides only —
   the page markup knows nothing about these.
   ========================================================================== */

/* Floating preview toolbar. Must read as A TOOL, not as page content — the first
   version was inline 10px grey text with dashed borders and got mistaken for a title. */
.ob-devbar {
  position:fixed; left:16px; bottom:16px; z-index:90;
  width:296px; max-width:calc(100vw - 32px);   /* narrow — it wraps rather than stretching wide */
  background:rgba(18,18,22,.94); border:1px solid var(--border-strong); border-radius:12px;
  box-shadow:0 18px 50px rgba(0,0,0,.55); backdrop-filter:blur(12px); overflow:hidden;
}
.ob-devbar.is-collapsed { width:auto; }
.ob-devbar-toggle {
  display:flex; align-items:center; gap:8px; width:100%; padding:9px 14px;
  font-size:11px; font-weight:600; color:var(--text-2); background:rgba(255,255,255,.03);
  border:none; border-bottom:1px solid var(--border-subtle); text-align:left;
}
.ob-devbar-toggle:hover { color:var(--text-1); }
.ob-devbar-dot { width:7px; height:7px; border-radius:50%; background:var(--amber); flex-shrink:0; }
.ob-devbar.is-collapsed .ob-devbar-toggle { border-bottom:none; }
.ob-devbar.is-collapsed .ob-devbar-body { display:none; }
.ob-devbar-body { padding:13px 15px 15px; display:flex; flex-direction:column; gap:11px; }
.ob-devrow { display:flex; align-items:center; gap:12px; }
.ob-devlabel {
  font-size:10px; font-family:'JetBrains Mono',ui-monospace,monospace; letter-spacing:.1em;
  text-transform:uppercase; color:var(--text-3); width:78px; flex-shrink:0;
}

/* EVERY item here is a real button: its own surface, border and depth. The previous version left
   unselected items transparent and borderless, so they read as plain text. */
/* Segmented control. Born in onboarding as .ob-seg; .ds-seg is the same thing under a neutral name so
   any screen can use it (the billing page's monthly/yearly switch is the first). Promoted rather than
   copied, per DESIGN_SYSTEM 15.6: a second copy of these rules is a second thing to keep in step.
   Works for <button> children and for <a> children (a server-rendered switch that needs no JS). */
.ob-seg, .ds-seg { display:flex; flex-wrap:wrap; gap:6px; }
.ob-seg > *, .ds-seg > * {
  display:inline-flex; align-items:center; justify-content:center; white-space:nowrap; cursor:pointer;
  font-size:12px; font-weight:600; line-height:1; padding:8px 13px; border-radius:8px;
  color:var(--text-1); background:var(--bg-elevated); border:1px solid var(--border);
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset, 0 1px 2px rgba(0,0,0,.35);
  transition:background .14s, border-color .14s, transform .06s, box-shadow .14s;
}
.ob-seg > *:hover, .ds-seg > *:hover {
  background:var(--bg-hover); border-color:var(--border-strong);
  box-shadow:0 1px 0 rgba(255,255,255,.07) inset, 0 3px 8px rgba(0,0,0,.4);
}
.ob-seg > *:active, .ds-seg > *:active { transform:translateY(1px); box-shadow:0 1px 2px rgba(0,0,0,.4); }
.ob-seg > *:focus-visible, .ds-seg > *:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.ds-seg > * { text-decoration:none; }        /* the links in a server-rendered switch */
.ob-seg > .is-on, .ds-seg > .is-on {
  background:var(--grad); color:#fff; border-color:transparent;
  box-shadow:0 2px 12px rgba(139,61,255,.45); text-shadow:0 1px 2px rgba(0,0,0,.25);
}
.ob-seg > .is-on:hover, .ds-seg > .is-on:hover { background:var(--grad); border-color:transparent; }

@media (max-width:640px) {
  .ob-devbar { left:8px; right:8px; bottom:8px; max-width:none; }
  .ob-devbar-body { max-height:52vh; overflow-y:auto; }
  .ob-devrow { align-items:flex-start; flex-direction:column; gap:6px; }
  .ob-devlabel { width:auto; }
}

/* --- chrome off: a first-run flow is a linear task, so drop the app furniture ---
   THE DEFAULT for every signed-in /onboarding since 2026-09-16 (owner); the class is put on <body> by
   the server (onboarding.html body_class), and only the operator's ?chrome=on turns it off.
   What replaces the furniture is onboarding.html's brand row: the mark top left and .ob-exit top
   right, both to the dashboard. With the frame gone the page IS the /start frame, so the gutters and
   the footer below copy GET /start's exactly: the two onboarding pages look the same on a phone. */
body.ob-nochrome #app-sidebar,
body.ob-nochrome #sidebar-scrim,
body.ob-nochrome #app-main > header { display:none !important; }
body.ob-nochrome #app-main { margin-left:0 !important; }
/* Top padding is main's own py-6 (24px), the same as /start: the brand row is the first line now.
   The 44px that used to be here was for a hidden state whose first line was a tiny language link.
   Side gutters: /start uses px-4 below 640px, the app's main uses px-6 everywhere. */
@media (max-width:639px) {
  body.ob-nochrome #app-main > main { padding-left:16px; padding-right:16px; }
}
/* The footer sits inside #app-main, which is a flex COLUMN at full width once the sidebar margin is
   gone, so on its own it hugs the left edge of the window while the card is centred (the same defect
   /start had on 2026-09-15). width:100% because auto side margins on a flex item would otherwise
   shrink it to its content. The max-width and gutters are shared with .ob-guest-footer below. */
body.ob-nochrome .app-legal-footer { width:100%; margin-left:auto; margin-right:auto; }

/* THE WAY BACK when the frame is hidden: the small x at the right of onboarding.html's brand row.
   Ported from the approved mockup (designs/mockups/onboarding-motivation-question.html, .s20-x): no
   border and no fill at rest, so it never competes with the question; hover lifts it like the shell's
   other icon controls.
   DRAWN AT 30px, TOUCHED AT 44px. The ::before reaches 7px past every edge, which makes the tap target
   the 44px minimum while the visible box stays the approved size. Those 7px land inside gaps of 16px
   or more (the language link to its left, the card below, the page gutter to its right), so the
   larger target never covers another control and never widens the page. */
.ob-exit {
  position:relative; flex:0 0 30px; width:30px; height:30px;
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:8px; background:transparent; color:var(--text-3);
  transition:background .14s, color .14s;
}
.ob-exit::before { content:''; position:absolute; inset:-7px; }
.ob-exit:hover { background:var(--bg-elevated); color:var(--text-1); }
.ob-exit:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* The operator's preview tools in the SHOWN state (?chrome=on), desktop only. The devbar is rendered
   inside #app-main, and the black look makes #app-main its own stacking layer (`z-index:1`, the BLACK
   block below), so no z-index on the devbar can lift it over the sidebar (z-40, a sibling of
   #app-main): the tools were drawn UNDER the menu, bottom left (seen by the owner on live,
   2026-09-16). Start them to the right of the 228px sidebar instead. The hidden default has no
   sidebar to collide with, and below 1024px the sidebar is a closed drawer. */
@media (min-width:1024px) {
  body.ob-page:not(.ob-nochrome) .ob-devbar { left:244px; }
}

/* THE PAYMENT STEP IS WIDER. #step-credits sits in its own 1100px wrapper (onboarding.html), while the
   top row and the footer follow the 720px column, so on that one step the mark and the x floated
   ~190px inside the card's edges. While it is showing, widen both to the same 1100px so they frame
   it. Changes on the same click that swaps the card and scrolls to the top, so it reads as the frame
   following the card. Keep 1100 in step with the wrapper's max-w-[1100px].
   Below 1024px neither max-width binds (the card already fills the column), so no other widths.
   :has() only: a browser without it keeps the 720px row and footer, the pre-2026-09-16 layout. The
   guest page never renders #step-credits, so none of this can match there. */
body:has(#step-credits:not(.hidden)) .ob-toprow { max-width:1100px; }
@media (min-width:1024px) {
  /* 1100 + the two 32px gutters .ob-guest-footer's rule gives the footer at this width */
  body.ob-nochrome:has(#step-credits:not(.hidden)) .app-legal-footer { max-width:1164px; }
}

/* --- FLAT: same palette, no nested containers, more air, more contrast ---
   Tests the theory that "grey" is really "boxes inside boxes". Colour unchanged. */
body.ob-look-flat .ob-page-card,
body.ob-look-flat #step-describe,
body.ob-look-flat #step-understanding,
body.ob-look-flat #step-plan,
body.ob-look-flat #step-credits,
body.ob-look-flat #step-runconfirm,
body.ob-look-flat #step-reading,
body.ob-look-flat #step-building {
  background:transparent; border:none; box-shadow:none; padding-left:0; padding-right:0;
}
body.ob-look-flat .bg-surface { background:transparent; }
body.ob-look-flat .ob-check,
body.ob-look-flat .ob-price { font-size:12.5px; color:var(--text-2); }
body.ob-look-flat h1 { font-size:26px; letter-spacing:-.03em; }
body.ob-look-flat .ob-row { background:transparent; border-color:var(--border); }
/* one hairline instead of a box around each filter block */
body.ob-look-flat #step-plan .bg-surface {
  border:none; border-top:1px solid var(--border-subtle); border-radius:0; padding:16px 0 0;
}

/* ==========================================================================
   BLACK — the DEFAULT look for onboarding. §2 of the design system defines this
   first-impression mode for login/signup/verify/invite; onboarding postdates
   that list but belongs to it (low density, pre-value, the screen straight after
   signup). Charcoal is deliberately calm for hours of dense data, which is right
   for the creators table and wrong for a first impression.
   ONE coherent light source from the top-left, and every card's bright edge
   faces it — lighting and objects telling the same story is what reads as
   composed rather than as effects piled on.
   ========================================================================== */
body.ob-look-black { background:#000; }
body.ob-look-black #app-main,
body.ob-look-black #app-main > main { background:transparent; }
body.ob-look-black::before {
  content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
  background:radial-gradient(900px circle at 10% -8%, rgba(139,92,246,.20), transparent 58%);
}
body.ob-look-black #app-main { position:relative; z-index:1; }

/* panels: edge lighting via a gradient BORDER, brightest at the top-left corner */
body.ob-look-black #step-describe,
body.ob-look-black #step-understanding,
body.ob-look-black #step-plan,
body.ob-look-black #step-credits,
body.ob-look-black #step-runconfirm,
body.ob-look-black #step-reading,
body.ob-look-black #step-building {
  border:1px solid transparent;
  background:
    linear-gradient(rgba(12,12,16,.90), rgba(10,10,13,.90)) padding-box,
    linear-gradient(150deg, rgba(255,255,255,.20), rgba(255,255,255,.05) 38%, rgba(255,255,255,.02) 70%) border-box;
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset, 0 30px 90px rgba(0,0,0,.6);
  backdrop-filter:blur(14px);
}
body.ob-look-black .ob-row,
body.ob-look-black #step-plan .bg-surface,
body.ob-look-black #step-credits .bg-surface,
body.ob-look-black #step-runconfirm .bg-surface,
body.ob-look-black #step-understanding .bg-surface {
  border:1px solid transparent;
  background:
    linear-gradient(#141417,#141417) padding-box,
    linear-gradient(150deg, rgba(255,255,255,.18), rgba(255,255,255,.045) 38%, rgba(255,255,255,.02) 70%) border-box;
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset;
}
body.ob-look-black .ob-theirtext { background:rgba(255,255,255,.03); border-color:rgba(255,255,255,.07); }

/* the inference sits on the same card as its neighbours (variant B) */
body.ob-look-black .ob-infer {
  border:1px solid transparent;
  background:
    linear-gradient(#141417,#141417) padding-box,
    linear-gradient(150deg, rgba(255,255,255,.18), rgba(255,255,255,.045) 38%, rgba(255,255,255,.02) 70%) border-box;
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset;
}

/* --- inputs that respond. The big box is the most-touched thing in the flow
   and did nothing on hover; waking up what the user is already looking at does
   more than any background change. --- */
body.ob-page textarea,
body.ob-page input[type="text"] { transition:background .16s, border-color .16s, box-shadow .16s; }
body.ob-page textarea:hover,
body.ob-page input[type="text"]:hover { background:#26262C; border-color:rgba(255,255,255,.18); }
body.ob-page textarea:focus,
body.ob-page input[type="text"]:focus {
  background:#24212E; border-color:var(--accent); outline:none;
  box-shadow:0 0 0 3px rgb(var(--c-accent) / .18), 0 0 22px rgb(var(--c-accent) / .14);
}

/* --- dividers instead of boxes inside boxes: the inner blocks sat within a few
   percent of the card holding them, so a card read as one grey mass. --- */
body.ob-page .ob-check { padding-left:0; }
body.ob-page #step-plan .block > .ob-check:first-of-type { border-top:1px solid var(--border-subtle); padding-top:9px; margin-top:2px; }

/* --- WARM: neutrals biased toward the accent hue instead of pure grey, scoped
   to this surface only. A pure neutral reads as unstyled; a hue-biased one reads
   as chosen. Same structure, same brightness. --- */
body.ob-look-warm #app-main > main { background:#0C0B10; }
body.ob-look-warm #step-describe,
body.ob-look-warm #step-understanding,
body.ob-look-warm #step-plan,
body.ob-look-warm #step-credits,
body.ob-look-warm #step-runconfirm,
body.ob-look-warm #step-reading,
body.ob-look-warm #step-building { background:#16141C; border-color:rgba(180,160,255,.10); }
body.ob-look-warm .bg-surface { background:#1B1824; border-color:rgba(180,160,255,.09); }
body.ob-look-warm .ob-row { background:#1B1824; border-color:rgba(180,160,255,.09); }
body.ob-look-warm .ob-theirtext { background:#1B1824; }
body.ob-look-warm textarea, body.ob-look-warm input[type="text"] { background:#211D2C !important; }

/* (the old inline .ob-jump strip was folded into the floating preview toolbar above) */

/* On a phone the 5-column group row can't hold its shape (the role label + count + name all
   compete), so stack the label above the name and let the toggle keep its own column. */
@media (max-width:640px) {
  .ob-rhead {
    grid-template-columns:40px 1fr 22px;
    grid-template-areas:"tgl role caret" "tgl name caret" "tgl num caret";
    row-gap:2px;
  }
  .ob-rhead .ds-toggle { grid-area:tgl; align-self:center; }
  .ob-rhead .ob-rrole { grid-area:role; }
  .ob-rhead .ob-rname { grid-area:name; }
  .ob-rhead .ob-rnum { grid-area:num; text-align:left; }
  .ob-rhead .ob-caret { grid-area:caret; align-self:center; }
  .ob-price { flex-direction:column; align-items:flex-start; gap:1px; }
  .ob-hero-hrs { font-size:34px; }
}

@media (prefers-reduced-motion:reduce) {
  .ob-dots i, .ob-theirtext mark.ob-hl { animation:none; }
}

/* ── Choice dial (DESIGN_SYSTEM §8.10) ────────────────────────────────────────
   The confirm dialog's "cost is a CHOICE, not a scare" control: stacked option
   cards, the selected one carrying a gradient ring. Used by showConfirm({options}).
   Not a radio input by design — the whole card is the hit target. */
.ds-pick { display:flex; flex-direction:column; gap:9px; margin:14px 0 18px; }
.ds-pick-opt {
  display:block; width:100%; text-align:left; cursor:pointer;
  background:var(--bg-surface); border:1px solid var(--border); border-radius:var(--r-md);
  padding:13px 15px; transition:background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.ds-pick-opt:hover { background:var(--bg-hover); }
.ds-pick-opt .ds-pick-top { display:flex; align-items:center; gap:8px; }
.ds-pick-opt .ds-pick-dot {
  width:15px; height:15px; border-radius:50%; flex:0 0 15px;
  border:1.5px solid var(--text-3); position:relative; transition:border-color .15s ease;
}
.ds-pick-opt .ds-pick-label { font-size:13.5px; font-weight:600; color:var(--text-1); }
.ds-pick-opt .ds-pick-tag {
  font-size:10.5px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  padding:2px 7px; border-radius:var(--r-pill); background:rgba(139,61,255,.16); color:#C7A5FF;
}
/* Savings variant — green, because it's money off, not a label. Matches .chip-green's hue. */
.ds-pick-opt .ds-pick-tag.is-save {
  background:rgba(34,197,94,.16); color:#7EE6A8; font-size:11.5px; letter-spacing:.02em; padding:2px 8px;
}
.ds-pick-opt .ds-pick-sub { font-size:12.5px; color:var(--text-2); line-height:1.5; margin:6px 0 0 23px; }
.ds-pick-opt[aria-checked="true"] {
  border-color:transparent; background:var(--bg-elevated);
  box-shadow:0 0 0 1.5px var(--accent), 0 0 18px rgba(139,61,255,.22);
}
.ds-pick-opt[aria-checked="true"] .ds-pick-dot { border-color:var(--accent); }
.ds-pick-opt[aria-checked="true"] .ds-pick-dot::after {
  content:""; position:absolute; inset:2.5px; border-radius:50%; background:var(--grad);
}
.ds-pick-opt:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* ---- CARD layout for a picker (showConfirm {cards:true}) -------------------------------------
   For a decision where the NUMBERS are the decision - how many creators, at what cost - rather than
   a preference between two descriptions. The rows above put the label first and the number in a
   pill; these put the number first, big and mono, because that is what is being compared.
   See DESIGN_SYSTEM 15.5a. Do not hand-roll a second version of this in a page: extend it here.

   Every card is EXPLICIT about being a choice: an unchosen card is dimmed and carries a "Choose
   this" button, the chosen one is ringed and ribboned. That is deliberate over the subtler dot-only
   treatment, because the first card version of this dialog read as two stat tiles and nobody could
   tell it was asking a question. */
.ds-pick.is-cards { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:18px; }
@media (max-width:520px) { .ds-pick.is-cards { grid-template-columns:1fr; } }
.ds-pick-card {
  position:relative; text-align:left; cursor:pointer; width:100%;
  background:var(--bg-surface); border:1px solid var(--border); border-radius:var(--r-md);
  padding:14px; opacity:.62;
  transition:background .15s ease, border-color .15s ease, box-shadow .15s ease, opacity .15s ease;
}
.ds-pick-card:hover { background:var(--bg-hover); opacity:1; }
.ds-pick-card:focus-visible { outline:2px solid var(--accent); outline-offset:2px; opacity:1; }
.ds-pick-card[aria-checked="true"] {
  opacity:1; border-color:transparent; background:var(--bg-elevated);
  box-shadow:0 0 0 1.5px var(--accent), 0 0 18px rgba(139,61,255,.22);
}
.ds-pick-card .ds-card-num {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-variant-numeric:tabular-nums;
  font-size:25px; font-weight:700; letter-spacing:-.02em; line-height:1.1; color:var(--text-1);
}
.ds-pick-card .ds-card-of { font-size:11px; color:var(--text-3); margin-top:2px; }
.ds-pick-card .ds-card-label { font-size:13.5px; font-weight:700; color:var(--text-1); margin-top:8px; }
.ds-pick-card .ds-card-exp { font-size:11.5px; color:var(--text-2); line-height:1.45; margin-top:9px; }
.ds-pick-card .ds-pick-tag { display:inline-block; margin-top:6px; }
/* The ribbon says which one is live in one word, from across the dialog. */
.ds-pick-card .ds-card-ribbon {
  display:none;
  position:absolute; top:-9px; left:50%; transform:translateX(-50%); white-space:nowrap;
  font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  padding:3px 10px; border-radius:var(--r-pill); background:var(--accent); color:#fff;
}
.ds-pick-card[aria-checked="true"] .ds-card-ribbon { display:block; }
/* The grid needs headroom for the ribbon, which sits outside the card's own box. */
.ds-pick.is-cards { padding-top:9px; }
/* A disabled primary. Used when nothing is preselected: there is nothing to save yet, and a button
   that looks pressable but does nothing is worse than one that says it is waiting for you. */
.btn:disabled, .btn[disabled] { opacity:.45; cursor:not-allowed; box-shadow:none; filter:none; }
/* Icons across the top of a card, saying what that answer is MADE OF before anything is picked.
   Without them the dialog opens as two grey boxes and the platform badges only appear after you
   have already committed, which is the wrong way round. */
.ds-pick-card .ds-card-icons { display:flex; align-items:center; gap:5px; margin-bottom:11px; min-height:22px; }
/* Scales whatever badge markup is dropped in here, INCLUDING window.platformBadge's own 18px
   version, by out-specifying its utility classes. Deliberately CSS and not a string replace on that
   helper's HTML: a replace breaks silently the day the helper's classes change, and the failure
   would be a wrong-sized icon nobody notices in review. */
.ds-pick-card .ds-card-icons > span { width:22px; height:22px; border-radius:7px; flex:0 0 22px; }
.ds-pick-card .ds-card-icons > span > svg { width:13px; height:13px; }
/* There is NO "choose this" button on the card. The ribbon, the accent ring and the dimming of the
   other card already say which one is picked; a fourth signal that was also unclickable (the card
   is the click target) read as clutter. */
/* A note that belongs to ONE option (the platform strip, a warning): shown only while that option
   is the chosen one, because a caveat under the buttons is boilerplate nobody reads. */
.ds-pick-optnote { margin-top:14px; }
.ds-pick-optnote:empty { display:none; }
/* .ds-pick-note carries margin-top:-8px so it tucks under the option list. With an option note in
   between, that negative margin pulled the small print up INTO the warning box. Reset it when the
   two are siblings rather than changing the base rule, which every other dialog still relies on. */
.ds-pick-optnote + .ds-pick-note { margin-top:14px; }
/* Label on its OWN line, chips on the next one. Inline, the first chip sat beside the label and the
   rest wrapped underneath, so one platform looked different from the others for no reason. */
.ds-plat-strip {
  display:flex; flex-direction:column; align-items:flex-start; gap:8px;
  padding:11px 13px; border:1px solid var(--border); border-radius:var(--r-md);
  background:var(--bg-surface);
}
.ds-plat-strip .ds-plat-k {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.13em;
  text-transform:uppercase; color:var(--text-3);
}
.ds-plat-strip .ds-plat-chips { display:flex; align-items:center; gap:14px; flex-wrap:wrap; }

/* The split bar: the proportion, in colour, before a word is read.
   The DM half uses the BRAND GRADIENT rather than amber on purpose - amber means "something is
   wrong", and there is nothing wrong with a creator having no email address, it is just how they
   are reached. Amber stays reserved for the pacing warning, which is a real caution. */
/* The 3px gap is not decoration. The brand gradient ENDS in blue and the email segment IS blue, so
   without it the two halves run together and the split - the only thing this bar exists to show -
   becomes invisible. Caught on the rendered page; the DOM said both segments were correct. */
.ds-split-bar { display:flex; gap:3px; height:9px; border-radius:var(--r-pill); overflow:hidden;
  background:var(--bg-hover); margin-bottom:8px; }
.ds-split-bar i:first-child { border-radius:var(--r-pill) 0 0 var(--r-pill); }
.ds-split-bar i:last-child { border-radius:0 var(--r-pill) var(--r-pill) 0; }
.ds-split-bar i { display:block; height:100%; }
/* THE GRADIENT STOPS AT PURPLE. The full brand gradient ends in blue, so against any blue second
   segment the bar read as one continuous sweep and the split - the only thing it exists to show -
   was invisible. Compared four treatments on the rendered page: full-gradient-vs-blue had no
   discernible boundary, purple-vs-blue was marginal, purple-vs-cyan is unmistakable. */
.ds-split-bar .is-dm { background:linear-gradient(95deg,#F72FE0 0%,#A855F7 100%); }
.ds-split-bar .is-email { background:#22D3EE; }
.ds-split-key { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--text-2); margin-bottom:2px; }
.ds-split-key span { display:inline-flex; align-items:center; gap:6px; }
.ds-split-key i { width:8px; height:8px; border-radius:3px; display:inline-block; }
.ds-split-key i.is-dm { background:linear-gradient(95deg,#F72FE0 0%,#A855F7 100%); }
.ds-split-key i.is-email { background:#22D3EE; }
.ds-split-key b { font-family:'JetBrains Mono',ui-monospace,monospace; font-variant-numeric:tabular-nums;
  color:var(--text-1); font-weight:600; }
.ds-plat-chip { display:inline-flex; align-items:center; gap:7px; font-size:12.5px; color:var(--text-2); }
.ds-plat-chip b { font-family:'JetBrains Mono',ui-monospace,monospace; color:var(--text-1); font-weight:600; }
.ds-warnline {
  display:flex; gap:9px; align-items:flex-start; margin-top:11px; padding:10px 12px;
  border-radius:9px; background:rgba(251,191,36,.08); border:1px solid rgba(251,191,36,.22);
  font-size:12.5px; color:#F5D98B; line-height:1.5;
}
.ds-warnline svg { flex:0 0 15px; margin-top:1px; }
.ds-quietline { display:flex; gap:9px; align-items:flex-start; margin-top:8px;
  font-size:12px; color:var(--text-3); line-height:1.45; }
/* Plain-language note under the dial — explains a term used in the options without
   putting a paragraph above them. Quiet by design: read only if you want it. */
.ds-pick-note {
  font-size:11.5px; color:var(--text-3); line-height:1.55;
  margin:-8px 0 16px; padding-left:2px;
}
/* Compact "already decided" bar: replaces the dial once a choice is remembered. Still a confirm
   (the click spends credits) but no longer a question. The Change link reopens the dial. */
.ds-mode {
  display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:14px 0 18px;
  padding:11px 13px; background:var(--bg-surface); border:1px solid var(--border);
  border-radius:var(--r-md);
}
.ds-mode .ds-mode-label { font-size:13px; color:var(--text-1); font-weight:500; }
.ds-mode .ds-mode-change {
  margin-left:auto; font-size:12px; color:var(--accent); background:none; border:none;
  cursor:pointer; padding:0; font-family:inherit;
}
.ds-mode .ds-mode-change:hover { text-decoration:underline; }

/* "Remember this choice" toggle, bottom-left of a dialog footer, opposite the buttons. */
.ds-remember { display:flex; align-items:center; gap:9px; cursor:pointer; user-select:none; }
.ds-remember span { font-size:12.5px; color:var(--text-2); }

/* ---- PURCHASE CONFIRM: the receipt body of the checkout dialog -------------------------------
   Owner pick 2026-08-25, Option A of designs/mockups/checkout-confirm-redesign.html. Rendered
   through showConfirm's `preface`, so it is markup the caller supplies - these classes are where
   its look lives, the same arrangement as .ds-split-bar above. Do not hand-roll a second version
   in a page.

   The problem it fixes: price, credit maths, rollover, cancellation and inbox were ONE dense
   paragraph with no hierarchy, so nothing was skimmable and the outcome the buyer is actually
   paying for was buried mid-sentence. Rows make each fact findable in a glance. */
.ds-buy-head { display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:14px; }
.ds-buy-head .nm { font-size:17px; font-weight:800; color:var(--text-1); letter-spacing:-.01em; }
.ds-buy-head .pr {
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:20px; font-weight:700;
  color:var(--text-1); white-space:nowrap; font-variant-numeric:tabular-nums;
}
.ds-buy-head .pr span { font-size:11px; color:var(--text-3); font-weight:400; }
/* The hours anchor REUSES .ob-hero (onboarding's pack hero) rather than cloning it: it is the same
   claim, in the same words, and a second copy is how the two screens end up disagreeing. Only the
   scale differs, because a modal has less room than a full screen. */
.ob-hero.is-compact { padding:16px 18px; }
.ob-hero.is-compact .ob-hero-hrs { font-size:36px; }
.ds-buy-hours { margin-bottom:16px; }
.ds-buy-rows { margin-bottom:2px; }
.ds-buy-row {
  display:flex; justify-content:space-between; align-items:baseline; gap:14px;
  padding:8px 0; border-bottom:1px solid var(--border-subtle); font-size:12.5px;
}
.ds-buy-rows .ds-buy-row:last-child { border-bottom:none; }
.ds-buy-row .k { color:var(--text-3); }
.ds-buy-row .v { color:var(--text-1); font-weight:600; text-align:right; font-variant-numeric:tabular-nums; }
/* The quiet half of a value ("(about 55 creators)"): still part of the same fact, so it stays on
   the row rather than becoming a line of its own. <i> only for brevity in the markup, not italics. */
.ds-buy-row .v i { color:var(--text-3); font-weight:400; font-style:normal; }
/* Green means exactly one thing across the product: money you keep. Same value as .chip-green. See billing.html for WHICH purchases may show this - it is not shown on every one. */
.ds-buy-badge {
  display:flex; align-items:center; gap:10px; margin-top:14px;
  background:rgba(34,197,94,.08); border:1px solid rgba(34,197,94,.25);
  border-radius:var(--r-md); padding:10px 12px;
}
.ds-buy-badge .ic {
  width:26px; height:26px; border-radius:50%; background:rgba(34,197,94,.18);
  display:flex; align-items:center; justify-content:center; flex:none;
}
.ds-buy-badge .ic svg { stroke:#7EE6A8; display:block; }
.ds-buy-badge .tx { font-size:12px; color:var(--text-2); line-height:1.4; }
.ds-buy-badge .tx b { color:#7EE6A8; font-weight:700; }
.ds-buy-note { font-size:11.5px; color:var(--text-3); line-height:1.5; margin-top:14px; }
/* A consent tick box may carry ONE link: the policy it defers to. Underlined, not just tinted,
   because it sits inside 12.5px body text where colour alone is a weak affordance. */
.cf-check-lbl a { color:var(--accent); text-decoration:underline; }
.cf-check-lbl a:hover { color:var(--text-1); }

@media (prefers-reduced-motion:reduce) { .ds-pick-opt { transition:none; } }

/* ---------------------------------------------------------------------------
   LEGAL DOCUMENTS (/terms, /privacy, /refunds)
   Long-form reading on the public pure-black surface (DESIGN_SYSTEM §2). The only
   place in the app with real prose, so it gets a reading measure and generous
   leading; everything else (colors, radii, chips) reuses the tokens.
   --------------------------------------------------------------------------- */
.legal { max-width:70ch; font-size:15px; line-height:1.72; color:var(--text-2); }
.legal h1 { font-size:30px; font-weight:800; letter-spacing:-.02em; color:var(--text-1); margin:0 0 6px; }
.legal h2 {
  font-size:17px; font-weight:700; letter-spacing:-.005em; color:var(--text-1);
  margin:38px 0 8px; scroll-margin-top:72px;
}
.legal p { margin:0 0 14px; }
/* Tailwind's preflight sets `list-style:none` on every ul/ol, so these MUST restore it: in a legal
   document the list structure carries meaning, and without markers the items read as stray
   paragraphs. Caught only by looking at the rendered page - the standalone draft had its own CSS. */
.legal ul { margin:0 0 14px; padding-left:22px; list-style:disc outside; }
.legal ol { margin:0 0 14px; padding-left:22px; list-style:decimal outside; }
.legal li { margin:6px 0; }
.legal li::marker { color:var(--text-3); }
.legal strong { color:var(--text-1); font-weight:600; }
.legal a { color:var(--blue); text-decoration:none; border-bottom:1px solid rgba(75,141,255,.35); }
.legal a:hover { color:var(--text-1); border-bottom-color:var(--border-strong); }
.legal-meta { font-size:12.5px; color:var(--text-3); margin:0 0 26px; }

/* Wide tables must scroll inside their own box, never widen the page. */
.legal-table-wrap { overflow-x:auto; margin:0 0 16px; }
.legal table { width:100%; border-collapse:collapse; font-size:13.5px; min-width:520px; }
.legal th, .legal td {
  text-align:left; padding:9px 12px; border-bottom:1px solid var(--border-subtle); vertical-align:top;
}
.legal th {
  color:var(--text-1); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.06em;
}

/* Pulled-out notice at the top of the privacy policy: the creator opt-out route is the one thing a
   non-customer arrives looking for, so it must not be buried in section 5. */
.legal-callout {
  background:var(--bg-surface); border:1px solid var(--border); border-left:3px solid var(--accent);
  border-radius:var(--r-md); padding:15px 18px; margin:0 0 26px;
}
.legal-callout b { display:block; color:var(--text-1); font-size:14px; margin-bottom:4px; }
.legal-callout p { margin:0; font-size:14px; }

/* Worked refund example: a receipt, not a table. */
.legal-example {
  background:var(--bg-surface); border:1px solid var(--border); border-radius:var(--r-md);
  padding:16px 20px; margin:0 0 16px;
}
.legal-example .row {
  display:flex; justify-content:space-between; gap:16px; padding:5px 0; font-size:14px;
}
.legal-example .row span:last-child { font-variant-numeric:tabular-nums; white-space:nowrap; }
.legal-example .row.total {
  border-top:1px solid var(--border); margin-top:8px; padding-top:11px;
  color:var(--text-1); font-weight:600;
}

/* App-shell footer (base.html) — quiet, never competes with the working surface. */
.app-legal-footer {
  margin-top:auto; padding:18px 24px 22px; font-size:11.5px; color:var(--text-3);
  display:flex; gap:14px; flex-wrap:wrap; align-items:center;
}
.app-legal-footer a { color:var(--text-3); }
.app-legal-footer a:hover { color:var(--text-2); }

/* The guest onboarding page only (GET /start, base.html's logged-out branch adds `ob-guest-footer`).
   That page is `<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">` holding a 720px centred card,
   and this footer is main's SIBLING - so it inherited main's full 1280px column and its links landed
   roughly 250px left of the card, looking stuck to the edge of the window.

   THE MATHS, so a future edit does not break it by touching one half. Everything is border-box, so
   max-width includes the padding: set max-width to 720 + both gutters and the padding to one gutter,
   and the content box is exactly 720px centred in the same viewport main centres its own column in.
   The three gutters are Tailwind's px-4 / sm:px-6 / lg:px-8 (16 / 24 / 32) at the same breakpoints,
   so the footer and the card move together. Two classes beats Tailwind's single `.max-w-7xl`, which
   is why this overrides it without !important.
   ALSO SIGNED-IN /onboarding with the frame hidden (body.ob-nochrome, 2026-09-16), whose main has the
   same 16px phone gutter (the chrome-off block) and 24px above that. At 1024px and wider the two
   gutters differ (32 here, 24 in main) and it does not matter: both boxes are centred in the same
   full-width column and the content box is 720px either way, so the links still start under the
   card's left edge. */
.app-legal-footer.ob-guest-footer,
body.ob-nochrome .app-legal-footer { max-width:752px; padding-left:16px; padding-right:16px; }
@media (min-width:640px)  { .app-legal-footer.ob-guest-footer, body.ob-nochrome .app-legal-footer { max-width:768px; padding-left:24px; padding-right:24px; } }
@media (min-width:1024px) { .app-legal-footer.ob-guest-footer, body.ob-nochrome .app-legal-footer { max-width:784px; padding-left:32px; padding-right:32px; } }

@media (max-width:640px) {
  .legal { font-size:14.5px; }
  .legal h1 { font-size:25px; }
}

/* Steps panel: modifiers that were inline styles in the template. Kept here so the panel's spacing
   and disabled state live with the rest of the design system rather than being retyped per use. */
.steps-need-row.is-separated { margin-top:12px; border-top:1px solid var(--border-subtle); padding-top:12px; }
.steps-note.is-spaced { margin-top:10px; }
/* The audio/CDN upkeep strip: not a step of its own, so it sits under the whole panel rather than
   inside any one pane. */
.steps-repair { border-top:1px solid var(--border-subtle); padding:10px 20px 14px; }
.steps-repair-row { display:flex; flex-wrap:wrap; gap:8px; }
.steps-repair p { color:var(--text-3); font-size:11.5px; margin-top:8px; max-width:70ch; }
/* A member cannot write project settings, so the "Ask an admin" text is a STATEMENT, not an action.
   It reuses .steps-quiet-link for type + colour but must not look or behave clickable. */
.steps-quiet-link.is-static { text-decoration:none; cursor:default; }
/* Disabled run button. Was `opacity-40 cursor-not-allowed` utilities alongside .btn-run in eight
   places; the disabled look belongs to the component, not to whoever writes the markup. */
.btn-run:disabled, .btn-run[disabled] { opacity:.4; cursor:not-allowed; }
/* A stage the project's preset does not include, but which can still be run by hand. */
.btn-run.is-optional { opacity:.7; }

/* ==========================================================================
   KEYWORD HELPER - the "Get ideas" panel inside the Add search dialog
   (campaign_detail.html). Design: KEYWORD_HELPER_DESIGN_2026-08-16.md, mockup
   designs/mockups/keyword-helper-in-dialog.html option 1.

   NO GRADIENT in this block. The dialog already spends its one gradient moment
   on the primary "Add" button, so the mode toggle and the ideas panel use
   surface lightness and the solid accent instead (DESIGN_SYSTEM §1, §15.7).
   ========================================================================== */

/* A settings line that collapses to what it is set to, with the real controls
   one click away. Same disclosure idiom as .steps-details / .ob-details (a
   <details> whose summary loses its native marker and gains a triangle), but
   unprefixed because this one is a shared control rather than one screen's
   furniture. Everything stays LEFT aligned: no margin-left:auto pushing the
   action to the far edge, which is the right-aligned-text-link treatment the
   owner turned down on the suggestions list. */
/* One line at the dialog's normal width, wrapping only when it genuinely must.
   Measured, not guessed: at 12.5px with 8px gaps the row needs 489px against
   the 398px a max-w-md dialog offers, so it always broke into three rows. 12px
   type, 6px gaps and a compact chip (below) bring it to ~447px, and the dialog
   itself went max-w-md -> max-w-lg for the rest. flex-wrap stays: a genuinely
   long combination, e.g. "date posted, last 6 months, 100 pages", should wrap
   rather than overflow. */
.ds-disclose > summary { display:flex; flex-wrap:wrap; align-items:center; gap:5px; cursor:pointer;
  list-style:none; font-size:12px; color:var(--text-2); }
/* The chip is the widest single item on the row; the design-system recipe is
   unchanged, just one step tighter inside this dense control. */
.ds-disclose .chip { font-size:10.5px; padding:2px 8px; }
.ds-disclose > summary::-webkit-details-marker { display:none; }
.ds-disclose > summary::before { content:'\25B8'; color:var(--text-3); font-size:9px; }
.ds-disclose[open] > summary::before { content:'\25BE'; }
.ds-disclose > summary:hover { color:var(--text-1); }
.ds-disclose > summary:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:var(--r-sm); }
/* A step smaller than the values it introduces: it is the label, they are the answer. */
.ds-disclose-lead { color:var(--text-3); font-size:11.5px; white-space:nowrap; }
/* The values are one phrase: if the row has to wrap, it wraps around them, never
   through the middle of "no date filter". */
.ds-disclose-val { color:var(--text-1); white-space:nowrap; }
/* Change / Hide / Back to recommended: quiet underlined text, never buttons. They
   sit next to the values they act on, in reading order. */
.ds-disclose-act { font-size:11.5px; color:var(--text-3); text-decoration:underline;
  background:none; border:none; padding:0; font-family:inherit; cursor:pointer; white-space:nowrap; }
.ds-disclose-act:hover { color:var(--text-2); }
.ds-disclose[open] .ds-disclose-more, .ds-disclose:not([open]) .ds-disclose-less { display:none; }
.ds-disclose-body { margin-top:11px; padding-top:11px; border-top:1px solid var(--border-subtle); }

/* The mode toggle: write my own, or get ideas. A track with two pills - the
   pre-learned segmented-control idiom, not tabs and not two buttons. */
.ds-seg { display:inline-flex; background:var(--bg-base); border:1px solid var(--border);
  border-radius:var(--r-pill); padding:2px; }
.ds-seg button { background:none; border:none; color:var(--text-3); font-family:inherit;
  font-size:12px; font-weight:600; padding:6px 14px; border-radius:var(--r-pill); cursor:pointer;
  transition:background .14s, color .14s; }
.ds-seg button:hover { color:var(--text-2); }
.ds-seg button.on { background:var(--bg-hover); color:var(--text-1); }
.ds-seg button.on:hover { color:var(--text-1); }
.ds-seg button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* The panel. Dashed while it is still waiting to be asked: an empty dashed box
   reads as "something goes here", a solid one reads as "something is missing". */
.kw-ideas { border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-base);
  padding:12px; }
.kw-ideas.is-empty { border-style:dashed; }

/* The suggestion list. Whole row is the hit target - a 14px checkbox is not. */
.kw-list { border:1px solid var(--border); border-radius:var(--r-md); overflow:hidden; margin-top:8px; }
.kw-row { display:flex; align-items:flex-start; gap:9px; padding:8px 10px;
  border-top:1px solid var(--border-subtle); cursor:pointer; }
.kw-row:first-child { border-top:none; }
.kw-row:hover { background:var(--bg-hover); }
/* Ticked is the DEFAULT state here (everything arrives ticked), so the tint is
   deliberately faint: it marks the norm, and the eye should be drawn to what
   the user has turned OFF, which is the row that loses it. */
.kw-row.is-picked { background:rgba(139,92,246,.08); }
.kw-row input[type="checkbox"] { margin-top:2px; flex:none; }
.kw-body { flex:1; min-width:0; }
.kw-word { display:block; font-size:12.5px; font-weight:600; color:var(--text-1); }
.kw-why { display:block; font-size:11px; color:var(--text-3); margin-top:1px; line-height:1.4; }

/* Select-all: the same idiom as the creators table, left aligned above the list.
   The `.ds-form .kw-all` half is load-bearing, not belt-and-braces: this label lives inside a
   .ds-form, and `.ds-form label { display:block }` is more specific than a bare `.kw-all`, so the
   row silently lost its flex layout (and therefore its gap) until the specificity matched. */
.kw-all, .ds-form .kw-all { display:flex; align-items:center; gap:8px; margin-top:11px; margin-bottom:0;
  cursor:pointer; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px;
  font-weight:500; letter-spacing:.14em; text-transform:uppercase; color:var(--text-3);
  user-select:none; }
.kw-all:hover, .ds-form .kw-all:hover { color:var(--text-2); }

.kw-note { font-size:11px; color:var(--text-3); margin-top:8px; line-height:1.45; }

/* ==== MEMBER ACCESS PICKER (the 8 capability checkboxes in the invite / change-access dialogs) ====
   Same specificity trap as .kw-all directly above: these rows live inside a .ds-form, and
   `.ds-form label { display:block }` beats a bare Tailwind `flex`, so the checkbox stacked ON TOP OF
   its own label instead of sitting beside it. The `.ds-form` half of each selector is load-bearing.
   Also kills the inherited `margin-bottom:5px`, which double-spaced an already tall list. */
.cap-row, .ds-form .cap-row { display:flex; align-items:flex-start; gap:9px; margin-bottom:0;
  padding:5px 0; cursor:pointer; user-select:none; }
.cap-row input[type="checkbox"] { margin-top:2px; flex:none; }
.cap-body { flex:1; min-width:0; }
.cap-name { display:block; font-size:12.5px; font-weight:600; color:var(--text-1); }
.cap-why { display:block; font-size:11px; color:var(--text-3); margin-top:1px; line-height:1.4; }
/* (.cap-dialog, a per-dialog `max-height:88vh`, lived here. The invite / change-access dialogs are
   the tallest in the app once all 8 rows are expanded; they now use the shared .ds-modal standard
   below, like every other dialog, instead of a height patch of their own.) */
/* The plain-words answer to "what can this person actually do", under each member in the team list.
   Deliberately WRAPS rather than truncating: the whole point of the line is that it can be read at
   a glance, and an ellipsis would hide the one part that differs between two people. */
.member-access { font-size:11px; color:var(--text-3); line-height:1.45; margin-top:3px; }
.kw-round { display:block; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px;
  color:var(--text-3); margin-top:10px; }
/* The cost line. Amber, because it is the one thing in this dialog that is
   about money - but it states what ticking does NOT do, so it reassures rather
   than warns (KEYWORD_HELPER_DESIGN §4). */
.kw-cost { margin-top:11px; padding:8px 11px; border-radius:var(--r-md);
  background:rgba(251,191,36,.06); border-left:2px solid var(--amber);
  font-size:11.5px; color:#FBD375; line-height:1.45; }
.kw-cost .rest { color:var(--text-2); }

/* Get Ideas redesign (round 2, keyword-ideas-magic-redesign-v3.html): two cards replacing the old
   .ds-seg two-tab toggle - "write your own" and "ask AI" are not equal-weight peers (one is a plain
   textbox, the other is a whole request-and-review flow), and squeezing that into "just a tab" is
   why the AI path went unnoticed. Both cards visible at once instead of one hiding the other. */
.kw-pick-row { display:flex; gap:10px; }
.kw-pick-card { flex:1; text-align:left; border:1px solid var(--border); border-radius:var(--r-md);
  padding:12px 14px; background:var(--bg-elevated); cursor:pointer; font-family:inherit;
  transition:border-color .15s; }
.kw-pick-card:hover { border-color:var(--border-strong); }
.kw-pick-card:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* The selected card gets the one gradient moment this row is allowed - whichever is ON, not
   permanently tied to "AI" - so it reads as a normal selection state, not a permanent AI badge. */
.kw-pick-card.on { border-color:transparent;
  background:linear-gradient(var(--bg-elevated),var(--bg-elevated)) padding-box,
             var(--grad) border-box; }
.kw-pick-icn { display:flex; align-items:center; justify-content:center; width:24px; height:24px;
  border-radius:7px; font-size:12px; margin-bottom:8px; background:var(--bg-hover); color:var(--text-2); }
.kw-pick-icn-ai { background:var(--grad); color:#fff; }
.kw-pick-card b { display:block; font-size:12.5px; color:var(--text-1); }
.kw-pick-card p { font-size:10.5px; color:var(--text-3); margin-top:2px; line-height:1.4; }

/* The "AI already knows..." context line. Split into a plain sentence + a fully-styled chip row on
   its own line, on purpose - an earlier draft put checkmarked+bold phrases INLINE inside a sentence
   and it read as half-designed, half-not, on the same line. Each chip only renders when the fact it
   states is actually true for this project (kw_ctx_* booleans computed server-side, pages.py
   campaign_detail route) - never claim a fact the AI is not actually using. */
.kw-ctx { margin-top:10px; }
.kw-ctx-label { font-size:11.5px; color:var(--text-3); margin-bottom:7px; }
.kw-ctx-chips { display:flex; flex-wrap:wrap; gap:6px; }
.kw-ctx-chip { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--text-2);
  background:var(--bg-hover); border:1px solid var(--border-subtle); border-radius:var(--r-pill);
  padding:4px 10px 4px 8px; }
.kw-ctx-chip .ck { color:var(--green); font-size:10px; }

@media (max-width:640px) {
  /* The ask row stacks so the input never squeezes the button off the edge. */
  .kw-ask { flex-direction:column; align-items:stretch; }
  .kw-ask .btn { justify-content:center; }
  .kw-pick-row { flex-direction:column; }
}

/* ---- Setup status ---------------------------------------------------------------------------
   One line, always visible above the six steps, replacing a readiness list that was buried inside
   step 6's pane where you only found it by opening that step.
   Amber at worst, NEVER red: on a new project three missing things is normal, not broken, and a
   red badge for "you have not finished typing yet" trains people to ignore the thing. */
.setup-status{display:inline-flex;align-items:center;gap:8px;margin-bottom:12px;cursor:pointer;
  background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:999px;
  padding:6px 14px;font-size:12.5px;color:var(--text-2);font-family:inherit;
  transition:border-color .13s,background .13s}
.setup-status:hover{border-color:var(--border-strong);background:var(--bg-hover)}
.setup-status .dot{width:7px;height:7px;border-radius:99px;flex:none}
.setup-status b{color:var(--text-1);font-weight:650}
.setup-status .sep{color:var(--text-3)}
.setup-status .detail{color:var(--text-2)}
.setup-status.setup-amber .dot{background:#FBBF24}
.setup-status.setup-amber b{color:#FBD873}
.setup-status.setup-green .dot{background:#22C55E}
.setup-status.setup-green b{color:#7EE6A8}

/* ============================================================================
   LIVENESS PRIMITIVES — shared "the AI is visibly working" recipes.
   Added 2026-08-23 from the magic-UI mockup pass (SESSION_HANDOFF_2026-08-23_MAGIC_UI_EXPLORATION.md).
   Used by: showToast() entrance, the creator overlay's score/chip reveal, the Comment Mining feed,
   the steps rail's current-step pulse, the Breakdowns loading skeleton, and the billing balance
   payoff. One definition each so all of these stay visually consistent and get fixed together.
   `window.countUp()` (base.html) is the JS half of this — a locale-safe (en-US) count-up used by
   every one of the above.
   ============================================================================ */

/* Page-to-page navigation: the sidebar and top bar hold still, only #vt-main (base.html's one
   per-page content wrapper, INSIDE the header — not #app-main, the outer column div that wraps
   the header too) crossfades between the old page and the new one. Native cross-document
   View Transitions — pure progressive enhancement: zero JS, zero backend change, and a browser that
   doesn't support it yet just keeps today's exact instant swap. Owner-picked 2026-08-25 over a top
   loading bar and a full-screen branded wipe (designs/mockups/page-life-motion.html) specifically
   because it can't get caught lying on a slow page: unlike a JS-timed animation, the browser itself
   gates the fade on the new document actually being ready to paint, so a slow server response just
   takes as long as it takes instead of finishing early (jump-cut) or stalling on a static screen.
   Disabling the ROOT transition and naming only #vt-main is what keeps the sidebar/header from
   visibly fading too — without it every same-origin nav would crossfade the WHOLE page, including
   the active-nav-item highlight changing underneath the sidebar, which is exactly the motion we do
   NOT want there (see the hover-motion restraint rule at the top of this stylesheet's own history:
   frequent surfaces stay calm, novelty goes on rare/deliberate moments only). */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation:none}
#vt-main{view-transition-name:page-content}
@media (prefers-reduced-motion:reduce){
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}
}

/* A number that just landed gets a brand-tinted glass sheen + a sparkle glint — the "arrived" cue for
   a count-up. Reused by the overlay score, the funnel cards, the pricing toggle, and the billing
   balance. Chosen 2026-08-23 after a live A/B of six sweep shapes and three colour/add-on axes
   (designs/mockups/reveal-animation-options-round2.html): the owner picked the CLASSIC sweep width
   (the one already shipped elsewhere, not the narrower "hot core" I'd recommended) + brand gradient
   tint (not white) + sparkle dust. `window.revealSheen(el)` (base.html) is the JS half: it toggles
   `.go` AFTER a count-up finishes and places the sparkle glints.
   `overflow:hidden` is load-bearing, not decoration — without it the sweep band isn't clipped to the
   glyphs and sits as a visible static shape beside the number, both mid-sweep and (worse) forever
   after, because nothing then exists to hide it again. Every keyframe below declares opacity at
   EVERY stop through 100% for the same reason: leaving a gap for the browser to "hold" the last
   value across is exactly what let an earlier draft of this get stuck fully visible. */
.reveal-sheen{position:relative;overflow:hidden;display:inline-block}
.reveal-sheen::after{content:"";position:absolute;top:-20%;bottom:-20%;left:0;width:38%;pointer-events:none;
  background:linear-gradient(100deg,rgba(247,47,224,.05),rgba(180,120,255,.92) 50%,rgba(75,110,245,.05));
  transform:translateX(-220%) skewX(-16deg);opacity:0}
.reveal-sheen.go::after{animation:reveal-sweep .85s cubic-bezier(.3,.8,.3,1) 1}
@keyframes reveal-sweep{
  0%{transform:translateX(-220%) skewX(-16deg);opacity:0}
  10%{opacity:1}
  85%{opacity:1}
  100%{transform:translateX(420%) skewX(-16deg);opacity:0}
}
.reveal-spark{position:absolute;font-size:.42em;color:#fff;opacity:0;pointer-events:none;
  text-shadow:0 0 8px rgba(255,255,255,.9),0 0 14px rgba(139,92,246,.8)}
.reveal-spark.go{animation:reveal-spark-pop .7s ease-out forwards}
@keyframes reveal-spark-pop{
  0%{opacity:0;transform:scale(.2) rotate(0deg)}
  35%{opacity:1;transform:scale(1.15) rotate(20deg)}
  100%{opacity:0;transform:scale(.5) rotate(35deg)}
}

/* A row/quote/chip arriving one at a time instead of all at once — Blur-Focus Rise, chosen from the
   same A/B pass. Add `.reveal-item` at build time (starts invisible) and `.show` after insertion (CSS
   animations play automatically on mount, no double-rAF trick needed) — stagger by giving each item
   an increasing `animation-delay` inline.
   TWO variants, picked by content type, not by screen: `.reveal-item` alone (plain resolve) is for
   TEXT you have to read — the "why this score" flags, a Comment Mining quote — where a moving glint
   next to the words would compete with reading them. `.reveal-item.reveal-chip` adds a sparkle
   exactly as it snaps into focus — for compact badge-like marks (AI Assessment chips, labels) where a
   glint reads as a small reward and never sits next to body text. Don't add `.reveal-chip` to a text
   row, and don't leave it off a chip — that split is the actual decision, not a style preference. */
.reveal-item{opacity:0;filter:blur(5px);transform:translateY(6px) scale(.97)}
.reveal-item.show{animation:reveal-in .45s cubic-bezier(.22,1,.36,1) forwards}
@keyframes reveal-in{
  0%{opacity:0;filter:blur(5px);transform:translateY(6px) scale(.97)}
  100%{opacity:1;filter:none;transform:none}
}
.reveal-item.reveal-chip{position:relative}
.reveal-item.reveal-chip .reveal-spark{top:-6px;right:-4px;font-size:11px}
.reveal-item.reveal-chip.show .reveal-spark{animation:reveal-spark-pop .5s ease-out .3s forwards}

/* Toast entrance — showToast() used to just appear fully-formed; this is the one addition (existing
   fade-out on dismissal is untouched). */
@keyframes toast-in{from{opacity:0;transform:translateX(16px) scale(.97)}to{opacity:1;transform:none}}
.toast-pop{animation:toast-in .32s cubic-bezier(.22,1,.36,1)}

/* Skeleton loading row — DESIGN_SYSTEM.md §19.2/19.7's "conveys a pile with zero reading" idiom,
   first use inside the product (previously marketing-mockup only). Stack a few with the opacity
   ladder below (1 / .72 / .45 / .22) so the fade itself implies "and more coming". */
.skel-row{display:flex;align-items:center;gap:10px;padding:9px 0}
.skel-avatar{width:26px;height:26px;border-radius:50%;background:var(--bg-hover);flex:none;
  animation:skel-pulse 1.6s ease-in-out infinite}
.skel-bar{height:8px;border-radius:999px;background:var(--bg-hover);animation:skel-pulse 1.6s ease-in-out infinite}
@keyframes skel-pulse{0%,100%{opacity:1}50%{opacity:.55}}

/* Steps rail — current-step ring breathes ONLY while a job is actually running for that step
   (toggled via the `.step-live` class from refreshJobQueue's poll). No new colour, motion only —
   the panel's own rule is NO GRADIENT anywhere in it. */
.steps-rail-row.step-live .steps-rail-num{animation:rail-pulse 2s ease-in-out infinite}
@keyframes rail-pulse{0%,100%{box-shadow:0 0 0 2px rgba(139,92,246,.35)}50%{box-shadow:0 0 0 5px rgba(139,92,246,.16)}}
.rail-live-chip{display:inline-flex;align-items:center;gap:5px;font-size:10px;color:#7EE6A8;font-weight:600;margin-left:6px}
.rail-live-dot{width:5px;height:5px;border-radius:50%;background:#22C55E;position:relative}
.rail-live-dot::after{content:"";position:absolute;inset:0;border-radius:50%;background:#22C55E;opacity:.6;
  animation:rail-ping 1.6s cubic-bezier(0,0,.2,1) infinite}
@keyframes rail-ping{75%,100%{transform:scale(2.4);opacity:0}}

/* Comment Mining — real audience quotes shown as an actual comment feed instead of blockquotes.
   `comment_mining_quotes` is {text, video_id} ONLY (verified in app/services/comment_mining.py) —
   no commenter name is ever captured, so the avatar is a fixed comment-bubble glyph, not a
   per-name-hash initial. The "who" line names the real VIDEO the quote came from instead of a
   fabricated username — real data, not an invented identity. */
.cm-feed-head{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
.cm-feed-count{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:20px;font-weight:800;color:var(--cyan)}
.cm-feed-label{font-size:12px;color:var(--text-2)}
.cm-bubble{display:flex;gap:9px;margin-bottom:10px}
/* Gradient quote-mark avatar (not a per-name hash - comment_mining_quotes carries no commenter
   identity, see the JS comment above, so every quote gets the same brand-tinted mark rather than a
   fabricated per-person color). Uses --grad, the app's one signature gradient, same as .btn-grad. */
.cm-avatar{width:24px;height:24px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;
  background:var(--grad)}
.cm-avatar svg{width:12px;height:12px;fill:#fff}
.cm-body{background:var(--bg-surface);border:1px solid var(--border-subtle);border-radius:0 10px 10px 10px;
  padding:7px 10px;max-width:100%;min-width:0}
.cm-src-line{font-size:10px;font-weight:600;color:var(--text-3);margin-bottom:2px}
.cm-text{font-size:12.5px;color:var(--text-1);line-height:1.45}
.cm-src{margin-top:6px}
.cm-src a{display:inline-flex;align-items:center;gap:4px;font-size:10.5px;color:#5EEAD4;text-decoration:none;
  background:rgba(34,211,238,.10);border-radius:var(--r-pill);padding:3px 9px}
.cm-src a svg{width:8px;height:8px;fill:currentColor}
.cm-src a:hover{background:rgba(34,211,238,.18)}
/* LIKES ON AN AUDIENCE QUOTE (owner pick A, 2026-10-21, designs/mockups/panel-comment-likes.html):
   a filled red heart + short count top right of the bubble (owner: always filled red, never an
   outline), then under the quote the video it sat under (a grey line that IS the link) and the AI's
   reading in grey italics. All three are parsed out of the saved quote text at draw time
   (window.parseCmQuote, creator-panel.js); a quote with no count gets no heart. */
.cm-body.cm-grow{flex:1}   /* full width, so the heart sits at the bubble's right edge */
.cm-head{display:flex;align-items:center;gap:8px;margin-bottom:3px}
.cm-head .cm-src-line{margin-bottom:0}
.cm-likes{margin-left:auto;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;
  background:var(--bg-hover);border-radius:var(--r-pill);padding:2px 8px}
.cm-likes svg{width:11px;height:11px;fill:var(--red);stroke:var(--red);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.cm-likes b{font-family:'JetBrains Mono',ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--text-1)}
.cm-where{display:flex;align-items:flex-start;gap:5px;margin-top:6px;font-size:11px;line-height:1.4;color:var(--text-3);text-decoration:none;min-width:0}
a.cm-where:hover{color:#5EEAD4}
.cm-where svg{width:8px;height:8px;fill:currentColor;flex:none;margin-top:3px}
.cm-why{margin-top:4px;font-size:11px;line-height:1.45;color:var(--text-3);font-style:italic}

/* Launch flight — a paper plane leaves the control you pressed and flies to where the work
   actually shows up. Owner pick 2026-08-25 from designs/mockups/hover-magic-options.html §10x:
   variant C ("the Squadron", 4 staggered planes) for a stage run, variant E1 ("Away") for an
   email send. `window.launchFlight` / `window.composeSendOff` (base.html) are the JS half.
   The plane is an inline SVG filled with the brand gradient, NOT the ✈ emoji — that renders as
   a full-colour blue jet on Windows, which is both off-palette and not a paper plane.
   position:fixed because the flight crosses the page (button -> #job-banner-list), and the JS
   drives `transform` per frame rather than using a CSS keyframe: the banner is EMPTY when the
   planes take off and grows into a real card while they are still in the air, so a pre-computed
   translate would land them where the banner used to be. */
.flight-plane{position:fixed;left:0;top:0;z-index:70;pointer-events:none;will-change:transform;
  filter:drop-shadow(0 0 5px rgba(139,61,255,.6));opacity:1;transition:opacity .18s ease-in}
.flight-plane.is-gone{opacity:0}
.flight-plane svg{display:block}
/* The long crossing's trail (added 2026-09-06 for the compose send-off, which flies from the
   composer docked bottom-right to the Sent folder in the left rail — most of the window wide).
   OPT-IN, and it must stay that way: the stage-run flight hops a few hundred pixels with FOUR
   planes, and four trails over that distance is a smear, not a path. Dots are sampled from where
   the plane really is each frame rather than being a pre-drawn path, so they can never disagree
   with the flight. z-index sits just under .flight-plane so the plane always leads its own trail. */
.flight-trail{position:fixed;width:4px;height:4px;margin:-2px 0 0 -2px;border-radius:50%;
  background:var(--grad);pointer-events:none;z-index:69;animation:flight-trailfade .76s ease-out forwards}
@keyframes flight-trailfade{0%{opacity:.85;transform:scale(1)}55%{opacity:.5;transform:scale(.85)}100%{opacity:0;transform:scale(.35)}}
/* the arrival glint — same sparkle keyframe the score/chip reveals already use, not a new effect */
.flight-spark{position:fixed;z-index:71;pointer-events:none;font-size:12px;color:#fff;
  margin:-7px 0 0 -5px;text-shadow:0 0 8px rgba(255,255,255,.9),0 0 14px rgba(139,92,246,.8);
  animation:reveal-spark-pop .7s ease-out forwards}
/* the destination confirming the arrival: one hairline of brand light across its top edge.
   Deliberately NOT a glow bloom — a soft outer shadow here bleeds upward onto the controls
   above the banner (that exact bug is why the mockup's first draft looked crowded). */
.flight-lit{position:relative}
.flight-lit::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:var(--grad);
  transform-origin:left center;pointer-events:none;z-index:3;
  animation:flight-edge .6s cubic-bezier(.22,1,.36,1)}
@keyframes flight-edge{
  0%{transform:scaleX(0);opacity:1}
  62%{transform:scaleX(1);opacity:1}
  100%{transform:scaleX(1);opacity:0}
}
/* Compose send-off (E1): the message you wrote lifts off the card as the plane. Removing the
   class restores the textarea instantly, which is what we want on a send FAILURE — the customer
   needs their text back the moment the error toast appears, not after a fade. */
.compose-liftoff{transform:scale(.72) translateY(-6px);opacity:0;filter:blur(3px);
  transition:transform .32s cubic-bezier(.5,0,.75,0),opacity .28s ease-in,filter .28s ease-in}

/* ---------------------------------------------------------------------------------------------
   Hours-saved sidebar widget (2026-08-26). The owner's own words: the single most important
   number in the app, so it earns treatment nothing else in the sidebar gets — three deliberate,
   named exceptions to the app's usual restraint, each one an owner call, not an oversight:
   1. A violet cursor-tracked hover spotlight (.card-spot's exact recipe, sized down) on a piece
      of persistent sidebar chrome — normally reserved for occasional-hover surfaces (dashboard
      project cards), not something seen dozens of times a session.
   2. A continuous ambient starfield — tried once for ordinary stat cards and explicitly rejected
      there (designs/mockups/hover-magic-options.html §6: "flashes for no reason... annoying").
      Kept here anyway, deliberately, only on this one widget.
   3. Real paper planes (.flight-plane, reused not redrawn) landing on a background-updated
      number — every other use of that family departs from a real click on the SAME page; this
      one arrives from off the corner because there is no on-page departure point (the job that
      earned the hours may have started on a different page entirely). Fires ONLY when a stage
      genuinely completes with real hours earned (window.hoursSavedArrival, below) — never on job
      start (nothing's earned yet) and never once per small progress tick, which stays the plain
      quiet window.statSpark() reaction every other stat in the app already uses honestly. --------- */
.hsw{background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:var(--r-md);
  padding:12px 14px;margin-bottom:6px;position:relative;overflow:hidden;display:block}
.hsw:hover{border-color:var(--border-strong)}
/* On /hours-saved the card IS the menu entry (the separate nav row was removed 2026-09-23), so it
   carries the active state the row used to. */
.hsw.is-active{border-color:var(--border-strong)}
/* SIDEBAR ON A SHORT WINDOW (2026-09-23). The full card is ~140px; on a window under 700px tall the
   pinned block (card + Credits + account) would leave the menu too little room, so the card folds
   to one row like Credits: label left, number right. Measured on the owner's screen: ~550px tall
   (150% Windows scaling), where the full layout needs ~600px. `.ds-side` in the selector because
   base.html's script adds Tailwind's `block` to reveal the card, and that must not win over flex;
   `:not(.hidden)` because the same weight would otherwise also beat `hidden` and draw the card
   with a dash before its number arrives. The number keeps its id (#hsw-value), so the
   plane-landing moment still finds it. */
@media (max-height:700px){
  .ds-side .hsw:not(.hidden){display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px}
  .ds-side .hsw .ml{font-size:14px}
  .ds-side .hsw .big{font-size:15px;margin-top:0}
  .ds-side .hsw .split,.ds-side .hsw .delta{display:none}
}
.hsw .ml,.hsw .big,.hsw .delta,.hsw .split{position:relative;z-index:1}
.hsw .big{font-size:22px;font-weight:800;margin-top:3px;font-variant-numeric:tabular-nums;display:block}
.hsw .delta{font-size:10.5px;color:var(--green);font-weight:700;margin-top:3px}
.hsw .split{display:flex;align-items:baseline;justify-content:space-between;font-size:11px;
  color:var(--text-3);margin-top:7px;padding-top:7px;border-top:1px solid var(--border-subtle)}
.hsw .split b{color:var(--text-1);font-variant-numeric:tabular-nums;font-size:12.5px}
.hsw-glow{position:absolute;inset:0;opacity:0;transition:opacity .2s;pointer-events:none;z-index:0;
  background:radial-gradient(90px circle at var(--mx,50%) var(--my,50%),rgba(139,92,246,.20),transparent 65%)}
.hsw:hover .hsw-glow{opacity:1}
.hsw-stars{position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:0;border-radius:inherit}
.hsw-star{position:absolute;width:2px;height:2px;border-radius:50%;background:#fff;opacity:0;
  animation:hsw-twinkle var(--tdur,4s) ease-in-out var(--tdel,0s) infinite}
@keyframes hsw-twinkle{0%,100%{opacity:0}50%{opacity:var(--tmax,.35)}}
/* Landing impact on the number itself — plays once window.hoursSavedArrival's plane(s) land. */
.hsw .big.impact{animation:hsw-impact .5s cubic-bezier(.2,.8,.3,1) 1}
@keyframes hsw-impact{
  0%{filter:brightness(1) drop-shadow(0 0 0 rgba(255,255,255,0));transform:scale(1)}
  30%{filter:brightness(1.6) drop-shadow(0 0 10px rgba(255,255,255,.7));transform:scale(1.06)}
  100%{filter:brightness(1) drop-shadow(0 0 0 rgba(255,255,255,0));transform:scale(1)}
}
@media (prefers-reduced-motion:reduce){
  .hsw-glow{display:none}
  .hsw-star{animation:none!important;opacity:0!important}
  .hsw .big.impact{animation:none!important}
}

/* ---------------------------------------------------------------------------------------------
   Hours-saved dedicated page hero (2026-08-26) - the "Particle Converge" reveal: a continuous
   starfield (bigger/richer than the sidebar widget's, since this is a one-time destination, not
   chrome shown on every screen - see the widget's own comment above for why that distinction is
   the whole reason both are allowed to exist), light gathering into the count-up, then a plain ≈
   before the days-equivalent figure (NOT a growing line - that read as an unlabelled divider in
   review, ≈ is the actual symbol for "same thing, different unit"). --------------------------- */
.hsp-stage{position:relative;display:inline-block}
.hsp-stars{position:absolute;inset:-26px -50px;pointer-events:none;overflow:visible;z-index:0}
/* CONSTANT keyframes (2026-09-15, ported back from the landing and pricing mockups, where it was
   measured). The old version peaked at opacity:var(--tmax), and a keyframe that reads a custom
   property cannot run on the compositor: every star cost a main-thread style recalc on every frame,
   for as long as the page stayed open. Now each star's PEAK opacity sits on a wrapper (.hsp-starw,
   position, size and opacity set inline by the page's buildStarfield) and the star inside animates
   0 -> 1 with fixed values. Nested opacities multiply, so the look is identical.
   Builders: hours_saved.html here; pricing.html and landing.html carry the same rules in their own
   style blocks. A builder that still sets --tmax on a bare .hsp-star now twinkles at FULL brightness,
   so build the wrapper. */
.hsp-starw{position:absolute}
.hsp-star{position:absolute;inset:0;border-radius:50%;background:#fff;opacity:0;animation:hsp-twinkle var(--tdur,3.4s) ease-in-out var(--tdel,0s) infinite}
@keyframes hsp-twinkle{0%,100%{opacity:0;transform:scale(.5)}50%{opacity:1;transform:scale(1)}}
.hsp-sparkle{position:absolute;pointer-events:none;color:#fff;font-size:var(--ssize,11px);opacity:0;
  text-shadow:0 0 6px rgba(180,150,255,.9),0 0 10px rgba(139,92,246,.6);animation:hsp-sparkle-pulse var(--sdur,4.2s) ease-in-out var(--sdel,0s) infinite}
@keyframes hsp-sparkle-pulse{0%,100%{opacity:0;transform:scale(.4) rotate(0deg)}45%{opacity:.95;transform:scale(1) rotate(25deg)}55%{opacity:.95}100%{opacity:0;transform:scale(.4) rotate(50deg)}}
.hsp-motes{position:absolute;inset:-40px;pointer-events:none;z-index:0}
.hsp-mote{position:absolute;width:5px;height:5px;border-radius:50%;background:radial-gradient(circle,#fff,rgba(139,92,246,.2));box-shadow:0 0 8px 2px rgba(139,92,246,.6);opacity:0;top:50%;left:50%}
.hsp-mote.go{animation:hsp-mote-fly var(--dur,900ms) ease-out var(--del,0ms) forwards}
@keyframes hsp-mote-fly{0%{opacity:0;transform:translate(var(--sx),var(--sy)) scale(1)}15%{opacity:1}80%{opacity:.9}100%{opacity:0;transform:translate(0,0) scale(.3)}}
.hsp-num{font-size:64px;font-weight:800;letter-spacing:-.02em;line-height:1;position:relative;z-index:1;display:inline-block}
.hsp-num .unit{font-size:22px;font-weight:700;color:var(--text-2);margin-left:6px}
.hsp-num.bloom::after{content:"";position:absolute;left:20%;top:50%;width:10px;height:10px;border-radius:50%;
  background:radial-gradient(circle,rgba(255,255,255,.9),rgba(139,92,246,0) 70%);transform:translate(-50%,-50%) scale(0);opacity:0}
.hsp-num.bloom.go::after{animation:hsp-bloom-pop 700ms ease-out 750ms 1}
@keyframes hsp-bloom-pop{0%{transform:translate(-50%,-50%) scale(0);opacity:0}40%{opacity:1}100%{transform:translate(-50%,-50%) scale(9);opacity:0}}
.hsp-connector{height:28px;display:flex;align-items:center;position:relative;z-index:1;margin-top:2px}
.hsp-eq{font-size:19px;font-weight:700;color:var(--text-2);opacity:0;transform:scale(.5);position:relative;display:inline-block}
.hsp-eq.go{animation:hsp-eq-pop 500ms cubic-bezier(.34,1.4,.4,1) forwards}
@keyframes hsp-eq-pop{0%{opacity:0;transform:scale(.4)}60%{opacity:1;transform:scale(1.15)}100%{opacity:1;transform:scale(1)}}
.hsp-eq::after{content:"";position:absolute;left:50%;top:50%;width:8px;height:8px;border-radius:50%;
  background:radial-gradient(circle,rgba(255,255,255,.9),rgba(126,230,168,0) 70%);transform:translate(-50%,-50%) scale(0);opacity:0;pointer-events:none}
.hsp-eq.go::after{animation:hsp-eq-burst 550ms ease-out 80ms 1}
@keyframes hsp-eq-burst{0%{transform:translate(-50%,-50%) scale(0);opacity:0}35%{opacity:1}100%{transform:translate(-50%,-50%) scale(6);opacity:0}}
.hsp-dwrap{opacity:0;transform:translateY(10px) scale(.96);position:relative;z-index:1}
.hsp-dwrap.show{opacity:1;transform:none;transition:opacity .5s ease,transform .5s cubic-bezier(.2,.9,.3,1.2)}
.hsp-dnum{font-size:30px;font-weight:800;letter-spacing:-.01em}
.hsp-dlabel{font-size:12px;color:var(--text-3);margin-top:3px;max-width:340px}
@media (prefers-reduced-motion:reduce){
  .hsp-star,.hsp-sparkle{animation:none!important;opacity:0!important}
  .hsp-num.bloom::after,.hsp-eq::after{animation:none!important}
  .hsp-dwrap{opacity:1!important;transform:none!important}
}

/* Purchase-only elevation (A3/B3, on top of C3/U3) — owner pick 2026-08-25 from
   designs/mockups/purchase-moment-elevated.html. `window.purchaseCelebrate` (base.html) is the JS
   half. Two pieces, both reserved for a real payment and used nowhere else in the app:
   the Seal (`.pc-seal`) — one brand-gradient line traced slowly (1.4s — every other motion in the
   app resolves under 1s) all the way around the card, and IDENTICAL for a credit pack and a
   subscription on purpose (it is meant to become "you paid us", not a per-purchase decoration).
   `pathLength="100"` on the traced rect makes the stroke-dasharray maths size-independent.
   the Thank-You (`.pc-t1`/`.pc-t2`) — the only place in the app that addresses the customer by name
   at a happy moment. Both reuse the existing `reveal-in` keyframe (LIVENESS PRIMITIVES above) rather
   than inventing a new reveal. */
.pc-box{position:relative}
.pc-seal{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:4;opacity:0}
.pc-seal.go{opacity:1}
.pc-seal rect{fill:none;stroke:url(#pcSealGrad);stroke-width:2;stroke-linecap:round;
  stroke-dasharray:100;stroke-dashoffset:100}
.pc-seal.go rect{animation:pc-draw 1.4s cubic-bezier(.35,.02,.2,1) forwards}
@keyframes pc-draw{0%{stroke-dashoffset:100;opacity:1}88%{stroke-dashoffset:0;opacity:1}100%{stroke-dashoffset:0;opacity:0}}
.pc-settle{animation:pc-settle-kf 1.6s cubic-bezier(.22,1,.36,1)}
@keyframes pc-settle-kf{0%{transform:none}22%{transform:translateY(-3px)}100%{transform:none}}
.pc-thanks{min-height:34px;margin-top:6px}
.pc-t1,.pc-t2{opacity:0;filter:blur(4px);transform:translateY(5px);display:block}
.pc-t1{font-size:14px;font-weight:800;color:var(--text-1);letter-spacing:-.01em}
.pc-t2{font-size:11.5px;color:var(--text-3);margin-top:1px}
.pc-t1.on,.pc-t2.on{animation:reveal-in .55s cubic-bezier(.22,1,.36,1) forwards}

/* Bulk email send — the human-composed screen's own send flow (bulk_email.html), separate from
   every other "sending something" flow already covered by launchFlight/composeSendOff above.
   `window.bulkSendFlight` (base.html) is the JS half. Owner pick 2026-08-25: B1 for an immediate
   send (a stream of planes off the real recipient count, then the real total counts up — NOT the
   literal recipient count of planes, which would be slow and imply a false precision), P1 for a
   paced/scheduled send with real per-day numbers (planes file into the days they actually go out
   on), P2 as the fallback when there are no day numbers yet (a brand-new sending account). */
.bulk-days{display:flex;gap:10px;margin-top:4px}
.bulk-day{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:4px;
  opacity:0;transform:translateY(4px);transition:opacity .3s ease,transform .3s ease}
.bulk-day.lit{opacity:1;transform:none}
.bulk-day-stack{display:flex;flex-wrap:wrap;gap:2px;justify-content:center;min-height:16px}
.bulk-day-dot{width:5px;height:5px;border-radius:50%;background:var(--border-strong);opacity:0;transition:opacity .15s}
.bulk-day-dot.on{opacity:1;background:var(--accent)}
.bulk-day-cap{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:10px;font-weight:700;color:var(--text-2)}
.bulk-day-lbl{font-size:9.5px;color:var(--text-3)}
.bulk-hold{display:flex;align-items:center;gap:2px;min-height:20px}
.bulk-hold-plane{opacity:0;transform:scale(.7);transition:opacity .2s ease,transform .2s ease}
.bulk-hold-plane.on{opacity:1;transform:scale(1)}
.bulk-hold-plane svg{display:block;filter:drop-shadow(0 0 3px rgba(139,61,255,.5))}

/* ============================================================================
   REFERRAL ARRIVAL — the one moment the referral programme has.
   Added 2026-08-25 (designs/mockups/referral-join-signal.html, option 2 of 4).
   `window.referralArrival()` (base.html) is the JS half.

   WHY THIS IS NOT showToast(). The generic toast renders a flat coloured box from
   `textContent` alone: one line, no icon, no count. The copy here NEEDS two lines,
   and the second line is the load-bearing one — a join earns the referrer nothing
   yet, so "someone joined" on its own reads as "money has landed" and would
   devalue the real earned-credits mail that comes later. One purpose-built card,
   used in exactly one place, is the smaller lie-free option.

   WHY IT IS ALLOWED TO BE THIS EXPENSIVE-LOOKING. Every other motion in the app is
   sized for something you see all day. This fires a handful of times in a
   customer's LIFETIME, at the moment their own sharing paid off, so it is the one
   surface where a little ceremony is proportionate rather than noisy.

   The card is DECORATION over a record that is written regardless: the Notification
   row lands whether or not any browser was open (services/notifications.py
   on_referral_signup). If this CSS or its JS ever breaks, the customer still finds
   out via the bell — they just lose the live pop. ============================================================================ */
/* BOTTOM-right, not top-right, and that is a collision fix rather than a taste call: #toast-container
   is `fixed top-16 right-4` and the notification panel opens at right:16px/top:60px — and the email
   toast fires from the SAME 60-second poll callback as this card, so a referral landing in the same
   cycle as a new reply would have stacked the two on the same pixels. Bottom-right is clear (the
   onboarding devbar is bottom-LEFT), and it also gives the hand-off glint a longer, legible arc up
   to the bell. Deliberately BELOW the panel's z-85: if the panel is open the feed refreshes itself
   and already shows the row, so the card being covered there loses nothing. */
.ref-arrival{position:fixed;right:16px;bottom:18px;z-index:80;width:330px;max-width:calc(100vw - 32px);
  display:flex;gap:11px;padding:13px 14px;border-radius:14px;overflow:hidden;
  background:linear-gradient(180deg,var(--bg-elevated),var(--bg-surface));
  border:1px solid var(--border);box-shadow:0 22px 60px rgba(0,0,0,.6),0 0 0 1px rgba(139,92,246,.14);
  opacity:0;transform:translateX(26px) scale(.97)}
/* the brand hairline along the top edge IS the "this is good news" cue — deliberately not a green
   success tint, which in this app means "a run finished", and not a glow bloom, which bleeds onto
   the header controls directly above it (the same bug that made .flight-lit a hairline too) */
.ref-arrival::before{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:var(--grad)}
.ref-arrival.in{animation:ref-arrive-in .52s cubic-bezier(.22,1,.36,1) forwards}
.ref-arrival.out{animation:ref-arrive-out .34s cubic-bezier(.4,0,1,1) forwards}
@keyframes ref-arrive-in{to{opacity:1;transform:none}}
/* leaves UPWARD, in the direction the glint travels, so the exit and the hand-off read as one move */
@keyframes ref-arrive-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-14px) scale(.97)}}
.ref-arrival-gift{width:34px;height:34px;border-radius:11px;flex-shrink:0;display:flex;align-items:center;
  justify-content:center;background:linear-gradient(95deg,#F72FE0 0%,#8B3DFF 50%,#4B6EF5 100%);
  box-shadow:0 0 18px rgba(139,61,255,.45)}
.ref-arrival-gift svg{width:17px;height:17px;color:#fff}
.ref-arrival-t{font-size:12.9px;font-weight:800;letter-spacing:-.01em;line-height:1.35;color:var(--text-1)}
.ref-arrival-b{font-size:11.5px;color:var(--text-2);line-height:1.55;margin-top:4px}
.ref-arrival-n{display:inline-flex;align-items:baseline;gap:5px;margin-top:9px;padding:4px 9px;border-radius:999px;
  background:rgba(139,92,246,.13);border:1px solid rgba(139,92,246,.24)}
.ref-arrival-n b{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:13px;font-weight:700;color:#C9B5FF}
.ref-arrival-n span{font-size:10.3px;color:var(--text-2)}
.ref-arrival-x{position:absolute;top:7px;right:8px;width:20px;height:20px;border-radius:6px;border:0;
  background:transparent;color:var(--text-3);cursor:pointer;font-size:13px;line-height:1;font-family:inherit}
.ref-arrival-x:hover{background:var(--bg-hover);color:var(--text-1)}
/* the hand-off glint the card throws at the bell as it leaves, so the temporary thing points at
   where the permanent thing lives. Same look as .flight-spark, positioned by rAF not keyframes. */
.ref-spark{position:fixed;z-index:81;pointer-events:none;font-size:12px;color:#fff;margin:-6px 0 0 -5px;
  text-shadow:0 0 8px rgba(255,255,255,.9),0 0 14px rgba(139,92,246,.85)}

/* ---- Creators-table bulk bar: grouped menus (2026-08-25) --------------------------------------
   Two classes only, both repeated enough times in project_detail.html that inlining them would be
   nine copies of the same string to keep in step. Everything else in that bar is a Tailwind
   utility, matching the "Columns" menu it sits beside. */
/* A choice inside a bulk menu. Pressing one IS the action - there is no Apply button any more, so
   there is no state where the button is pressable and the choice is still blank. */
.bulk-pill{font-size:11.5px;padding:4px 10px;border-radius:var(--r-pill);border:1px solid var(--border-strong);
  color:var(--text-2);background:transparent;cursor:pointer;font-family:inherit;line-height:1.4}
.bulk-pill:hover{border-color:var(--accent);color:var(--text-1)}
.bulk-pill:disabled{opacity:.45;cursor:not-allowed}
/* The chosen step in "Redo a step", and the open state of a group button - you have to be able to
   see which menu you are inside when four of them sit in a row. */
.bulk-pill.is-on,.bulk-grp-btn.is-open{border-color:var(--accent);color:#C9B5FF;
  background:rgba(139,92,246,.12)}

/* Two rows inside the Status/Priority menus above ("Mark as approved", "Skip for now") get a
   semantic color: green for approving, amber for deprioritizing (owner call 2026-08-26, mockup
   designs/mockups/bulk-toolbar-and-charts-review.html §1 "Option I" - tinted bg + colored border +
   a bonded colored icon circle). The mockup drew this as a compact toolbar PILL because that is
   what this bar looked like when it was made; the grouped-menu redesign above replaced the toolbar
   with dropdown rows that must keep their explaining sentence (this app's "no silent actions" rule),
   which a pill has no room for - same recipe, rounded-md row instead of rounded-pill button. "Back
   to normal" (the undo) and the assessment/outreach-status controls elsewhere in this bar keep their
   own existing styling on purpose: only the two forward-committing actions get this treatment. */
.bulk-row-combo{display:flex;align-items:flex-start;gap:10px;width:100%;text-align:left;
  border:1px solid;border-radius:var(--r-md);padding:8px 10px;cursor:pointer;font-family:inherit;
  transition:filter .15s}
.bulk-row-combo:hover{filter:brightness(1.2)}
.bulk-row-combo .ic{width:22px;height:22px;border-radius:50%;flex:none;margin-top:1px;
  display:flex;align-items:center;justify-content:center}
.bulk-row-combo .ic svg{display:block}
.bulk-row-combo.c-green{background:rgb(var(--c-ok) / .10);border-color:rgb(var(--c-ok) / .45)}
.bulk-row-combo.c-green .ic{background:rgb(var(--c-ok) / .28)}
.bulk-row-combo.c-green .ic svg{stroke:#B7F3CB}
.bulk-row-combo.c-green .rt{color:#7EE6A8}
.bulk-row-combo.c-amber{background:rgb(var(--c-warn) / .10);border-color:rgb(var(--c-warn) / .45)}
.bulk-row-combo.c-amber .ic{background:rgb(var(--c-warn) / .28)}
.bulk-row-combo.c-amber .ic svg{stroke:#FDE49B}
.bulk-row-combo.c-amber .rt{color:#FBD375}

/* ---- setup gate: Detective AI's reply -------------------------------------
   The label that says who is talking, and the way its words arrive. Owner picked M2 (the words
   resolve out of blur, staggered) plus the favicon as the mark, 2026-08-28 - see
   designs/mockups/onboarding-setup-gate-flow.html sections 13 and 14.

   NOTHING here adds a wait. The reply is already back before any of this runs (the model answers
   in about a second), so this is a reveal of text that exists, never a loading state standing in
   front of one. That distinction was the owner's own call and it is why there is no shimmer,
   spinner or typewriter anywhere in this block.

   The mark is the SAME asset as the sidebar's .ds-logo and the browser favicon (icon-192.png),
   not a new AI glyph - one product, one mark. The gradient sits underneath as a fallback so the
   label still reads if the image ever fails to load. */
.sg-ai { display:inline-flex; align-items:center; gap:7px; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:8.5px; letter-spacing:.1em; text-transform:uppercase; color:#C4B5FD; margin-bottom:7px; }
.sg-ai-mark { width:14px; height:14px; border-radius:50%; flex:0 0 14px;
  background-image:url('/static/icons/icon-192.png?v=3'), var(--grad);
  background-size:cover; background-position:center; background-repeat:no-repeat;
  box-shadow:0 0 9px rgba(139,61,255,.45); }
.sg-w { display:inline-block; opacity:0; transform:translateY(7px); filter:blur(5px);
  animation:sgWordIn .5s cubic-bezier(.2,.75,.3,1) forwards; }
@keyframes sgWordIn { to { opacity:1; transform:none; filter:blur(0); } }

/* The wait, and the two things it can turn into. NO NEW MOTION LIVES HERE: the dots are
   MOTION_SYSTEM.md primitive 10 (.ob-dots, a real wait the customer is genuinely on), the reply
   resolves with primitive 14 (.sg-w, a reveal because the answer is already back), and the next
   question arrives with primitive 4 (.reveal-item + window.revealItems). These two classes are
   layout and type only.

   The wait deliberately carries the mark WITHOUT the name: pressing Continue can end two ways, and
   showing "Detective AI" before we know whether it has anything to say promises a reply that may
   never come (owner caught exactly that, 2026-08-28). The mark alone says "this is being handled",
   which is true either way, and it sits where the sentence will be so the name simply joins it. */
.sg-wait { display:flex; align-items:center; gap:9px; margin-top:16px; min-height:22px; }
/* Sits INSIDE the .sg-ai eyebrow, after a separator - it inherits that line's size and spacing, so
   the only thing it adds is the colour that marks an accepted answer apart from an ask. */
.sg-got-k { color:#4ADE80; }

/* The inference on question 1. Ported from onboarding's `.ob-infer` (designs/mockups/
   onboarding-understanding-moment.html §01, live in onboarding.html): the SAME card as its
   neighbours, distinguished by its label and slightly larger text rather than by paint - the owner
   picked that treatment there and re-picking it here keeps one device, not two. */
.sg-infer { margin-top:14px; padding:14px 16px; border-radius:var(--r-md);
  background:var(--bg-surface); border:1px solid var(--border-subtle); }
.sg-infer-tag { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:8.5px; letter-spacing:.12em;
  text-transform:uppercase; color:#A78BFA; margin-bottom:7px; }
.sg-infer p { font-size:13px; color:var(--text-1); line-height:1.6; font-weight:500; }
.sg-infer-acts { display:flex; gap:8px; margin-top:11px; align-items:center; }

/* ---- THE RULES' OTHER THREE SCREENS: the two-route intro (20A), the AI-tool hand-over (21A/21B)
   and the closing screen that proposes the searches (17B). Ported 1:1 from
   designs/mockups/steps-panel-step0-options.html sections 17, 20 and 21.

   WHY THESE ARE CLASSES AND THE REST OF THE TAKEOVER IS INLINE STYLE. The gate's question screens
   build their markup as inline-styled strings, which is fine for one-off type and spacing. These
   three carry repeated ROWS (four answer rows, N phrase chips, two route cards), and a repeated
   row written as an inline style is the same declaration copied per item - it drifts the first
   time one of them is edited. Everything below is tokens only; no new colour or radius is invented
   here.

   21A deliberately reuses `.ai-tabs/.ai-tab/.ai-code/.ai-copy/.ai-once` from the Connect-an-AI
   panel further down this file rather than the mockup's `.ct-*` twins. It is the same hand-over in
   a different frame, and two stylings of one thing is how they stop matching. */
.sg-set { margin-top:18px; border:1px solid var(--border-subtle); border-radius:var(--r-md); overflow:hidden; }
.sg-set-row { display:flex; gap:10px; padding:9px 12px; border-top:1px solid var(--border-subtle); font-size:12.5px; }
.sg-set-row:first-child { border-top:none; }
.sg-set-row .t { color:var(--green); flex:none; width:14px; text-align:center; font-size:11px; line-height:1.5; }
.sg-set-row .k { color:var(--text-2); flex:none; width:150px; }
.sg-set-row .v { color:var(--text-3); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* A row whose answer has not arrived yet (21B). Italic + the quiet colour, so "Not yet" cannot be
   mistaken for something the customer actually wrote. */
.sg-set-row.is-wait .v { color:var(--text-3); font-style:italic; }
.sg-next { margin-top:18px; padding-top:16px; border-top:1px solid var(--border-subtle); }
.sg-kw { display:flex; flex-wrap:wrap; gap:7px; margin-top:12px; }
.sg-kw label { display:inline-flex; align-items:center; gap:7px; background:var(--bg-base);
  border:1px solid var(--border); border-radius:var(--r-pill); padding:6px 12px; font-size:12.5px;
  color:var(--text-1); cursor:pointer; }
.sg-kw input { accent-color:var(--accent); margin:0; }
.sg-kw-add { display:flex; gap:8px; margin-top:10px; }
.sg-kw-add .ds-input { flex:1; min-width:0; }
.sg-plat { display:flex; gap:7px; margin-top:12px; align-items:center; font-size:12px;
  color:var(--text-3); flex-wrap:wrap; }
/* The chips on 17B are real buttons, because they TOGGLE - a span with an onclick cannot be
   reached with a keyboard, and this one decides how many campaigns get made. `.chip` carries no
   button reset of its own, so the three lines that stop a browser painting its own button are
   here rather than in `.chip`, where they would touch every static chip in the app. */
.sg-plat .chip { font-size:10.5px; padding:2px 9px; cursor:pointer;
  border:0; font-family:inherit; }
.sg-fld { margin-top:12px; max-width:420px; }
.sg-fld label { display:block; font-size:12.5px; font-weight:500; color:var(--text-2); margin-bottom:5px; }
.sg-fld .ds-input { width:100%; display:block; }
.sg-fld .teach { font-size:11.5px; color:var(--text-3); margin-top:5px; line-height:1.45; }
/* The two routes on 20A. One column on a phone: side by side at 390px would give each card about
   170px, which is narrower than its own headline. */
.sg-route-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:16px; }
.sg-route { border:1px solid var(--border); border-radius:var(--r-md); padding:16px;
  background:var(--bg-base); display:flex; flex-direction:column; }
.sg-route.primary { border-color:rgba(139,92,246,.5); }
/* The card's own label, NOT the takeover's eyebrow pill. The shared `.eyebrow` is a bordered pill
   with its own background and padding, and this rule only ever overrode the type - so each route
   card wore a stretched empty pill round two words (seen by shooting 20A, 2026-09-14). The reset
   is explicit rather than a new class name because the markup says `.eyebrow` in both mockups. */
.sg-route .eyebrow { display:block; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:9.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--text-3);
  margin:0; border:0; padding:0; background:none; border-radius:0; }
.sg-route h4 { font-size:14px; font-weight:700; margin-top:6px; line-height:1.3; }
.sg-route p { font-size:12.5px; color:var(--text-2); margin-top:6px; line-height:1.5; flex:1; }
.sg-route .act { margin-top:12px; }
@media (max-width:560px) { .sg-route-row { grid-template-columns:1fr; } }
/* THE VENDOR MARKS on 20A's AI card (round 6, owner: "add icons"). The symbols themselves are
   defined once in project_detail.html and referenced with <use>, so four logos cost four <use>
   elements and not four copies of a path. `currentColor` deliberately: they read as one row of
   marks belonging to the card, not as a strip of stickers. */
.sg-marks { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px; }
.sg-marks .sg-mark { width:18px; height:18px; flex:0 0 18px; color:var(--text-1); }
.sg-marks small { font-size:11px; color:var(--text-3); margin-left:2px; line-height:1.45; }
/* 21A AS THREE NUMBERED STEPS (round 6). The screen was a headline, a command, a sentence and two
   buttons, with nothing saying they happened in an order - which is what the owner read as "hard to
   understand". The numerals are the fix, so they are a real element rather than a "1." typed into
   copy that a translator would have to keep. */
.sg-ct-steps { margin-top:14px; display:flex; flex-direction:column; gap:12px; }
.sg-ct-step { display:flex; gap:12px; align-items:flex-start; }
.sg-ct-step .no { width:22px; height:22px; border-radius:50%; flex:none; display:grid;
  place-items:center; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px;
  font-weight:700; background:var(--accent-soft); color:#C4B5FD; margin-top:1px; }
/* min-width:0 or a long one-line command stops the flex row from ever shrinking. */
.sg-ct-step .bd { flex:1; min-width:0; }
.sg-ct-step .bd b { display:block; font-size:13.5px; margin-bottom:6px; }
.sg-ct-step .bd .ai-code, .sg-ct-step .bd .sg-say { margin-top:0; }
.sg-ct-note { font-size:11.5px; color:var(--text-3); margin-top:8px; line-height:1.5; }
.sg-ct-note a { color:var(--text-2); text-decoration:underline; text-decoration-style:dotted; }
/* The one sentence to say in the connected tool (21A). Dashed, because it is something to repeat
   rather than something to copy: there is no Copy button on it. */
.sg-say { display:flex; gap:12px; align-items:baseline; margin-top:14px; padding:10px 12px;
  border:1px dashed var(--border); border-radius:var(--r-md); font-size:13px; flex-wrap:wrap; }
.sg-say .k { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--text-3); white-space:nowrap; }
.sg-say .v { color:var(--text-1); font-weight:600; }
.sg-acts { margin-top:19px; display:flex; align-items:center; gap:15px; flex-wrap:wrap; }
.sg-foot { margin-top:15px; padding-top:12px; border-top:1px solid var(--border-subtle);
  font-size:11.5px; color:var(--text-3); line-height:1.5; }

@media (prefers-reduced-motion:reduce) {
  /* Text must never depend on an animation to become visible - under reduce-motion the words are
     simply there, in their final state. */
  .sg-w { animation:none !important; opacity:1 !important; transform:none !important; filter:none !important; }
  .reveal-sheen.go::after, .reveal-item.show, .toast-pop, .skel-avatar, .skel-bar,
  .steps-rail-row.step-live .steps-rail-num, .rail-live-dot::after { animation:none !important; }
  /* The card still SHOWS under reduce-motion — the message is information, not decoration, and
     withholding it would be an accessibility bug dressed up as politeness. Only the motion goes.
     The JS refuses to spawn the travelling glint at all; this is the belt-and-braces half. */
  .ref-arrival { opacity:1 !important; transform:none !important; }
  .ref-arrival.in, .ref-arrival.out { animation:none !important; }
  .ref-spark { display:none !important; }
  /* filter too (2026-09-17): `.reveal-item` starts at blur(5px), and with the animation switched off
     above nothing ever cleared it, so under reduce-motion every revealed row and chip stayed blurred. */
  .reveal-item { opacity:1 !important; transform:none !important; filter:none !important; }
  /* the JS also refuses to spawn planes at all under reduce-motion; these are the belt-and-braces
     half, so a stray node from an in-flight launch can never keep animating */
  .flight-plane, .flight-spark, .flight-trail { display:none !important; }
  .flight-lit::before { animation:none !important; opacity:0 !important; }
  .compose-liftoff { transition:none !important; }
  .pc-seal { display:none !important; }
  .pc-settle { animation:none !important; }
  .pc-t1.on, .pc-t2.on { animation:none !important; opacity:1 !important; filter:none !important; transform:none !important; }
  .bulk-day, .bulk-day-dot, .bulk-hold-plane { transition:none !important; opacity:1 !important; transform:none !important; }
}

/* ============================================================================
   "YOUR RULES" — the un-numbered row above the six steps, and its pane.
   Added 2026-08-29. Same charcoal vocabulary as the steps panel above; NO gradient in here for
   the same reason that panel has none (one gradient moment per view, spent elsewhere on this page).

   The rail row deliberately has no number and no connector: the six steps are a sequence you move
   through, the rules are a thing you own and never finish, so putting them ON the numbered track
   would imply you tick them off and move on.
   ============================================================================ */
.rules-mark { width:26px; height:26px; border-radius:8px; flex:none; display:grid; place-items:center;
  background:rgba(251,191,36,.12); color:#FBD375; margin-top:1px; }
.rules-mark.is-set { background:rgba(34,197,94,.12); color:#7EE6A8; }
.rules-mark svg { width:14px; height:14px; }
.steps-rail-row.no-track::after { display:none; }
.rail-sep { height:1px; background:var(--border-subtle); margin:8px 12px 0; }
.rail-sep-lbl { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.14em;
  text-transform:uppercase; color:var(--text-3); padding:8px 12px 4px; }

/* The context line: why any of this exists, before any of it is shown. */
.rules-ctx { max-width:62ch; font-size:13px; color:var(--text-2); line-height:1.6; margin-top:12px; }
.rules-ctx b { color:var(--text-1); font-weight:600; }

/* ---- one numeric rule -----------------------------------------------------------------------
   The format is a CONTRAST PAIR, not a paragraph: what falls under the line and what clears it,
   opposed. The owner's own explanations were already contrasts ("under 20-30 seconds these are a
   trending sound with nobody talking; 60 is where someone speaks to camera"), and a paragraph was
   the thing hiding that shape. Owner pick 2026-08-29 over three other formats. */
.rul-wrap { max-width:680px; margin-top:6px; }
.rul-row { padding:17px 0; border-top:1px solid var(--border-subtle); }
.rul-row:first-child { border-top:none; }
.rul-top { display:flex; align-items:center; gap:11px; flex-wrap:wrap; }
.rul-ico { width:28px; height:28px; border-radius:8px; flex:none; display:grid; place-items:center;
  background:var(--bg-hover); color:var(--text-3); }
.rul-ico svg { width:14px; height:14px; }
.rul-name { font-size:14px; font-weight:650; color:var(--text-1); }
.rul-val { margin-left:auto; display:flex; align-items:baseline; gap:7px; }
/* Inter with tabular-nums, never mono: a thousands separator in a mono face gets its own character
   cell, so "1,000" renders as "1 , 000" (DESIGN_SYSTEM 15.7). field-sizing keeps the dashed rule
   the width of the digits; the explicit widths are the fallback where it is unsupported. */
.rul-val input { width:4.4ch; field-sizing:content; min-width:1.4ch; font-size:21px; font-weight:800;
  letter-spacing:-.02em; text-align:right; background:transparent; border:none;
  border-bottom:2px dashed rgb(var(--c-ink) / .16); color:var(--text-1); font-family:inherit;
  font-variant-numeric:tabular-nums; padding:0 2px 2px; }
.rul-val input.w2 { width:2.1ch; }
.rul-val input.w1 { width:1.4ch; }
/* The maximum followers box (2026-10-03): 500,000 is six digits, and an empty box shows a word. */
.rul-val input.rul-num-wide { width:7ch; }
.rul-val input::placeholder { color:var(--text-3); font-weight:600; font-size:15px; }
.rul-val input:focus { outline:none; border-bottom-color:var(--accent); }
.rul-val .u { font-size:12.5px; color:var(--text-3); }
/* The default marker, two states. On the default it states a fact ("Default"). Once the value is
   changed it shows what the default WAS and doubles as the way back, so one quiet control does
   both jobs and no reset button has to be added beside it. "Recommended" was rejected: it is a
   judgement, and it silently becomes false the moment someone types a different number. */
.rul-pill { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.11em;
  text-transform:uppercase; padding:3px 8px; border-radius:var(--r-pill); white-space:nowrap;
  background:rgba(34,197,94,.11); color:#7EE6A8; border:1px solid rgba(34,197,94,.25); }
.rul-pill.mute { background:var(--bg-hover); color:var(--text-3); border-color:var(--border-subtle); }
.rul-pill.reset { background:var(--bg-hover); color:var(--text-2); border-color:var(--border);
  cursor:pointer; display:inline-flex; align-items:center; gap:5px; font-family:inherit;
  letter-spacing:.11em; }
.rul-pill.reset:hover { border-color:var(--border-strong); color:var(--text-1); }
.rul-pill.reset svg { width:10px; height:10px; }
.rul-pair { margin-top:12px; display:flex; flex-direction:column; gap:7px; }
.rul-line { display:grid; grid-template-columns:14px 108px 1fr; gap:10px; align-items:baseline; font-size:12.5px; }
.rul-line .d { width:7px; height:7px; border-radius:50%; margin-top:5px; }
.rul-line .c { color:var(--text-1); font-weight:600; font-variant-numeric:tabular-nums; }
.rul-line .t { color:var(--text-2); line-height:1.5; }
.rul-line.out .d { background:rgb(var(--c-bad) / .75); }
.rul-line.out .c { color:var(--text-3); }
.rul-line.in .d { background:var(--green); }
.rul-note { margin-top:11px; font-size:12px; color:var(--text-3); line-height:1.55; max-width:62ch; }
.rul-note b { color:#FBD375; font-weight:600; }
.rul-group { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-top:22px; }
.rul-group .when { font-size:11.5px; color:var(--text-3); }

/* The pending bar. Editing is inline (a dialog covers the contrast pair that explains the number
   you are changing), so this is what carries the confirm instead: it appears only once a value
   actually differs from what is saved, says what the change would do, and nothing is written until
   Save is pressed. Sits UNDER the group so the explanations stay on screen the whole time. */
.rul-pending { margin-top:18px; max-width:680px; border:1px solid rgba(139,92,246,.35);
  background:rgb(var(--c-accent) / .06); border-radius:var(--r-md); padding:13px 15px;
  display:flex; align-items:flex-start; gap:16px; flex-wrap:wrap; }
.rul-pending-body { flex:1; min-width:220px; font-size:12.5px; color:var(--text-2); line-height:1.55; }
.rul-pending-body b { color:var(--text-1); font-weight:650; display:block; margin-bottom:6px; }
.rul-pending-line { display:flex; gap:9px; align-items:baseline; padding:2px 0;
  font-variant-numeric:tabular-nums; }
.rul-pending-line span:first-child { flex:1; }
.rul-pending-line .was { color:var(--text-3); }
.rul-pending-line .arrow { color:var(--text-3); }
.rul-pending-acts { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }

/* ---- The pane diet (2026-08-29) -------------------------------------------------------------
   The price, beside the count it is charged against. It used to sit in paragraph three of the
   "what happens in this step" disclosure, which is the wrong place for the one number this whole
   panel exists to protect. */
.steps-cost { display:inline-flex; align-items:center; gap:7px; font-size:12.5px; color:var(--text-2);
  background:var(--bg-hover); border-radius:var(--r-pill); padding:4px 12px; }
.steps-cost b { color:var(--text-1); font-weight:700; font-variant-numeric:tabular-nums; }

/* An all-green checklist, collapsed. The summary IS the head, so there is one row rather than a
   head stacked on a control. Same triangle idiom as the other disclosures in this panel. */
.steps-checks-all-ok > summary { padding:8px 12px; background:var(--bg-hover); font-size:12.5px;
  color:var(--text-2); cursor:pointer; list-style:none; display:flex; align-items:baseline;
  gap:9px; flex-wrap:wrap; }
.steps-checks-all-ok > summary::-webkit-details-marker { display:none; }
.steps-checks-all-ok > summary b { color:var(--text-1); font-weight:600; }
.steps-checks-all-ok > summary::before { content:'\25B8'; color:var(--text-3); font-size:9px;
  align-self:center; }
.steps-checks-all-ok[open] > summary::before { content:'\25BE'; }
.steps-checks-all-ok > summary:hover { background:var(--bg-elevated); }
/* The rows keep their own top borders, so the body needs none of its own. */
.steps-checks-all-ok-body { border-top:1px solid var(--border-subtle); }
/* Same stage numbers, moved from under the title to the foot of the pane. */
.steps-pane-stages.is-footnote { margin-top:20px; padding-top:12px;
  border-top:1px solid var(--border-subtle); }

@media (max-width:560px) {
  /* The fixed condition column squeezes the description into a ribbon on a phone, so stack the
     condition above its own text instead of beside it. */
  .rul-line { grid-template-columns:14px 1fr; row-gap:2px; }
  .rul-line .c, .rul-line .t { grid-column:2; }
  .rul-val { margin-left:0; width:100%; }
}

/* ---- CONNECT AN AI ----------------------------------------------------------------------------
   The customer's own AI, connected to one project over MCP. Approved design:
   designs/mockups/connect-an-ai.html. Everything that already exists is reused as-is (.card-edge,
   .chip, .btn*, .ds-input, .ds-pick*, .mono-label, .eyebrow); this block is only the pieces the
   design system genuinely did not have - a real code block with a copy bar, the client tab strip,
   and the two artefacts that carry the whole idea: the "waiting to hear from it" line and the
   whoami answer.
   Prefixed .ai- and used on ONE screen, same convention as the .ob- onboarding block above. */

/* the connection row: the connected-mailbox row's shape, not a new one */
/* flex-wrap is NOT optional here. Two of the row's states (the recovery line, the Manage drawer)
   add a full-width child, and without wrapping that child is forced onto the same flex line - it
   crushes the name and the chips into a column two words wide and drops the label entirely. Seen
   at 1440 on 2026-09-05 before this was set. */
.ai-conn { display:flex; align-items:center; flex-wrap:wrap; gap:13px; padding:13px 14px;
  background:var(--bg-elevated); border:1px solid var(--border-subtle); border-radius:var(--r-md); }
.ai-conn + .ai-conn, .ai-wait + .ai-conn, .ai-whoami + .ai-conn { margin-top:10px; }
.ai-conn.is-off { opacity:.55; }
.ai-conn-mark { width:32px; height:32px; border-radius:8px; background:var(--bg-base);
  border:1px solid var(--border); display:grid; place-items:center; flex-shrink:0; }
/* min-width keeps the identity readable rather than letting it collapse to a two-word column when
   the row is tight; it wraps the action buttons onto their own line instead, which is the right
   thing to give up first. */
.ai-conn-main { min-width:180px; flex:1; }
.ai-conn-name { font-size:13.5px; font-weight:650; color:var(--text-1);
  display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ai-conn-name .txt { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; }
.ai-conn-sub { font-size:11.5px; color:var(--text-3); margin-top:3px;
  font-family:'JetBrains Mono',ui-monospace,monospace; }
.ai-conn-act { display:flex; align-items:center; gap:7px; flex-shrink:0; }
.ai-manage { width:100%; margin-top:12px; padding-top:12px; border-top:1px solid var(--border-subtle); }
.ai-manage-row { display:flex; gap:9px; align-items:center; flex-wrap:wrap; }
.ai-manage-note { font-size:11.5px; color:var(--text-3); margin-top:9px; line-height:1.5; }
/* they closed the window before copying. Reads as routine, never as their mistake. */
.ai-recover { width:100%; margin-top:11px; padding-top:11px; font-size:12.5px; color:var(--text-2);
  line-height:1.55; border-top:1px solid var(--border-subtle); }

/* handed over, nothing has said hello yet. NEVER shown for a key that has been used - the state is
   decided server side off last_used_at, which only an authenticated /mcp call can set. */
.ai-wait { display:flex; align-items:center; gap:10px; margin-top:11px; background:var(--bg-base);
  border:1px solid var(--border-subtle); border-radius:var(--r-md); padding:12px 13px; }
.ai-pulse { width:7px; height:7px; border-radius:50%; background:var(--amber); flex-shrink:0;
  animation:ai-bp 1.9s ease-in-out infinite; }
@keyframes ai-bp { 0%,100% { opacity:.35; transform:scale(.86); } 50% { opacity:1; transform:scale(1); } }
@media (prefers-reduced-motion:reduce) { .ai-pulse { animation:none; opacity:1; } }
.ai-wait .t { font-size:12.5px; color:var(--text-2); line-height:1.55; }
.ai-wait .t b { color:var(--text-1); font-weight:600; }

/* the proof: what the AI itself is told when it asks who it is */
.ai-whoami { margin-top:11px; background:var(--bg-base); border:1px solid var(--border-subtle);
  border-radius:var(--r-md); padding:12px 13px; }
.ai-whoami-top { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
.ai-whoami-top .who { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px;
  letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
.ai-whoami-body { font-size:13px; line-height:1.65; color:var(--text-2);
  font-variant-numeric:tabular-nums; }
.ai-whoami-body b { color:var(--text-1); font-weight:650; }

/* the handover panel. ONE gradient moment on this screen, and it is here. */
/* The border matters more than it looks: on the dark backdrop the surface fill alone gave the card
   no visible top edge, so a card that started 24px down read as one clipped by the window.
   SCROLLS INSIDE ITSELF rather than making the backdrop scroll: this card is tall (a command, a
   warning, the steps and five clients) and without a ceiling the page scrolled instead, which took
   the close button and the eyebrow off screen together. Bounded here, the header and the close
   stay put and only the body moves. */
/* Sits INLINE under the connection now, not as a fixed overlay. No max-height and no internal
   scroll: the page scrolls, which is what stops it ever being clipped by the window or the
   browser's own chrome. The four patches that came before this (padding, centring, a ceiling, a
   close button) were all treating a symptom of being pinned to the viewport. */
.ai-hand { position:relative; background:var(--bg-surface); border:1px solid var(--border);
  border-radius:var(--r-lg); padding:24px; overflow:hidden; }
.ai-hand-x { position:absolute; top:12px; right:12px; z-index:2; width:30px; height:30px;
  display:grid; place-items:center; border-radius:var(--r-sm); border:1px solid transparent;
  background:transparent; color:var(--text-3); cursor:pointer; }
.ai-hand-x:hover { background:var(--bg-hover); color:var(--text-1); border-color:var(--border); }
.ai-hand::before { content:''; position:absolute; top:-170px; left:50%; transform:translateX(-50%);
  width:600px; height:320px; pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(139,92,246,.16),transparent 68%); }
.ai-hand-in { position:relative; }
.ai-hand h3 { font-size:19px; font-weight:800; letter-spacing:-.02em; margin-top:9px;
  color:var(--text-1); }
.ai-hand .lede { font-size:13px; color:var(--text-2); margin-top:6px; line-height:1.6;
  max-width:560px; }

/* a real code block. The artefact IS the product of this screen, so it gets the frame. */
.ai-code { margin-top:15px; background:#08080B; border:1px solid var(--border);
  border-radius:var(--r-md); overflow:hidden; }
.ai-code-top { display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:9px 12px; border-bottom:1px solid var(--border-subtle); background:rgba(255,255,255,.02); }
.ai-code-top .where { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px;
  color:var(--text-3); letter-spacing:.04em; min-width:0; overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap; }
.ai-copy { font-family:inherit; font-size:11.5px; font-weight:600; color:var(--text-2);
  background:transparent; border:1px solid var(--border); border-radius:var(--r-sm);
  padding:4px 10px; cursor:pointer; display:inline-flex; gap:6px; align-items:center;
  flex-shrink:0; }
.ai-copy:hover { color:var(--text-1); border-color:var(--border-strong); }
/* WRAPS, does not scroll sideways. These commands are ~180 characters, so `white-space:pre` put
   everything past "...-a-18" off the right edge behind a thin scrollbar that is easy to miss - the
   owner could not see the URL or the key he was about to paste (2026-09-05). There is a Copy
   button, so exact line shape is worth nothing here and being able to READ the command is worth a
   lot. break-all because the long token is a URL and a key, neither of which has spaces to break
   at. */
.ai-code pre { margin:0; padding:13px; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:12px; line-height:1.75; color:#D6D6DE; white-space:pre-wrap; word-break:break-all; }
/* the hero command is the single most important thing on the screen: same component, one modifier */
.ai-code-hero { border-color:var(--border-strong); box-shadow:0 18px 50px rgba(0,0,0,.45); }
.ai-code-hero pre { font-size:13.5px; line-height:1.85; padding:17px; color:#EDEDF2; }

/* client picker, revealed behind "Using Cursor, Codex or Windsurf instead?" */
.ai-tabs { display:flex; gap:6px; margin-top:16px; flex-wrap:wrap; }
.ai-tab { font-family:inherit; font-size:12px; font-weight:600; padding:6px 12px;
  border-radius:var(--r-pill); background:transparent; border:1px solid var(--border);
  color:var(--text-3); cursor:pointer; display:inline-flex; align-items:center; gap:6px; }
.ai-tab[aria-selected="true"] { background:var(--bg-hover); color:var(--text-1);
  border-color:var(--border-strong); }
.ai-tab.is-dead { opacity:.45; }
/* The vendor mark, at text size beside the tool's own name. `currentColor` so it dims with the
   unselected tab and lifts with the selected one, which is what makes five logos read as one
   control rather than as a row of stickers. */
.ai-tab-lg { width:14px; height:14px; flex:0 0 14px; color:currentColor; }
/* neutral, never green. Green would say "this is the easy one", and only the one-command route is. */
.ai-tab-n { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.06em;
  text-transform:uppercase; color:var(--text-3); border:1px solid var(--border-subtle);
  border-radius:var(--r-pill); padding:1px 5px; }
.ai-panel[hidden] { display:none; }
.ai-tip { font-size:12px; color:var(--text-3); margin-top:11px; line-height:1.55; }
.ai-tip b { color:var(--text-2); font-family:'JetBrains Mono',ui-monospace,monospace; font-weight:500; }
.ai-mono { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11.5px; color:var(--text-2);
  background:rgba(255,255,255,.05); border-radius:4px; padding:1px 5px; }
/* the manual route is always there and never competes with the one-click one: a link that silently
   does nothing on somebody's machine must not leave them with no way through. */
.ai-fallback { margin-top:20px; padding-top:16px; border-top:1px solid var(--border-subtle); }
.ai-fallback-h { font-size:12px; color:var(--text-3); font-weight:600; }
.ai-step { display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--text-2);
  margin:13px 0 8px; }
.ai-step b { width:17px; height:17px; border-radius:50%; background:var(--bg-hover); color:var(--text-2);
  display:grid; place-items:center; font-size:10px; font-weight:700;
  font-family:'JetBrains Mono',ui-monospace,monospace; flex-shrink:0; }
.ai-dead { background:var(--bg-base); border:1px solid var(--border-subtle);
  border-radius:var(--r-md); padding:16px; }
.ai-dead-h { font-size:13.5px; font-weight:650; color:var(--text-2); }
.ai-dead-b { font-size:12.5px; color:var(--text-3); margin-top:6px; line-height:1.6; max-width:520px; }

/* shown once, and it has to look like it means it */
.ai-once { display:flex; gap:9px; align-items:flex-start; margin-top:14px; padding:11px 12px;
  background:rgba(245,158,11,.06); border:1px solid rgba(245,158,11,.2); border-radius:var(--r-md); }
.ai-once .t { font-size:12px; color:#FBBF24; line-height:1.55; }
.ai-once .t b { font-weight:700; }
.ai-steps { display:flex; gap:7px; align-items:center; margin-top:18px; flex-wrap:wrap; }
.ai-stepdot { display:flex; align-items:center; gap:7px; font-size:11.5px; color:var(--text-3); }
.ai-stepdot b { width:17px; height:17px; border-radius:50%; background:var(--bg-hover);
  color:var(--text-2); display:grid; place-items:center; font-size:10px; font-weight:700;
  font-family:'JetBrains Mono',ui-monospace,monospace; }

/* the optional daily limit, inside the create dialog. It only appears for the spending choice. */
.ai-spend-row { display:flex; align-items:center; gap:9px; margin:9px 0 0 23px; flex-wrap:wrap; }
.ai-spend-lab { font-size:12.5px; color:var(--text-2); }
.ai-spend-in { display:inline-flex; align-items:center; background:var(--bg-base);
  border:1px solid var(--border); border-radius:var(--r-sm); padding:0 10px; }
.ai-spend-in .cur { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:13px; color:var(--text-3); }
/* wide enough for the "no limit" placeholder, not just for a number: empty is the default here, so
   a clipped placeholder would read as a broken field. */
/* !important, and it is the honest fix rather than laziness. This is the ONE input in the app that
   is deliberately not a box: the little $ frame around it is the field, and the input inside it is
   bare. The FORM CONTROL TOWER's app-wide `input:focus:not(...)x7` and its `input:hover` twin are
   (0,7,1), so a plain class selector loses to them; matching that would mean repeating seven :not()
   clauses, which nobody would ever safely edit again. The dialog that holds this field also skips
   `.ds-form` for the same reason (see project_detail.html). The focus ring is not lost, it moves to
   the frame via :focus-within above. */
.ai-spend-in input {
  width:92px; background:none !important; border:none !important; border-radius:0 !important;
  padding:8px 4px; color:var(--text-1);
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:14px; font-weight:600; }
.ai-spend-in input:focus, .ai-spend-in input:hover {
  outline:none; box-shadow:none !important; }
.ai-spend-in input::placeholder { color:var(--text-3); font-weight:400; font-size:12.5px; }
.ai-spend-in:focus-within { border-color:var(--accent); box-shadow:0 0 0 3px rgba(139,92,246,.16); }
.ai-spend-hint { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px;
  letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
/* Sits directly under the spend field and answers the two questions the label cannot: which window,
   and whose. The owner settled this wording after saying twice that the label alone was unclear. */
.ai-spend-sub { margin:5px 0 0 23px; font-size:11.5px; color:var(--text-3); line-height:1.5; }
.ai-note { margin:9px 0 0 23px; font-size:11.5px; color:var(--text-3); line-height:1.5;
  display:flex; gap:7px; }
.ai-note svg { flex-shrink:0; margin-top:2px; }
/* the zero state is ONE quiet line, not a card: this sits on the busiest screen in the app and most
   customers will never use it, so it earns a single row until somebody presses the button. */
.ai-zero { display:flex; align-items:center; justify-content:space-between; gap:18px; }
.ai-zero .t { font-size:13.5px; color:var(--text-2); line-height:1.55; }
.ai-zero .t b { color:var(--text-1); font-weight:600; }

@media (max-width:640px) {
  .ai-zero { flex-direction:column; align-items:flex-start; gap:12px; }
  .ai-conn { flex-wrap:wrap; }
  .ai-conn-act { width:100%; justify-content:flex-end; }
  .ai-hand { padding:17px; }
  .ai-spend-row, .ai-note { margin-left:0; }
}

/* ============================================================================================
   THE FIRST-RUN PAGE (app/templates/_first_run_panel.html, mockup section 15).
   Ported 1:1 from designs/mockups/steps-panel-step0-options.html. Six classes, all of them only
   used before a project's first search has run, so nothing else in the app can drift with them.
   Everything else on that page reuses the existing steps-* / kw-* / chip / btn classes.
   ============================================================================================ */
/* Steps 2 to 6, folded into one line. Dashed, quiet, and deliberately not clickable: they open as
   each one has people waiting, so on an empty project they are a promise, not six empty panes. */
.rail-rest { margin:6px 4px 2px; padding:10px 11px; border-radius:var(--r-md);
  border:1px dashed var(--border-subtle); font-size:11.5px; color:var(--text-3); line-height:1.5; }
.rail-rest b { color:var(--text-2); font-weight:600; }
/* The one headline on the page. Bigger than .steps-pane-title because on this page it IS the page. */
.start-h { font-size:26px; font-weight:800; letter-spacing:-.028em; line-height:1.18; }
.start-sub { font-size:13.5px; color:var(--text-2); margin-top:8px; max-width:52ch; line-height:1.55; }
/* "Your rules are saved" - a settled fact, so it reads as a pill rather than as another task. */
.start-done { display:inline-flex; align-items:flex-start; flex-wrap:wrap; gap:4px 8px; margin-top:14px;
  font-size:12.5px; line-height:1.5; color:var(--text-2); background:var(--bg-base);
  border:1px solid var(--border-subtle); border-radius:var(--r-pill); padding:6px 14px; max-width:100%; }
.start-done .tick { color:var(--green); font-size:11px; line-height:1.55; }
.start-done a { color:var(--text-2); text-decoration:underline; text-decoration-style:dotted; }
/* The saved phrases, once there are some (15D). */
.srch-chips { display:flex; flex-wrap:wrap; gap:7px; margin-top:14px; }
.srch-chips span { display:inline-flex; align-items:center; gap:8px; background:var(--bg-base);
  border:1px solid var(--border); border-radius:var(--r-pill); padding:5px 12px; font-size:12.5px;
  color:var(--text-1); }
.srch-chips span i { font-style:normal; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:9.5px; letter-spacing:.08em; color:var(--text-3); }
/* The quiet lines on the first-run page: the cost ceiling, "Know your keywords already?", "nothing
   runs until you press it" and "Show them anyway". Ported from the mockup (2026-09-14, round 6) -
   it was the one class the first-run page asked for that never landed, so all four rendered at full
   body size and white, which made the page's loudest text its four footnotes. Found by shooting the
   real page at 390px, not by reading the markup. */
.pg-quiet { margin-top:11px; font-size:11.5px; color:var(--text-3); line-height:1.5; }
.pg-quiet a { color:var(--text-3); text-decoration:underline; text-decoration-style:dotted; }
/* The early door to the AI-tool connection, for someone who skipped the rules intro. */
.mcp-line { display:flex; align-items:center; gap:10px; padding:9px 18px;
  border-bottom:1px solid var(--border-subtle); font-size:12.5px; color:var(--text-3); }
.mcp-line a { color:var(--text-2); text-decoration:underline; text-decoration-style:dotted; }
.mcp-line .ic { width:16px; height:16px; border-radius:4px;
  background:linear-gradient(135deg,#8B5CF6,#EC4899); flex:none; }

@media (max-width:640px) {
  /* A pill that has wrapped to three lines is a box, so let it be one. */
  .start-done { border-radius:var(--r-md); }
  .start-h { font-size:22px; }
}

/* ==============================================================================================
   THE STEPS PANEL ON A PHONE (mockup section 22B, picked by the owner 2026-09-14 after seeing the
   walk's 390px screenshots: "on phones the steps panel has so much text near each other, too many
   elements").

   ONE BLOCK FOR BOTH PANELS, and that is the whole design. The small first-run page
   (_first_run_panel.html) and the full six-step panel (project_detail.html) already draw the SAME
   `steps-*` classes, so these rules reach both by saying nothing about which page they are on.
   Scoping them to `#first-run-panel` or `#steps-panel` would have meant two copies of the same five
   rules, drifting the first time one page changed.

   WHAT GOES, AND WHY EACH ONE IS SAFE TO LOSE AT THIS WIDTH:
     .steps-rail-mini   the secondary line under a rail row. The row's own title and chip already
                        say what it is and whether it is your turn.
     .rail-rest         the folded "then five more steps..." paragraph. Replaced, not deleted:
                        `.rail-chip` below carries the same fact in four words.
     .steps-pane-eyebrow  "Step 1 of 6" (the numbered rail row says it) plus, on the full panel, the
                        "Back to step N" link - on a phone the rail sits directly above the pane, so
                        it IS the way back. No price lives here: the first-run page states its
                        ceiling in its own paragraph and every run confirms its cost in a dialog.
     .steps-panel-head p  the "tap a step to see what it does" intro. A paragraph explaining a
                        control the customer can simply tap.
   ============================================================================================== */
/* The phone's version of `.rail-rest`. BOTH are always in the markup and a media query picks one,
   rather than JS rewriting the paragraph: the phone needs FEWER WORDS, which no amount of restyling
   one long sentence can produce. */
.rail-chip { display:none; align-items:center; gap:6px; margin:4px 12px 8px;
  font-size:11.5px; color:var(--text-3); line-height:1.45; }

@media (max-width:560px) {
  .steps-rail-mini { display:none; }
  .rail-rest { display:none; }
  .rail-chip { display:inline-flex; }
  .steps-pane-eyebrow { display:none; }
  .steps-panel-head p { display:none; }
}

/* ==============================================================================================
   WRITTEN MESSAGES AND INACTIVE CREATORS, where people look (launch fixes B5 + B11, 2026-09-17).
   Ported 1:1 from the owner-picked frames in designs/mockups/overlay-and-email-redesign-options.html
   section 11 (its .mc-mchip / .mc-draftbar / .mc-blank / .mc-jump / .mc-dmbox proposal rules).
   Everything else these surfaces wear is the existing .chip / .sg-ai / .banner vocabulary.
   ============================================================================================== */
/* The Message chip (All Creators column + the panel's jump chip) carries a 12px glyph. */
.msg-chip svg { width:12px; height:12px; flex:none; }
/* The jump chip is a real button: it scrolls the panel to the message box. */
.msg-jump-row { display:flex; align-items:center; gap:10px; }
button.chip.msg-jump { border:0; font-family:inherit; cursor:pointer; }
button.chip.msg-jump:hover { filter:brightness(1.2); }
.msg-jump .msg-jump-arrow { width:12px; height:12px; }
/* The targets it scrolls to stay clear of the panel's sticky header. */
#email-section, #dm-card { scroll-margin-top:72px; }
/* The line above a box that starts filled: who wrote it + "Start blank". */
.draft-bar { display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-bottom:8px; min-height:18px; }
.draft-bar .sg-ai { margin-bottom:0; }
.draft-blank { font:inherit; font-size:11.5px; color:var(--text-3); background:none; border:0; padding:0;
  cursor:pointer; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; }
.draft-blank:hover { color:var(--text-1); }
/* The DM box, now editable (it was a paragraph). The global form rules add hover + focus. */
.draft-box { width:100%; background:var(--bg-base); border:1px solid var(--border); border-radius:var(--r-md);
  color:var(--text-1); font:inherit; font-size:13px; line-height:1.55; padding:9px 11px; resize:vertical; }
/* "Inactive 10 mo" under the name in the panel header. */
.panel-inactive { display:inline-block; margin-top:5px; }
/* ...and on the frozen name column of every creators grid: inline on a desktop, under the handle on a
   phone, where the frozen column is too narrow and the chip would otherwise be cut off. */
@media (max-width:640px) {
  .cc-inactive { display:block; margin-top:3px; }
}

/* =================================================================================================
   THE CREATOR FACE (2026-10-21, owner-approved: designs/mockups/creator-photos.html). ONE component,
   drawn by window.creatorFace / window.extFace (base/format-helpers.js) on the creators tables, the
   creator panel header (also the Emails reading pane), its conversation, the Emails list and Drafts.
   The circle ALWAYS paints the coloured initial; a saved photo is an <img> laid ON TOP at a fixed
   size. No photo, a missing file (404) or a slow connection: the initial underneath shows, nothing
   moves, no broken-image icon. A failed photo hides itself through ONE document-level error listener
   (format-helpers.js) that adds .no-img; never an inline onerror (security audit H6/H7).
   Size comes in through --s, the initial's type size through --fs, the corner mark's through --pm.
   ================================================================================================= */
.cface { position:relative; display:inline-flex; flex:none; width:var(--s, 24px); height:var(--s, 24px); }
.cface .ini { width:100%; height:100%; border-radius:50%; display:inline-flex; align-items:center;
  justify-content:center; color:#fff; font-weight:700; line-height:1; font-size:var(--fs, 10px); }
.cface img { position:absolute; inset:0; width:100%; height:100%; border-radius:50%; object-fit:cover; }
/* A faint inner ring, so a dark photo does not melt into the dark page. */
.cface.has-img:not(.no-img)::after { content:""; position:absolute; inset:0; border-radius:50%;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.12); pointer-events:none; }
.cface.no-img img { display:none; }
/* People who are not creators (an outside sender on Emails) get a GREY letter, never a gradient,
   so a stranger never looks like one of your creators. */
.cface.ext .ini { background:var(--bg-hover); color:var(--text-2); }
/* The platform on the face's corner (creator panel, pick B: it replaces the "TikTok" word). */
.cface .pm { position:absolute; right:-3px; bottom:-3px; z-index:2; width:var(--pm, 18px); height:var(--pm, 18px);
  border-radius:5px; display:inline-flex; align-items:center; justify-content:center;
  box-shadow:0 0 0 2px var(--ring, var(--bg-base)), inset 0 0 0 1px rgba(255,255,255,.2); }
.cface .pm svg { width:calc(var(--pm, 18px) * .6); height:calc(var(--pm, 18px) * .6); }
.cface .pm-tiktok { background:#000; }
.cface .pm-instagram { background:linear-gradient(135deg,#F72FE0,#8B3DFF,#4B6EF5); }
.cface .pm-youtube { background:#FF0033; }
/* Emails list (pick A): the face takes the unread dot's old column and the dot sits on its corner. */
.cface-slot { position:relative; display:flex; flex:none; margin-right:12px; }
.cface-udot { position:absolute; left:-3px; top:-2px; z-index:3; width:11px; height:11px; border-radius:50%;
  background:#3B82F6; box-shadow:0 0 0 2px var(--bg-surface); }
/* The creator panel header: face, then the title block. min-width:0 so a long handle still truncates. */
.panel-head-who { display:flex; align-items:center; gap:12px; min-width:0; }
.panel-head-who > .min-w-0 { min-width:0; }
#panel-face:empty { display:none; }

/* =================================================================================================
   THE HOME CARD - one project, its six steps, and what each step made.
   Mockup: designs/mockups/dashboard-reorder-options.html item 13b (#home-v2), option C, picked by
   the owner 2026-09-18. Ported here verbatim from that mockup's own frame CSS: the proportions are
   the thing he approved (six equal columns, the step you are on twice as wide, one calm row), so
   they are not a starting point to improve on.

   Reuses the app's existing pieces rather than restating them: .card / .card-spot / .card-glow for
   the shell, .steps-rail-num for the number pill, .chip-* for the chip, .bar-track/.bar-fill for the
   progress bar, .cm-* for the audience quote, .btn-grad/.btn-outline for the one action.
   ================================================================================================= */
.hm-card { position:relative; display:block; padding:22px 24px; color:inherit; text-decoration:none; }
.hm-card.card-spot { cursor:pointer; transition:border-color .15s, transform .15s; }
.hm-card.card-spot:hover { border-color:var(--border-strong); transform:translateY(-2px); }
/* .card-glow's light is sized for a short card; on a tall one its circle stopped short of its own
   box and drew a hard edge. closest-side keeps the fade inside the box at any height. */
.hm-card.card-glow::after { width:62%; height:140%; top:-50%; right:-18%;
  background:radial-gradient(closest-side,rgba(139,61,255,.18),transparent); }
@media (prefers-reduced-motion:reduce) { .hm-card.card-spot:hover { transform:none; } }
.hm-head { position:relative; display:flex; align-items:flex-start; gap:10px; }
.hm-head .proj-card-head { margin-bottom:6px; }
.hm-head .mono-label { display:block; }
.hm-name { font-size:21px; font-weight:800; letter-spacing:-.02em; line-height:1.2; color:var(--text-1);
  margin-top:6px; }
.hm-card.is-big .hm-name { font-size:26px; }
.hm-desc { font-size:13.5px; color:var(--text-2); margin-top:3px; line-height:1.5; }
/* THE PROJECT DESCRIPTION (2026-10-02, owner pick "Option A, plus the website source row",
   designs/mockups/project-header-mailbox-options.html). ONE split, app/services/project_description.py,
   feeds this card and the project page header, so they never disagree; markup for the header is
   app/templates/_project_description.html. The card shows the first sentence of the customer's own
   words, never a website's text, two lines at most. (The folded-card rule above still hides it: three
   classes beat one.) */
.hm-desc { display:-webkit-box; -webkit-line-clamp:2; line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
/* The header: one sentence, then "Full description" (opens #pd-desc-modal), and one chip per website
   setup read, first on the meta line (a link to the site, new tab). */
.pd-desc { max-width:880px; }
.pd-desc-more { font:inherit; font-size:13px; text-decoration:underline; text-underline-offset:2px;
  background:none; border:0; padding:0; cursor:pointer; white-space:nowrap; }
.pd-src-chip { max-width:100%; min-width:0; text-decoration:none; transition:filter .15s; }
.pd-src-chip svg { flex:none; }
.pd-src-chip:hover { filter:brightness(1.3); }
/* Title and buttons share the first row; the sentence and the meta line take the full width under
   both (every option in the mockup). Laptop and up only: below 1024px the header is already one column
   (title, sentence, meta, then the buttons). display:contents lifts the title column's children into
   the grid without moving any markup; the buttons are the header's other child div. */
@media (min-width:1024px) {
  #pd-page-header { display:grid; grid-template-columns:minmax(0,1fr) auto; column-gap:24px; row-gap:0; align-items:start; }
  #pd-page-header > .pd-head-text { display:contents; }
  #pd-page-header > .pd-head-text > * { grid-column:1 / -1; }
  #pd-page-header > .pd-head-text > h1 { grid-column:1; grid-row:1; min-width:0; }
  #pd-page-header > .pd-head-text ~ div { grid-column:2; grid-row:1; }
}
/* The Full description window (a WINDOW: one .ds-modal-x, no Close button). Its source rows are the
   setup's own .ob-src rows, full width and on the darker ground so they read on the elevated card. */
.pd-desc-panel { max-width:576px; }
.pd-desc-head { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px; }
.pd-desc-head .mono-label { display:block; font-size:10px; margin-bottom:3px; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.pd-desc-head h3 { font-size:18px; font-weight:700; color:var(--text-1); letter-spacing:-.01em; }
.ob-src.pd-desc-src { display:flex; max-width:none; background:var(--bg-base); }
.pd-desc-own { font-size:14px; line-height:1.6; color:var(--text-1); margin:10px 2px 20px; white-space:pre-line;
  overflow-wrap:anywhere; }
.pd-desc-site { font-size:12.5px; line-height:1.6; color:var(--text-2); white-space:pre-line; overflow-wrap:anywhere;
  margin:10px 2px 20px; padding-left:12px; border-left:2px solid var(--border); }
.pd-desc-foot { font-size:12.5px; color:var(--text-3); padding-top:12px; border-top:1px solid var(--border-subtle); }
.pd-desc-foot b { color:var(--text-2); font-weight:600; }
/* Rare and destructive actions live behind this, never on the card face (project rule: one primary
   action per row, the footguns in an overflow). */
.hm-more { margin-left:auto; flex:none; width:30px; height:30px; border-radius:8px; display:grid;
  place-items:center; color:var(--text-3); background:none; border:1px solid transparent; cursor:pointer; }
.hm-more:hover, .hm-more.is-open { background:var(--bg-hover); border-color:var(--border-subtle);
  color:var(--text-1); }
.hm-menu { position:absolute; right:0; top:36px; z-index:6; min-width:196px; padding:6px;
  box-shadow:0 20px 50px rgba(0,0,0,.5); }
.hm-menu button { display:flex; align-items:center; gap:9px; width:100%; padding:8px 10px;
  border-radius:8px; font:inherit; font-size:13px; color:var(--text-2); background:none; border:0;
  text-align:left; cursor:pointer; }
.hm-menu button:hover { background:var(--bg-hover); color:var(--text-1); }
.hm-menu .is-danger, .hm-menu .is-danger:hover { color:#FCA5A5; }
.hm-menu .is-danger:hover { background:rgba(239,68,68,.12); }
/* The one line naming the step you are on. Hidden on a desktop, where the lit column says it; it is
   the phone's replacement for that column (see the phone block at the end). */
.hm-stepline { display:none; align-items:center; gap:8px; flex-wrap:wrap; margin-top:12px;
  font-size:13.5px; color:var(--text-2); }
.hm-stepline b { color:var(--text-1); font-weight:650; }
.hm-stepline .rail-live-chip { margin-left:0; }
.hm-next { display:flex; align-items:center; gap:12px 14px; flex-wrap:wrap; margin-top:20px;
  padding-top:18px; border-top:1px solid var(--border-subtle); }
.hm-next .quiet { margin-left:auto; }

/* --- the small pieces each step holds --- */
.hm-pile { display:flex; align-items:center; margin-bottom:10px; }
.hm-pile > span { margin-left:-8px; box-shadow:0 0 0 2px var(--bg-base); }
.hm-pile > span:first-child { margin-left:0; }
.hm-pile .more { height:28px; border-radius:999px; padding:0 10px; display:inline-flex; align-items:center;
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; font-weight:600;
  color:var(--text-2); background:var(--bg-hover); }
.hm-who { display:flex; align-items:center; gap:9px; padding:7px 0; border-top:1px solid var(--border-subtle);
  min-width:0; }
.hm-who .nm { min-width:0; flex:1; line-height:1.3; }
.hm-who .nm b { display:block; font-size:13px; font-weight:600; color:var(--text-1); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.hm-who .nm span { display:block; font-size:11.5px; color:var(--text-3); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.hm-who .sc { font-family:'JetBrains Mono',ui-monospace,monospace; font-weight:600; font-size:13px;
  color:#7EE6A8; }
.hm-andmore { font-size:12px; color:var(--text-3); margin-top:8px; }
.hm-reply { display:flex; gap:9px; align-items:flex-start; }
.hm-bubble { flex:1; min-width:0; background:var(--bg-surface); border:1px solid var(--border);
  border-radius:0 12px 12px 12px; padding:9px 11px; }
.hm-bubble .who { font-size:11.5px; color:var(--text-3); margin-bottom:3px; }
.hm-bubble .who b { color:var(--text-2); font-weight:600; }
.hm-bubble p { font-size:12.5px; color:var(--text-1); line-height:1.5; }
.hm-replymeta { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:10px;
  font-size:12px; color:var(--text-3); }

/* --- a step's figure. Figures above 20px are Inter + tabular, never mono (DESIGN_SYSTEM 15.7). --- */
.hv-fig { display:flex; align-items:baseline; gap:6px 8px; flex-wrap:wrap; margin-top:8px; line-height:1; }
.hv-fig b { font-size:24px; font-weight:800; letter-spacing:-.02em; color:var(--text-1);
  font-variant-numeric:tabular-nums; }
.hv-fig b.is-good { color:#7EE6A8; }
.hv-fig span { font-size:12px; color:var(--text-3); line-height:1.3; }
/* "+46 new": the finished-search band's own new-creator count, as a mark. Never shown with a live
   dot, because /api/jobs/queue carries no creator count - see dashboard.html. */
.hv-new { display:inline-flex; align-items:center; gap:5px; font-size:11.5px; font-weight:650;
  color:#7EE6A8; background:rgba(34,197,94,.12); border-radius:999px; padding:3px 8px; line-height:1.1;
  white-space:nowrap; font-variant-numeric:tabular-nums; }
/* a search phrase with how it last stopped */
.hv-qs { display:flex; flex-wrap:wrap; gap:6px; }
.hv-q { display:inline-flex; align-items:center; gap:7px; border:1px solid var(--border); border-radius:999px;
  padding:4px 10px; font-size:12px; color:var(--text-1); background:var(--bg-surface); max-width:100%;
  min-width:0; }
.hv-q b { font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
.hv-q i { font-style:normal; font-size:11px; color:var(--text-3); white-space:nowrap; }
.hv-q.is-more i { color:#7EE6A8; }
.hv-q.is-rest { border-style:dashed; color:var(--text-3); background:none; }
.hv-prog { display:flex; align-items:center; gap:10px; margin-top:12px; font-size:12px; color:var(--text-2); }
.hv-prog .bar-track { flex:1; height:5px; min-width:48px; }
.hv-prog b { font-weight:650; color:var(--text-1); font-variant-numeric:tabular-nums; white-space:nowrap; }
.hv-prog > span:last-child { white-space:nowrap; }
.hv-fol { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11.5px; color:var(--text-3);
  white-space:nowrap; }
.hv-hours { flex:none; text-align:right; margin-right:6px; }
.hv-hours b { display:block; font-size:26px; font-weight:800; letter-spacing:-.02em; line-height:1;
  color:var(--text-1); font-variant-numeric:tabular-nums; }
.hv-hours b small { font-size:.55em; font-weight:700; color:var(--text-2); margin-left:1px; }
.hv-hours span { display:block; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px;
  letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); margin-top:6px; }
/* the audience quote, on the app's own .cm-* bubble. A quote carries no commenter, so a narrow step
   drops the quote-mark avatar, which would leave no room for the words. */
.hv-quote-bare .cm-body { border-radius:10px; }
.hv-quote-bare .cm-text { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical;
  overflow:hidden; }
.hv-mailmini { background:var(--bg-base); border:1px solid var(--border); border-radius:9px;
  padding:8px 10px; min-width:0; }
.hv-mailmini .to { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--text-3);
  white-space:nowrap; overflow:hidden; }
.hv-mailmini .to b { color:var(--text-2); font-weight:600; overflow:hidden; text-overflow:ellipsis; }
.hv-mailmini .sub { font-size:12px; font-weight:650; color:var(--text-1); margin-top:5px; line-height:1.35;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
/* an address as plain text, not a pill: a narrow step has no room for a pill's padding, and a cut
   address says nothing. */
.hv-email { display:inline-flex; align-items:center; gap:6px; max-width:100%; min-width:0; font-size:12px;
  color:var(--text-2); }
.hv-email svg { flex:none; color:var(--text-3); }
.hv-email span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.hv-rmini .hm-bubble p { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical;
  overflow:hidden; }
.hv-rmini .hm-replymeta { margin-top:8px; }
/* a face with its AI score, small enough for a narrow step */
.hv-scs { display:flex; flex-wrap:wrap; gap:6px 10px; }
.hv-sc { display:inline-flex; align-items:center; gap:6px; }
.hv-sc b { font-family:'JetBrains Mono',ui-monospace,monospace; font-weight:600; font-size:12px;
  color:#7EE6A8; }
/* the gradient-initial avatar, same formula as window.creatorCell, rendered server-side here */
.hv-av { display:inline-flex; align-items:center; justify-content:center; border-radius:999px;
  font-weight:700; color:#fff; flex:none; }

/* --- C: six steps in one line, each column holding what that step made --- */
.hv-spine { position:relative; display:grid; margin:24px -12px 0; }
.hv-spine.has-loop { margin-top:40px; }
.hv-col { position:relative; min-width:0; padding:0 12px; }
.hv-top { position:relative; height:26px; }
.hv-top .steps-rail-num { margin-top:0; }
.hv-top::after { content:""; position:absolute; top:13px; left:34px; right:-16px; height:1px;
  background:var(--border); }
.hv-col:last-child .hv-top::after { display:none; }
.hv-col.ln-ok .hv-top::after { background:rgba(34,197,94,.55); }
.hv-col.is-done .steps-rail-num { color:var(--green); }
.hv-col.is-cur .steps-rail-num { background:var(--accent-soft); color:#C4B5FD;
  box-shadow:0 0 0 2px rgba(139,92,246,.35); }
.hv-col.step-live .steps-rail-num { animation:rail-pulse 2s ease-in-out infinite; }
.hv-lbl { display:flex; align-items:flex-start; gap:6px 7px; flex-wrap:wrap; margin-top:10px;
  min-height:33px; font-size:12.5px; font-weight:600; color:var(--text-2); line-height:1.3; }
.hv-col.is-cur .hv-lbl { color:var(--text-1); font-size:13.5px; }
.hv-lbl .chip { font-size:10.5px; padding:2px 7px; }
.hv-lbl .rail-live-chip { margin-left:0; }
.hv-art { margin-top:12px; min-width:0; }
.hv-col:not(.is-cur) .hv-art { opacity:.82; }
.hv-mini { font-size:12px; color:var(--text-3); margin-top:8px; line-height:1.4; }
/* the step you are on: its own light, NOT a box around it. This is the single most load-bearing
   line in the block - a hard-edged panel here was what made the rejected variant look worse. */
.hv-col.is-cur::before { content:""; position:absolute; left:0; right:0; top:-22px; bottom:-18px;
  background:radial-gradient(closest-side,rgba(139,92,246,.12),transparent); pointer-events:none; }
.hv-col > * { position:relative; }
.hv-col .hm-pile { margin-bottom:0; }
.hv-col .hm-pile > span { box-shadow:0 0 0 2px var(--bg-surface); }
.hv-col .hm-faces { grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 14px; }
.hv-col .hm-who { padding:6px 0; }
/* it comes round again: a hairline arch from the last step back down onto step 1 (never a gradient) */
.hv-loop { position:absolute; top:-22px; left:25px; height:14px; border:1px solid rgba(139,92,246,.42);
  border-bottom:0; border-radius:12px 12px 0 0; pointer-events:none; }
.hv-loop::after { content:""; position:absolute; left:-5px; bottom:-5px; border:4px solid transparent;
  border-top:5px solid rgba(167,139,250,.75); border-bottom:0; }
@media (prefers-reduced-motion:reduce) { .hv-col.step-live .steps-rail-num { animation:none; } }

@media (max-width:640px) {
  .hm-card { padding:18px; display:flex; flex-direction:column; }
  .hm-card.card-glow::after { opacity:.6; }
  /* on a phone the one next step comes straight after where the project is, before the figures */
  .hm-card > * { order:3; }
  .hm-card > .hm-head, .hm-card > .hm-stepline { order:0; }
  .hm-card > .hm-next { order:2; margin-top:16px; padding-top:0; border-top:0; }
  .hm-card.is-big .hm-name { font-size:22px; }
  .hm-stepline { display:flex; }
  .hm-next .btn { width:100%; justify-content:center; }
  .hm-next .quiet { margin-left:0; }
  .hm-head { flex-wrap:wrap; }
  .hv-hours { order:3; flex-basis:100%; text-align:left; display:flex; align-items:baseline; gap:8px;
    margin:10px 0 0; }
  .hv-hours b { font-size:20px; }
  .hv-hours span { margin-top:0; }
  .hm-card > .hv-spine { order:3; }
  .hv-spine, .hv-spine.has-loop { display:flex; flex-direction:column; margin:20px 0 0; }
  .hv-loop { display:none; }
  .hv-col { display:grid; grid-template-columns:26px minmax(0,1fr); column-gap:12px; padding:0 0 16px; }
  .hv-col > .hv-top { grid-column:1; grid-row:1 / span 3; height:auto; }
  .hv-col > :not(.hv-top) { grid-column:2; }
  .hv-top::after { left:13px; right:auto; top:32px; bottom:-16px; width:1px; height:auto; }
  .hv-lbl { margin-top:4px; min-height:0; }
  .hv-fig { margin-top:6px; }
  .hv-col:not(.is-cur) .hv-art { display:none; }
  .hv-col:not(.is-cur) .hv-fig b { font-size:17px; }
  /* A STEP THAT IS NOT YOUR TURN IS ONE LINE: its name on the left, its number on the right, instead
     of the number wrapping onto a second line under the name. Five finished steps cost about 300px
     of scrolling stacked and about 200px on one line, and on a phone that is the difference between
     reaching the step you are on with a flick or with two. DESIGN_SYSTEM 18.4d: fold, do not shrink -
     the type size is untouched, it is the number of LINES that goes down. .hv-top keeps its row span
     so the vertical connector between steps still draws. */
  .hv-col:not(.is-cur) { grid-template-columns:26px minmax(0,1fr) auto; column-gap:12px;
    padding-bottom:13px; }
  .hv-col:not(.is-cur) > .hv-lbl { grid-column:2; grid-row:1; margin-top:3px; }
  .hv-col:not(.is-cur) > .hv-fig { grid-column:3; grid-row:1; margin-top:0; justify-content:flex-end;
    flex-wrap:nowrap; }
  .hv-col:not(.is-cur) .hv-fig span { max-width:96px; text-align:right; }
  .hv-col.is-cur::before { left:-14px; right:-14px; top:-10px; bottom:4px; }
}

/* ==============================================================================================
   THE RUN WINDOW  (app/templates/_run_window.html)
   The dialog a customer sees before a run starts: who it covers, what it costs, how long it takes.
   Ported 1:1 from designs/mockups/run-a-step-audit.html section 3 (owner-approved over six review
   rounds, SESSION_HANDOFF_2026-09-20_UX_STAGE_DISCOVERABILITY.md 6d).

   IT EXTENDS showConfirm, IT DOES NOT REPLACE IT. The two choice rows are the app's own
   .ds-pick-opt with its real selected ring; only the icon tile, the price/time pair and the plan
   rows underneath are new. Nothing here redefines a .ds-pick-* rule - if a choice row ever looks
   wrong, the bug is in the block near .ds-pick above, not here.
   ============================================================================================== */

/* --- A DIALOG NEVER GROWS PAST THE SCREEN (owner's first review: "dialogs that never grow past
   the screen"). That is now the app-wide .ds-modal standard (see DIALOGS near .ds-form, and
   DESIGN_SYSTEM.md §15.5), which showConfirm uses like every other dialog: the card caps at the
   screen and only its .ds-modal-body middle scrolls, the buttons sitting below it. This block's
   own .cf-card cap (a whole-card scroll with a sticky footer) was folded into that the same day,
   because two height rules on one card is how the old per-dialog patches drifted apart.
   What survives here is only what the standard does not say: */
/* The hairline appears ONLY on a dialog whose middle really overflows (showConfirm measures it and
   adds .is-scroll). Drawing it always would put a new line above the buttons of every dialog in
   the app for no reason. Full-bleed (the card's p-5 is 20px), so the line reaches both edges. */
.cf-foot.is-scroll { padding:12px 20px 0; margin:4px -20px 0; border-top:1px solid var(--border-subtle); }
/* The run window (showConfirm opts.wide) is 470px, the width of its approved drawing, not
   max-w-md's 448. The three fixed number columns take 277px of the plan row, so at 448 "Write the
   outreach" plus its paper-plane mark wrapped the mark onto a line of its own. Two classes to
   out-rank Tailwind's single .max-w-md whatever order the two stylesheets load in. */
.ds-modal-panel.cf-wide { max-width:470px; }
/* ON A PHONE the run window's Cancel / Start are thumb targets (44px, MOBILE_RESPONSIVENESS.md). The
   shared footer's compact buttons measured 25px tall at 390px (2026-10-01, step 1 in the run window).
   Scoped to the run window on purpose: changing every showConfirm footer is a separate, wider call.
   Three classes plus the element so it out-ranks the buttons' own !py-1.5 utility. */
@media (max-width: 560px) {
  .ds-modal-panel.cf-wide .cf-foot button.btn { min-height:44px; padding-left:16px !important; padding-right:16px !important; }
}
/* The chosen option's 18px glow would otherwise push a horizontal scrollbar onto the middle the
   moment it scrolls (a scroll box clips on both axes). */
.ds-modal-top .ds-modal-body { overflow-x:hidden; }

/* --- "WHICH PLAN DO YOU WANT TO USE?" (onboarding, obPlanChoice; owner's pick D2 "the saved plan as
   a small receipt", designs/mockups/replace-plan-dialog.html, 2026-10-01). Two cards in one shell,
   each with its OWN button (showConfirm `answersInBody`, so there is no footer). Deliberately not the
   .ds-pick card picker: a picker is tick-then-confirm, and these are two doors. No gradient anywhere
   (that was D1): the two answers weigh the same, so nothing on either card may read as the
   recommended one. Values ported from the mockup's .pv-* rules onto the app's tokens. */
.rp-pair { margin-top:8px; }
.rp { border:1px solid var(--border); border-radius:14px; background:var(--bg-surface);
      padding:15px; display:flex; flex-direction:column; min-width:0; }
.rp + .rp { margin-top:10px; }
.rp-top { display:flex; align-items:center; gap:9px; margin-bottom:11px; min-width:0; }
.rp-mk { width:26px; height:26px; border-radius:8px; flex:0 0 26px; display:flex; align-items:center;
         justify-content:center; background:rgba(139,61,255,.14); color:#C7A5FF; }
.rp-mk svg { width:14px; height:14px; }
.rp-ttl { font-size:10.5px; font-weight:700; letter-spacing:.07em; text-transform:uppercase; color:var(--text-3);
          min-width:0; }
/* The count is the only state the saved plan shows: plans are saved the moment they are shown, so a
   "Ready" badge would be a promise we cannot keep. */
.rp-cnt { margin-left:auto; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11.5px;
          color:var(--text-2); white-space:nowrap; flex:none; }
/* overflow-wrap:anywhere on everything a customer typed: one pasted link with no spaces would
   otherwise push the card, and the whole dialog, wider than a 390px phone. */
.rp-nm { font-size:15px; font-weight:600; line-height:1.35; margin:0; color:var(--text-1); overflow-wrap:anywhere;
         display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2; overflow:hidden; }
.rp-words { font-size:12.5px; color:var(--text-2); line-height:1.55; margin:7px 0 0; overflow-wrap:anywhere;
            display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2; overflow:hidden; }
/* What they just typed, when no product name is known yet (nothing has read it): their own words are
   the card's subject, so they are ink, not the grey of a sub-line, and four lines at most. */
.rp-said { font-size:13.5px; color:var(--text-1); line-height:1.5; margin:0; overflow-wrap:anywhere;
           white-space:pre-line;
           display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:4; line-clamp:4; overflow:hidden; }
.rp-rule { height:1px; background:var(--border); margin:12px 0; }
.rp-tick { display:flex; flex-direction:column; gap:6px; min-width:0; }
.rp-tick > span { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-2); min-width:0; }
.rp-tick svg { width:12px; height:12px; flex:0 0 12px; color:#7EE6A8; }
.rp-term { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rp-tick > .rp-more { color:var(--text-3); padding-left:20px; }
.rp .rp-btn { width:100%; margin-top:14px; justify-content:center; padding:8px 14px; font-size:13px; }
/* A finger, not a cursor: 44px is the smallest target a phone can hit reliably (DESIGN_SYSTEM 18). */
@media (max-width:640px) { .rp .rp-btn { min-height:44px; } }

/* --- THE TWO CHOICES: an icon tile, then price and time on BOTH rows, so "this step only" and
   "keep going" compare at a glance. (Before the mockup's third round the price sat in a box above
   the choices and vanished the moment "keep going" was picked.) */
.ds-pick-opt .ds-pick-ico { width:22px; height:22px; border-radius:7px; flex:0 0 22px;
  background:var(--bg-hover) center/11px no-repeat; }
.ds-pick-opt .ds-pick-ico.one { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 2l7 4-7 4z' fill='%23C4B5FD'/%3E%3C/svg%3E"); }
.ds-pick-opt .ds-pick-ico.fly { background:rgb(var(--c-special) / .2) center/13px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%23C084FC'/%3E%3Cpath d='M22 2 L11 13 L2 9 Z' fill='%23fff' opacity='.55'/%3E%3C/svg%3E"); }
/* The icon pushes the label 30px right, so the explanation line under it has to move with it or it
   reads as belonging to the dot instead of the label. */
.ds-pick-opt.has-ico .ds-pick-sub { margin-left:53px; }
.pk-row { flex-wrap:wrap; row-gap:4px; }
.pk-amt { margin-left:auto; display:flex; gap:12px; align-items:baseline; white-space:nowrap;
  font-family:'JetBrains Mono',monospace; font-size:11.5px; color:var(--text-3); }
.pk-amt b { color:var(--text-1); font-weight:600; }
/* ON A PHONE a price with its band ("about 2,013 credits, usually 1,031 to 4,216") is wider than the
   option and ran off its right edge, clipped mid-word (2026-10-01, seen at 390px on step 1's window;
   steps 2 to 5 had it too whenever a band showed). It wraps there instead, right-aligned. */
@media (max-width: 560px) {
  .pk-amt { white-space:normal; flex-wrap:wrap; justify-content:flex-end; row-gap:2px; text-align:right; max-width:100%; }
}

/* --- THE PLAN UNDER "KEEP GOING": one row per step, three FIXED number columns under their own
   headers (owner's fourth review: "there is no way to know what these numbers mean"). */
.pr { display:flex; align-items:center; gap:10px; padding:8px 6px; margin:0 -6px;
  border-radius:var(--r-md); font-size:13px; color:var(--text-1); }
.pr[data-far] { cursor:pointer; }
.pr[data-far]:hover { background:rgb(var(--c-ink) / .04); }
.pr .steps-rail-num { width:22px; height:22px; font-size:10.5px; border-radius:7px; }
.pr .pr-nm { flex:1; min-width:0; font-weight:600; }
.pr .pr-am { display:flex; gap:10px; font-family:'JetBrains Mono',monospace; font-size:11.5px;
  color:var(--text-3); white-space:nowrap; font-variant-numeric:tabular-nums; }
.pr .pr-am i { font-style:normal; display:inline-block; text-align:right; }
.pr .pr-am i:nth-child(1) { width:56px; }
.pr .pr-am i:nth-child(2) { width:74px; }
.pr .pr-am i:nth-child(3) { width:58px; }
.pr .pr-am b { color:var(--text-1); font-weight:600; }
.pr.hd { padding-top:0; padding-bottom:2px; cursor:default; }
.pr.hd:hover { background:none; }
.pr.hd .pr-am i { font-size:9px; letter-spacing:.12em; text-transform:uppercase; color:var(--text-3); }
.pr.off { color:var(--text-3); }
.pr.off .pr-nm { font-weight:500; }
.pr.off .pr-am b { color:var(--text-3); font-weight:500; }
.pr.off .steps-rail-num { opacity:.6; }
/* The step that still needs an answer from the person. THE ONLY AMBER IN THIS WHOLE FILE'S BLOCK,
   and it is correct here because it means "something needs you", not "a rule left someone out".
   A rule leaving creators out is the product working - that stays grey (.rf-kept below). */
/* Selector must out-rank `.pr .pr-am i:nth-child(1)` (width:56px). As `.pr .pr-ask` it lost, so
   "needs one answer" was squeezed into the 56px creators column and ran off the card's right edge
   (caught by tools/shooter/shoot_run_window.mjs, the 'ask' shot). */
.pr .pr-am i.pr-ask { width:auto; color:#FBD375; }
.pr-tick { flex:none; width:17px; height:17px; border-radius:5px; border:1.5px solid var(--border-strong);
  background:transparent; }
.pr.on .pr-tick { border-color:transparent; background:var(--accent) center/11px no-repeat
  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 13 4 4L19 7'/%3E%3C/svg%3E"); }
/* The first row is the step you clicked: it always runs, so its tick is filled but muted rather
   than absent - an empty box there would read as "this one is off". */
.pr.lock .pr-tick { background-color:rgb(var(--c-accent) / .45); }
.pr .fly { display:inline-block; width:14px; height:14px; margin-left:7px; vertical-align:-2px;
  background:center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%23C084FC'/%3E%3Cpath d='M22 2 L11 13 L2 9 Z' fill='%23fff' opacity='.55'/%3E%3C/svg%3E"); }
.pr-fine { font-size:12px; color:var(--text-3); margin:10px 0 0; line-height:1.55; }

/* --- THE ONE-TIME QUESTION step 5 cannot start without (outreach_dm_policy). Amber, deliberately:
   this is the one state in the run window where something genuinely needs the person. */
.pl-ask { margin:10px 0 0 23px; padding:10px 12px; border-radius:var(--r-md);
  border:1px solid rgba(251,191,36,.28); background:rgba(251,191,36,.06); font-size:12.5px;
  color:var(--text-2); }
.pl-ask b { color:#FBD375; font-weight:650; }
.pl-ask .two { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; }
.pl-ask .two span { border:1px solid var(--edge); border-radius:var(--r-pill); padding:3px 11px;
  color:var(--text-1); cursor:pointer; }
.pl-ask .two span.on { border-color:var(--accent); background:rgb(var(--c-accent) / .16); }

/* --- THE FACTS BOX (who the run covers, and the two money lines when credits cover only part). */
.rf-facts { border:1px solid var(--border-subtle); border-radius:var(--r-md); margin:4px 0 14px; }
.rf-facts > div { display:flex; justify-content:space-between; gap:14px; padding:9px 12px;
  font-size:13px; border-bottom:1px solid var(--border-subtle); }
.rf-facts > div:last-child { border-bottom:none; }
.rf-facts span { color:var(--text-3); }
.rf-facts b { font-weight:600; text-align:right; }
.rf-line { font-size:13px; color:var(--text-2); margin:14px 0 0; line-height:1.5; }
.rf-link { color:#A8C7FF; text-decoration:underline; text-decoration-style:dotted; cursor:pointer;
  text-underline-offset:3px; }
/* One row per reason nobody here can move. Grey, never amber: a rule that filtered creators out is
   the product doing its job. */
.rf-reason { display:grid; grid-template-columns:34px 1fr; gap:4px 8px; font-size:13px; padding:9px 0;
  border-top:1px solid var(--border-subtle); color:var(--text-2); }
.rf-reason b { font-family:'JetBrains Mono',monospace; color:var(--text-1); }
/* THE FILTER LINE, a win and not a warning: grey shield, "Filtered out N not worth your credits".
   Defined here rather than in wave 3 so the window and the project card cannot end up with two
   versions of it. Nothing in the window renders it yet - the estimate endpoint does not return a
   held-back count (see launch-tests/agent-reports/wave2-W1.md). */
.rf-kept { display:flex; align-items:flex-start; gap:8px; font-size:12.5px; color:var(--text-3);
  margin:14px 0 0; line-height:1.5; }
.rf-kept::before { content:""; flex:none; width:15px; height:15px; margin-top:1px;
  background:center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23A1A1AA' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E%3C/svg%3E"); }
.rf-kept b { color:var(--text-2); font-weight:600; }
.rf-kept .rf-link { color:var(--text-2); }

@media (max-width:600px) {
  /* On a phone the three number columns drop UNDER the step name instead of squeezing it into
     three lines. DESIGN_SYSTEM 18.4d: fold, do not shrink. */
  .pr { flex-wrap:wrap; row-gap:3px; }
  .pr .pr-nm { flex:1 1 140px; }
  /* 59px lines the numbers up under the step name. 208px = the three fixed columns (56+74+58) plus
     two 10px gaps; the columns cannot shrink below their set widths, so at 320px (row content 246px)
     a flat 59px pushed the time column 21px into the card's padding. The indent now gives way first,
     and only as much as needed: 59px from ~360px up, 38px at 320. Change a column width, change 208. */
  .pr .pr-am { flex:1 0 100%; padding-left:max(0px, min(59px, calc(100% - 208px))); text-align:left; }
  .ds-pick-opt.has-ico .ds-pick-sub { margin-left:0; }
  .pl-ask { margin-left:0; }
}

/* --- THE RUNNING BANNER (window.runBanner, _run_window.html): section 3b of the approved drawing.
   The track IS the dashboard card's spine (.hv-spine/.hv-col/.hv-fig/.hv-mini above), reused as it
   stands; only the card, its top row and the tk-* step states below are new. Blue while it runs
   (the app's info tint), amber when YOU stopped it, red only when something failed. */
.rb { position:relative; border:1px solid rgb(var(--c-info) / .35); background:rgb(var(--c-info) / .07);
  border-radius:var(--r-md); padding:16px 18px; margin-bottom:10px; }
.rb.rb-wait { border-color:var(--border); background:var(--bg-surface); }
.rb.stop { border-color:rgba(251,191,36,.4); background:rgba(251,191,36,.06); }
.rb.bad { border-color:rgba(239,68,68,.4); background:rgba(239,68,68,.06); }
.rb-top { display:flex; align-items:center; gap:10px 12px; flex-wrap:wrap; }
.rb-who { font-size:12.5px; color:var(--text-2); min-width:0; }
/* The ONE big figure while it runs: time left. */
.rb-time { margin-left:auto; text-align:right; line-height:1; }
.rb-time b { font-size:22px; font-weight:800; letter-spacing:-.02em; color:var(--text-1);
  font-variant-numeric:tabular-nums; }
.rb-time span { display:block; font-family:'JetBrains Mono',monospace; font-size:9.5px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--text-3); margin-top:5px; }
.rb-act { font-size:12.5px; background:none; border:0; padding:0; font-family:inherit; }
.rb-h { font-size:14.5px; font-weight:650; margin:0 0 8px; color:var(--text-1); padding-right:28px; }
.rb-wait .rb-h { font-size:14px; margin:14px 0 4px; padding-right:0; }
.rb-sub { font-size:12.5px; color:var(--text-2); margin:0; }
.rb-leave { font-size:12px; color:var(--text-3); margin:16px 0 0; }
.rb-meta { font-size:12px; color:var(--text-3); margin:16px 0 12px; }
.rb-foot { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
.rb-go { padding:6px 11px; font-size:12.5px; }
/* Dismiss, for a stopped banner only - a running one has Stop, never a way to hide a run. */
.rb-x { position:absolute; top:8px; right:10px; width:26px; height:26px; border-radius:var(--r-sm);
  border:0; background:none; color:var(--text-3); font-size:18px; line-height:1; cursor:pointer; }
.rb-x:hover { background:var(--bg-hover); color:var(--text-1); }
.rb .hv-spine.rb-track { margin:18px -12px 0; }
.rb .rb-bar { margin-top:9px; height:5px; }
.hv-col.tk-dim .hv-fig b, .hv-col.tk-dim .hv-lbl { color:var(--text-3); }
.hv-col.tk-dim .hv-mini, .hv-col.tk-dim .hv-fig span { opacity:.7; }
/* Where a run stopped: amber = you stopped it (something needs you: carry on or not), red = a step
   failed. Never amber for a rule that left creators out - that is the product working. */
.hv-col.tk-stop .steps-rail-num { background:rgba(251,191,36,.16); color:#FBD375;
  box-shadow:0 0 0 2px rgba(251,191,36,.35); }
.hv-col.tk-bad .steps-rail-num { background:rgba(239,68,68,.16); color:#FCA5A5;
  box-shadow:0 0 0 2px rgba(239,68,68,.35); }
/* The plane rides the current step's connector at the progress point: this run keeps going by
   itself. Plane alone = one flight; the plane in an orbit is autopilot (wave 4), never this. */
.tk-plane { position:absolute; top:4px; width:18px; height:18px; margin-left:-9px; z-index:2; pointer-events:none;
  background:center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg transform='rotate(45 12 12)'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%23C084FC'/%3E%3Cpath d='M22 2 L11 13 L2 9 Z' fill='%23fff' opacity='.55'/%3E%3C/g%3E%3C/svg%3E");
  filter:drop-shadow(0 0 6px rgba(192,132,252,.7)); transition:left .6s linear; }
@media (prefers-reduced-motion:reduce) { .tk-plane { transition:none; } }
/* Time left and Stop travel as ONE group, so a narrow banner never strands "Stop" on a line of its
   own away from the run it stops. */
.rb-end { margin-left:auto; display:flex; align-items:center; gap:12px; }
@media (max-width:640px) {
  /* On a phone the spine is the DASHBOARD'S OWN phone layout (steps stacked, figure on the right,
     the .hv-* @media block above) - the real component wins over the drawing, which kept four
     squeezed columns. So nothing here re-styles the columns; it only undoes the desktop margins,
     which pushed the stacked track 6px past the card. The plane rides a HORIZONTAL connector, so it
     has nowhere honest to sit on a stacked list: the live step's pulse carries "this one" instead. */
  .rb { padding:14px; }
  .rb .hv-spine.rb-track { margin:16px 0 0; }
  .rb .tk-plane { display:none; }
}

/* ======================================================================================
   AUTOPILOT MARK + SIDE-MENU WIDGET (wave 3, agent A1). Markup: app/templates/_autopilot_mark.html
   (Jinja macro ap_mark + window.apMark, one shape). Ported 1:1 from designs/mockups/
   run-a-step-audit.html section 5 (".ap-chip" v2 + ".sb-ap"); class names are namespaced
   (.is-working not .work, .ap-side-* not .sb-ap .top) because bare .off/.paused already style
   other things here (.pr.off).
   - Plane IN AN ORBIT = standing autopilot. Plane ALONE (.tk-plane above, rotated, no ring) = one
     run you started. Keep them visibly different: that difference is the whole meaning.
   - WORKING = the ring BREATHES (box-shadow pulse, the idea of the dashboard's rail-pulse).
     NOTHING TURNS: three turning versions failed at 16px and the owner called it off. No fourth.
   - Purple is --c-special ("the app did this by itself"), so green keeps meaning "done". The light
     tints below are the mockup's; they are scoped custom properties because :root is not this
     block's to edit. Nothing outside this block reads them.
   ====================================================================================== */
.ap-chip, .ap-side { --ap-ink:#D8B4FE; --ap-ink-hi:#E9D5FF; --ap-ring:rgba(192,132,252,.7);
  --ap-amber:#FBD375; --ap-amber-deep:#FBBF24; }
.ap-chip { position:relative; display:inline-flex; align-items:center; gap:6px; border-radius:var(--r-pill);
  padding:3px 11px 3px 27px; font-size:12px; font-weight:650; line-height:1.3; white-space:nowrap;
  background:rgb(var(--c-special) / .16); color:var(--ap-ink); border:1px solid rgb(var(--c-special) / .42); }
/* the plane (two-tone, un-rotated: it flies up-right inside its orbit) */
.ap-chip::before { content:""; position:absolute; left:6px; top:50%; width:16px; height:16px; margin-top:-8px;
  background:center/10px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%23D8B4FE'/%3E%3Cpath d='M22 2 L11 13 L2 9 Z' fill='%23fff' opacity='.5'/%3E%3C/svg%3E"); }
/* the orbit: one whole thin ring, never a gapped or spinning one */
.ap-chip::after { content:""; position:absolute; left:6px; top:50%; width:16px; height:16px; margin-top:-8px;
  box-sizing:border-box; border-radius:50%; border:1.5px solid var(--ap-ring); }
.ap-chip.is-working::after { animation:ap-breathe 1.8s ease-out infinite; }
@keyframes ap-breathe { 0% { box-shadow:0 0 0 0 rgba(192,132,252,.55); } 100% { box-shadow:0 0 0 7px rgba(192,132,252,0); } }
.ap-chip.is-paused { background:rgba(251,191,36,.14); color:var(--ap-amber); border-color:rgba(251,191,36,.45); }
.ap-chip.is-paused::before { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%23FBD375'/%3E%3C/svg%3E"); }
.ap-chip.is-paused::after { border-color:rgba(251,215,117,.75); border-style:dashed; }
.ap-chip.is-off { background:var(--bg-hover); color:var(--text-3); border-color:var(--border); }
.ap-chip.is-off::before { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M22 2 L15 22 L11 13 L2 9 Z' fill='%2371717a'/%3E%3C/svg%3E"); }
.ap-chip.is-off::after { display:none; }
/* Reduced motion: no breathing. A still, faint halo keeps "working" distinguishable from "on"
   (the words say it too: "Autopilot · working" / "Working"). Added beyond the mockup on purpose. */
@media (prefers-reduced-motion:reduce) {
  .ap-chip.is-working::after { animation:none; box-shadow:0 0 0 3px rgba(192,132,252,.28); }
}

/* The side-menu widget, directly above Credits (mockup ".sb-ap"). margin-bottom 6px, not the
   drawing's 4px: it matches the hours-saved card above it (.hsw), so the stack keeps one rhythm. */
.ap-side { display:block; position:relative; border-radius:var(--r-md); padding:10px 11px; margin-bottom:6px;
  border:1px solid rgb(var(--c-special) / .38);
  background:linear-gradient(160deg, rgb(var(--c-special) / .14), rgb(var(--c-special) / .03) 70%); }
.ap-side.is-actionable { cursor:pointer; }
.ap-side.is-actionable:hover { border-color:rgb(var(--c-special) / .6); }
.ap-side.is-actionable:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.ap-side-top { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.ap-side-top .ap-chip { border:none; background:none; padding-left:24px; font-size:13px; color:var(--ap-ink-hi); }
.ap-side-top .ap-chip::before, .ap-side-top .ap-chip::after { left:0; }
.ap-side-state { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9.5px; letter-spacing:.12em;
  text-transform:uppercase; color:var(--ap-ink); }
.ap-side-ln { font-size:11.5px; color:var(--text-3); margin-top:6px; line-height:1.4; font-variant-numeric:tabular-nums; }
.ap-side-ln b { color:var(--text-1); font-weight:600; }
.ap-side-mt { height:3px; border-radius:2px; background:rgb(var(--c-ink) / .1); margin-top:7px; overflow:hidden; }
.ap-side-mt i { display:block; height:100%; background:#C084FC; border-radius:2px; }
.ap-side.is-off { border-color:var(--border-subtle); background:none; }
.ap-side.is-off .ap-side-top .ap-chip { color:var(--text-2); }
.ap-side.is-off .ap-side-state { color:var(--text-3); }
.ap-side.is-paused { border-color:rgba(251,191,36,.4); background:linear-gradient(160deg, rgba(251,191,36,.10), transparent 70%); }
.ap-side.is-paused .ap-side-top .ap-chip, .ap-side.is-paused .ap-side-state { color:var(--ap-amber); }
.ap-side.is-paused .ap-side-mt i { background:var(--ap-amber-deep); }

/* ==================================================================================================
   RUN A STEP, BUILD WAVE 3 (agent P): the status card, its one-row map, the slide-in step details
   and the table's run strip. Design: designs/mockups/run-a-step-audit.html sections 1, 1b and 2
   (round 11 for the rules panel). Owner decisions: SESSION_HANDOFF_2026-09-20_UX_STAGE_DISCOVERABILITY
   §6c/§6d/§6f. These classes are NEW (pc- = project card, sd- = step drawer); the ported ones they sit
   beside (.btn-run, .rf-kept, .rf-link, .rul-*, .steps-*) keep their own rules above.
   ================================================================================================== */

/* ---- ONE run button, two volumes (mockup "The run button, six ways", pick A) --------------------
   .loud is the single action the app recommends RIGHT NOW (the status card). Never two loud buttons
   on one screen. .dim is a step in the run strip that is not the first one with work; .none is a
   step with nobody waiting in the current view (it cannot be pressed). */
.btn-run.loud { background:var(--grad); border-color:transparent; color:#fff; font-weight:700;
  text-shadow:0 1px 3px rgba(0,0,0,.55); box-shadow:var(--grad-glow); }
.btn-run.loud:hover { background:var(--grad); filter:brightness(1.08); }
.btn-run.loud::before { background-color:#fff;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 2l7 4-7 4z' fill='%238B3DFF'/%3E%3C/svg%3E"); }
.btn-run.dim { background:transparent; border-color:var(--edge); }
.btn-run.dim::before { background-color:var(--bg-hover);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 2l7 4-7 4z' fill='%23a1a1aa'/%3E%3C/svg%3E"); }
.btn-run.dim:hover { border-color:var(--accent); }
.btn-run.none, .btn-run.none:disabled { opacity:.38; cursor:default; background:transparent; border-color:var(--edge); }
.btn-run.none::before { display:none; }
/* The step, said as a quiet WORD inside the button ("STEP 2  Judge 64"), not a second icon: a small
   number in a circle beside a label is the universal COUNT badge, so it read as two numbers. The
   word comes from t('pc_step_n') in markup, never from CSS content, so it translates. */
.pc-stepw { font-size:9.5px; letter-spacing:.12em; text-transform:uppercase; font-weight:700;
  color:var(--text-3); white-space:nowrap; }
.btn-run.loud .pc-stepw { color:rgba(255,255,255,.8); }

/* ---- THE STATUS CARD ---------------------------------------------------------------------------- */
.pc-card { padding:22px 24px; }
.pc-top { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
.pc-top .mono-label { margin:0; }
/* Who the card is about ("Everyone in this project"). The mockup's .rf-cap, which app.css never had. */
.pc-cap { font-size:11.5px; color:var(--text-3); }
.pc-filter { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:10px 0 0; font-size:12px; }
.pc-filter[hidden] { display:none; }
.pc-filter .chip { font-size:11px; }
.pc-head { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-top:11px; }
.pc-n { font-size:38px; font-weight:900; letter-spacing:-.035em; line-height:1; color:var(--text-1);
  font-variant-numeric:tabular-nums; }
.pc-n:empty { display:none; }
.pc-say { font-size:17px; font-weight:600; color:var(--text-1); }
/* An "open" card (step 1, step 6, nobody waiting) leads with the step's own instruction, which is a
   sentence rather than a count's caption, so it is set at reading size rather than headline size. */
.pc-card[data-pc-count="0"] .pc-say { font-size:14px; font-weight:500; color:var(--text-2); line-height:1.55; }
.pc-what { font-size:13.5px; color:var(--text-2); margin:9px 0 0; line-height:1.55; max-width:70ch; }
.pc-act { margin-top:18px; display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
.pc-cost { font-size:12.5px; color:var(--text-3); font-variant-numeric:tabular-nums; }
.pc-linkbtn { background:none; border:0; padding:0; font:inherit; }
.pc-last { font-size:12.5px; color:var(--text-3); margin:12px 0 0; line-height:1.5; }
.pc-last-k { color:var(--text-2); font-weight:600; }
.pc-last .rf-link { color:var(--text-2); }

/* The one-row map. STATUS, not run buttons: a cell opens that step's details. Ported from the mockup's
   .rf-map/.rf-mapcell; the number circle is the same mark the old rail used. */
.pc-map { display:flex; flex-wrap:wrap; margin-top:22px; padding-top:18px; border-top:1px solid var(--border-subtle); }
/* A <button> centres its content vertically by default, which sank every cell whose neighbour had a
   two-line status. Column flex from the top keeps the seven bars on one line. */
.pc-cell { flex:1 1 128px; min-width:128px; padding:0 12px 10px 0; background:none; border:0; text-align:left;
  font:inherit; color:inherit; cursor:pointer; border-radius:var(--r-sm);
  display:flex; flex-direction:column; align-items:stretch; justify-content:flex-start; }
.pc-cell:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.pc-bar { display:block; height:2px; border-radius:2px; background:var(--bg-hover); margin-bottom:9px; }
.pc-cell.is-done .pc-bar { background:var(--green); }
.pc-cell.is-now .pc-bar { background:var(--accent); }
.pc-cell.is-ask .pc-bar { background:var(--amber); }
.pc-nm { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--text-2); }
.pc-num { display:inline-flex; align-items:center; justify-content:center; width:17px; height:17px; flex:none;
  border-radius:50%; border:1px solid var(--border-strong); font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:10px; font-weight:700; color:var(--text-2); }
.pc-cell.is-done .pc-num { color:var(--green); }
.pc-cell.is-now .pc-nm { color:var(--text-1); font-weight:650; }
.pc-cell.is-now .pc-num { border-color:var(--accent); background:var(--accent); color:#fff; }
.pc-cell:hover .pc-nm, .pc-cell.is-sel .pc-nm { color:var(--text-1); }
.pc-ct { display:block; font-size:12px; color:var(--text-3); margin:3px 0 0 23px; line-height:1.4; }
.pc-cell[data-pc-cell="0"] .pc-ct { margin-left:0; }
.pc-cell.is-now .pc-ct { color:#C7A5FF; }
.pc-cell.is-ask .pc-ct { color:#FBD375; }
/* The cell whose details are open in the slide-in. */
.pc-cell.is-sel .pc-bar { box-shadow:0 0 0 1px rgb(var(--c-ink) / .25); }
@media (max-width:560px) {
  /* fold, don't shrink (DESIGN_SYSTEM 18.4d): two cells a row, same type sizes */
  .pc-card { padding:18px 16px; }
  .pc-n { font-size:32px; }
  .pc-say { font-size:15.5px; }
  .pc-cell { flex:1 1 44%; min-width:0; }
}

/* ---- THE SLIDE-IN: a step's details, and "Your rules" -------------------------------------------
   The same component the creator overlay already taught (#creator-panel, base.html: 480px, right
   edge, corner glow, full-screen sheet on a phone per DESIGN_SYSTEM 18.3). NO scrim on purpose: the
   rules panel's whole loop is "change a number and watch the card's count change", so the card has to
   stay visible beside it. It sits under the creator overlay (z 80) and every dialog (z 120). */
#step-drawer { position:fixed; top:0; right:0; bottom:0; width:480px; max-width:100%; z-index:75;
  background:var(--bg-surface); border-left:1px solid var(--border); overflow-y:auto; overscroll-behavior:contain;
  box-shadow:-30px 0 80px rgba(0,0,0,.5); transform:translateX(100%); visibility:hidden;
  transition:transform .18s ease, visibility 0s linear .18s; }
#step-drawer.is-open { transform:none; visibility:visible; transition:transform .18s ease, visibility 0s; }
@media (prefers-reduced-motion:reduce) { #step-drawer, #step-drawer.is-open { transition:none; } }
.sd-glow { pointer-events:none; position:absolute; top:0; right:0; width:288px; height:160px;
  background:radial-gradient(120% 140% at 100% 0%,rgba(139,61,255,.22),transparent 60%); }
/* THE PINNED BAR (2026-09-23; markup + reasons in project_detail.html above .sd-bar, the design is
   pick B of designs/mockups/run-a-step-audit.html #step-nav): previous, the route, next, close.
   position:sticky works because #step-drawer itself is the scrolling box. See-through over the glow
   at the top; solid once the pane title has scrolled under it (#step-drawer.is-stuck). */
.sd-bar { position:sticky; top:0; z-index:4; display:flex; align-items:center; gap:10px; padding:14px 14px 12px 21px;
  border-bottom:1px solid transparent; transition:background .15s, border-color .15s; }
#step-drawer.is-stuck .sd-bar { background:rgb(var(--c-surface) / .94); backdrop-filter:blur(8px);
  border-bottom-color:var(--border-subtle); }
.sd-arrow { flex:none; width:28px; height:28px; border-radius:8px; display:grid; place-items:center; padding:0;
  background:none; border:1px solid var(--border); color:var(--text-2); cursor:pointer; }
.sd-arrow:hover { border-color:var(--border-strong); background:var(--bg-hover); color:var(--text-1); }
.sd-arrow[disabled] { opacity:.3; cursor:default; background:none; border-color:var(--border); color:var(--text-2); }
.sd-arrow svg { width:14px; height:14px; }
/* The route: every stop the SAME colour (owner, 2026-09-23: different colours were confusing); only
   the open stop stands out, with the brand-gradient edge. Line pieces between the stops flex, so the
   row fits a 480px slide-in and a 320px phone alike. */
.sd-track { display:flex; align-items:center; flex:1; min-width:0; }
.sd-step { flex:none; display:inline-flex; align-items:center; justify-content:center; height:26px; min-width:26px;
  padding:0 6px; border-radius:var(--r-pill); background:none; border:1px solid transparent; cursor:pointer;
  font-family:'JetBrains Mono',ui-monospace,monospace; font-size:12px; font-weight:700; line-height:1; color:var(--text-2); }
.sd-step svg { width:12px; height:12px; }
.sd-step:hover { color:var(--text-1); }
.sd-step.is-view { padding:0 9px; color:var(--text-1);
  background:linear-gradient(rgb(var(--c-elevated)),rgb(var(--c-elevated))) padding-box, var(--grad) border-box;
  box-shadow:0 0 18px rgba(139,61,255,.25); }
.sd-ln { flex:1 1 8px; min-width:6px; height:1px; background:rgb(var(--c-ink) / .16); }
.sd-close { flex:none; width:32px; height:32px; border-radius:8px;
  display:grid; place-items:center; color:var(--text-3); background:transparent; border:1px solid transparent;
  font-size:20px; line-height:1; cursor:pointer; }
.sd-close:hover { background:var(--bg-hover); border-color:var(--border-subtle); color:var(--text-1); }
/* Top padding is small because the bar above already gives the pane its air. */
#step-drawer .steps-panes { position:relative; padding:8px 21px 28px; min-height:0; }
#step-drawer .steps-pane-title { font-size:21px; letter-spacing:-.02em; }
@media (max-width:640px) {
  /* A full-screen sheet on a phone, pinned by BOTH edges like #creator-panel (18.3). */
  #step-drawer { left:0; right:0; width:auto; }
}

/* ---- THE RUN STRIP at the table (mockup section 2, pick A) -------------------------------------
   A button per step carrying the count FOR THE CURRENT VIEW. Real buttons, never tiles: a big number
   over a small label is how this app draws a STATISTIC, so tiles read as information. Hidden while
   the table shows everyone; the card above already covers that case. */
.pc-strip { background:rgba(30,58,138,.2); border:1px solid rgb(30,64,175); border-radius:var(--r-md);
  padding:12px; margin-bottom:10px; }
.pc-strip[hidden] { display:none; }
.pc-strip-top { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; font-size:13px; }
.pc-strip-top .pc-strip-who { color:#93C5FD; font-weight:600; }
.pc-strip-top .pc-strip-note { color:var(--text-3); font-size:12.5px; }
.pc-strip-top .pc-strip-clear { margin-left:auto; color:var(--text-2); font-size:13px; }
.pc-strip-top .pc-strip-clear:hover { color:var(--text-1); }
.pc-strip-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.pc-strip-row .btn-run { padding:7px 12px; font-size:12.5px; }

/* ---- THE CLOSER-LOOK OVERRIDE in the table's Depth column (copy-lab group 3, redrawn as two
   one-button cases). Flagged: one quiet "use them anyway". Passed: "rule them out" behind a ⋯. */
.pd-dc { display:inline-flex; align-items:center; gap:6px; }
.pd-dc-use { font-size:11px; padding:2px 7px; border-radius:var(--r-sm); border:1px solid var(--edge);
  background:transparent; color:var(--text-2); cursor:pointer; white-space:nowrap; }
.pd-dc-use:hover { border-color:var(--accent); color:var(--text-1); }
.pd-dc-more { font-size:12px; line-height:1; padding:2px 6px; border-radius:var(--r-sm); border:1px solid var(--edge);
  background:transparent; color:var(--text-3); cursor:pointer; }
.pd-dc-more:hover { color:var(--text-1); }
.pd-dc-menu { position:fixed; z-index:90; min-width:180px; background:var(--bg-surface); border:1px solid var(--border);
  border-radius:var(--r-md); box-shadow:0 12px 30px rgba(0,0,0,.45); padding:4px; }
.pd-dc-menu[hidden] { display:none; }
.pd-dc-menu button { display:block; width:100%; text-align:left; padding:7px 10px; border-radius:var(--r-sm);
  background:none; border:0; color:#FCA5A5; font-size:13px; cursor:pointer; }
.pd-dc-menu button:hover { background:var(--bg-hover); }
/* A menu item that is NOT destructive (the creator panel's re-run actions): the menu's default red
   is for "Rule them out" only. */
.pd-dc-menu button.is-neutral { color:var(--text-1); }
/* Above the creator panel, which the menu is opened from. */
#cp-dc-menu { z-index:95; }

/* ---- CREATOR PREVIEW PAGES (2026-10-22, the owner's picks in designs/mockups/drafts-review.html
   #their-page: Drafts option C, creator panel option B). "Open" with its arrow, the quiet status words,
   the panel's "Their page" block and its ⋯ button. The menu itself is .pd-dc-menu (#cp-prv-menu), the
   panel's existing overflow look, red because its one item removes.
   PREFIX `prv-`, NOT `pp-`: _preset_panel.html already owns .pp-row and friends (a bottom border), and a
   shared name drew that border inside this block on the project page. ---- */
a.prv-open { display:inline-flex; align-items:center; gap:4px; color:#C4B5FD; font-weight:600;
  text-decoration:none; white-space:nowrap; }
a.prv-open:hover { color:#DDD6FE; text-decoration:underline; text-underline-offset:3px; }
/* Two selectors' worth of weight on purpose: the Drafts page's own `.dr-fact svg` (14px, grey) would
   otherwise restyle the arrow. */
a.prv-open > svg { width:12px; height:12px; flex:none; margin:0; color:currentColor; }
.prv-muted { color:var(--text-3); }
.prv-ok { color:#7EE6A8; }
.prv-block .prv-row { display:flex; align-items:flex-start; gap:10px; }
.prv-block .prv-com { font-size:14px; font-weight:700; color:var(--text-1); overflow-wrap:anywhere; }
.prv-block .prv-st { font-size:12.5px; margin-top:3px; }
.prv-block a.prv-open { font-size:13px; margin-top:2px; }
.prv-kebab { width:28px; height:28px; border-radius:8px; display:inline-flex; align-items:center;
  justify-content:center; color:var(--text-3); background:none; border:0; cursor:pointer; flex:none; }
.prv-kebab:hover, .prv-kebab[aria-expanded="true"] { background:var(--bg-hover); color:var(--text-1); }
#cp-prv-menu { z-index:95; }

/* ---- "YOUR RULES" AS ROUND 11 (designs/mockups/run-a-step-audit.html #round11, owner-decided
   2026-09-22). Round 8's two objects - an account and a video - carrying the numbers, polished; plus
   the saved-videos row for the videos-we-listen-to rule. Ported 1:1 from the mockup's ra- and r11-
   rules, with the owner's colour decisions applied as FINAL (NEVER write a star followed by a slash
   in this comment: it closes the comment early and silently swallows the rule after it, which is
   exactly what the first version of this line did to .r11-band): the "Default" pill stays green (the
   .rul-pill above), the heart stays red, and the purple stays OUT (no step circle, no accent border on
   a lit frame - brightness already says "lit"). Do not re-mute the green or the red in a tidy-up. */
.r11-band { display:flex; align-items:center; gap:14px; margin-top:14px; }
.r11-obj { width:54px; flex:none; display:flex; justify-content:center; }
.r11-band .ra-stats { flex:1; min-width:0; }
/* Everything that explains an object hangs under its CELLS, not under the object (54 + 14 gap). */
.r11-under { margin-left:68px; }
.r11-what { font-size:11.5px; color:var(--text-3); margin:9px 0 0 68px; line-height:1.45; max-width:52ch; }
.r11-what.flush { margin-left:0; }
/* the account: an elevated disc with a real edge and a top light, and the platform badge */
.r11-av { position:relative; width:50px; height:50px; flex:none; border-radius:50%;
  background:linear-gradient(180deg, var(--bg-hover), var(--bg-elevated));
  border:1px solid var(--border); display:grid; place-items:center; color:var(--text-2);
  box-shadow:inset 0 1px 0 rgb(var(--c-ink) / .06); }
.r11-av > svg { width:22px; height:22px; }
.r11-pb { position:absolute; right:-3px; bottom:-3px; width:19px; height:19px; border-radius:6px;
  display:grid; place-items:center; background:#000; border:2px solid var(--bg-surface); }
.r11-pb svg { width:11px; height:11px; }
/* the video: a true 9:16 frame carrying only the two numbers the rules are about */
.r11-vid { position:relative; width:50px; height:89px; border-radius:8px; overflow:hidden;
  border:1px solid var(--border); flex:none;
  background:radial-gradient(120% 60% at 50% 0%, rgb(var(--c-ink) / .07), transparent 62%),
             linear-gradient(180deg, #202027 0%, #141418 58%, #101014 100%); }
.r11-vid .play { position:absolute; left:50%; top:26%; transform:translate(-50%,-50%);
  width:13px; height:13px; color:rgb(var(--c-ink) / .26); }
.ra-stamp { position:absolute; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:8.5px;
  letter-spacing:.02em; background:rgba(0,0,0,.74); color:#fff; padding:1px 4px; border-radius:3px;
  font-variant-numeric:tabular-nums; line-height:1.4; }
.r11-vid .ra-stamp { left:4px; bottom:4px; }
/* one item on the rail, clear of the stamp's row: at 50px wide the two would otherwise collide */
.r11-rail { position:absolute; right:3px; bottom:20px; display:flex; flex-direction:column;
  align-items:center; gap:1px; color:#fff; pointer-events:none; }
.r11-rail svg { width:14px; height:14px; filter:drop-shadow(0 1px 2px rgba(0,0,0,.7)); }
.r11-rail .n { font-size:8px; font-weight:700; font-variant-numeric:tabular-nums; text-shadow:0 1px 2px rgba(0,0,0,.85); }
.r11-heart { display:inline-flex; align-items:center; gap:5px; }
.r11-heart svg { width:13px; height:13px; color:#FF4D6D; flex:none; }
/* the saved videos, and the ones we pay to listen to */
.r11-bank { display:flex; align-items:center; gap:14px; margin-top:14px; flex-wrap:wrap; }
.r11-bank .ra-stats { flex:1 1 150px; min-width:0; }
.r11-bank .ra-stat { align-items:flex-start; text-align:left; padding:0; }
.r11-bank .ra-stat .rul-val { justify-content:flex-start; }
.r11-strip { display:flex; gap:4px; flex:none; }
.r11-strip .t { position:relative; width:23px; height:41px; border-radius:4px; overflow:hidden;
  border:1px solid var(--border); background:linear-gradient(180deg, #1e1e25 0%, #131317 60%, #101014 100%); }
.r11-strip .t.on { border-color:var(--border-strong); background:linear-gradient(180deg, #2a2a33 0%, #1a1a20 60%, #141418 100%); }
.r11-strip .t.off { opacity:.22; }
.r11-ear { position:absolute; left:50%; bottom:5px; transform:translateX(-50%); display:flex;
  align-items:flex-end; gap:1.5px; height:8px; }
.r11-ear i { width:1.5px; background:var(--text-2); border-radius:1px; }
.r11-ear i:nth-child(1) { height:38%; } .r11-ear i:nth-child(2) { height:100%; }
.r11-ear i:nth-child(3) { height:60%; } .r11-ear i:nth-child(4) { height:44%; }
.r11-strip .t.off .r11-ear { display:none; }
/* the number-over-label cells the objects share */
.ra-stats { display:flex; flex:1; min-width:0; }
.ra-stat { flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center;
  padding:0 3px; border-radius:var(--r-md); }
.ra-stat .rul-val { margin-left:0; justify-content:center; width:auto; }
.ra-stat .n { font-size:21px; font-weight:800; letter-spacing:-.02em; line-height:1.15; color:var(--text-1); }
.ra-stat .l { font-size:11px; color:var(--text-3); line-height:1.3; margin-top:4px; margin-bottom:7px; }
.ra-stat .rul-pillwrap { margin-top:auto; }
.ra-stat.dim .n { color:var(--text-3); opacity:.45; }
.ra-stat.dim .l { opacity:.55; }
/* The rule you arrived at from a held-back reason ("See why" -> "Your rules"). */
.ra-stat.is-landed { background:rgb(var(--c-accent) / .07); box-shadow:0 0 0 1px rgb(var(--c-accent) / .30); }
.ra-fold-h { font-size:11.5px; color:var(--text-2); font-weight:600; margin:14px 0 0; }
.r11-under > .ra-fold-h:first-child { margin-top:10px; }
/* What a customer cannot change, stated rather than hidden. */
.pn-excl { margin-top:22px; border-top:1px solid var(--border-subtle); padding-top:16px; }
.pn-excl .mono-label { margin:0; }
.pn-excl .steps-note { margin-top:7px; }
.pn-excl ul { margin:9px 0 0; padding:0; list-style:none; display:flex; flex-wrap:wrap; gap:6px; }
.pn-excl li { font-size:12px; color:var(--text-2); background:var(--bg-hover); border:1px solid var(--border-subtle);
  border-radius:var(--r-pill); padding:3px 10px; }
.pn-step5 { margin-top:14px; }

/* ---- "LANGUAGES THEY SPEAK" (the language rule, 2026-09-27). Ported from option A of
   designs/mockups/language-requirement.html, the owner's pick: a row in Your rules whose object is a
   speech bubble carrying the language codes (as the video's stamp carries the length), the language
   tags, and a Change button that opens the one picker (_creator_languages_picker.html). */
.lg-bub { position:relative; width:50px; height:50px; flex:none; border-radius:50%;
  background:linear-gradient(180deg, var(--bg-hover), var(--bg-elevated)); border:1px solid var(--border);
  display:grid; place-items:center; color:var(--text-2); box-shadow:inset 0 1px 0 rgb(var(--c-ink) / .06); }
.lg-say { position:relative; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:8.5px; font-weight:700;
  letter-spacing:.05em; color:var(--text-1); background:var(--bg-base); border:1px solid var(--border-strong);
  border-radius:6px; padding:2px 5px; line-height:1.35; white-space:nowrap; transform:translateY(-2px); }
.lg-say::after { content:""; position:absolute; left:7px; bottom:-4px; width:6px; height:6px; background:var(--bg-base);
  border-right:1px solid var(--border-strong); border-bottom:1px solid var(--border-strong); transform:rotate(45deg); }
.lg-bub svg { width:22px; height:22px; }
.lg-band { display:flex; align-items:flex-start; gap:14px; margin-top:14px; }
.lg-band .r11-obj { padding-top:1px; }
.lg-body { flex:1; min-width:0; }
.lg-tags { display:flex; flex-wrap:wrap; gap:6px; }
.lg-tag { display:inline-flex; align-items:baseline; gap:6px; font-size:14px; font-weight:650; color:var(--text-1);
  background:var(--bg-elevated); border:1px solid var(--border); border-radius:8px; padding:4px 10px; line-height:1.3; }
.lg-meta { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:9px; }
.lg-meta .l { font-size:11px; color:var(--text-3); }
.lg-meta .lg-change { margin-left:auto; }
/* the picker */
.lg-pick-lead { font-size:12.5px; color:var(--text-2); line-height:1.5; margin:0 0 12px; }
.lg-anyrow { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:9px 11px; margin:10px 0 4px;
  border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-surface); font-size:13px; color:var(--text-1); }
.lg-list { margin-top:10px; }
.lg-list .mono-label { display:block; margin:10px 0 4px; }
.lg-list .lg-none { padding:8px; }
.lg-opt { display:flex; align-items:center; gap:10px; width:100%; text-align:left; padding:7px 8px; border-radius:var(--r-sm);
  background:none; border:0; color:var(--text-2); font:inherit; font-size:13px; cursor:pointer; }
.lg-opt:hover { background:var(--bg-hover); color:var(--text-1); }
.lg-opt .tk { flex:0 0 15px; height:15px; border-radius:4px; display:inline-flex; align-items:center; justify-content:center;
  font-size:10px; color:#fff; border:1px solid rgba(255,255,255,.25); }
.lg-opt.is-on { color:var(--text-1); }
.lg-opt.is-on .tk { background:var(--grad); border-color:transparent; }
.lg-opt .nm { flex:1; min-width:0; }
.lg-opt .ct { font-size:11.5px; color:var(--text-3); font-variant-numeric:tabular-nums; }
.lg-list.is-off { opacity:.4; pointer-events:none; }
.lg-foot { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:14px; padding-top:12px; border-top:1px solid var(--border-subtle); }
.lg-foot .say { flex:1 1 180px; font-size:12px; color:var(--text-2); line-height:1.45; }
.lg-foot .say b { color:var(--text-1); font-weight:650; }
.lg-foot .acts { display:flex; gap:8px; margin-left:auto; }
/* the include dialog's split bar (DESIGN_SYSTEM 15.5a: the number first, as a picture) */
.lg-split { display:flex; height:10px; border-radius:999px; overflow:hidden; background:var(--bg-hover); margin:2px 0 10px; }
.lg-split i { display:block; height:100%; }
.lg-legend { display:flex; flex-direction:column; gap:3px; font-size:12px; color:var(--text-2); line-height:1.45; margin-bottom:14px; }
.lg-legend span { display:flex; gap:8px; align-items:baseline; }
.lg-legend i { width:8px; height:8px; border-radius:50%; flex:none; transform:translateY(-1px); }
.lg-legend b { color:var(--text-1); font-weight:650; font-variant-numeric:tabular-nums; min-width:22px; }
/* the setup's run screen: "Creators who speak: Spanish · Change" */
.ob-langline { display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; font-size:12.5px; color:var(--text-2); }
.ob-langline b { color:var(--text-1); font-weight:650; }
@media (max-width:420px) { .lg-band { gap:12px; } }
/* In the slide-in the Save bar pins to the foot while it shows: the number may be edited at the top
   of a long column, and Save is then the only thing on screen that matters. */
#step-drawer .rul-pending { position:sticky; bottom:0; z-index:2; margin-left:-21px; margin-right:-21px;
  border-radius:0; border-left:none; border-right:none; border-bottom:none; background:var(--bg-elevated);
  box-shadow:0 -18px 30px rgba(8,8,11,.55); padding:13px 21px 16px; }
/* "Priced, never scored": the proposed figure is plain, never green for more or amber for fewer. */
.rul-pending-line .now { color:var(--text-1); font-weight:600; }
@media (max-width:420px) {
  /* fold, don't shrink: the two cells that carry no rule go, so the one that does gets the room */
  .ra-stat.dim { display:none; }
  .r11-under, .r11-what { margin-left:0; }
}

/* ==============================================================================================
   AUTOPILOT WINDOW, OFFER LINE, AUTOPILOT BANNER (wave 4, agent A). Markup:
   app/templates/_autopilot_window.html, _autopilot_offer.html, _run_window.html (autopilotHtml).
   Spec: designs/mockups/run-a-step-audit.html section 5 (".dlg", ".rt-flow", ".rf-facts",
   ".ap-zone"). Namespaced ap-/apw- so nothing here restyles another screen. NO AMBER in this block:
   the one amber autopilot state (paused) is the mark's own, in the block above.
   ============================================================================================== */
/* The side-menu card stays hidden until its state has been read for the project in view
   (_autopilot_window.html feeds it; before a project's first full pass it stays hidden). */
.ap-side:not(.is-fed) { display:none; }

/* Autopilot is the actor: its own purple edge, so "the app did this by itself" is never mistaken
   for "you ran this" (mockup .ap-zone). Two classes, so it beats .rb's own edge without !important. */
.rb.ap-zone, .ap-offer.ap-zone { border-color:rgb(var(--c-special) / .42);
  background:linear-gradient(90deg, rgb(var(--c-special) / .13), rgb(var(--c-special) / .03) 62%); }

/* THE OFFER LINE: a line that waits, never a popup. */
.ap-offer { display:flex; gap:10px 12px; align-items:center; flex-wrap:wrap; margin-top:13px;
  padding:10px 12px; border:1px solid; border-radius:var(--r-md); }
.ap-offer[hidden] { display:none; }
.ap-offer-q { font-size:13px; color:var(--text-1); line-height:1.45; }
.ap-offer-go { margin-left:auto; background:none; border:none; padding:0; cursor:pointer; }
.ap-offer-go .ap-chip { cursor:pointer; }
.ap-offer-go:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:var(--r-pill); }
.ap-offer-no { font-size:12.5px; background:none; border:none; padding:0; }

/* THE WINDOW (Dialog standard card; these only lay out its inside). */
.apw-head { display:flex; justify-content:space-between; align-items:flex-start; gap:12px; margin-bottom:12px; }
.apw-mark { margin-bottom:10px; }
.apw-flow { display:flex; align-items:center; gap:7px; flex-wrap:wrap; font-size:12.5px; color:var(--text-2);
  margin:0 0 14px; font-variant-numeric:tabular-nums; }
.apw-flow i { font-style:normal; color:var(--text-3); }
.apw-flow .is-off { opacity:.42; }
/* the step number: the same numbered circle the run buttons and the rail use */
.apw-n { display:inline-flex; align-items:center; justify-content:center; width:17px; height:17px;
  border-radius:50%; border:1px solid var(--border-strong); font-family:'JetBrains Mono',monospace;
  font-size:10px; font-weight:700; color:var(--text-2); margin-right:6px; vertical-align:1px; }
.apw-facts > div { align-items:center; }
.apw-facts b { font-weight:600; color:var(--text-1); }
/* .ds-form makes every field full width; inside a facts row they size to their content. */
.ds-form .apw-facts select, .ds-form .apw-facts input { width:auto; padding:6px 10px; font-size:13px; }
.ds-form .apw-facts #apw-limit { width:96px; text-align:right; font-family:'JetBrains Mono',monospace;
  font-weight:600; font-variant-numeric:tabular-nums; }
.apw-limit { white-space:nowrap; }
.apw-mixrow > span { display:none; }
.rf-facts .apw-mix { flex:1; font-weight:500; color:var(--text-3); text-align:left; line-height:1.45; }
.rf-facts .apw-never { font-weight:500; color:var(--text-3); }
.apw-shead { margin:16px 0 8px; }
.apw-searches { border:1px solid var(--border-subtle); border-radius:var(--r-md); }
.apw-srow { display:flex; align-items:center; justify-content:space-between; gap:8px 12px; flex-wrap:wrap;
  padding:9px 12px; border-bottom:1px solid var(--border-subtle); }
.apw-srow:last-child { border-bottom:none; }
.apw-sq { display:flex; align-items:center; gap:8px; min-width:0; flex-wrap:wrap; }
.apw-sq .chip { font-size:11px; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.apw-cap { font-size:12px; color:var(--text-3); }
.ds-form .apw-srow select { width:auto; max-width:100%; padding:6px 10px; font-size:12.5px; }
.apw-note { margin:10px 0 0; font-size:12.5px; color:var(--text-3); }
.apw-note[hidden] { display:none; }
.apw-offline { margin:14px 0 0; font-size:12.5px; }
.apw-offline .rf-link { background:none; border:none; padding:0; color:var(--text-2); }
.apw-foot { display:flex; align-items:center; gap:8px 12px; flex-wrap:wrap; margin-top:16px; }
.apw-err { flex:1 1 180px; margin:0; font-size:12.5px; color:var(--text-2); }
.apw-btns { display:flex; gap:8px; margin-left:auto; }
@media (max-width:480px) {
  .apw-facts > div { flex-wrap:wrap; }
  .apw-srow select { width:100%; }
}

/* ==================================================================================================
   CAMPAIGN + KEYWORD RUN CONTROLS (run-a-step build wave 4, agent C). campaign_detail.html:
   the search-finished moment (#cd-finish), the reasons behind "Filtered out N" on a page with no
   slide-in (.pc-why, drawn by _run_card.html), a keyword row's compact card (.kw-run / .kw-then),
   and the run strip's quiet "whole campaign" look (.cd-strip.is-all). Everything else these pieces
   use is the project page's card and strip above (.pc-*, .btn-run, .rf-kept) - ported, not redrawn.
   Design: designs/mockups/run-a-step-audit.html section 4.
   ================================================================================================== */
/* The finished moment: chip + what ran, the headline numbers, one run button and "Later". */
.cd-finish { padding:16px 20px; }
.cd-finish-top { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px; }
.cd-finish-what { font-size:12.5px; color:var(--text-2); }
.cd-finish-head { font-size:14px; font-weight:650; color:var(--text-1); margin:0 0 10px; }
.cd-finish-act { display:flex; align-items:center; gap:10px 14px; flex-wrap:wrap; }
.cd-finish-act .btn-run { padding:7px 13px; font-size:12.5px; }
.cd-finish-later { color:var(--text-2); font-size:12.5px; }
.cd-finish-ap { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0; }
/* "See why", in place: the rule reasons summed into the card's "Filtered out N". */
.pc-why { list-style:none; margin:8px 0 0 23px; padding:0; display:flex; flex-direction:column; gap:5px;
  font-size:12.5px; color:var(--text-2); }
.pc-why[hidden] { display:none; }
.pc-why li { display:flex; align-items:baseline; gap:9px; }
.pc-why-n { min-width:44px; text-align:right; font-variant-numeric:tabular-nums; font-weight:650; color:var(--text-1); }
/* A keyword row's next step. The row's own buttons are 12px; this one matches them. */
.btn-run.kw-run { padding:4px 10px; font-size:12px; }
.kw-then { font-size:11.5px; color:var(--text-3); font-variant-numeric:tabular-nums; }
/* The strip for the WHOLE campaign reads as a toolbar, not as a selection: the blue tint is the
   project page's "the table is narrowed" signal, and here that state is the exception. */
.cd-strip.is-all { background:transparent; border-color:var(--border-subtle); }
.cd-strip.is-all .pc-strip-who { color:var(--text-2); }
/* A map cell for steps 2 to 5 points at its strip button (the map never runs anything itself). */
.btn-run.cd-flash { box-shadow:0 0 0 2px var(--accent); }
.cd-say-quiet { font-size:14px; font-weight:500; color:var(--text-2); }
@media (max-width:560px) {
  /* fold, don't shrink: a keyword row's buttons take their own line under the keyword */
  .kw-actions { flex-wrap:wrap; }
  .cd-finish { padding:14px 16px; }
}

/* ============================================================================
   THE EMAIL CARD - one email with each part highlighted by where it came from.
   Ported 1:1 from landing.html "THE EMAIL - the anatomy of one message" (2026-09-26), with the
   landing's colour names mapped to the app's (--ink2 -> --text-2, --ink-muted -> --text-3). SCOPED
   under .mailcard on purpose: .thread / .msg are generic names, and the two labels are renamed mc-exlab /
   mc-mlab because developers.html owns .exlab / .mlab (tests/test_developers_page.py fails on any app.css
   class that collides with that page's). landing.html keeps
   its own copy for now; switching it to class="mailcard thread" is a pure refactor (see the outreach
   inputs handoff). Used by the "How your emails are written" dialog (_outreach_inputs.html).
   ============================================================================ */
.mailcard.thread { position:relative; background:#0E0E13; border:1px solid var(--border);
  border-radius:var(--r-lg); padding:18px; box-shadow:0 30px 80px rgba(0,0,0,.5); }
.mailcard .mc-exlab { display:inline-flex; align-items:center; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--text-3);
  border:1px dashed var(--border-strong); border-radius:999px; padding:3px 10px; line-height:1.5;
  white-space:nowrap; background:var(--bg-base); margin-bottom:12px; }
.mailcard .mc-mlab { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10.5px; letter-spacing:.15em;
  text-transform:uppercase; color:var(--text-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
/* mc- on every part below: landing.html uses the plain names (.msg, .mbody, .srcrow, .sw...) in its own
   inline copy, and tests/test_landing_page.py fails when app.css starts styling a class that page uses. */
.mailcard .mc-msg { background:#101014; border:1px solid var(--border-subtle); border-radius:12px; padding:14px 16px; }
.mailcard .mc-mhead { display:flex; justify-content:space-between; align-items:center; margin-bottom:9px; gap:10px; }
.mailcard .mc-msubj { font-size:12.5px; color:var(--text-3); margin-bottom:8px; }
.mailcard .mc-msubj b { color:var(--text-1); font-weight:600; }
.mailcard .mc-mbody { font-size:14px; line-height:1.75; color:var(--text-2); overflow-wrap:anywhere; }
.mailcard .mc-mbody p + p { margin-top:8px; }
.mailcard .mc-mbody b { color:var(--text-1); font-weight:600; }
.mailcard .mc-mbody .oe-pre { white-space:pre-line; }
.mailcard .mc-mbody mark { background:rgba(139,61,255,.20); color:#CBB4FF; padding:1px 4px; border-radius:4px; }
.mailcard .mc-mbody mark[data-action] { cursor:pointer; }
.mailcard .mc-mbody mark.mc-mk2 { background:rgba(34,211,238,.16); color:#7EE2F2; }
.mailcard .mc-mbody mark.mc-mk3 { background:rgba(34,197,94,.16); color:#7EE6A8; }
/* gold: "always include" (new in the dialog; the homepage has no fourth input) */
.mailcard .mc-mbody mark.mc-mk4 { background:rgba(245,200,107,.16); color:#F5C86B; }
.mailcard .mc-srcleg { display:flex; flex-direction:column; gap:8px; padding:14px 4px 4px; }
.mailcard .mc-srcrow { display:flex; align-items:center; gap:10px; font-size:13px; color:var(--text-3);
  background:none; border:0; padding:0; text-align:left; font-family:inherit; }
.mailcard button.mc-srcrow { cursor:pointer; }
.mailcard button.mc-srcrow:hover { color:var(--text-2); }
.mailcard .mc-srcrow b { color:var(--text-2); font-weight:600; }
.mailcard .mc-sw { width:10px; height:10px; border-radius:3px; flex:none; }
.mailcard .mc-sw-v { background:rgba(139,61,255,.7); } .mailcard .mc-sw-cy { background:rgba(34,211,238,.62); }
.mailcard .mc-sw-gr { background:rgba(34,197,94,.62); } .mailcard .mc-sw-go { background:rgba(245,200,107,.62); }
.mailcard .mc-sw-wh { background:var(--text-1); } .mailcard .mc-sw-no { background:rgba(248,113,113,.6); }
/* a part that is still missing: a dashed square saying what to add */
.mailcard .mc-sw-miss { background:none; border:1px dashed var(--text-3); }

/* ============================================================================
   "HOW YOUR EMAILS ARE WRITTEN" (_outreach_inputs.html; outreach inputs, option C, 2026-09-26).
   Left: an accordion of five sections (one open at a time). Right: the email card above, pinned and
   scrolling on its own. Approved mockup: designs/mockups/outreach-before-first-email.html (option C).
   ============================================================================ */
.oe-c { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,400px); gap:20px; align-items:start; }
.oe-2col { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
.oe-acc { border:1px solid var(--border); border-radius:12px; overflow:hidden; }
.oe-acc-s + .oe-acc-s { border-top:1px solid var(--border-subtle); }
.oe-acc-h { display:flex; align-items:flex-start; gap:10px; width:100%; padding:11px 13px; background:none; border:0;
  text-align:left; cursor:pointer; color:var(--text-1); font-family:inherit; }
.oe-acc-h:hover { background:var(--bg-hover); }
.oe-acc-h .mk { flex:none; width:15px; text-align:center; font-size:12px; line-height:1.5; }
.oe-acc-h .t { flex:1; min-width:0; font-size:13.5px; font-weight:600; line-height:1.35; }
.oe-acc-h .t small { display:block; font-weight:400; font-size:12px; color:var(--text-3); margin-top:2px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.oe-acc-h .req { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.1em;
  text-transform:uppercase; color:#FBD375; border:1px solid rgba(251,211,117,.35); border-radius:999px;
  padding:1px 7px; flex:none; margin-top:2px; }
.oe-acc-h .req[hidden] { display:none; }
.oe-acc-h .car { flex:none; color:var(--text-3); transition:transform .15s; margin-top:2px; }
.oe-acc-s.open .oe-acc-h .car { transform:rotate(90deg); }
.oe-acc-b { padding:2px 13px 14px 38px; }
.mk-ok { color:var(--green); } .mk-need { color:#FBD375; } .mk-opt { color:var(--text-3); }
.oe-lbl { display:block; font-size:12px; color:var(--text-3); margin:10px 0 4px; }
.oe-lbl i { font-style:normal; opacity:.75; }
.oe-hint { font-size:11.5px; color:var(--text-3); line-height:1.5; margin-top:6px; }
.oe-hint-top { margin:0 0 7px; }
.oe-inline { margin:0 0 0 8px; display:inline; }
.oe-ended { color:#FBD375; }
.oe-seg { display:flex; gap:6px; flex-wrap:wrap; }
.oe-segb { font-size:13px; color:var(--text-2); border:1px solid var(--border); border-radius:999px; padding:6px 13px;
  background:none; cursor:pointer; font-family:inherit; }
.oe-segb:hover { color:var(--text-1); }
.oe-segb.is-on { color:var(--text-1); border-color:rgb(var(--c-accent) / .7); background:rgb(var(--c-accent) / .12); }
.oe-sign { margin-top:14px; padding:12px 14px; border:1px solid var(--border); border-radius:10px; background:var(--bg-base);
  font-size:13px; line-height:1.6; color:var(--text-1); white-space:pre-line; }
.oe-sign small { display:block; font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.11em;
  text-transform:uppercase; color:var(--text-3); margin-bottom:6px; white-space:normal; }
.oe-sec { padding-top:14px; margin-top:16px; border-top:1px solid var(--border-subtle); }
/* Sentence fields grow with what is typed (owner, 2026-09-26); names, numbers and dates stay one line. */
textarea.oe-grow { resize:none; overflow:hidden; min-height:38px; line-height:1.45; }
.oe-list + .oe-list { margin-top:16px; }
.oe-li { display:flex; align-items:center; gap:8px; }
.oe-li + .oe-li { margin-top:7px; }
.oe-li textarea.oe-grow { flex:1; min-width:0; }
/* The one delete control on every removable row: an icon button, quiet until hovered. */
.oe-del { flex:none; width:32px; height:32px; display:inline-grid; place-items:center; border-radius:8px;
  border:1px solid transparent; background:none; color:var(--text-3); cursor:pointer; }
button.oe-del:hover { color:#F87171; border-color:var(--border); background:var(--bg-hover); }
button.oe-mv:hover { color:var(--text-1); }
.oe-deal { border:1px solid var(--border); border-radius:10px; padding:10px 11px; background:var(--bg-base); }
.oe-deal + .oe-deal { margin-top:9px; }
.oe-deal .oe-row2 { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto; gap:8px; align-items:center; }
.oe-deal .oe-date { max-width:220px; }
.oe-deal-f { display:flex; align-items:flex-start; gap:6px; padding:0; }
.oe-deal-h { flex:1; min-width:0; display:flex; align-items:flex-start; gap:8px; padding:10px 11px; background:none; border:0;
  text-align:left; cursor:pointer; color:var(--text-2); font-size:13px; border-radius:10px; font-family:inherit; }
.oe-deal-h:hover { background:var(--bg-hover); }
.oe-deal-h .t { flex:1; min-width:0; }
.oe-deal-h .t b { color:var(--text-1); font-weight:600; }
.oe-deal-h .t small { display:block; color:var(--text-3); font-size:12px; margin-top:2px; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.oe-deal-h .car { flex:none; color:var(--text-3); margin-top:3px; }
.oe-deal-f .oe-del { margin:6px 6px 0 0; }
.oe-rulebox { border:1px solid var(--border); border-radius:10px; padding:8px 11px 10px; background:var(--bg-base); margin-top:10px; }
.oe-rulehead { display:flex; align-items:center; justify-content:space-between; margin-bottom:2px; }
.oe-ruleacts { display:inline-flex; gap:2px; }
.oe-rule { display:flex; align-items:center; gap:6px; flex-wrap:wrap; font-size:12.5px; color:var(--text-2); padding:4px 0; }
/* !important: .ds-form's full-width default is (0,3,1) and a rule row's controls sit side by side. */
.ds-form .oe-rule select, .ds-form .oe-rule input { width:auto !important; padding:6px 10px; font-size:13px; }
.oe-w { width:38px; flex:none; font-size:12.5px; color:var(--text-3); }
.oe-w-auto { width:auto; }
.oe-rule .oe-cf { min-width:170px; }
.ds-form .oe-rule .oe-cn { max-width:120px; width:120px !important; }
/* The words condition gets its own full line under its dropdown (it is a sentence, and grows). */
.ds-form .oe-rule .oe-cw { flex:1 1 100%; min-width:0; width:auto !important; margin-left:44px; }
.oe-usual { margin-top:12px; }
.oe-splitrow { margin-top:12px; }
.oe-split { margin-top:10px; font-size:12.5px; color:var(--text-2); line-height:1.6; list-style:none; padding:0; }
.oe-split b { color:var(--text-1); }
.oe-link { font-size:12px; color:var(--text-2); background:none; border:0; padding:0; text-decoration:underline;
  text-underline-offset:3px; cursor:pointer; font-family:inherit; }
/* Pinned beside the inputs, and scrollable on its own: on a short laptop screen the sign-off would
   otherwise sit below the fold of a pinned box, where no scroll can reach it. */
.oe-ex { position:sticky; top:0; max-height:calc(100dvh - 200px); overflow-y:auto; overscroll-behavior:contain; padding-right:2px; }
.oe-ex-head { display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap; margin-bottom:8px; }
.oe-ex-k { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:9px; letter-spacing:.11em; text-transform:uppercase; color:var(--text-3); }
.oe-mini { display:flex; gap:4px; flex-wrap:wrap; align-items:center; }
.oe-mini button { font-size:11.5px; color:var(--text-2); border:1px solid var(--border); border-radius:999px; padding:3px 9px;
  background:none; cursor:pointer; font-family:inherit; }
.oe-mini button.on { color:var(--text-1); border-color:rgb(var(--c-accent) / .6); background:rgb(var(--c-accent) / .12); }
.oe-gap { width:6px; }
.oe-gets { font-size:12px; color:var(--text-2); margin:2px 0 9px; line-height:1.5; }
.oe-foot { font-size:11.5px; color:var(--text-3); line-height:1.55; margin-top:9px; }
.oe-realrow { margin-top:10px; }
.oe-foot-row { display:flex; align-items:center; justify-content:flex-end; gap:8px; flex-wrap:wrap; margin-top:20px; }
.oe-foot-note { margin:0 auto 0 0; }
.oe-dirty { display:flex; align-items:center; font-size:13px; color:#FBD375; margin-right:auto; }
@media (max-width:860px) {
  .oe-c { grid-template-columns:minmax(0,1fr); }
  .oe-ex { position:static; max-height:none; }
  .oe-acc-b { padding-left:13px; }
  .oe-2col { grid-template-columns:minmax(0,1fr); }
  .ds-form .oe-rule .oe-cw { margin-left:0; }
}
/* ===== THE AGE CHECK (2026-09-26) =============================================================
   Ported from designs/mockups/age-review.html's `.ag-*` proposals (owner's round-2 pick D + the
   one-at-a-time viewer + the creator panel block). Markup: app/templates/_age_review.html (the
   viewer and window.agePanelBlock) and base.html (the panel mounts the block at its top). The
   evidence card is ONE component used in both places; the `.is-compact` variant is the panel's. */
.ag-ev { display:flex; flex-direction:column; gap:12px; min-width:0; }
.ag-who { display:flex; align-items:center; gap:10px; min-width:0; }
.ag-who .nm { font-size:14px; font-weight:650; color:var(--text-1); line-height:1.3; overflow-wrap:anywhere; }
.ag-who .sub { font-size:12px; color:var(--text-3); line-height:1.4; }
.ag-av { width:34px; height:34px; border-radius:50%; flex:none; display:inline-flex; align-items:center;
  justify-content:center; font-size:13px; font-weight:700; color:#fff; }
.ag-av.sm { width:26px; height:26px; font-size:11px; }
.ag-src { display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
.ag-src svg { width:13px; height:13px; flex:none; color:var(--text-2); }
.ag-src .dt { letter-spacing:.06em; text-transform:none; font-size:11px; }
.ag-body { display:flex; gap:14px; align-items:flex-start; min-width:0; }
.ag-q { margin:0; font-size:16px; line-height:1.55; color:var(--text-2); font-weight:500; min-width:0; overflow-wrap:anywhere; }
.ag-q mark { background:rgba(251,191,36,.15); color:#FDE68A; border-radius:4px; padding:1px 3px;
  -webkit-box-decoration-break:clone; box-decoration-break:clone; font-weight:600; }
.ag-q.guess { font-size:15px; color:var(--text-1); }
.ag-note { display:block; font-size:12px; color:var(--text-3); line-height:1.5; margin-top:5px; font-weight:400; }
/* The video it was said in: a phone-shaped tile with the moment on it. A link, never a player. */
.ag-tile { position:relative; flex:none; width:46px; height:80px; border-radius:9px; border:1px solid var(--border-strong);
  background:radial-gradient(90% 60% at 50% 30%,rgba(255,255,255,.10),transparent 70%),linear-gradient(180deg,#26262e,#141418);
  display:grid; place-items:center; color:#fff; text-decoration:none; transition:border-color .15s; }
.ag-tile:hover { border-color:rgba(167,139,250,.6); }
.ag-tile svg { width:16px; height:16px; opacity:.9; }
.ag-tile .t { position:absolute; left:4px; right:4px; bottom:4px; text-align:center; font-family:'JetBrains Mono',ui-monospace,monospace;
  font-size:9.5px; font-weight:600; background:rgba(0,0,0,.66); border-radius:4px; padding:1px 0; font-variant-numeric:tabular-nums; }
.ag-open { display:inline-flex; align-items:center; gap:6px; margin-top:6px; font-size:12.5px; color:#A8C7FF;
  text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; font-weight:500; }
.ag-open:hover { color:var(--text-1); }
.ag-open svg { width:12px; height:12px; }
.ag-bio { display:flex; gap:10px; font-size:12.5px; color:var(--text-2); line-height:1.5; padding-top:10px;
  border-top:1px solid var(--border-subtle); overflow-wrap:anywhere; }
.ag-bio .k { flex:none; color:var(--text-3); width:28px; }
.ag-ev.is-compact { gap:8px; }
.ag-ev.is-compact .ag-q { font-size:14px; }
.ag-ev.is-compact .ag-tile { width:36px; height:62px; border-radius:7px; }
.ag-ev.is-compact .ag-tile svg { width:13px; height:13px; }
/* The viewer ("See who"). A WINDOW: the shared .ds-modal-x in the title row, no Close button. */
.ag-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.ag-count { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:11px; color:var(--text-3);
  font-variant-numeric:tabular-nums; white-space:nowrap; }
.ag-prog { height:3px; border-radius:999px; background:var(--bg-hover); overflow:hidden; margin:6px 0 14px; }
.ag-prog i { display:block; height:100%; background:rgb(var(--c-ink) / .55); transition:width .25s; }
.ag-evbox { border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-surface); padding:14px; }
.ag-ask { font-size:12.5px; color:var(--text-2); margin:14px 0 8px; font-weight:600; overflow-wrap:anywhere; }
.ag-foot { display:flex; align-items:center; gap:8px; margin-top:10px; }
.ag-foot[hidden], .ag-prog[hidden] { display:none; }
/* The two answers: compact, equal weight, one at each edge; neither wears the gradient. */
.ag-btns { display:flex; justify-content:space-between; gap:8px; flex-wrap:wrap; }
.ag-btns .btn[aria-pressed="true"] { border-color:rgba(167,139,250,.6); color:#fff; background:rgba(139,61,255,.14); }
@media (max-width:480px) { .ag-btns .btn { flex:1 1 100%; justify-content:center; } }
.ag-ledger { margin:4px 0 0; border-top:1px solid var(--border-subtle); }
.ag-lrow { display:flex; align-items:center; gap:10px; padding:9px 0; border-bottom:1px solid var(--border-subtle); font-size:13px; color:var(--text-1); }
.ag-lrow .h { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ag-lrow .chip { font-size:11px; padding:1px 8px; }
.ag-done-say { font-size:13px; color:var(--text-2); line-height:1.55; margin:12px 0 0; }
.ag-rmore { padding:10px 0 0; font-size:12.5px; color:var(--text-3); }
.ag-slip { animation:agSlip .22s ease-out; }
@keyframes agSlip { from { opacity:0; transform:translateX(10px); } to { opacity:1; transform:none; } }
@media (prefers-reduced-motion:reduce) { .ag-slip { animation:none; } .ag-prog i { transition:none; } }
/* The block at the TOP of the creator panel (read before any score). */
.ag-pblock .ag-ph { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:10px; }
.ag-pblock .ag-ph .l { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.ag-pblock .ag-ph .chip { white-space:nowrap; }
.ag-acts { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.ag-acts .btn { flex:1 1 auto; justify-content:center; }
.ag-state { font-size:12.5px; color:var(--text-2); line-height:1.5; margin-top:10px; }
.ag-state b { color:var(--text-1); font-weight:650; }
.ag-pblock.is-quiet .ag-q { font-size:13px; color:var(--text-3); }
.ag-pblock.is-quiet .ag-q mark { background:rgba(255,255,255,.07); color:var(--text-2); }
@media (max-width:640px) { .ag-q { font-size:15px; } .ag-acts .btn { flex:1 1 100%; } }
/* showToast's optional action ("Undo"): base.html showToast(msg, type, {action, onAction}). */
.toast-act { margin-left:10px; color:#A8C7FF; background:none; border:0; font:inherit; cursor:pointer;
  text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; }

/* ===== THE OUT-OF-CREDITS WINDOW (2026-09-29) ==================================================
   app/templates/_credit_options.html (window.openCreditOptions). Ported from
   designs/mockups/out-of-credits.html, owner's picks "A · Two doors" + "1 · a Today box". The plan
   card is the real .plc-* and the packs the real .pkr-*; only the pieces below are new. */
/* The fact that decides it: what the action needs against what the account holds. */
.oc-meter { margin:2px 0 14px; }
.oc-meter .row { display:flex; justify-content:space-between; align-items:baseline; gap:10px; font-size:12.5px; color:var(--text-2); }
.oc-meter .row b { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:15px; color:var(--text-1); font-weight:700; }
.oc-meter .row .have b { color:#FBD375; }
.oc-meter .bar { position:relative; height:6px; border-radius:999px; background:var(--bg-hover); margin-top:7px; overflow:hidden; }
.oc-meter .bar i { position:absolute; left:0; top:0; bottom:0; border-radius:999px; background:linear-gradient(90deg,#FBD375,#F5B94A); }
.oc-member { font-size:12.5px; color:var(--text-2); line-height:1.5; padding:10px 12px; border:1px dashed var(--border-strong); border-radius:10px; margin:0 0 12px; }
.oc-member b { color:var(--text-1); }
/* Two doors: the plan on the left, the packs on the right; one column on a phone. */
.oc-doors { display:grid; grid-template-columns:minmax(0,300px) minmax(0,1fr); gap:16px; align-items:start; }
.oc-doors.is-one { grid-template-columns:minmax(0,1fr); }
@media (max-width:760px) { .oc-doors { grid-template-columns:minmax(0,1fr); } }
.oc-door-h { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--text-3); margin:0 0 8px; }
.oc-plc { height:auto; }
.oc-covers { display:inline-flex; align-items:center; gap:5px; font-size:11.5px; color:#4ADE80; margin-top:4px; }
.oc-covers svg { width:12px; height:12px; }
/* THE TODAY BOX (the owner's "more emphasis that you pay the difference, prorated, now"): what is
   charged today and what it adds today, right under the plan's monthly price. */
.oc-today { margin:10px 0 12px; padding:10px 12px; border-radius:10px; border:1px solid rgba(167,139,250,.45); background:rgba(139,61,255,.12); }
.oc-today .row { display:flex; justify-content:space-between; align-items:baseline; gap:10px; }
.oc-today .row + .row { margin-top:4px; }
.oc-today .k { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:#C4B5FD; }
.oc-today b { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:18px; font-weight:700; color:var(--text-1); }
.oc-today b.cr { font-size:15px; color:#4ADE80; }
.oc-today .why { font-size:11.5px; color:var(--text-2); line-height:1.45; margin-top:7px; }
.oc-runrow { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:16px; padding-top:14px; border-top:1px solid var(--border-subtle); font-size:13px; color:var(--text-2); }
.oc-runrow b { color:var(--text-1); }
/* The /billing upgrade step's receipt lines (owner's pick "1 · Receipt lines"). */
.up-lines { border:1px solid var(--border); border-radius:12px; overflow:hidden; margin:2px 0 4px; }
.up-line { display:flex; justify-content:space-between; align-items:baseline; gap:12px; padding:10px 14px; font-size:13px; color:var(--text-2); }
.up-line + .up-line { border-top:1px solid var(--border-subtle); }
.up-line b { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:14px; color:var(--text-1); white-space:nowrap; }
.up-line small { display:block; font-size:11.5px; color:var(--text-3); margin-top:2px; }
.up-line.is-total { background:var(--bg-hover); }
.up-line.is-total b { font-size:16px; }
.up-line .cr { color:#4ADE80; }

/* ── BRAND CURSOR (owner's pick 2026-10-09; mockup designs/mockups/cursor.html) ─────────────────────
   One purple arrow on EVERY page (app.css is loaded everywhere through _ds_head.html), and it does NOT
   change over buttons or links: the owner asked for no hand. That is why this is `*` + !important: the
   hand comes from ~170 separate `cursor:pointer` rules and `cursor-pointer` classes, and overriding
   them one by one would miss the next one anyone writes.
   The price of `*` + !important is that it ALSO flattens the cursors that do a job. Those are put back
   below, by name. tests/test_brand_cursor.py fails when a new grab/move/resize/wait/text cursor appears
   that is not on that list, because otherwise it would silently become the arrow (nothing breaks, it
   just stops showing "you can drag this").
   Deliberately NOT put back (owner: nothing changes on hover): pointer, help, not-allowed, default.
   Disabled buttons still look faded, which is what tells people they are off.
   Art: tools/cursor/build.mjs makes cursor.png (32px) + cursor@2x.png (64px) and PRINTS the three lines
   below, which carry those PNGs inline as data: addresses. Inline on purpose (2026-10-09): as separate
   files the browser fetched the picture after the styles and showed the system arrow meanwhile, on
   every page change. The test fails if these lines and the PNG files ever disagree. "3 3" is the tip,
   the pixel that clicks; it must match TIP in build.mjs. Three declarations, last one a browser
   understands wins: plain PNG for any browser, then the 1x/2x set so high-res screens get the sharp one. */
*, *::before, *::after {
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAER0lEQVR4nOxXbWhbZRQ+9yO9aZOQtlumVru12SzYietcHaKlPzqKyqYIg0FBKPpD/wl1TnEgRuyPikgRQarWj+Gsa0EFBR2sW/fRdSMb7IOxj7ZL17RLaLY2aZs09yO5d+fcm2zplm43W7JfO3DIfc993/c855znvG8uCwDM9PT0U4qibI/H45s8Hg9PNnhIwgwMDJQ2NjZ6WZZ9mgyqqp5HIB/Y7fZ+GqJqUEBh6+rqXk871w0sW2uz2fYmEol/MTO1ZIICCovR2tKD/bsUmAkYAXMc91p5efkZWZa/8/l8j0GBysIODg6eSA8YjPWXnSL0/6pAfF4HwlkslveqqqouSpK0A8tlzTcQ2uxJURQPCoKwZuxsEv78StZfCCUMvPgGD8+/wgPHG5M1TbuMcz8pKSn5C/LED6qvODc3d5QGK2u5m86kBQ0O7VHg549FGPYmDbQMs7q4uLgP+XE4EomshzzwgzaQJycn99OAnK9cyy2aMBvS4J9vZfjjCwmmxlTdhvx4yel0nkR+7EKiVsIDANEBtLa2HsX2i5HBvS77XleHVfjtMwn+65JhfkbPPIP8eAuJegH54UEuOeA++EELKOTlWIYeh8PRFMGIu7eLd11kKQKo38zDxi0W/ZkE+RFAIB9hifbgMAkmhcKlvIrhcPgwGUpXMFD2xN0DUZCnx/5OwE8finDuSBLUpBpDflRYrdbd1LZ9fX1FkAMAfc+hoaF9aWP1c5ypxdGwBnt/kGG3R7EFLxv8wLK829TUtAWMzJoCQAVVWlpa/JjCYTK615kDkJbQFRX+/17GTBhjnudfoB8wwYlU0+k1i1M7ulyumspnWNwEIJFYemGRALB6AwcVa1hdV6xi9YOMZHR0dBBMdkYaAGVBmpiYOIAA3tbb8VkOfKfv5JKtlIENr/KwfhMPFuvid8lkct7v93fV19efSQV1z4OKzQCgdHR0HMd2jJKhOks7uipZeOdLATZuvuUcD6XZaDR6KhAI/N7e3r7N7Xb/iOZZegUmJLNGi9qRCNb1/q12XFbBQMunAljt+hIN5530er09zc3Np3C8gErnSBh1DlUEk63IZzwTjSVsx0MEwF7GQDm240xQwyMY4M22m85hZGTkm5qamh58nEGljEmoCqqcitz0HXF7nmVsx/70oDrVDWsbOCh73HAeDAZ70Hk3Po6hXkW9DkbKF1IgcrqgMgFktuMlMrjrjMvp5a0WfQLWe6qtre1rfAygRsCIPE22+7oZb88AbbaAZRigwapaFrbuEMCxzIh+fHy8u7e3l5xT2nNK9VKS7cRh8abzNzQ0bMO/Z1any3Aei8VOIOE+D4VC18CIPC+S9cjDfz4JdObDm64STzUnEbOzs3MnnvHjYESvQp6EWcJGXb4c1YUqpJxOgVH3nImWKwAS4oYlBYRLOaXezkvdzQDI9q4g3wcP7QtoKSnoR8cjAI8AmJEbAAAA//9Gcvi0AAAABklEQVQDAIQmqxsvD9yeAAAAAElFTkSuQmCC") 3 3, auto !important;
  cursor: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAER0lEQVR4nOxXbWhbZRQ+9yO9aZOQtlumVru12SzYietcHaKlPzqKyqYIg0FBKPpD/wl1TnEgRuyPikgRQarWj+Gsa0EFBR2sW/fRdSMb7IOxj7ZL17RLaLY2aZs09yO5d+fcm2zplm43W7JfO3DIfc993/c855znvG8uCwDM9PT0U4qibI/H45s8Hg9PNnhIwgwMDJQ2NjZ6WZZ9mgyqqp5HIB/Y7fZ+GqJqUEBh6+rqXk871w0sW2uz2fYmEol/MTO1ZIICCovR2tKD/bsUmAkYAXMc91p5efkZWZa/8/l8j0GBysIODg6eSA8YjPWXnSL0/6pAfF4HwlkslveqqqouSpK0A8tlzTcQ2uxJURQPCoKwZuxsEv78StZfCCUMvPgGD8+/wgPHG5M1TbuMcz8pKSn5C/LED6qvODc3d5QGK2u5m86kBQ0O7VHg549FGPYmDbQMs7q4uLgP+XE4EomshzzwgzaQJycn99OAnK9cyy2aMBvS4J9vZfjjCwmmxlTdhvx4yel0nkR+7EKiVsIDANEBtLa2HsX2i5HBvS77XleHVfjtMwn+65JhfkbPPIP8eAuJegH54UEuOeA++EELKOTlWIYeh8PRFMGIu7eLd11kKQKo38zDxi0W/ZkE+RFAIB9hifbgMAkmhcKlvIrhcPgwGUpXMFD2xN0DUZCnx/5OwE8finDuSBLUpBpDflRYrdbd1LZ9fX1FkAMAfc+hoaF9aWP1c5ypxdGwBnt/kGG3R7EFLxv8wLK829TUtAWMzJoCQAVVWlpa/JjCYTK615kDkJbQFRX+/17GTBhjnudfoB8wwYlU0+k1i1M7ulyumspnWNwEIJFYemGRALB6AwcVa1hdV6xi9YOMZHR0dBBMdkYaAGVBmpiYOIAA3tbb8VkOfKfv5JKtlIENr/KwfhMPFuvid8lkct7v93fV19efSQV1z4OKzQCgdHR0HMd2jJKhOks7uipZeOdLATZuvuUcD6XZaDR6KhAI/N7e3r7N7Xb/iOZZegUmJLNGi9qRCNb1/q12XFbBQMunAljt+hIN5530er09zc3Np3C8gErnSBh1DlUEk63IZzwTjSVsx0MEwF7GQDm240xQwyMY4M22m85hZGTkm5qamh58nEGljEmoCqqcitz0HXF7nmVsx/70oDrVDWsbOCh73HAeDAZ70Hk3Po6hXkW9DkbKF1IgcrqgMgFktuMlMrjrjMvp5a0WfQLWe6qtre1rfAygRsCIPE22+7oZb88AbbaAZRigwapaFrbuEMCxzIh+fHy8u7e3l5xT2nNK9VKS7cRh8abzNzQ0bMO/Z1any3Aei8VOIOE+D4VC18CIPC+S9cjDfz4JdObDm64STzUnEbOzs3MnnvHjYESvQp6EWcJGXb4c1YUqpJxOgVH3nImWKwAS4oYlBYRLOaXezkvdzQDI9q4g3wcP7QtoKSnoR8cjAI8AmJEbAAAA//9Gcvi0AAAABklEQVQDAIQmqxsvD9yeAAAAAElFTkSuQmCC") 1x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIoElEQVR4nOxbaWxUVRQ+M/M6dKFWW6hQKaW0rBWagAQoCEIkVn5oEPiFgiQQ4w+MS9mChMQo4QcSQgJREbqoBCuKlEYtLbUgIhQExEBB7WZpa/e9s3bGc96b27nzZm+n7UzbL7npe+/eN3PP9p1z750KYIWCawxmJ3+HDQSwCq1sa2t7ZuzYsdsUCkUs3jf29PRc7+jo+CEyMrIE703ce8NGEaLg2ITu7u6XQkJCvnEwxoSKyKqtrX0/Nja2Hu97QFLAsFACCa8qLi5+Mjg4+FNnY1Qq1euTJk0q0el0aXgfRO9Y3lVAgIOECEpKSnoN3T7SzdhwtVq932QyPcCwWAtS+CghwBUhKgCR6ukLqKgpyBOnjUZjQWtrazLYekPAKUJUAFrVIO+4mGmAlv+chzmGxbMRERHFer3+s6qqqglgS6gBA5EDtFpthbxDgT3pu7RQ+IUBdN1OFaFC7yF+uI/8sAMCkB/EidbU1FyVd8QnK5H+AW5dMMLx93RwO98IZpPTz3kM+eFD9KT7gcYPNEFzWlpasdls1vIdk2erQCVI19pOM1zMMkD6bi38e8+5FpAfplr4oTBQ+IEmNQ5bJFruOE5+Gd/57UE9lP/RY/fS1GQVrHg1CJ6Y4Fomg8GQXldXt9ef6weyjhGboaWl5Vd559RkpcOXylApIj986ZIfKL1stvBDWnZ2dij4IT/QZMgyhqtXrxbJO+OTVU5fFPkhzwifEz8UuOWH/evWrbuDXrYGrPzgF2FBEwjB9hi2KLRULk42nh9wYocWWmrde21kjAKe36iGyUlKl+OwrL7U3Nz8dnR09D2Q1hdMdUMSGkrLBKgO0KOF7LPBXJVHH9RcY4bsAzo4e0jvrn5YPn78+JtYPxyrqKiIhiGuH5gCRB549OjRL/IBznjAGUpvS/zw81du64ctcXFxtL54Nysri7xwSPhBYfniYJDCYBzm8muYzkLZgB5UzZGtGvGvtwgJV8CStQIkrxDEwsoZMAWXYjG2OzQ09BxInDRoYSHWAZYvJRH1XV1dN/gBVAvEPe1ZGMih6TBDQYZUP1Q9cFk/JOBSPBvrhwv19fVzYBCJkimA8YChqanJLh3GexkGchA/fP2RxA9t9W754Qbyw9HB4gcmGSmAvEDvKB1Om983D5CD+IGyStEpA+i1TocJyA9bkR8eYlhsA2l9MWDc0Lsdhk0N1nSYg+kwgR94EifeXOu7kAwlflgnwFziBxeiYdosLC0t3Thjxow6sFaTPgMLAT4MKB3+Jh/oqijqC7qRH/LTDZC5W+eSHzAsViYkJNBWHVtp+tQTWAgwBYhEWFlZeVk+0NN6wFs0VptEfvj+sHN+QCUs1mg0e8CWHH0CBfeXPrg3HaLrXVMqlaH84MObNWDsQzr0BvNTBVi6NgiCgm2fY6qsx/lMx0sNSIYygQ/AewBLh2IY4C7xDfnguDkD4wU8fv/JCBl7tKDtsvUGTJXRZ86ciQJrGPjEC/j8ZhMGmA6vyAf3Nx16CvUYBeg09s+Rm4iofcoDCtk1fTiVpREZGRkzNm3aVMAP7mwxwydvacGXUOO3TUxQwlPTlBBDLVElPpMD9xZKMTOtxMtWsIZBvzOCXAHydHgOvzSRf+HkTq1Y2PQH0VOUWB6rRIHHx3rmVXgw83FMTMwRvGzBRlbwiQIE7tphOoyKirJRAGWD5pq+MeHERCWkrBG8ziiYAe6g8FkwALtKcvXb8EB5efkl+QtT+1APxKDga7erYcO+MV4Jj8yvr66uzsRF0psgWV0PPi6GBAfPWFlsWLBgQTGuDjXIwL1ROWmmEoIwSAx68AiU1lZsCAJPgQuiZgy92s7Ozr8yMzNP7dy5swYfd2DrwqYDH3uBXAF26RBXh9dxs/Q5NoBWh5OTVGJd7w5LXgmCxWsEp/1Ya3S1t7ffxlPpUtya/6egoODvffv2EcmJHggS2ZHgTAGD4gF26ZBXAIHSoTsFkNXJ+o6AXqXD3eLzBw8ePH3o0KF2y3fxTVQ+SApgTWd5boIB5IDeObKJ5OXlFco7E+e5iGPMJS9sUTsUHmPa2NDQkHvgwIGNSGrHUHhy7yZsjdgauNZoaeQNZH2Kf58Lb5muw2du02E6psMmB+kwaakKXnxDbfccra7Nz8/flZqaehdvu8Hessz6jIN6uGsmuM93iBx5gKN06ODozN4LFPgoZY094aHw3YWFhdtR+Jt422xpZHnK6WTlNrDGOSmHMT59P68An8NZCMjTod3q0FE6nLNMgIhoW6dC4TsxjNJWrVpFwrdwjWK/E6xeoAerFzChB0xwBldlmDwddvGdsbOkdMhA2WHxy/Zx//Dhw6OrV6++BZKlqZHg9FnMyszteYEH7QjNlQfIV4fF/ADa5Z3MbZbSzk54lK31cW+vavbs2TkgCc1bnAk+qMI6gisPcLs6TJwnvS6gJyxyYP2ysrJ0kGKbL2SYxf3ioNTdSqQ3HRYVFdmVxTMXotUjFbAST4rDIuysXzlr1qzzIFmdiE0uvF9A4aaPpcNwbOOwKjyBdfki8AAlJSV70f2/A4nxyQP4MtZv4C4E+HSow73Cs+ABkC/+ROF/BGtKG5AixhdwFwI8D+hQqDysCa64egGzRXtOTs4HIMU8Izy/szyDu7WpfNNUwMVLcUpKSlxwcHC8fDCu5Bpyc3PfWb9+/X2wpjtSgF9an6DwcAxRPNsxfhxbxOXLlxfiYcXysLCwBCS8RsTd6dOnU8xTvLPqjkJgWChA/D0hSEoIt7Qwyz15EeMJEpilPSa8T7auBgqCB2N4ImQgoYjcxoCkABqjtzxjixwW+34Nb7aXe3kAJG9gjT9iZwsYVs/7RbHjCt7ur7MDCfZrDnZMxZfOQ/67H2/QlwMGPjPI/7sk4P6zpL8nLHIFBBz85geLQ4XBOezzY4wqAEY4RhUAIxyjCoARjlEFwAjHqAJghON/AAAA//9KTHw1AAAABklEQVQDAGdf8tmu9iQWAAAAAElFTkSuQmCC") 2x) 3 3, auto !important;
  cursor: image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAER0lEQVR4nOxXbWhbZRQ+9yO9aZOQtlumVru12SzYietcHaKlPzqKyqYIg0FBKPpD/wl1TnEgRuyPikgRQarWj+Gsa0EFBR2sW/fRdSMb7IOxj7ZL17RLaLY2aZs09yO5d+fcm2zplm43W7JfO3DIfc993/c855znvG8uCwDM9PT0U4qibI/H45s8Hg9PNnhIwgwMDJQ2NjZ6WZZ9mgyqqp5HIB/Y7fZ+GqJqUEBh6+rqXk871w0sW2uz2fYmEol/MTO1ZIICCovR2tKD/bsUmAkYAXMc91p5efkZWZa/8/l8j0GBysIODg6eSA8YjPWXnSL0/6pAfF4HwlkslveqqqouSpK0A8tlzTcQ2uxJURQPCoKwZuxsEv78StZfCCUMvPgGD8+/wgPHG5M1TbuMcz8pKSn5C/LED6qvODc3d5QGK2u5m86kBQ0O7VHg549FGPYmDbQMs7q4uLgP+XE4EomshzzwgzaQJycn99OAnK9cyy2aMBvS4J9vZfjjCwmmxlTdhvx4yel0nkR+7EKiVsIDANEBtLa2HsX2i5HBvS77XleHVfjtMwn+65JhfkbPPIP8eAuJegH54UEuOeA++EELKOTlWIYeh8PRFMGIu7eLd11kKQKo38zDxi0W/ZkE+RFAIB9hifbgMAkmhcKlvIrhcPgwGUpXMFD2xN0DUZCnx/5OwE8finDuSBLUpBpDflRYrdbd1LZ9fX1FkAMAfc+hoaF9aWP1c5ypxdGwBnt/kGG3R7EFLxv8wLK829TUtAWMzJoCQAVVWlpa/JjCYTK615kDkJbQFRX+/17GTBhjnudfoB8wwYlU0+k1i1M7ulyumspnWNwEIJFYemGRALB6AwcVa1hdV6xi9YOMZHR0dBBMdkYaAGVBmpiYOIAA3tbb8VkOfKfv5JKtlIENr/KwfhMPFuvid8lkct7v93fV19efSQV1z4OKzQCgdHR0HMd2jJKhOks7uipZeOdLATZuvuUcD6XZaDR6KhAI/N7e3r7N7Xb/iOZZegUmJLNGi9qRCNb1/q12XFbBQMunAljt+hIN5530er09zc3Np3C8gErnSBh1DlUEk63IZzwTjSVsx0MEwF7GQDm240xQwyMY4M22m85hZGTkm5qamh58nEGljEmoCqqcitz0HXF7nmVsx/70oDrVDWsbOCh73HAeDAZ70Hk3Po6hXkW9DkbKF1IgcrqgMgFktuMlMrjrjMvp5a0WfQLWe6qtre1rfAygRsCIPE22+7oZb88AbbaAZRigwapaFrbuEMCxzIh+fHy8u7e3l5xT2nNK9VKS7cRh8abzNzQ0bMO/Z1any3Aei8VOIOE+D4VC18CIPC+S9cjDfz4JdObDm64STzUnEbOzs3MnnvHjYESvQp6EWcJGXb4c1YUqpJxOgVH3nImWKwAS4oYlBYRLOaXezkvdzQDI9q4g3wcP7QtoKSnoR8cjAI8AmJEbAAAA//9Gcvi0AAAABklEQVQDAIQmqxsvD9yeAAAAAElFTkSuQmCC") 1x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIoElEQVR4nOxbaWxUVRQ+M/M6dKFWW6hQKaW0rBWagAQoCEIkVn5oEPiFgiQQ4w+MS9mChMQo4QcSQgJREbqoBCuKlEYtLbUgIhQExEBB7WZpa/e9s3bGc96b27nzZm+n7UzbL7npe+/eN3PP9p1z750KYIWCawxmJ3+HDQSwCq1sa2t7ZuzYsdsUCkUs3jf29PRc7+jo+CEyMrIE703ce8NGEaLg2ITu7u6XQkJCvnEwxoSKyKqtrX0/Nja2Hu97QFLAsFACCa8qLi5+Mjg4+FNnY1Qq1euTJk0q0el0aXgfRO9Y3lVAgIOECEpKSnoN3T7SzdhwtVq932QyPcCwWAtS+CghwBUhKgCR6ukLqKgpyBOnjUZjQWtrazLYekPAKUJUAFrVIO+4mGmAlv+chzmGxbMRERHFer3+s6qqqglgS6gBA5EDtFpthbxDgT3pu7RQ+IUBdN1OFaFC7yF+uI/8sAMCkB/EidbU1FyVd8QnK5H+AW5dMMLx93RwO98IZpPTz3kM+eFD9KT7gcYPNEFzWlpasdls1vIdk2erQCVI19pOM1zMMkD6bi38e8+5FpAfplr4oTBQ+IEmNQ5bJFruOE5+Gd/57UE9lP/RY/fS1GQVrHg1CJ6Y4Fomg8GQXldXt9ef6weyjhGboaWl5Vd559RkpcOXylApIj986ZIfKL1stvBDWnZ2dij4IT/QZMgyhqtXrxbJO+OTVU5fFPkhzwifEz8UuOWH/evWrbuDXrYGrPzgF2FBEwjB9hi2KLRULk42nh9wYocWWmrde21kjAKe36iGyUlKl+OwrL7U3Nz8dnR09D2Q1hdMdUMSGkrLBKgO0KOF7LPBXJVHH9RcY4bsAzo4e0jvrn5YPn78+JtYPxyrqKiIhiGuH5gCRB549OjRL/IBznjAGUpvS/zw81du64ctcXFxtL54Nysri7xwSPhBYfniYJDCYBzm8muYzkLZgB5UzZGtGvGvtwgJV8CStQIkrxDEwsoZMAWXYjG2OzQ09BxInDRoYSHWAZYvJRH1XV1dN/gBVAvEPe1ZGMih6TBDQYZUP1Q9cFk/JOBSPBvrhwv19fVzYBCJkimA8YChqanJLh3GexkGchA/fP2RxA9t9W754Qbyw9HB4gcmGSmAvEDvKB1Om983D5CD+IGyStEpA+i1TocJyA9bkR8eYlhsA2l9MWDc0Lsdhk0N1nSYg+kwgR94EifeXOu7kAwlflgnwFziBxeiYdosLC0t3Thjxow6sFaTPgMLAT4MKB3+Jh/oqijqC7qRH/LTDZC5W+eSHzAsViYkJNBWHVtp+tQTWAgwBYhEWFlZeVk+0NN6wFs0VptEfvj+sHN+QCUs1mg0e8CWHH0CBfeXPrg3HaLrXVMqlaH84MObNWDsQzr0BvNTBVi6NgiCgm2fY6qsx/lMx0sNSIYygQ/AewBLh2IY4C7xDfnguDkD4wU8fv/JCBl7tKDtsvUGTJXRZ86ciQJrGPjEC/j8ZhMGmA6vyAf3Nx16CvUYBeg09s+Rm4iofcoDCtk1fTiVpREZGRkzNm3aVMAP7mwxwydvacGXUOO3TUxQwlPTlBBDLVElPpMD9xZKMTOtxMtWsIZBvzOCXAHydHgOvzSRf+HkTq1Y2PQH0VOUWB6rRIHHx3rmVXgw83FMTMwRvGzBRlbwiQIE7tphOoyKirJRAGWD5pq+MeHERCWkrBG8ziiYAe6g8FkwALtKcvXb8EB5efkl+QtT+1APxKDga7erYcO+MV4Jj8yvr66uzsRF0psgWV0PPi6GBAfPWFlsWLBgQTGuDjXIwL1ROWmmEoIwSAx68AiU1lZsCAJPgQuiZgy92s7Ozr8yMzNP7dy5swYfd2DrwqYDH3uBXAF26RBXh9dxs/Q5NoBWh5OTVGJd7w5LXgmCxWsEp/1Ya3S1t7ffxlPpUtya/6egoODvffv2EcmJHggS2ZHgTAGD4gF26ZBXAIHSoTsFkNXJ+o6AXqXD3eLzBw8ePH3o0KF2y3fxTVQ+SApgTWd5boIB5IDeObKJ5OXlFco7E+e5iGPMJS9sUTsUHmPa2NDQkHvgwIGNSGrHUHhy7yZsjdgauNZoaeQNZH2Kf58Lb5muw2du02E6psMmB+kwaakKXnxDbfccra7Nz8/flZqaehdvu8Hessz6jIN6uGsmuM93iBx5gKN06ODozN4LFPgoZY094aHw3YWFhdtR+Jt422xpZHnK6WTlNrDGOSmHMT59P68An8NZCMjTod3q0FE6nLNMgIhoW6dC4TsxjNJWrVpFwrdwjWK/E6xeoAerFzChB0xwBldlmDwddvGdsbOkdMhA2WHxy/Zx//Dhw6OrV6++BZKlqZHg9FnMyszteYEH7QjNlQfIV4fF/ADa5Z3MbZbSzk54lK31cW+vavbs2TkgCc1bnAk+qMI6gisPcLs6TJwnvS6gJyxyYP2ysrJ0kGKbL2SYxf3ioNTdSqQ3HRYVFdmVxTMXotUjFbAST4rDIuysXzlr1qzzIFmdiE0uvF9A4aaPpcNwbOOwKjyBdfki8AAlJSV70f2/A4nxyQP4MtZv4C4E+HSow73Cs+ABkC/+ROF/BGtKG5AixhdwFwI8D+hQqDysCa64egGzRXtOTs4HIMU8Izy/szyDu7WpfNNUwMVLcUpKSlxwcHC8fDCu5Bpyc3PfWb9+/X2wpjtSgF9an6DwcAxRPNsxfhxbxOXLlxfiYcXysLCwBCS8RsTd6dOnU8xTvLPqjkJgWChA/D0hSEoIt7Qwyz15EeMJEpilPSa8T7auBgqCB2N4ImQgoYjcxoCkABqjtzxjixwW+34Nb7aXe3kAJG9gjT9iZwsYVs/7RbHjCt7ur7MDCfZrDnZMxZfOQ/67H2/QlwMGPjPI/7sk4P6zpL8nLHIFBBz85geLQ4XBOezzY4wqAEY4RhUAIxyjCoARjlEFwAjHqAJghON/AAAA//9KTHw1AAAABklEQVQDAGdf8tmu9iQWAAAAAElFTkSuQmCC") 2x) 3 3, auto !important;
}
/* Typing: an I so people can aim between two letters. Ours (owner's pick 2026-10-09): the usual I shape in
   brand purple with a white outline, same size as the system one, hotspot in its middle. Made by
   tools/cursor/build.mjs (cursor-text.png / @2x) and inline for the same reason as the arrow. Falls back
   to the system `text` cursor. The blinking typing line stays the browser's white on purpose (owner). */
input:not([type=button]):not([type=submit]):not([type=reset]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]):not([type=file]):not([type=image]):not([type=hidden]),
textarea, [contenteditable]:not([contenteditable="false"]), [contenteditable]:not([contenteditable="false"]) *,
[data-lq] {
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVR4nOxWvUoDQRCevfwYY4w/YKEkQezUCLb2iimtrKwUH8AqrZZCnsBCG7XwBYQTfQJBxYioWMSLNiqeGIhGvPXbyxlIozeHcM1+8LG7M8vsd3PLzBoUMgwKGVqAFsASIKWMg7ugDVqg6dHybNtghBNTcDYjeKH+JvfNzU9KpgWNTDb13544BDvNLsUokRLTQohDvzGjxEP29UnSzfGXuzg7andOzUWVgGFigJuBAQyn0qEh69KhvfUP1z5f7KDsqEHCoHssJ5CBF78xWXcAgR8xZHBQITduUKpXuFRz2Gbgz3AOZwvwREgM52qOTCArLVeZAoB7B34D63f+QBciLUALCF1A0Dogfdr+BDsD6Aeq4OTVHOUXlbHlylMAcN8DqhlVUYLNStmhmi1dVi4cVZYP4K+CfZyY3G64/PwgN3ZW36lRb/fFO4kW1hLUPygW0S+2/Mbk3oGrZJooNxahrh4BNo01m9wHSbLbXV4z4rEzoPYXwRWwAd55rhwYA0v4+hIxEKiD/Sd0IdICQhfwDQAA//8Xg0+cAAAABklEQVQDAB8kka1BYaaVAAAAAElFTkSuQmCC") 16 16, text !important;
  cursor: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVR4nOxWvUoDQRCevfwYY4w/YKEkQezUCLb2iimtrKwUH8AqrZZCnsBCG7XwBYQTfQJBxYioWMSLNiqeGIhGvPXbyxlIozeHcM1+8LG7M8vsd3PLzBoUMgwKGVqAFsASIKWMg7ugDVqg6dHybNtghBNTcDYjeKH+JvfNzU9KpgWNTDb13544BDvNLsUokRLTQohDvzGjxEP29UnSzfGXuzg7andOzUWVgGFigJuBAQyn0qEh69KhvfUP1z5f7KDsqEHCoHssJ5CBF78xWXcAgR8xZHBQITduUKpXuFRz2Gbgz3AOZwvwREgM52qOTCArLVeZAoB7B34D63f+QBciLUALCF1A0Dogfdr+BDsD6Aeq4OTVHOUXlbHlylMAcN8DqhlVUYLNStmhmi1dVi4cVZYP4K+CfZyY3G64/PwgN3ZW36lRb/fFO4kW1hLUPygW0S+2/Mbk3oGrZJooNxahrh4BNo01m9wHSbLbXV4z4rEzoPYXwRWwAd55rhwYA0v4+hIxEKiD/Sd0IdICQhfwDQAA//8Xg0+cAAAABklEQVQDAB8kka1BYaaVAAAAAElFTkSuQmCC") 1x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAADT0lEQVR4nOyay0tUURzHv/fOOJmUIloWFjpTmhBYEi4sWtQuELJCWhuFQSASrYo2bdxEgSFEi/oDetgmqKigB9UiH1ELzUdCmfQQQ8N8zMzte2auY7ecGILfbeicD/zu73rPOTNzvp7fufee37GhOTY0xwgAzTECQHOMANAcIwA0xwgAzQnCBxzHOUy3i1ZIG6G9df0jmkXbSSunhV0/QbtnWdYVCGNBEHZcjbDrtIY0VWZcn5um/AatkULEIYS0AKfpzsAB3vfHMToQR95KIK/AwtqNNs+TXz896WBsMJ7w01NAaaWNdZtS0XmSArRBCGkBXtNtHuqOofP8nKcsmANU72YEUpyXD6KIRb1tD5wIIbwloE57KUANhJCeA8rUYWzo9xEcnQe670TTNvzAEeEKEIYg0neBN+oQqQkgJ5R5o5xlwIaagOczpJAOgX1ITmSIMgJGXsUS88Bwbwzjo46nblGphcjWAEorbJRXBxIh4rKHIXAbQogKoKAILXTnaKl/qYr3iy0z+D6VFGE5J8Oj7bkIeANSxUcrO98BQcSfA9iBdopwF8l7fR2tSXU0v8hKCZBfbC10Xl1Q9/5ntMds2w9hfHkQYkf66PooRA99U/Li0lVpHazfDZ/wRYAlcdKWiIflz5h3AWQfYo+9S5GNAvg6Ks16ADTHCADNMQJAc4wA0BwjADTHCADNMa/DyD7M6zB8xIQA/hXpV/7+vxDganAVksvitRlUb2b9LvqHXB0WzQopxAVgZ1rpzuKXxMjkl8Vl4alxJ3HNzQ00u5ejbHucIlyAINKpsb10N9W5So2NDccxN+3gaWcUH0e8I70kbGN7QxChPKbOIzaCi7nEeopwC0JIC/CCbpvK9F5tm8X8XGbtVHL04KllWBNOzNHPKUAdhJC+C1Sqg0qGZtp5xfws2/TEFv6sgiDSAgyog8r62gFvQUm5jfpjoYStLvP+DFXX3RuQ+gwppEPgCN0ldf71k4PBrhhWrbcTqfAVhd6v/jbhJFLmn9/FUVEbQEFxqvyQ5GYpP9LjapPUfvwd19j5RgjiSyLyD9vknri/YQeS2+MiSG6rUdvk7rPzlyGMr5nYbMSsB0BzjADQHCMANMcIAM0xAkBztBfgBwAAAP//vmuFrAAAAAZJREFUAwDPKeycpKKL3gAAAABJRU5ErkJggg==") 2x) 16 16, text !important;
  cursor: image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVR4nOxWvUoDQRCevfwYY4w/YKEkQezUCLb2iimtrKwUH8AqrZZCnsBCG7XwBYQTfQJBxYioWMSLNiqeGIhGvPXbyxlIozeHcM1+8LG7M8vsd3PLzBoUMgwKGVqAFsASIKWMg7ugDVqg6dHybNtghBNTcDYjeKH+JvfNzU9KpgWNTDb13544BDvNLsUokRLTQohDvzGjxEP29UnSzfGXuzg7andOzUWVgGFigJuBAQyn0qEh69KhvfUP1z5f7KDsqEHCoHssJ5CBF78xWXcAgR8xZHBQITduUKpXuFRz2Gbgz3AOZwvwREgM52qOTCArLVeZAoB7B34D63f+QBciLUALCF1A0Dogfdr+BDsD6Aeq4OTVHOUXlbHlylMAcN8DqhlVUYLNStmhmi1dVi4cVZYP4K+CfZyY3G64/PwgN3ZW36lRb/fFO4kW1hLUPygW0S+2/Mbk3oGrZJooNxahrh4BNo01m9wHSbLbXV4z4rEzoPYXwRWwAd55rhwYA0v4+hIxEKiD/Sd0IdICQhfwDQAA//8Xg0+cAAAABklEQVQDAB8kka1BYaaVAAAAAElFTkSuQmCC") 1x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAADT0lEQVR4nOyay0tUURzHv/fOOJmUIloWFjpTmhBYEi4sWtQuELJCWhuFQSASrYo2bdxEgSFEi/oDetgmqKigB9UiH1ELzUdCmfQQQ8N8zMzte2auY7ecGILfbeicD/zu73rPOTNzvp7fufee37GhOTY0xwgAzTECQHOMANAcIwA0xwgAzQnCBxzHOUy3i1ZIG6G9df0jmkXbSSunhV0/QbtnWdYVCGNBEHZcjbDrtIY0VWZcn5um/AatkULEIYS0AKfpzsAB3vfHMToQR95KIK/AwtqNNs+TXz896WBsMJ7w01NAaaWNdZtS0XmSArRBCGkBXtNtHuqOofP8nKcsmANU72YEUpyXD6KIRb1tD5wIIbwloE57KUANhJCeA8rUYWzo9xEcnQe670TTNvzAEeEKEIYg0neBN+oQqQkgJ5R5o5xlwIaagOczpJAOgX1ITmSIMgJGXsUS88Bwbwzjo46nblGphcjWAEorbJRXBxIh4rKHIXAbQogKoKAILXTnaKl/qYr3iy0z+D6VFGE5J8Oj7bkIeANSxUcrO98BQcSfA9iBdopwF8l7fR2tSXU0v8hKCZBfbC10Xl1Q9/5ntMds2w9hfHkQYkf66PooRA99U/Li0lVpHazfDZ/wRYAlcdKWiIflz5h3AWQfYo+9S5GNAvg6Ks16ADTHCADNMQJAc4wA0BwjADTHCADNMa/DyD7M6zB8xIQA/hXpV/7+vxDganAVksvitRlUb2b9LvqHXB0WzQopxAVgZ1rpzuKXxMjkl8Vl4alxJ3HNzQ00u5ejbHucIlyAINKpsb10N9W5So2NDccxN+3gaWcUH0e8I70kbGN7QxChPKbOIzaCi7nEeopwC0JIC/CCbpvK9F5tm8X8XGbtVHL04KllWBNOzNHPKUAdhJC+C1Sqg0qGZtp5xfws2/TEFv6sgiDSAgyog8r62gFvQUm5jfpjoYStLvP+DFXX3RuQ+gwppEPgCN0ldf71k4PBrhhWrbcTqfAVhd6v/jbhJFLmn9/FUVEbQEFxqvyQ5GYpP9LjapPUfvwd19j5RgjiSyLyD9vknri/YQeS2+MiSG6rUdvk7rPzlyGMr5nYbMSsB0BzjADQHCMANMcIAM0xAkBztBfgBwAAAP//vmuFrAAAAAZJREFUAwDPKeycpKKL3gAAAABJRU5ErkJggg==") 2x) 16 16, text !important;
}
/* Dragging and resizing. Selectors copied from where each cursor is set (named in the test). */
.cursor-move { cursor:move !important; }
.cursor-wait { cursor:wait !important; }
.cursor-col-resize { cursor:col-resize !important; }
.tabulator .tabulator-col-resize-handle:hover { cursor:ew-resize !important; }
.tabulator-row .tabulator-row-resize-handle:hover { cursor:ns-resize !important; }
.proj-grip { cursor:grab !important; }
.proj-grip:active { cursor:grabbing !important; }
body.js .flowframe { cursor:grab !important; }
body.js .flowframe.grabbing { cursor:grabbing !important; }
.of-stage, .of-rbtn { cursor:grab !important; }
.of-stage.grabbing, .of-rbtn.on { cursor:grabbing !important; }
.sp-d-range:not(:disabled) { cursor:grab !important; }
.sp-d-range:not(:disabled):active { cursor:grabbing !important; }
/* A drag in progress sets the cursor on <body> from JS (dashboard grabbing, creator panel resize);
   keep it while the drag lasts, wherever the mouse wanders. */
body[style*="cursor: grabbing"], body[style*="cursor: grabbing"] * { cursor:grabbing !important; }
body[style*="cursor: col-resize"], body[style*="cursor: col-resize"] * { cursor:col-resize !important; }
