/*
  Authority Builder — the press.

  The world is a printer's imposition sheet. See DESIGN.md. Two things here are
  load-bearing rather than decorative and should not be "simplified":

    The color bar is the progress indicator. Four process inks, one per pipeline
    stage, inking in as each completes.

    The collation staircase down the spine of the review sheet is the proof of
    the 1:1 mapping. On a real signature it exists so a binder can see at a
    glance that nothing is out of sequence. That is exactly what it does here.

  Written mobile-first: base rules are the phone, min-width queries add the
  press table.
*/

/* ------------------------------- tokens -------------------------------- */

:root {
  /* Process inks — kept for the color bar (4 stages). */
  --ink-c: #0077b6;
  --ink-m: #c2185b;
  --ink-y: #d4a017;
  --ink-k: #2c2520;
  --ink-reject: #c43c2e;

  /* ── Color system ──
     Bold, saturated, alive. Orange drives action, green signals money,
     blue earns trust, red demands attention. Nothing washed out. */
  --growth: #1a8a5a;
  --growth-hover: #14774d;
  --growth-light: rgba(26, 138, 90, 0.12);
  --growth-bg: #e0f5ea;
  --trust: #2563eb;
  --trust-light: rgba(37, 99, 235, 0.10);
  --trust-bg: #e0ecff;
  --action: #cc4512;
  --action-hover: #b53b0e;
  --action-light: rgba(204, 69, 18, 0.12);
  --on-action: #fff;
  --alert: #dc2626;
  --alert-light: rgba(220, 38, 38, 0.10);
  /* QA-T4 (2026-10-16): text ON an --alert fill, the --on-action twin. White
     on light-mode red is 4.83:1; dark mode's --alert is a pastel red, so its
     on-colour flips to the dark stock (white there was 2.77:1, QA J8). Same
     for --growth and --trust fills. Rule: text on an accent fill is
     var(--on-<accent>), never a literal #fff (tests/phone-layout-floor). */
  --on-alert: #fff;
  --on-growth: #fff;
  --on-trust: #fff;

  --stock: #ffffff;
  --desk: #e8e5dc;
  --rule: #d5d0c6;
  --rule-soft: #e2ded5;
  --text: #1a1a1a;
  --text-secondary: #37352f;
  --text-muted: #5a5550;

  /* Legacy aliases */
  --ink-k-60: var(--text-muted);
  --ink-k-40: #9a948e;
  /* 2026-08-20 craft pass: --sheet and --slug were referenced in 20+ rules
     (card/panel/toast/badge/modal backgrounds, several label/timestamp
     colors) but never actually declared anywhere in this file — confirmed
     live via getComputedStyle, both resolved to "". Every background using
     var(--sheet) with no fallback was silently rendering fully transparent
     (rgba(0,0,0,0), verified on .onboard-modal__panel), and every bare
     var(--slug) text color was falling back to CSS's inherited value
     instead of the intended muted tone. One prior touch (2026-08-12, the
     .slug__identity-menu fix) worked around this locally with a literal
     var(--stock) rather than fixing the shared token, so the bug persisted
     everywhere else. Aliased here instead of hunting every call site,
     matching the semantics already implied by the existing
     var(--ink-k-60, var(--slug)) fallback pairs elsewhere in this file. */
  --sheet: var(--stock);
  --slug: var(--ink-k-60);
  /* Same root cause, wider net: a whole-file grep (every var(--x) call vs.
     every declared --x:) turned up seven more of these — a generic
     Tailwind-shaped naming convention (bg/bg-secondary/bg-alt/border/card/
     surface/text-primary/sans) that never matched this file's actual
     tokens (stock/desk/rule/text/ui), concentrated in the confetti toast,
     help/chat/feedback panels, the app-dialog input, the inbox, email
     campaign steps, and the gallery grid. Same failure mode as --sheet
     above: backgrounds render transparent, .inbox-list-item:hover shows no
     feedback at all, .ecamp-step and .app-dialog__input (the chat-panel
     field this was also fixed on was removed 2026-09-01 with the old Ask
     AI widget) have no visible surface. Aliased to this file's real equivalents
     rather than rewritten call-by-call, same reasoning as --sheet/--slug. */
  --bg: var(--stock);
  --bg-secondary: var(--stock-warm);
  --bg-alt: var(--stock-warm);
  --border: var(--rule);
  --card: var(--stock);
  --surface: var(--stock);
  --text-primary: var(--text);
  --sans: var(--ui);
  /* .toast's own text color (the flash() component fixed for invisibility
     earlier this same session, commit c8f0369) — a separate, unrelated
     undeclared token, not the same bug already fixed there. Bare
     `color: var(--ink)` with no fallback meant toast text rode whatever
     color it happened to inherit rather than the intended dark ink. */
  --ink: var(--text);

  /* Sidebar — warm sand ground */
  --sidebar-bg: #f5f1ea;
  --sidebar-text: #37352f;
  --sidebar-text-active: var(--text);
  --sidebar-rule: #e2ded5;
  --sidebar-hover: #ece8e0;
  --sidebar-active: rgba(229, 80, 26, 0.14);
  --sidebar-toggle-border: var(--rule);
  --sidebar-group: var(--text-muted);

  /* Tinted surfaces — warm and present */
  --stock-warm: #f7f4ee;
  --stock-amber: #fdf3dd;
  --stock-rose: #fdeee8;
  --stock-parchment: #f3efe6;
  --ink-y-dark: #5c4a14;
  --ink-y-mid: #8a6800;

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --manuscript: Charter, "Bitstream Charter", "Iowan Old Style", Georgia, serif;

  --radius: 8px;
  --radius-lg: 10px;
  --radius-pill: 999px;
  --crop: 9px;
  --crop-gap: 5px;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
  --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
  --snap: cubic-bezier(0.16, 1, 0.3, 1);
}

[data-theme="dark"] {
  --ink-k: #e8e5dc;
  --ink-reject: #f87171;

  --growth: #34d399;
  --growth-hover: #2ec48c;
  --growth-light: rgba(52, 211, 153, 0.15);
  --growth-bg: #1a3a2a;
  --trust: #60a5fa;
  --trust-light: rgba(96, 165, 250, 0.12);
  --trust-bg: #1e2d4a;
  --action: #fb923c;
  --action-hover: #f97316;
  --action-light: rgba(251, 146, 60, 0.15);
  --on-action: #1e1c1a;
  --alert: #f87171;
  --alert-light: rgba(248, 113, 113, 0.12);
  --on-alert: #1e1c1a;
  --on-growth: #1e1c1a;
  --on-trust: #1e1c1a;

  --stock: #1e1c1a;
  --desk: #141210;
  --rule: #3a3530;
  --rule-soft: #2e2a26;
  --text: #e8e5dc;
  --text-secondary: #c0bbb4;
  --text-muted: #9a9490;

  --ink-k-60: var(--text-muted);
  --ink-k-40: #6a6560;

  --sidebar-bg: #1a1816;
  --sidebar-text: #c0bbb4;
  --sidebar-text-active: var(--text);
  --sidebar-rule: #2e2a26;
  --sidebar-hover: #2a2622;
  --sidebar-active: rgba(251, 146, 60, 0.18);
  --sidebar-toggle-border: var(--rule);
  --sidebar-group: var(--text-muted);

  --stock-warm: #22201c;
  --stock-amber: #2a2418;
  --stock-rose: #2a1e1a;
  --stock-parchment: #1e1c18;
  --ink-y-dark: #e0c860;
  --ink-y-mid: #d4a020;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.35);
  --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.3);

  color-scheme: dark;
}

/* Bug hunt (2026-09-10, Keeno's own phone, hours before a live
   presentation): the block above only ever matched an EXPLICIT
   data-theme="dark" attribute — set exactly once, by initThemeToggle()
   below, and only after someone clicks the toggle at least once. Anyone
   whose device is already in system dark mode but who has never touched
   that toggle (true of essentially every fresh visitor, and true of
   Keeno's own phone) got NONE of this — no color-scheme: dark (so the
   browser's own automatic dark-mode heuristics kick in against a page
   still carrying every :root light-mode value), and no dark token
   overrides at all. The terms/cookie modals were the visible symptom
   (white text over a background the OS's own automatic darkening had
   painted dark, or vice versa) but the root cause is app-wide: every
   component in this file reads these same custom properties, so a user
   in that exact state — no explicit toggle, system dark — was getting
   broken theming everywhere, not just on that one screen.
   Mirrors the block above EXACTLY, keep both in sync. Guarded by
   :not([data-theme="light"]) so an explicit light choice (the toggle
   button does set data-theme="light" when switching away from dark, see
   initThemeToggle() below) still wins over system preference — this
   only fills the gap where NEITHER explicit attribute is present. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink-k: #e8e5dc;
    --ink-reject: #f87171;

    --growth: #34d399;
    --growth-hover: #2ec48c;
    --growth-light: rgba(52, 211, 153, 0.15);
    --growth-bg: #1a3a2a;
    --trust: #60a5fa;
    --trust-light: rgba(96, 165, 250, 0.12);
    --trust-bg: #1e2d4a;
    --action: #fb923c;
    --action-hover: #f97316;
    --action-light: rgba(251, 146, 60, 0.15);
    --on-action: #1e1c1a;
    --alert: #f87171;
    --alert-light: rgba(248, 113, 113, 0.12);
    --on-alert: #1e1c1a;
    --on-growth: #1e1c1a;
    --on-trust: #1e1c1a;

    --stock: #1e1c1a;
    --desk: #141210;
    --rule: #3a3530;
    --rule-soft: #2e2a26;
    --text: #e8e5dc;
    --text-secondary: #c0bbb4;
    --text-muted: #9a9490;

    --ink-k-60: var(--text-muted);
    --ink-k-40: #6a6560;

    --sidebar-bg: #1a1816;
    --sidebar-text: #c0bbb4;
    --sidebar-text-active: var(--text);
    --sidebar-rule: #2e2a26;
    --sidebar-hover: #2a2622;
    --sidebar-active: rgba(251, 146, 60, 0.18);
    --sidebar-toggle-border: var(--rule);
    --sidebar-group: var(--text-muted);

    --stock-warm: #22201c;
    --stock-amber: #2a2418;
    --stock-rose: #2a1e1a;
    --stock-parchment: #1e1c18;
    --ink-y-dark: #e0c860;
    --ink-y-mid: #d4a020;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
    --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
    --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.35);
    --shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.3);

    color-scheme: dark;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* 2026-08-12: this had no styling at all - a plain, always-visible link
   sitting above the header on every page. Standard pattern: off-screen
   until it actually receives keyboard focus (Tab from the very top of the
   page), which is the only time a sighted keyboard user needs it. */
.skip-link {
  position: absolute;
  top: -48px;
  left: 8px;
  z-index: 200;
  padding: 10px 16px;
  background: var(--action);
  color: var(--stock-warm);
  border-radius: var(--radius);
  font-family: var(--ui);
  font-weight: 600;
  text-decoration: none;
  transition: top 150ms var(--snap, ease);
}
.skip-link:focus {
  top: 8px;
}

body {
  margin: 0;
  background-color: var(--desk);
  background-image:
    repeating-linear-gradient(
      0deg,
      rgba(100, 80, 50, 0.14) 0px,
      rgba(100, 80, 50, 0.14) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      90deg,
      rgba(100, 80, 50, 0.10) 0px,
      rgba(100, 80, 50, 0.10) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      45deg,
      rgba(80, 60, 30, 0.04) 0px,
      rgba(80, 60, 30, 0.04) 1px,
      transparent 1px,
      transparent 4px
    ),
    repeating-linear-gradient(
      -45deg,
      rgba(80, 60, 30, 0.03) 0px,
      rgba(80, 60, 30, 0.03) 1px,
      transparent 1px,
      transparent 4px
    );
  background-size: 2px 2px, 2px 2px, 4px 4px, 4px 4px;
  color: var(--text);
  font-family: var(--ui);
  font-size: 16px;
  line-height: 1.6;
}

:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 3px;
}

::selection { background: var(--action); color: var(--on-action); }

::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
* { scrollbar-width: thin; scrollbar-color: var(--rule) transparent; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.skip-link:focus {
  position: fixed; top: 8px; left: 8px; z-index: 99999;
  width: auto; height: auto; clip: auto; overflow: visible;
  background: var(--action); color: var(--on-action);
  padding: 0.5rem 1rem; border-radius: var(--radius);
  font-weight: 600; text-decoration: none;
}
/* UI audit 2026-10-28 (D2): the JavaScript-off page. Fixed over the empty shell, so it is the first thing seen. */
.noscript-note {
  position: fixed; inset: auto 1rem auto 1rem; top: 5.5rem; z-index: 99999;
  max-width: 34rem; margin: 0 auto; padding: 1rem 1.25rem;
  background: var(--stock); color: var(--text);
  border: 1px solid var(--rule); border-left: 4px solid var(--action); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); font-size: 16px; line-height: 1.5;
}

/* THE [hidden] guard (QA round 1, 2026-10-16). The browser's own
   `[hidden] { display: none }` is a user-agent rule, so ANY author rule that
   sets display (`.btn{display:inline-flex}`, `.lm-stats{display:flex}`,
   `label{display:block}`, an inline style="display:grid") beats it and
   `el.hidden = true` silently does nothing. This bit the app again and again
   (.inbox-thread, .smart-badge, .publish-confirm, #publish, then
   #live-proof-run, #bl-pipeline, #lm-stats, #seo-stats), each patched with
   one more per-class `[hidden]` override. This one rule ends the class:
   `!important` beats every non-important author rule and inline style.
   The per-class `[hidden]` overrides further down are now redundant but
   harmless; other tests still pin some of them, so they stay.
   The ONE exception: `.onboard-wizard__nav > [hidden]` (see that rule) keeps
   the hidden Back button in layout as visibility:hidden so the dots and Next
   do not jump between wizard steps; its children are excluded here.
   Do not add `display: <anything but none> !important` to a selector that
   can match a hidden element: tests/hidden-specificity-sweep.test.js fails. */
[hidden]:not(.onboard-wizard__nav > *) {
  display: none !important;
}

/* Registration crosshairs live on the table, outside the sheet. */
/* Positioned against <main>, which is the press table. Fixed to the window
   instead, they sat underneath the dark slug bar and were never seen. */
.reg {
  position: absolute;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--ink-k-40);
  stroke-width: 1;
  display: none;
}
.reg--tl {
  top: 0.15rem;
  left: 0.4rem;
}
.reg--tr {
  top: 0.15rem;
  right: 0.4rem;
}

/* ------------------------------ slug line ------------------------------- */
/* The strip a printer reads to know which job and which plate this sheet is. */

.slug {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1.25rem;
  background-color: var(--stock-warm);
  background-image:
    repeating-linear-gradient(
      0deg,
      rgba(120, 100, 70, 0.07) 0px,
      rgba(120, 100, 70, 0.07) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      90deg,
      rgba(120, 100, 70, 0.05) 0px,
      rgba(120, 100, 70, 0.05) 1px,
      transparent 1px,
      transparent 2px
    );
  background-size: 2px 2px;
  color: var(--text);
  border-bottom: 1px solid var(--rule);
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
}

.slug__job {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}
.slug__job b {
  font-weight: 600;
}
.slug__sep,
.slug__job span:not(.slug__mark) {
  color: var(--text-muted);
}
#slug-client {
  /* QA-T13 (R4-08): see .slug__brand. The cap keeps a long name from
     pushing search and the account cluster onto a second header row on a
     normal desktop; the rest of the name is in the title. */
  min-width: 0;
  max-width: min(40ch, 36vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A four-ink chip: the job's plates, in stage order. */
.slug__mark {
  width: 34px;
  height: 10px;
  flex: none;
  background: linear-gradient(
    90deg,
    var(--ink-c) 0 25%,
    var(--ink-m) 25% 50%,
    var(--ink-y) 50% 75%,
    var(--rule) 75% 100%
  );
}

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

.sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 48px;
  height: 48px;
  padding: 0 14px;
  border: 2px solid var(--action);
  border-radius: var(--radius);
  background: var(--action);
  color: var(--on-action);
  font-size: 14px;
  font-weight: 700;
  font-family: var(--ui);
  cursor: pointer;
  flex: none;
  transition: all 200ms var(--snap);
}
.sidebar-toggle:hover {
  background: var(--action-hover);
  border-color: var(--action-hover);
  color: var(--on-action);
}

/* ----------------------------- search trigger ---------------------------- */

/* Client request (2026-09-01, Keeno): this read as a small, faint toolbar
   icon rather than an inviting search control on both viewports/themes.
   Root cause was the fill token, not the size: --desk is a page-background
   token, and the slug bar's own background is --stock-warm -- those two
   sit only a few RGB points apart in both themes, so the border+fill all
   but disappeared into the row around it no matter how it was padded.
   Rebuilt on this file's own established "real input field" recipe (see
   input[type=text] etc. above: --stock fill + 1.5px --rule border +
   --shadow-sm) instead of inventing a new one -- --stock is a genuine
   surface color a full step away from --stock-warm in both themes, the
   same way .sheet (the paper) reads as a distinct surface against --desk
   (the table it sits on). Widened for real (flex-basis, not just padding)
   and split icon+label vs. the kbd badge across that width via
   justify-content so the extra room reads as a search field, not a
   stretched button. */
.slug__search {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  /* 2026-08-12: identity-badge used to carry margin-left:auto to push
     itself + everything after it to the right edge — moved here now that
     search is visually first-of-the-right-group (identity-badge reordered
     to the true end via `order`, see its own rule). */
  margin-left: auto;
  /* flex-shrink stays on (the `1`) so .slug__brand's client name can still
     claim room before the row wraps — only the resting/grow width moved.
     230/176 rather than a rounder 260/200: wide enough to read as a real
     field at the 1280px+ widths this app is actually used at, without
     needlessly dropping the ~1024-1200px range (a real laptop viewport
     band) into an earlier three-row wrap than the unwidened trigger had. */
  flex: 0 1 230px;
  min-width: 176px;
  padding: 0.55rem 0.65rem 0.55rem 0.95rem;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--stock);
  box-shadow: var(--shadow-sm);
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--ui);
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
  transition: all 200ms var(--snap);
}
.slug__search:hover {
  border-color: var(--text-muted);
  box-shadow: var(--shadow-md);
  color: var(--text);
}
.slug__search:focus-visible {
  border-color: var(--action);
  outline: none;
  /* color-mix over a literal rgba (see the global input focus-ring above,
     which hardcodes light-mode orange) so the ring is the *current*
     theme's --action, not always the light one. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--action) 25%, transparent), var(--shadow-sm);
}
.slug__search-main {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}
.slug__search-main svg {
  flex: none;
}
.slug__search-label {
  white-space: nowrap;
}
.slug__search-kbd {
  flex: none;
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  background: var(--rule-soft);
  padding: 1px 6px;
  border-radius: 3px;
  color: var(--text-muted);
}

@media (max-width: 719px) {
  .slug__search {
    /* Below the press-table breakpoint, .slug's own flex-wrap already
       gives this its own full row on its own (the brand row -- job strip
       + hamburger -- wraps first) — span it on purpose instead of leaving
       a narrow pill floating in an otherwise-empty row. Mobile ends up
       with the more prominent treatment of the two, which is fine per the
       brief: differ, don't just shrink. */
    flex-basis: 100%;
    width: 100%;
    margin-left: 0;
  }
}

/* Search palette layout pass (2026-09-02), small-phone discoverability:
   product review found .slug__search reading as just another grey-outlined
   pill next to .slug__theme-toggle's "What's new"/"Dark mode" buttons at
   this size — full-width (above) fixed the wasted-space problem but did
   nothing to make it visually distinct as the one control that jumps
   anywhere in the app. Tinted with --action (the same accent already used
   for the focus ring and .cmdk__result-icon--action) so it reads as the
   one colored, branded control in that row instead of blending in. */
@media (max-width: 480px) {
  .slug__search {
    background: var(--action-light);
    border-color: var(--action);
    color: var(--action);
  }
  .slug__search:hover,
  .slug__search:focus-visible {
    border-color: var(--action);
    color: var(--action);
  }
  /* The kbd badge names a physical keyboard shortcut (Cmd+K) that doesn't
     exist on a touch device — pure wasted width in the pill's
     justify-content: space-between layout once the label is the only
     thing left worth showing here. */
  .slug__search-kbd {
    display: none;
  }
}

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

.app-layout {
  display: flex;
  min-height: calc(100vh - 60px);
}

.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 8110; /* QA-T12: above its backdrop; see .sidebar__backdrop */
  width: 272px;
  /* 100vh includes the area behind iOS Safari's collapsing address bar, so a
     fixed-position element sized to it can extend below the actually-visible
     viewport (bottom sidebar items become unreachable). 100dvh (dynamic
     viewport height) is the purpose-built fix — falls back to 100vh on
     browsers that don't support dvh (Safari 15.4+, Chrome 108+, Firefox
     101+ all do). Item 24 (2026-08-09), a code-level review finding, not
     something live-tested in an actual Safari — this is a documented CSS
     spec-level browser difference, not a guess. */
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  background-color: var(--sidebar-bg);
  background-image:
    repeating-linear-gradient(
      0deg,
      rgba(90, 70, 40, 0.10) 0px,
      rgba(90, 70, 40, 0.10) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      90deg,
      rgba(90, 70, 40, 0.07) 0px,
      rgba(90, 70, 40, 0.07) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      45deg,
      rgba(70, 50, 20, 0.03) 0px,
      rgba(70, 50, 20, 0.03) 1px,
      transparent 1px,
      transparent 4px
    );
  background-size: 2px 2px, 2px 2px, 4px 4px;
  color: var(--sidebar-text);
  padding: 0.75rem 0;
  /* Market-ready gauntlet item 3 (2026-09-06): the 2026-08-30 fix moved the
     cookie banner to bottom-left specifically to stay clear of the MAIN
     content column, but that's also where this scrollable sidebar sits —
     confirmed live, the banner's fixed z-index-500 box sits directly on
     top of the last few nav items (Recommendations and below on a typical
     tenant's nav) for as long as it's shown, on both viewports. Reuses the
     same --cookie-banner-clearance custom property .scroll-top-fab already
     consumes rather than a new mechanism — adds to the existing 0.75rem
     baseline instead of replacing it, and is a no-op (0px) once the banner
     is dismissed or on a return visit where it never shows at all. */
  padding-bottom: calc(0.75rem + var(--cookie-banner-clearance, 0px));
  transform: translateX(-100%);
  transition: transform 350ms var(--snap);
  border-right: 1px solid var(--sidebar-rule);
}
.sidebar.open {
  transform: translateX(0);
}
.sidebar__group {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--sidebar-rule);
}
.sidebar__group:last-child {
  border-bottom: none;
}
/* Visual-appeal gauntlet (2026-08-30): these group headers used to look
   identical to plain section text at rest -- the only clickable cue was a
   small chevron that only visibly reacted once you'd already clicked it.
   A faint resting-state background reads as "this is a row you can press"
   before any interaction. */
.sidebar__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  min-height: 44px;
  padding: 0.7rem 1.15rem 0.5rem;
  border: 0;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--sidebar-hover, transparent) 35%, transparent);
  font-family: var(--ui);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sidebar-group);
  cursor: pointer;
  transition: color 200ms ease, transform 200ms ease, background 200ms ease;
}
.sidebar__label:hover {
  color: var(--action);
  background: var(--sidebar-hover);
}
.sidebar__chevron {
  flex: none;
  color: var(--sidebar-group);
  opacity: 0.75;
  transition: transform 250ms ease;
}
.sidebar__label[aria-expanded="true"] .sidebar__chevron {
  transform: rotate(90deg);
}
.sidebar__items {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 300ms ease;
}
.sidebar__items-inner {
  overflow: hidden;
}
.sidebar__group:not(.sidebar__group--open) .sidebar__items {
  grid-template-rows: 0fr;
}
.sidebar__group:not(.sidebar__group--open) .sidebar__chevron {
  transform: rotate(0deg);
}
.sidebar__item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: calc(100% - 0.75rem);
  min-height: 44px;
  padding: 0.5rem 1rem 0.5rem 1.15rem;
  margin-left: 0.75rem;
  border: 0;
  background: none;
  color: var(--sidebar-text);
  font: inherit;
  font-size: 15px;
  text-align: left;
  cursor: pointer;
  border-radius: var(--radius);
  transition: background 200ms ease, color 200ms ease, transform 200ms var(--snap), box-shadow 200ms ease;
}
a.sidebar__item { text-decoration: none; } /* S1: the Lanes entry is a link to /admin-lanes.html */
.sidebar__item:hover {
  color: var(--sidebar-text-active);
  background: var(--sidebar-hover);
}
.sidebar__item[aria-current="true"] {
  color: var(--action);
  background: var(--action-light);
  font-weight: 600;
}
.sidebar__emoji {
  font-size: 18px;
  flex: none;
  line-height: 1;
  filter: saturate(1.15);
  opacity: 1;
  min-width: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.sidebar__plate {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 8px;
  background: var(--sidebar-rule);
  border: none;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--sidebar-text);
  flex: none;
  transition: transform 200ms var(--snap), background 200ms ease;
}
.sidebar__item:hover .sidebar__plate {
  transform: scale(1.1);
}
.sidebar__item[aria-current="true"] .sidebar__plate {
  background: var(--action);
  color: var(--on-action);
  transform: scale(1.1);
}
/* QA-T12 (R3-2, round 3): at z-index 19/20 the open drawer and its dimmed
   backdrop sat UNDER the whole FAB tower (.fab-more, .scroll-top-fab,
   .assistant-bar__toggle at 900, .assist at 8000, the floating chat at
   8050), so the blue '...' button floated bright over the dimmed page and
   stayed tappable while the drawer was open. The drawer (and its backdrop)
   is the active surface while it is open: it now sits above that tower but
   still below every real modal (9000+), so a dialog raised from the drawer
   still opens on top of it. The persistent desktop sidebar (>= 1024px) sets
   its own z-index and never shows the backdrop. */
.sidebar__backdrop {
  position: fixed;
  inset: 0;
  z-index: 8100;
  background: rgba(0,0,0,0.4);
  display: none;
}
.sidebar__backdrop.visible {
  display: block;
}

.slug__theme-toggle {
  background: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 4px 8px;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  color: var(--text);
  transition: background .15s;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.slug__theme-toggle:hover { background: var(--sidebar-hover); }
.slug__theme-label {
  font-size: 12.5px;
  font-weight: 500;
}

/* Visual-appeal gauntlet (2026-08-30): header regrouping. .slug__brand
   holds the job strip + mobile nav toggle; .slug__account holds every
   account/utility control as one visually distinct cluster, separated
   from the primary search action by real space instead of even
   space-between distribution across 7 flat siblings. */
.slug__brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* QA-T13 (R4-08): with no min-width:0 this flex item could not shrink
     below its content, so a very long client name widened the header (and
     the page) to 1932px and #slug-client's ellipsis never engaged. The job
     strip and the name itself need the same, all the way down the chain;
     the full name is in #slug-client's title (app.js). */
  min-width: 0;
  flex: 0 1 auto;
}
.slug__account {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding-left: 0.75rem;
  border-left: 1px solid var(--rule);
}
@media (max-width: 900px) {
  .slug__account { border-left: none; padding-left: 0; }
}
@media (max-width: 600px) {
  /* Bug hunt (2026-08-30, "don't embarrass me" pass): .slug__account's 4
     items (What's new, Dark mode, Setup, the identity badge) never wrap —
     confirmed live at a real 375px width, their combined width (457px)
     forced the ENTIRE page 100px wider than the viewport, clipping the
     operator's own name/avatar off the right edge with no way to see it.
     flex-wrap here, plus letting each pill shrink to its icon alone,
     keeps every control reachable on a real phone. */
  .slug__account { flex-wrap: wrap; row-gap: 0.4rem; }
}

.slug__backend {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--sidebar-text);
}
.slug__backend .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-y);
}
.slug__backend[data-caching="true"] .dot {
  background: var(--ink-c);
}
/* 2026-08-20 craft pass: .slug has no mobile-specific layout at all — its
   ~8 children (logo, menu toggle, Action Required, identity badge, search,
   theme toggle, What's New, this backend readout) just wrap individually
   on a 375px viewport, since flex-wrap has nowhere else to put them.
   Measured live: ~330px of header chrome before any page content on every
   single mobile view, since .slug is shared, not per-view. This one is the
   safe row to reclaim — pure diagnostic status text, not a control, so
   hiding it below the app's own 600px breakpoint (matches every other
   mobile rule in this file) loses no functionality, unlike the buttons
   around it which are each some feature's only entry point. */
@media (max-width: 600px) {
  .slug__backend { display: none; }
}

/* QA round 1 (2026-10-16, D8): the sticky header measured 223px on a
   390x844 phone (26% of the screen) on all 49 views: brand row, a
   full-width search row and two rows of account chips. On phones it is
   now ONE row of about 64px: the client name (truncated), a 44px icon-only
   search button and Menu. The account cluster (.slug__account) is moved
   into the top of the nav drawer by initSlugShell() in app.js, so every
   control keeps exactly one entry point. .slug__brand uses display:contents
   so search can sit before Menu without reordering the DOM. The job strip
   gets flex-basis 0 so the row never wraps (wrap decisions use the basis,
   not min-width). If the account cluster is still in the header (script
   not run yet) it takes its own row via flex-basis 100%, which is the old
   behavior, not a broken one. */
@media (max-width: 600px) {
  .slug { gap: 0.5rem; padding: 0.5rem 0.75rem; }
  .slug__brand { display: contents; }
  .slug__job { flex: 1 1 0; }
  #slug-client { max-width: none; } /* the job strip is already capped by its flex basis here */
  .slug__job b, .slug__sep { display: none; }
  .slug__search {
    order: 1;
    flex: 0 0 44px;
    width: 44px;
    min-width: 44px;
    padding: 0;
    justify-content: center;
    margin-left: 0;
  }
  .slug__search-label, .slug__search-kbd { display: none; }
  .sidebar-toggle { order: 2; }
  .slug > .slug__account { order: 3; flex-basis: 100%; }
}
/* The same account cluster once initSlugShell() has moved it into the
   phone nav drawer: identity first and full width, 44px tap targets, and
   the identity menu opens in flow inside the drawer (the header version is
   position:fixed/absolute and would be clipped by the drawer's scroll). */
.sidebar > .slug__account {
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.25rem 1rem 0.75rem;
  margin-bottom: 0.25rem;
  border-left: none;
  border-bottom: 1px solid var(--sidebar-rule);
}
.sidebar > .slug__account .slug__identity { order: -1; flex-basis: 100%; min-width: 0; margin-right: 0; }
.sidebar > .slug__account .slug__theme-toggle,
.sidebar > .slug__account .slug__setup-tab { min-height: 44px; }
.sidebar > .slug__account .slug__identity-menu {
  position: static;
  min-width: 0;
  width: auto;
  max-width: none;
  margin-top: 0.4rem;
}

/* AI-workforce-for-small-business gauntlet (2026-08-13), Phase 1: same
   small-dot shape as .slug__backend .dot above, generalized with status
   modifiers instead of one hardcoded color - used on both the Client
   Profile uptime section and the Admin Dashboard client table. */
.status-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-muted);
}
.status-dot--up { background: var(--growth); }
.status-dot--down { background: var(--ink-reject); }
.status-dot--timeout { background: var(--action); }

.notice {
  margin: 0;
  padding: 0.6rem 1.25rem;
  background: linear-gradient(90deg, var(--stock-amber) 0%, var(--stock-warm) 100%);
  border-bottom: 1px solid var(--rule);
  font-size: 13px;
  color: var(--ink-y-dark);
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.notice:empty,
.notice[hidden] {
  display: none;
}
.notice__dismiss {
  flex: none;
  border: none;
  background: none;
  font-size: 18px;
  line-height: 1;
  color: inherit;
  opacity: 0.6;
  cursor: pointer;
  padding: 0 0.25rem;
}
.notice__dismiss:hover {
  opacity: 1;
}
/* QA round 1 (2026-10-16, D8): the admin "free testing mode" notice stays
   in normal flow (never sticky, so it scrolls away); on phones it is
   tighter so it costs about 3 short lines instead of 5, and its dismiss
   button gets a real 44px tap target. Placed after the base rules on
   purpose: an equal-specificity override earlier in the file loses. */
@media (max-width: 600px) {
  .notice { padding: 0.35rem 0.25rem 0.35rem 0.75rem; font-size: 12px; gap: 0.25rem; }
  .notice__dismiss { min-width: 44px; min-height: 44px; }
}

/* -------------------------------- sheet -------------------------------- */

main {
  position: relative;
  /* Bug hunt (2026-08-19, systematic sweep): a Playwright pass driving
     every view at both breakpoints found 52 real content collisions with
     the fixed FAB stack (.assist/.help-fab/.assistant-bar__toggle/
     .feedback-fab, all right:1-1.5rem) — not just one .roi-banner instance, but buttons,
     form fields, section titles, table filters, toast notifications, and
     empty-states across 30+ of this app's 34 views. A per-element fix
     doesn't scale to that; reserving the FAB stack's real footprint on
     the content container itself does, in one place, for every current
     and future view. Right padding was first added as a SEPARATE `main {
     padding-right: ... }` rule here — silently overridden by the
     `padding: 1.5rem 2rem 5rem` shorthand at the 720px+ breakpoint below
     (a shorthand always resets all 4 sides, cascade order made it win),
     which is why raising the value from 16rem to 20rem measurably did
     nothing. Folded directly into both breakpoints' own padding
     shorthand instead, so there's one source of truth per breakpoint,
     not two rules that can silently fight. 6rem was a rounder ~80px
     estimate of the icon-only FAB stack's mobile footprint; the cookie
     banner's own mobile override two sections down needed the same
     clearance and used the precise measurement instead (right:1rem +
     56px widest button = 4.5rem) — narrowed to match here too (2026-09-
     11 mobile-only pass, desktop's labeled buttons are wider and are a
     deliberate choice, left untouched). Reclaims 24px of content width
     on every mobile screen with the exact same collision protection. */
  padding: 0 4.5rem 4rem 0;
  /* main is a flex child of .app-layout; without this it won't shrink
     below its widest descendant's min-content width (the classic
     flexbox min-width:auto trap), which on mobile let a wide child
     (e.g. a fieldset) push the whole page into horizontal overflow. */
  min-width: 0;
}
/* QA round 1 (2026-10-16, D9): the 4.5rem right gutter above cost 72px of
   a 390px phone on every view. Phones keep 1rem: with .sheet's own 1rem
   margin + 1.5rem padding, sheet content then ends 8px short of the FAB
   column's left edge (the FABs sit at right:0.5rem, 44px wide, see their
   phone rules), so the tower cannot cover a field or button at any scroll
   position. The bottom padding (plus the sheet's 1rem margin) lets the
   last content on any view scroll clear above the 164px-tall tower. */
@media (max-width: 600px) {
  main { padding: 0 1rem 10rem 0; }
}

.sheet {
  background-color: var(--stock);
  background-image:
    repeating-linear-gradient(
      0deg,
      rgba(140, 120, 90, 0.06) 0px,
      rgba(140, 120, 90, 0.06) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      90deg,
      rgba(140, 120, 90, 0.04) 0px,
      rgba(140, 120, 90, 0.04) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      -45deg,
      rgba(120, 100, 70, 0.025) 0px,
      rgba(120, 100, 70, 0.025) 1px,
      transparent 1px,
      transparent 5px
    );
  background-size: 2px 2px, 2px 2px, 5px 5px;
  padding: 2rem 1.5rem 2.5rem;
  margin: 1rem;
  border-radius: var(--radius-lg);
  border: 1px solid var(--rule);
  box-shadow: var(--shadow-sm);
  animation: sheet-in 400ms var(--snap);
}
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(12px) scale(0.99); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.sheet__head {
  padding-bottom: 1.25rem;
  margin-bottom: 1.75rem;
  border-bottom: 2px solid var(--rule-soft);
}

h1 {
  margin: 0 0 0.5rem;
  font-size: 1.85rem;
  line-height: 1.15;
  letter-spacing: -0.025em;
  font-weight: 800;
  color: var(--text);
}
/* W4C (defect 15): a screen title is its words. The emoji in front of every H1 crowded the title on a phone and
   was read out by screen readers ("credit card Billing"); the sidebar keeps its icons. */
h1 .emoji {
  display: none;
}

h2 {
  font-size: 1.2rem;
  font-weight: 700;
  margin: 0;
  color: var(--text);
}

.lede {
  margin: 0.25rem 0 0;
  max-width: 62ch;
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1.5;
}

.hint {
  margin: 0.35rem 0 0;
  font-size: 15px;
  color: var(--text-secondary);
  max-width: 68ch;
  line-height: 1.6;
}

/* U4 (UI audit 2026-10-28): ONE empty-state system. Seven families had grown seven looks; the Inbox screen alone
   showed three at once (a hairline .feature-empty band, a dashed gradient .empty squeezed into the thread column, a
   faded .inbox-placeholder). They now share one surface, one rule, one icon scale and one text pair, taken from the
   warm icon + sentence + CTA pattern Quotes and Content Calendar already used (.empty-row--warm):
     block   .empty, .feature-empty        a card on the sheet: icon, headline, body, optional CTA
     cell    .empty-row--warm              the same, sized for one <td>
     pane    .inbox-placeholder            the same, with no card (it IS the empty right-hand pane)
     inline  .empty-inline, .empty-state   one sentence with a small icon
     line    .month-empty                  a plain status line inside a Month card (only the text pair is shared)
   Plain .empty-row stays plain on purpose: it carries Loading and Couldn't-load rows, which are not empty states
   (tests/empty-state-consistency.test.js). Nothing here is interactive, so no gradient, no dashed "drop zone"
   border and no hover lift: those three told a person the block could be pressed, and it could not. */
:root {
  --empty-surface: var(--stock-warm);
  --empty-rule: 1px solid var(--rule-soft);
  --empty-text: var(--text-muted); /* #5a5550 on #f7f4ee is about 7:1, #9a9490 on #22201c about 6:1: AA both themes */
  --empty-icon-block: 32px;
  --empty-icon-inline: 18px;
}
.empty {
  padding: 2rem 1.5rem;
  color: var(--empty-text);
  text-align: center;
  font-size: 15px;
  border: var(--empty-rule);
  border-radius: var(--radius-lg);
  background: var(--empty-surface);
  margin-top: 1rem;
}
.empty p {
  margin: 0 0 0.75rem;
  font-size: 15px;
  line-height: 1.55;
}
.empty .btn {
  margin-top: 0.5rem;
}
.empty__icon {
  display: block;
  font-size: var(--empty-icon-block);
  margin-bottom: 0.6rem;
  line-height: 1;
}
/* (0,2,0): `.empty p` (0,1,1) used to win over these, so every upgraded headline rendered at 17px instead of its
   own size and every body at 17px in the darker colour, which is why the audit saw body text louder than the head. */
.empty .empty__headline,
.empty__headline {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 0.35rem;
}
.empty .empty__body,
.empty__body {
  font-size: 14px;
  line-height: 1.55;
  color: var(--empty-text);
  max-width: 46ch;
  margin: 0 auto 1rem;
}
.empty__cta {
  margin-top: 0.25rem;
}
/* QA-T13 (R4-23): `.empty p { margin: 0 0 0.75rem }` (0,1,1) outranks
   .empty__body's own `margin: 0 auto 1rem` (0,1,0), so every upgraded empty
   state (upgradeEmptyStates in app.js: Tenant Admin and ~30 others) had its
   380px body paragraph pinned LEFT under a centred icon, heading and button.
   (0,2,0) here beats it; only the margin is restored (font size unchanged). */
.empty .empty__body { margin: 0 auto 1rem; }

/* Lighter-weight sibling of .empty for a single inline "nothing here yet"
   note under a subsection heading (Google Workspace's Query/Analytics
   trend rows) — the full dashed .empty card is right at home once per
   page; three of them stacked in a row reads as noise, not craft. */
.empty-inline {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.75rem 1rem;
  border: var(--empty-rule);
  border-radius: var(--radius);
  background: var(--empty-surface);
  color: var(--empty-text);
  margin: 0.35rem 0 1rem;
}
.empty-inline__icon {
  font-size: var(--empty-icon-inline);
  line-height: 1;
  flex: none;
}
.empty-inline .hint {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
}
/* Same [hidden]-vs-author-display bug this codebase keeps hitting (see
   .admin-impersonate, .onboard-modal elsewhere in this file): #gw-trend-
   lists carries its own inline style="display:grid", which outranks the
   browser's [hidden] rule regardless of specificity. Found live 2026-08-30
   verifying the issue #2 empty-state fix — with the query-trend inline
   note now correctly showing "no data yet," this div's "Growing" /
   "Declining" headers were rendering anyway, dangling with no content
   under them, one line below the note that just said there's nothing to
   show. Pre-existing, not introduced by this pass — surfaced by finally
   giving the "no data" case its own visible treatment to compare against. */
#gw-trend-lists[hidden] { display: none !important; }

/* ------------------------------- controls ------------------------------- */

.btn {
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 1.4rem;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--stock);
  color: var(--text);
  cursor: pointer;
  transition: all 200ms var(--snap);
}
/* A3 (2026-10-15): `.btn { display: inline-flex }` has equal specificity to
   the UA `[hidden] { display: none }` rule and wins, so app.js's
   `$('#publish').hidden = true` (while the publish-confirm box is open)
   never actually hid the Send button. Scoped to #publish on purpose: the
   onboarding nav relies on `.onboard-wizard__nav > [hidden]` being
   visibility:hidden, and a blanket .btn guard would break it.
   2026-10-16: superseded by the global `[hidden]` guard near the top of
   this file (which excludes that nav); kept as a redundant backstop. */
#publish[hidden] {
  display: none !important;
}
.btn:hover:not(:disabled) {
  /* Bug hunt (2026-09-10): same root cause as .cookie-banner above — the
     highest-traffic instance of it, since this is every plain .btn on the
     page. var(--text) flips to a LIGHT color in dark mode (it's the
     page's own body-text color, meant to sit on a dark surface, not to
     BE one) while this rule's own `color: #fff` stays hardcoded — light
     background, white text, broken contrast on every single button hover
     in dark mode. Literal, not a variable, so the hover treatment stays
     "invert toward ink" in both themes instead of only in light mode. */
  background: #1a1a1a;
  border-color: #1a1a1a;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
.btn:active:not(:disabled) {
  transform: translateY(0) scale(0.97);
  box-shadow: none;
}
.btn:focus-visible {
  outline: 2px solid var(--trust);
  outline-offset: 2px;
}
.btn:disabled {
  background: var(--rule-soft);
  border-color: var(--rule);
  color: var(--text-muted);
  cursor: not-allowed;
}

.btn--press {
  background: var(--action);
  border-color: var(--action);
  color: var(--on-action);
  font-weight: 700;
  box-shadow: 0 1px 3px var(--action-light);
}
.btn--press:hover:not(:disabled) {
  background: var(--action-hover);
  border-color: var(--action-hover);
  /* QA-T4 (J8): without this the hover inherits .btn:hover's literal #fff,
     which is 2.8:1 on dark mode's light orange. */
  color: var(--on-action);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px var(--action-light);
}
.btn--press:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 1px 2px var(--action-light);
}
.btn--press:disabled {
  background: var(--rule-soft);
  border-color: var(--rule);
  color: var(--text-muted);
  box-shadow: none;
}
.btn--big {
  padding: 0.95rem 2.25rem;
  font-size: 17px;
}
.btn--ghost {
  border-color: var(--rule);
  background: transparent;
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 500;
  padding: 0.45rem 1rem;
  min-height: 44px;
}
.btn--ghost:hover:not(:disabled) {
  background: var(--rule-soft);
  border-color: var(--rule);
  color: var(--text);
  transform: translateY(-1px);
}
.btn--reject {
  border-color: var(--alert);
  color: var(--alert);
}
.btn--reject:hover:not(:disabled) {
  background: var(--alert);
  border-color: var(--alert);
  color: var(--on-alert); /* QA-T4: not .btn:hover's #fff (2.77:1 in dark mode) */
}

label {
  display: block;
  margin-bottom: 1rem;
}
label > span {
  display: block;
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 0.35rem;
}
label small {
  display: block;
  margin-top: 0.35rem;
  font-size: 14px;
  color: var(--text-muted);
  line-height: 1.5;
}

input[type="text"],
input[type="url"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="color"],
select,
textarea {
  width: 100%;
  font: inherit;
  font-size: 16px;
  padding: 0.7rem 1rem;
  border: 1.5px solid var(--rule);
  border-radius: 10px;
  background: var(--stock);
  color: var(--text);
  box-shadow: var(--shadow-sm);
  transition: border-color 200ms ease, box-shadow 200ms ease, transform 200ms ease;
}
/* QA-T4 D7 (2026-10-16): the list above never named search, tel, date,
   time, month, week, datetime-local, or an <input> with NO type attribute
   (#qt-name, #svc-name, ...), so those rendered as bare 21px browser boxes
   beside fully styled fields. Same declarations, wrapped in :where() so
   they carry zero specificity: every existing per-view rule for one of
   these (.client-search, .table-filter, inline styles) still wins.
   width:100% only where the field is a label's own control (the form
   pattern everywhere here): a percentage width also drops a flex item's
   min-content floor, so a search box sharing an inline flex row with a
   select (#master-lib-search) would collapse to ~20px. */
:where(label) > :where(input[type="search"], input[type="tel"], input[type="date"], input[type="time"],
  input[type="datetime-local"], input[type="month"], input[type="week"], input:not([type])) {
  width: 100%;
}
:where(input[type="search"], input[type="tel"], input[type="date"], input[type="time"],
  input[type="datetime-local"], input[type="month"], input[type="week"], input:not([type])) {
  font: inherit;
  font-size: 16px;
  padding: 0.7rem 1rem;
  border: 1.5px solid var(--rule);
  border-radius: 10px;
  background: var(--stock);
  color: var(--text);
  box-shadow: var(--shadow-sm);
  transition: border-color 200ms ease, box-shadow 200ms ease, transform 200ms ease;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--action);
  outline: none;
  box-shadow: 0 0 0 3px rgba(204, 69, 18, 0.25), var(--shadow-sm);
  transform: translateY(-1px);
}
textarea {
  resize: vertical;
  line-height: 1.6;
}
input::placeholder,
textarea::placeholder {
  color: var(--ink-k-40);
}

.actions {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  align-items: center;
}

/* ------------------------------ Form A --------------------------------- */

.split {
  display: grid;
  gap: 2rem;
}
.split > * {
  /* Bug hunt (2026-08-30, "don't embarrass me" pass): a grid item's
     default min-width is auto, not 0 — it refuses to shrink below its
     content's natural min-content width no matter how narrow the grid
     track is. .split__list's #client-list has a .t-where span with
     white-space: nowrap (by design, so "Washington PA" doesn't itself
     wrap awkwardly) — without this, that nowrap text set a hard floor
     that propagated all the way up through .split__list, forcing the
     whole page 62px wider than a real 375px phone screen and clipping
     content off the right edge. This is the standard fix for the trap,
     and it's what lets .t-name's own text-overflow: ellipsis actually
     engage on a narrow viewport instead of never being reached.
  */
  min-width: 0;
}

.colhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 2px solid var(--rule);
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.75rem;
}

.client-search {
  width: 100%;
  margin-bottom: 0.5rem;
  font-size: 13px;
}

.tickets {
  list-style: none;
  margin: 0;
  padding: 0;
}
.tickets li {
  border-bottom: 1px solid var(--rule-soft);
}
.tickets button {
  width: 100%;
  text-align: left;
  padding: 0.75rem 0.75rem;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  border-radius: 10px;
  transition: all 200ms var(--snap);
}
.tickets button:hover {
  background: var(--desk);
  transform: translateX(3px);
}
.tickets button[aria-current="true"] {
  background: var(--action-light);
}
.tickets .t-name {
  font-weight: 600;
}
.tickets .t-where {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  margin-left: auto;
  white-space: nowrap;
}

.ticket-form__fields {
  display: grid;
  gap: 0 1.5rem;
}

fieldset.wp {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.25rem;
  margin: 1.5rem 0;
}
fieldset.wp legend {
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 0 0.4rem;
  color: var(--text-secondary);
}
.wp__result {
  margin: 0.75rem 0 0;
  font-size: 13.5px;
}
.wp__result[data-ok="true"] {
  color: var(--action);
}
.wp__result[data-ok="false"] {
  color: var(--ink-reject);
}
/* A16: connection check results on the Automation switches. */
.preflight-for { display: block; font-weight: 400; color: var(--text-secondary); }
.preflight-for[data-status="pass"], .preflight-checks li[data-status="pass"] { color: var(--action); }
.preflight-for[data-status="fail"], .preflight-checks li[data-status="fail"] { color: var(--ink-reject); }
.preflight-checks { margin: 0.5rem 0 0; padding-left: 1.1rem; font-size: 13px; }
.preflight-checks:empty { display: none; }
/* B8: connector health, a compact label/value list under the check. Same status colours as the check list. */
#client-automation-switches .connector-health { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 0.75rem; margin: 0.75rem 0 0.5rem; font-size: 13px; }
#client-automation-switches .connector-health:empty { display: none; }
#client-automation-switches .connector-health dt { font-weight: 600; color: var(--text); }
#client-automation-switches .connector-health dd { margin: 0; color: var(--text-secondary); }
#client-automation-switches .connector-health dd[data-status="pass"] { color: var(--action); }
#client-automation-switches .connector-health dd[data-status="fail"] { color: var(--ink-reject); }
#client-automation-switches .connector-health dd[data-status="warn"], #client-automation-switches .connector-health dt[data-status="warn"] { color: var(--ink-y-dark); }
#client-automation-switches .connector-health dt[data-status="fail"] { color: var(--ink-reject); }
#client-automation-switches .connector-health .connector-health__note { display: block; color: var(--text-muted); }
/* P7b: social image approval. One card per image: the picture, what it is for, its state, the decision. */
.slot-review { margin: 0.5rem 0 1.25rem; }
.slot-review__heading { margin: 0 0 0.25rem; font-size: 15px; font-weight: 600; color: var(--text); }
.slot-review__summary { margin: 0.25rem 0 0.75rem; font-size: 14px; color: var(--text-secondary); }
.slot-review__summary:empty { display: none; }
.slot-review__summary[data-ok="false"] { color: var(--ink-reject); }
.slot-review__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75rem; }
.slot-review__grid:empty { display: none; }
.slot-review__item { display: flex; flex-direction: column; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.slot-review__item[data-state="waiting"] { border-color: var(--ink-y-dark); }
.slot-review__media { margin: 0; aspect-ratio: 3 / 2; background: var(--rule-soft); display: flex; align-items: center; justify-content: center; }
.slot-review__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.slot-review__media a { font-size: 13px; padding: 0 0.75rem; text-align: center; color: var(--action); }
.slot-review__body { padding: 0.6rem 0.75rem 0.75rem; display: flex; flex-direction: column; gap: 0.2rem; flex: 1; font-size: 13px; }
.slot-review__title { margin: 0; font-weight: 600; font-size: 14px; color: var(--text); }
.slot-review__topic { margin: 0; color: var(--text-secondary); }
.slot-review__state { margin: 0.2rem 0 0; font-weight: 600; }
.slot-review__item[data-state="approved"] .slot-review__state { color: var(--growth); }
.slot-review__item[data-state="waiting"] .slot-review__state { color: var(--ink-y-dark); }
.slot-review__check { margin: 0; color: var(--text-muted); }
.slot-review__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: auto; padding-top: 0.5rem; }
/* P7c: the client's own photos. Same cards as the slot review; the picture is shown whole (never cropped). */
.photo-library__upload { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem 0.75rem; align-items: end; margin: 0.5rem 0 0.25rem; }
.photo-library__upload label { margin: 0; }
.photo-library__upload .btn { justify-self: start; }
.photo-library__help { display: block; font-weight: 400; color: var(--text-muted); }
.photo-library .slot-review__grid { grid-template-columns: 1fr; gap: 0.5rem; }
.photo-library .slot-review__item { flex-direction: row; align-items: stretch; }
.photo-library .slot-review__media { flex: none; align-self: flex-start; width: 112px; height: 112px; overflow: hidden; }
.photo-library .slot-review__media img { object-fit: contain; }
.photo-library .slot-review__body { min-width: 0; }
.photo-library .slot-review__title { overflow-wrap: anywhere; }
.photo-library__upload input[type="file"] { font: inherit; font-size: 13px; color: var(--text-secondary); max-width: 100%; }
.photo-library__upload input[type="file"]::file-selector-button { font: inherit; font-size: 13px; font-weight: 600; color: var(--text); background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: 0.4rem 0.8rem; margin-right: 0.6rem; cursor: pointer; }
.photo-library__upload input[type="file"]::file-selector-button:hover { border-color: var(--text-muted); }
.photo-library .slot-review__item[data-state="unusable"] { border-color: var(--ink-y-dark); }
.photo-library .slot-review__item[data-state="usable"] .slot-review__state { color: var(--growth); }
.photo-library .slot-review__item[data-state="unusable"] .slot-review__state { color: var(--ink-y-dark); }
/* M1: Meta insights panel. Same status colours and label/value list as connector health above. */
.meta-insights__notice { margin: 0 0 0.5rem; font-size: 13px; color: var(--ink-y-dark); }
.meta-insights__status { margin: 0.25rem 0; font-weight: 600; }
.meta-insights__status[data-state="connected"] { color: var(--action); }
.meta-insights__status[data-state="needs-reconnect"] { color: var(--ink-reject); }
.meta-insights__status[data-state="choose-page"] { color: var(--ink-y-dark); }
.meta-insights__warnings { margin: 0 0 0.5rem; padding-left: 1.1rem; font-size: 13px; color: var(--ink-y-dark); }
.meta-insights__warnings:empty { display: none; }
.meta-insights__setup { margin: 0.5rem 0 0.75rem; font-size: 13px; }
.meta-insights__setup summary { cursor: pointer; font-weight: 600; }
.meta-insights__checklist { margin: 0.5rem 0; padding-left: 1.25rem; }
.meta-insights__checklist li { margin: 0.25rem 0; }
.meta-insights__checklist code { overflow-wrap: anywhere; }
.meta-insights__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.5rem 0; }
/* P37: the WordPress file button sits under its notes with room to breathe (the notes have no bottom margin). */
.wp-export__download { margin-top: 0.75rem; }

/* O3: "Ready for a paying client?" on the client profile (admin). The state is a word, never colour alone; red stays
   for errors (DESIGN.md), so an open item is marked by its word and weight, not by an alarm. No motion. */
.golive { border: 1px solid var(--rule); border-radius: var(--radius); background: var(--surface); padding: 1rem 1.25rem; margin: 0 0 1.5rem; }
.golive__title { font-family: var(--ui); font-size: 16px; font-weight: 600; margin: 0 0 0.25rem; color: var(--text); text-wrap: balance; }
.golive__line { margin: 0 0 0.75rem; font-size: 14px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.golive__open, .golive__list { list-style: none; margin: 0; padding: 0; }
.golive__open:empty { margin: 0; }
.golive__item { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); column-gap: 0.75rem; row-gap: 0.15rem; padding: 0.65rem 0; border-top: 1px solid var(--rule); }
.golive__item:first-child { border-top: 0; }
/* Q6: the state word in the interface's own type, as written ("Needs you"): monospace capitals read as a code label. */
.golive__state { grid-row: 1 / span 3; padding-top: 0.05rem; font-family: var(--ui); font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.golive__item[data-state="needs-you"] .golive__state { color: var(--ink-y-dark); font-weight: 700; }
.golive__item[data-state="ready"] .golive__state { color: var(--growth); }
.golive__label { font-size: 14px; color: var(--text); }
.golive__why, .golive__fix { margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-secondary); max-width: 70ch; overflow-wrap: anywhere; }
.golive__fix { color: var(--text); }
.golive__done { margin-top: 0.75rem; border-top: 1px solid var(--rule); padding-top: 0.5rem; }
.golive__done > summary { cursor: pointer; display: list-item; min-height: 44px; padding: 12px 0; line-height: 20px; font-size: 13px; color: var(--text-secondary); } /* list-item keeps the disclosure triangle */
.golive__done > summary:focus-visible { outline: 2px solid var(--ink-c); outline-offset: 2px; border-radius: 4px; }
@media (max-width: 560px) {
  .golive { padding: 0.85rem 0.9rem; }
  .golive__item { grid-template-columns: minmax(0, 1fr); }
  .golive__state { grid-row: auto; padding-top: 0; }
}
.meta-insights__numbers { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 0.75rem; margin: 0.75rem 0 0; font-size: 13px; }
.meta-insights__numbers:empty { display: none; }
.meta-insights__numbers dt { font-weight: 600; color: var(--text); }
.meta-insights__numbers dd { margin: 0; color: var(--text-secondary); }
/* Sits in the form foot beside the save control, not in the WordPress block. */
#save-result {
  margin: 0;
  flex: 1 1 12rem;
  text-align: right;
}
#save-result:empty {
  display: none;
}

.ticket-form__foot {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  padding-top: 1.25rem;
  border-top: 1px solid var(--ink-k);
}

/* The cached-prefix readout. A real measurement against a real cliff. */
.gauge {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  min-width: 210px;
}
.gauge__bar {
  height: 8px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: repeating-linear-gradient(
    90deg,
    var(--rule-soft) 0 1px,
    transparent 1px 12px
  );
  position: relative;
  overflow: hidden;
}
.gauge__bar i {
  display: block;
  height: 100%;
  background: var(--action);
  border-radius: 3px;
}
.gauge[data-ok="false"] .gauge__bar i {
  background: var(--ink-reject);
}
.gauge__text {
  margin: 0.35rem 0 0;
  color: var(--ink-k-60);
}
.gauge[data-ok="false"] .gauge__text {
  color: var(--ink-reject);
}

/* ------------------------------ Form B --------------------------------- */

.setup {
  display: grid;
  gap: 0 1.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--rule);
}
.setup__go {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  align-items: flex-start;
  padding-top: 0.5rem;
}
/* Two different messages share this line and must not look alike. A hard stop
   means the button is off; a repeat warning means the button is on and the
   operator is being told something they may knowingly overrule. Colour alone
   cannot carry that difference, so each states its kind in words. */
.setup__blocked {
  margin: 0;
  font-size: 13.5px;
  max-width: 60ch;
  padding-left: 0.75rem;
  border-left: 2px solid transparent;
}
.setup__blocked:empty {
  display: none;
}
.setup__blocked::before {
  /* W4C (defect 15): one type system, sentence case in the interface face (was tracked mono capitals). */
  display: block;
  font: 600 12px / 1.4 var(--ui);
}

.setup__blocked[data-kind='blocked'] {
  color: var(--ink-reject);
  border-left-color: var(--ink-reject);
}
.setup__blocked[data-kind='blocked']::before {
  content: 'Cannot run';
}

.setup__blocked[data-kind='warning'] {
  color: var(--ink-k);
  border-left-color: var(--ink-y);
}
/* The yellow ink identifies the warning as an edge, never as text: at #E0A800
   it holds about 2.1:1 on stock and would fail as a label. */
.setup__blocked[data-kind='warning']::before {
  content: 'Worth checking';
  color: var(--ink-k-60);
}

.setup__blocked[data-kind='info'] {
  color: var(--text);
  border-left-color: var(--trust);
}
.setup__blocked[data-kind='info']::before {
  content: 'Setup note';
  color: var(--trust);
}

/* Same edge, same label-in-words rule as the three above. This one carries a
   list rather than a sentence, because "the article misses Stradigi's standard"
   is not actionable and "4 of 10 sections have bullet lists, exactly 4 must" is
   — the count is the whole value, so it gets its own line each. */
.setup__blocked[data-kind='standards'] {
  color: var(--ink-k);
  border-left-color: var(--ink-y);
  max-width: 72ch;
}
.setup__blocked[data-kind='standards']::before {
  content: 'Against Stradigi’s blog standard';
  color: var(--ink-k-60);
  margin-bottom: 0.35rem;
}
.setup__blocked[data-kind='standards'] p {
  margin: 0 0 0.35rem;
}
.setup__blocked[data-kind='standards'] ul {
  margin: 0;
  padding-left: 1.1rem;
  display: grid;
  gap: 0.2rem;
}

.ledger {
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--rule);
}
.ledger ul {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.ledger li {
  /* Matches the .pill--trust visual language used for informational
     status elsewhere in the app (Rankings, Location Pages, Clients) —
     a plain word in a chip reads as a word, not as code. */
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  background: var(--trust-light);
  color: var(--trust);
}

/* ------------------------- the color bar ------------------------------- */
/*
  Four process inks, one per stage, in press order. A patch is outlined while
  pending, hatched while on press, and solid once its plate is done. The state
  word is present in every case: state is never carried by color alone.
*/

.run {
  padding-top: 1.5rem;
}

.colorbar {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 1.5rem;
}

.patch {
  background: var(--stock);
  padding: 0.6rem;
}
.patch__ink {
  height: 34px;
  border: 1px solid var(--patch-ink, var(--ink-k));
  position: relative;
  overflow: hidden;
  background: var(--stock);
}
.patch__ink i {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  transform: scaleX(0);
  background: var(--patch-ink, var(--ink-k));
}

/* Done: the roller lays the ink down, left to right, and settles. */
.patch[data-state="done"] .patch__ink i {
  transform: scaleX(1);
  transition: transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* On press: a make-ready hatch, with one bounded sweep. */
.patch[data-state="running"] .patch__ink {
  background-image: repeating-linear-gradient(
    45deg,
    var(--patch-ink) 0 2px,
    transparent 2px 7px
  );
}
.patch[data-state="running"] .patch__ink::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.85), transparent);
  transform: translateX(-100%);
  animation: sweep 1.6s ease-in-out infinite;
}
@keyframes sweep {
  to {
    transform: translateX(100%);
  }
}

.patch__label {
  /* W4C (defect 15): sentence case (app.js PATCH_LABELS), not tracked mono capitals. */
  font-size: 12px;
  margin-top: 0.45rem;
  font-weight: 600;
}
.patch__state {
  font-size: 11px;
  color: var(--ink-k-40);
}
.patch[data-state="running"] .patch__state {
  color: var(--patch-ink);
  font-weight: 600;
}
.patch[data-state="done"] .patch__state {
  color: var(--ink-k-60);
}
/* A stage that was on press when the run failed. The plate is struck through
   rather than left inked, so a stopped run cannot be mistaken for a slow one. */
.patch[data-state="stopped"] .patch__state {
  color: var(--ink-reject);
  font-weight: 600;
}
.patch[data-state="stopped"] .patch__ink {
  background: repeating-linear-gradient(
    -45deg,
    var(--rule-soft) 0 4px,
    var(--stock) 4px 8px
  );
}
.patch[data-state="stopped"] .patch__ink i {
  display: none;
}

.patch--c {
  --patch-ink: var(--ink-c);
}
.patch--m {
  --patch-ink: var(--ink-m);
}
.patch--y {
  --patch-ink: var(--ink-y);
}
.patch--k {
  --patch-ink: var(--ink-k);
}

/* The run clock. Sits between the color bar and the streaming text, in the slug
   typeface — it is job data about the run, not part of the writing. */
.runclock {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.9rem;
  font-family: var(--ui);
  font-size: 12px;
  font-variant-numeric: tabular-nums; /* the elapsed time keeps its width as it ticks, without a mono face */
  color: var(--ink-k-60);
}
.runclock #run-clock-time {
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-k);
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--rule);
}
/* Past the expected window the readout says so in words as well as color. */
.runclock[data-slow="true"] #run-clock-time {
  color: var(--ink-reject);
  border-color: var(--ink-reject);
}
.runclock[data-slow="true"] #run-clock-note {
  color: var(--ink-reject);
}

/* Streamed text landing on the sheet as it is written. */
.press-out {
  font-family: var(--manuscript);
  font-size: 15px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 22rem;
  overflow-y: auto;
  padding: 1.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--stock-warm);
  color: var(--ink-k-60);
}
.press-out:empty {
  display: none;
}

/* A7: WordPress published a post immediately; must not read like a routine error. */
.run__error--live {
  border: 2px solid var(--alert);
  background: var(--alert-light);
  color: var(--alert);
  font-weight: 700;
}

.run__error {
  margin: 1rem 0 0;
  padding: 0.75rem 1rem;
  border: 1px solid var(--ink-reject);
  border-radius: var(--radius);
  background: rgba(196, 60, 46, 0.04);
  color: var(--ink-reject);
  font-size: 14px;
  /* Bug hunt (2026-08-31): the publish validation-gate message can now
     include a multi-line bulleted violations list — plain text collapses
     newlines by default, which ran the whole list into one unreadable
     line. Harmless for the existing single-line error case. */
  white-space: pre-line;
}

/* ------------------------------ Form C --------------------------------- */

.pick {
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 1.5rem;
}
.pick label {
  margin: 0;
  max-width: 34rem;
}

/*
  A misfeed: the sheet did not come through the folder the way it should have.
  On press this is caught and pulled, never quietly stacked with the good ones —
  which is the whole reason this block exists rather than ten empty boxes.
*/
.misfeed {
  position: relative;
  margin-bottom: 2rem;
  padding: 1.5rem 1.25rem;
  border: 1px solid var(--ink-reject);
  border-radius: var(--radius);
  background: var(--stock-rose);
}
/* A pulled sheet gets a slip taped across its head on the stacker. Same barred
   band as a stopped plate on the color bar, so a pull reads as a pull in both
   places without either one needing to be read. */
.misfeed::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 6px;
  background: repeating-linear-gradient(
    -45deg,
    var(--ink-reject) 0 5px,
    transparent 5px 10px
  );
}
.misfeed h2 {
  margin: 0 0 0.4rem;
  font-size: 15px;
  letter-spacing: -0.01em;
  color: var(--ink-reject);
}
.misfeed p {
  margin: 0 0 0.9rem;
  font-size: 14px;
  color: var(--ink-k-60);
  max-width: 52ch;
}
.misfeed .manuscript {
  background: var(--stock);
}

/*
  The imposition. Ten forms; each is one listicle point folded against the one
  social post that covers it. The staircase down the spine is the sequence made
  visible: step N sits at depth N, so a break in the order would be a break in
  the stair.
*/
.imposition {
  display: grid;
  gap: 1.5rem;
}

.imposed {
  position: relative;
  padding: 1.25rem 0.5rem 1.25rem 2.25rem;
  background-color: var(--action-light);
  border-radius: 6px;
  /* Crop marks: four corner Ls, drawn outside the trim. */
  background-image:
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule));
  background-repeat: no-repeat;
  background-size: var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop),
    var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop);
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

/* The collation mark: a stepped bar on the spine, one step per item. */
.imposed__step {
  position: absolute;
  left: 0;
  width: 10px;
  height: 26px;
  background: var(--ink-k);
}

.imposed__n {
  position: absolute;
  left: 0;
  bottom: 1.25rem;
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums; /* the row numbers still line up down the margin without a mono face */
  color: var(--ink-k-40);
}

.fold {
  display: grid;
  gap: 1.25rem;
}

.panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--rule);
}
/* U2 (UI audit 2026-10-28): the Review sheet's labels were 10px mono capitals ("ARTICLE POINT 1", "SOCIAL POST 1",
   "SEO TAGS", "STRADIGI CHECKS"): a terminal printout on the one screen a client may see over the operator's
   shoulder. Mono now marks only true code and IDs (URLs, search queries, step types, typed values); labels are the
   UI face in sentence case, quiet by size and colour instead of by tracking. Kept in capitals on purpose: the
   sidebar group headings, table column heads (.colhead, the phone card labels): a scanning aid, not a label. */
.panel__kind {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}
.panel__headline {
  font-weight: 600;
  font-size: 15px;
  line-height: 1.3;
}

/* No nested scrollbars on the review sheet. These boxes are the thing the
   operator came here to read; a 200-word section inside a 250px window hides
   two thirds of itself and hides it silently. Height is set from content by
   autoGrow() in app.js, so the sheet gets longer instead of the text getting
   shorter. `resize` is off because a manual drag would be overwritten on the
   next keystroke. */
.manuscript {
  font-family: var(--manuscript);
  font-size: 15.5px;
  line-height: 1.65;
  overflow-y: hidden;
  resize: none;
}
/* A read-only half is still legible, but must not invite an edit that would go
   nowhere. It reads as a printed proof rather than a box to type in. */
.manuscript[readonly] {
  background: var(--stock-parchment);
  border-style: dashed;
  color: var(--ink-k-60);
  cursor: default;
}

/* U5 (2026-10-28): each article box is a pair, the Markdown source and its rendered preview (app.js
   drawReviewPreview decides which shows, per box). The preview is a read surface, not a field: the same frame as
   the textarea it stands in for so the sheet does not jump when toggled, but no focus ring or input shadow, and
   the typography of "Preview as blog post" (.blog-preview__article) so it reads like the post it will become. */
.md-box[data-show="preview"] > textarea.manuscript { display: none; }
.md-box[data-show="markdown"] > .md-preview { display: none; }
.md-box[data-show="preview"] > .md-box__hint { display: none; }
.md-box__note { margin: 0 0 0.4rem; color: var(--text-secondary); }
.md-preview {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 15.5px;
  line-height: 1.65;
  color: var(--ink-k);
  padding: 0.7rem 1rem;
  border: 1.5px solid var(--rule-soft);
  border-radius: 10px;
  background: var(--stock);
  overflow-wrap: anywhere; /* a long URL in a link must not push a 390px column sideways */
}
.md-preview > :first-child { margin-top: 0; }
.md-preview > :last-child { margin-bottom: 0; }
.md-preview p { margin: 0 0 0.9em; }
.md-preview h2, .md-preview h3, .md-preview h4 { font-family: var(--ui); line-height: 1.3; margin: 1.2em 0 0.5em; }
.md-preview h2 { font-size: 1.2rem; }
.md-preview h3 { font-size: 1.05rem; }
.md-preview ul, .md-preview ol { margin: 0 0 0.9em; padding-left: 1.4rem; }
.md-preview li { margin-bottom: 0.35em; }
.md-preview a { color: var(--action); }
.md-preview blockquote { margin: 0 0 0.9em; padding-left: 0.9rem; border-left: 3px solid var(--rule); color: var(--text-secondary); }
.md-preview img { max-width: 100%; height: auto; }
.md-preview table { border-collapse: collapse; width: 100%; font-size: 0.9em; }
.md-preview th, .md-preview td { border: 1px solid var(--rule-soft); padding: 0.35rem 0.5rem; text-align: left; }

/* The toggle: one segmented control. aria-pressed carries the state (screen readers hear "pressed"); the filled
   segment is the same signal for the eye, so the state is never colour alone (the label differs too). */
.preview-trigger { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.review-mode {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--stock-warm);
}
.review-mode__btn {
  min-height: 38px;
  padding: 0 1rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-secondary);
  font: 600 14px / 1 var(--ui);
  cursor: pointer;
}
.review-mode__btn[aria-pressed="true"] {
  background: var(--stock);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}
.review-mode__btn:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; }
@media (pointer: coarse) { .review-mode__btn { min-height: 44px; } } /* the app's 44px touch floor, phones only */

.count {
  /* Plain sentence-case guidance, not a linter readout — this shows under
     every one of the 10 social posts at once, so a loud style here reads
     as 10 simultaneous errors rather than one quiet word-count note. */
  font-size: 13px;
  color: var(--ink-k-40);
  margin-top: 0.3rem;
}
.count[data-over="true"] {
  color: var(--ink-k-60);
}

/* AI-assisted single-section rewrite (BUILD-OUT-TODO.md §10) — one row per
   listicle point, and one shared row above the long-form box (see
   .regen-row--longform below). Kept visually quiet: this is a secondary
   action next to the manuscript itself, not the main thing on the panel. */
.regen-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.regen-instruction {
  flex: 1 1 220px;
  min-width: 0;
  font-size: 12.5px;
  padding: 0.4rem 0.6rem;
}
.regen-status {
  margin: 0.3rem 0 0;
  font-family: var(--ui);
  font-size: 12px;
  color: var(--ink-k-40);
}
.regen-status[data-state="ok"] {
  color: var(--growth);
}
.regen-status[data-state="error"] {
  color: var(--ink-reject);
}
.regen-row--longform {
  margin-top: 0.75rem;
}
.regen-row__field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 12.5px;
  /* QA-T4: a long section title in the select widened Review to 500px on a
     phone; the field may shrink and wrap (phone basis in the touch block). */
  min-width: 0;
  flex-wrap: wrap;
}
.regen-row__label {
  color: var(--ink-k-40);
  white-space: nowrap;
}
#longform-regen-section {
  font-size: 12.5px;
  padding: 0.35rem 0.5rem;
  min-width: 0;
  max-width: 100%;
}
/* QA-T4: a disabled select (e.g. after the package is sent) looked live. */
select:disabled { opacity: 0.5; cursor: not-allowed; }

.plate {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.2rem 0.5rem;
  border: 1px solid currentColor;
  border-radius: 3px;
  white-space: nowrap;
}
.plate--m {
  color: var(--ink-m);
}
.plate--y {
  color: var(--ink-y-mid);
}
.plate--k {
  color: var(--ink-k);
}

.longform {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--ink-k);
}
.longform__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.75rem;
}

/* The sheet is now as long as the writing on it, which is correct, but it puts
   the one thing the operator is working toward a very long scroll away. The
   send block rides the bottom of the window instead: on stock, above the sheet,
   with the top rule reading as the trim edge it already was. */
/* QA-T12 (R3-1, round 3, 2026-10-16): the bar used to hold the whole send
   block (status, links, learning badge, rating, hint AND the buttons), so it
   was 675px of an 844px phone (80%) and 344px of a 900px desktop (38%),
   covering the article while she read it. Only the action row rides the
   window now; .ship__note sits in the flow just above it (index.html). */
.ship {
  position: sticky;
  bottom: 0;
  z-index: 5;
  margin-top: 1rem;
  padding: 0.75rem 0 calc(0.75rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--ink-k);
  background: var(--stock);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.ship__note {
  margin-top: 2rem;
}
/* QA-T12 fix: the last Save / Send / Delete result, in the bar itself (see
   shipNotice in app.js). One or two lines at most; the full error stays in
   #publish-error below the bar. */
.ship__notice {
  margin: 0 auto 0 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.ship__notice[data-kind="error"] { color: var(--alert); }
.ship__notice-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-width: 0;
}
.ship__notice-more { flex: none; }
.ship__note p {
  margin: 0;
}
@media (max-width: 600px) {
  /* Phone: Send on its own full-width row, Save + Delete on the row below:
     two rows, about 16% of an 844px screen. */
  .ship .actions {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.5rem;
  }
  .ship #publish {
    grid-column: 1 / -1;
    order: -1;
    padding-block: 0.7rem;
  }
  /* The keyboard is up while a field on the sheet has focus: the bar goes
     back into the flow so it never sits over the field being typed in.
     Only fields that bring up a keyboard: a ticked checkbox (the offer
     confirmation, focused by showOfferGateMessage right before she sends)
     or a radio must keep the bar, and Send, on screen. */
  #review-body:has(input:not([type="checkbox"], [type="radio"], [type="button"], [type="submit"], [type="reset"], [type="range"], [type="color"], [type="file"], [type="image"]):focus, textarea:focus, select:focus, [contenteditable="true"]:focus) .ship {
    position: static;
  }
}
#ship-status {
  font-size: 14px;
  font-weight: 600;
}
/* W1A: Delete sits behind "More" in the Review bar (index.html #ship-more), not beside Save edits. The bar is
   pinned to the bottom of the screen, so the menu opens upwards. */
.ship__more { position: relative; }
.ship__more > summary { list-style: none; cursor: pointer; }
.ship__more > summary::-webkit-details-marker { display: none; }
.ship__more-menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.4rem);
  z-index: 6;
  padding: 0.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--stock);
  box-shadow: var(--shadow-lg);
  white-space: nowrap;
}
/* U6 (UI audit 2026-10-28): opening More put a red-outlined pill (.btn--reject) beside Save edits, the loudest thing
   in the bar for the one action nobody should reach for by accident. In the menu it is a plain text item now; red
   only under the pointer or keyboard focus, so it still reads as the destructive one at the moment of choosing, and
   app.js still asks before deleting. */
.ship__more-menu #delete-package {
  border-color: transparent;
  background: none;
  color: var(--text-secondary);
  font-weight: 500;
}
.ship__more-menu #delete-package:hover:not(:disabled),
.ship__more-menu #delete-package:focus-visible {
  background: var(--alert-light, var(--rule-soft));
  border-color: transparent;
  color: var(--alert);
}
/* W1A: the Setup checklist button, now in the Settings header (admins only). */
.settings-setup { margin: 0.75rem 0 0; }
/* Bug hunt (2026-08-31), finding #1/#2: "View draft" / "couldn't verify"
   links per article once sent. */
.ship-links {
  margin: 0.35rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 14px;
}
.ship-links li[data-state="unverified"] {
  color: var(--text-muted);
}
.help-section {
  margin-bottom: 1.25rem;
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius);
  padding: 0;
}
.help-section summary {
  padding: 0.75rem 1rem;
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  cursor: pointer;
  list-style: none;
}
.help-section summary::before {
  content: "▸ ";
}
.help-section[open] summary::before {
  content: "▾ ";
}
.help-section p {
  margin: 0 1rem 0.85rem;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.help-section p:first-of-type {
  margin-top: 0.25rem;
}
.publish-confirm {
  margin-top: 0.75rem;
  padding: 1rem 1.25rem;
  background: var(--action-light);
  border: 1px solid var(--action);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* Market-ready gauntlet item 6 (2026-09-06) — same bug class already found
   and fixed once for .smart-badge/.inbox-thread (see that comment): a
   plain `display: flex` here has equal specificity to the browser's own
   `[hidden] { display: none }` UA rule, and equal-specificity author rules
   always win — so app.js's real, reachable `$('#publish-confirm').hidden =
   true` (both after a real publish completes and on cancel) was silently
   not actually hiding this box. A systematic sweep of every class in this
   file with a top-level display:flex/grid, cross-checked against every
   real `.hidden =` toggle in app.js, found this one along with 2 others
   (.review-feedback__tags, .slug__setup-badge) — same fix, same reasoning,
   applied to all three. */
.publish-confirm[hidden] {
  display: none;
}
.publish-confirm__summary {
  margin: 0;
  font-size: 0.9rem;
  color: var(--ink-k);
  line-height: 1.5;
}
.publish-confirm .actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
}

/* ------------------------------ press table ----------------------------- */

/* Tablet-band audit (2026-09-02, product finding): this used to flip on at
   720px, which meant the 720-1023px band got BOTH the persistent sidebar
   (256px, position:sticky below) AND main's FAB-stack gutter (16rem
   padding-right, see main's own rule) at once — the real content column
   left over was too narrow for this app's forms/tables (wrapped labels,
   unreadable tables), confirmed live at 768/900/1023px. Below the new
   threshold, .sidebar's own base/mobile rule (position:fixed,
   transform:translateX(-100%), the drawer) already applies with no changes
   needed here — raising this min-width just delays how soon the desktop
   layout switches on, it doesn't touch the mobile fallback itself. 1024px
   matches every other "real desktop" boundary already used elsewhere in
   this file (see .slug__search's 1024-1200px comment above). */
@media (min-width: 1024px) {
  .reg {
    display: block;
  }

  .sidebar-toggle {
    display: none;
  }
  /* QA round 1 (2026-10-16, D4): top:0 let the sticky sidebar slide under
     the sticky .slug header (z-index 10) once the page scrolled, hiding
     its first items. --slug-h is the header's measured height, kept
     current by initSlugShell() in app.js (ResizeObserver); 64px is the
     fallback before that runs. Height subtracts it too so the last nav
     item stays reachable. */
  .sidebar {
    position: sticky;
    top: var(--slug-h, 64px);
    transform: none;
    flex: none;
    width: 256px;
    height: calc(100vh - var(--slug-h, 64px));
    height: calc(100dvh - var(--slug-h, 64px));
    z-index: 1;
  }
  .sidebar__backdrop {
    display: none !important;
  }
  main {
    flex: 1;
    min-width: 0;
    /* right value carries the FAB-stack clearance — see main's base rule
       comment above for why this has to live in every breakpoint's own
       shorthand rather than a separate padding-right rule.
       QA-T13 (R4-09): this was 16rem, sized for .feedback-fab's ~230px
       label. Since Punchlist #19 (2026-09-04) that pill and .help-fab are
       hidden behind .fab-more and only appear while that popover is open
       (an outside click closes it, setupFabMoreToggle in app.js), so the
       16rem reserved space for a widget that is normally not there and
       left tables about 800px of a 1440px screen (Social Scheduler actions,
       Admin's "View as", Local SEO NAP actions hidden). The resting tower is
       .assistant-bar__toggle "Ask" (about 88px, right:1.5rem), .assist
       (56px) and .fab-more (44px): 1.5rem + 88px = about 112px, so 8rem
       (128px) keeps the page about 16px clear of the widest one. */
    padding: 1.5rem 8rem 5rem 2rem;
  }
  .sheet {
    padding: 2.75rem;
    border: 1px solid var(--rule);
    border-radius: 6px;
    box-shadow:
      0 1px 3px rgba(42, 37, 34, 0.06),
      0 12px 28px -8px rgba(42, 37, 34, 0.12);
    /* Tablet-band audit (2026-09-02): already capped + centered before this
       pass — confirmed live at both 1440px and 2560px (a wide monitor
       doesn't stretch the sheet into an unreadable full-width measure,
       margin:auto centers the leftover space evenly in main's own
       asymmetric padding box). Left as-is on purpose, no change needed. */
    max-width: 1180px;
    margin: 0 auto;
  }
  h1 {
    font-size: 2rem;
  }
  .colorbar {
    grid-template-columns: repeat(4, 1fr);
  }
  .ticket-form__fields,
  .setup {
    /* Bug hunt (2026-08-19, systematic sweep): plain `1fr` grid tracks have
       an implicit min-width:auto — same trap as flexbox's min-width:auto
       (see main's own min-width:0 fix above) but for CSS Grid. A form row
       whose input's intrinsic content width exceeded the space left after
       the FAB-clearance padding on `main` refused to shrink and instead
       overflowed the container, poking back into the FAB zone no matter
       how much padding-right was added — confirmed via a systematic sweep
       across every view/breakpoint plus direct measurement (increasing
       main's padding 16rem->20rem had zero effect on these specific
       fields, ruling out "not enough space reserved" as the cause).
       minmax(0, 1fr) lets the tracks actually shrink to fit. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .setup__go {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: center;
    gap: 1.25rem;
  }
  .field--wide {
    grid-column: 1 / -1;
  }
  .split {
    /* Tablet-band audit (2026-09-02): trailing track was a bare `1fr` — same
       implicit min-width:auto grid-track trap as .ticket-form__fields/.setup
       above (see that rule's own comment), just not yet hit by this specific
       track until the sidebar-persistent breakpoint moved to 1024px and
       exposed it: a wide child (e.g. the client add/edit form's right
       column) could refuse to shrink and visually overlap .split__list's
       left column in the newly-widened-but-still-finite 1024-1200px band.
       minmax(0, 1fr) is this file's established fix for the trap. */
    grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
    gap: 2.5rem;
  }
  .ship {
    flex-direction: row;
    align-items: center;
    /* QA-T12: the note left the bar; the actions keep their right-hand spot. */
    justify-content: flex-end;
  }
  /* The result line takes the room left of the buttons and wraps inside it;
     the buttons stay one row (seen: a two-line result pushed Delete down). */
  .ship .actions { flex: none; flex-wrap: nowrap; }
  .ship__notice { flex: 1 1 auto; min-width: 0; }
  .ship__note {
    max-width: 46ch;
  }

  .imposed {
    background-color: transparent;
    border-radius: 0;
  }

  /* The fold: two panels of one sheet, joined by a dashed crease.

     The two halves are registered row by row rather than merely set side by
     side. Each panel becomes `display: contents` so its parts are placed into
     the fold's own grid, which means the label rows sit on one line and the two
     text boxes start on one line even when a headline wraps to two lines and
     its opposite number does not. Nesting the panels instead lets the halves
     drift by the height of one line, and a sheet whose whole argument is that
     item N and post N stay in register cannot be visibly out of register. */
  .fold {
    position: relative;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto auto auto auto;
    column-gap: 3rem;
    row-gap: 0;
  }
  .fold > .panel {
    display: contents;
  }
  .fold > .panel:first-child > * {
    grid-column: 1;
  }
  .fold > .panel:last-child > * {
    grid-column: 2;
  }
  .fold .panel__head {
    grid-row: 1;
  }
  .fold .panel__headline {
    grid-row: 2;
  }
  .fold .manuscript {
    grid-row: 3;
  }
  .fold .count {
    grid-row: 4;
  }
  /* Only the article (left) panel has these — the social panel's last row is
     .count above. Explicit rows rather than leaving them to auto-placement:
     the grid's implicit auto-flow would otherwise slot them into whatever
     row the algorithm finds free next, which for a row that only exists on
     one side of the fold means landing next to the OTHER panel's .count
     line instead of below it. */
  .fold .regen-row {
    grid-row: 5;
  }
  .fold .regen-status {
    grid-row: 6;
  }

  /* The crease itself, drawn down the middle of the gutter rather than hung off
     one panel's border, since the panels no longer generate boxes. Slightly
     darker than an ordinary rule so it reads as a fold and not as a divider,
     while staying a hairline. */
  .fold::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(50% - 0.5px);
    border-left: 1px dashed var(--ink-k-40);
    pointer-events: none;
  }
  /* B13: a Regular package has no article half, so its posts take the full width with no crease. */
  .fold.fold--single {
    grid-template-columns: 1fr;
  }
  .fold.fold--single > .panel > * {
    grid-column: 1;
  }
  .fold.fold--single::after {
    display: none;
  }
}

@media (min-width: 1000px) {
  .imposed {
    padding-left: 3rem;
  }
}

/* ------------------------------- keywords -------------------------------- */

.kw-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
  margin-bottom: 1.5rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--rule);
}
.kw-controls .setup__field {
  flex: 1;
  min-width: 200px;
  margin: 0;
}
/* Merged "Get more keywords" panel (UX gauntlet, 2026-08-30) — the two
   modes it holds (AI discovery, NLP expansion) reuse .kw-entry/.kw-table/
   .hint styling already defined below, this just spaces the panel itself
   and its tab pair, which reuses .blog-preview__tabs' look. */
#kw-more-wrap {
  margin-bottom: 1.5rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--rule);
}
.kw-mode-tabs {
  margin-bottom: 1rem;
}
/* Mobile bug found live (2026-09-06): unlike .blog-preview__tabs' short
   labels ("Top 10 article"), these two labels ("Discover new keywords
   (AI)", "Expand a keyword I have") are long enough that at narrow widths
   the shared .btn class's flex-shrink-to-content sizing collapsed each
   button down toward a near-square box, which .btn's pill border-radius
   then rendered as a squeezed circle with badly cramped wrapped text.
   flex: 1 alone isn't enough — #main's 6rem right padding for the FAB
   stack (same root cause already fixed once for the Setup Checklist
   header, see that comment) leaves only ~200px here at mobile widths,
   so splitting that in half between two tabs still isn't enough room for
   either label. Stacking them vertically below 600px gives each the full
   ~200px instead of half of it. */
.kw-mode-tabs button[data-kw-mode-tab] {
  flex: 1;
  min-width: 0;
}
@media (max-width: 600px) {
  .kw-mode-tabs {
    flex-direction: column;
  }
}
/* Same active-tab treatment as .blog-preview__tabs button[data-preview-tab]
   .is-active above — that rule is scoped to [data-preview-tab] specifically,
   so it doesn't match this pair's [data-kw-mode-tab] attribute. Duplicated
   rather than widening the other selector, since these are two different
   tab pairs with two different attribute names. */
.kw-mode-tabs button[data-kw-mode-tab].is-active {
  background: var(--action);
  color: var(--stock-warm);
  border-color: var(--action);
}

.kw-entry {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1.5rem;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--desk);
}
@media (min-width: 720px) {
  .kw-entry { grid-template-columns: 1fr 1fr; }
}
.kw-entry .field--wide {
  grid-column: 1 / -1;
}
.kw-entry .actions {
  grid-column: 1 / -1;
  padding-top: 0.5rem;
}

.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* flex/grid ancestors (e.g. main) won't shrink this below its content's
     natural width without an explicit min-width:0 on the chain. */
  min-width: 0;
  transition: box-shadow 150ms ease;
}
/* QA-T13 (R4-09): a table wider than its box scrolled sideways with no sign
   that it did (the scrollbar is overlaid on macOS/iOS), so columns such as
   Admin's "View as" were silently off-screen. app.js (initTableScrollCues)
   sets these two classes from scrollLeft/scrollWidth; an inset shadow on
   the box's own edge says "more this way". Inset box-shadow is painted by
   the scroll container itself, so it stays at the edge while the rows move. */
.table-scroll.table-scroll--more-right { box-shadow: inset -16px 0 12px -12px rgba(42, 37, 34, 0.24); }
.table-scroll.table-scroll--more-left { box-shadow: inset 16px 0 12px -12px rgba(42, 37, 34, 0.24); }
.table-scroll.table-scroll--more-left.table-scroll--more-right {
  box-shadow: inset 16px 0 12px -12px rgba(42, 37, 34, 0.24), inset -16px 0 12px -12px rgba(42, 37, 34, 0.24);
}
[data-theme="dark"] .table-scroll.table-scroll--more-right { box-shadow: inset -16px 0 12px -12px rgba(0, 0, 0, 0.75); }
[data-theme="dark"] .table-scroll.table-scroll--more-left { box-shadow: inset 16px 0 12px -12px rgba(0, 0, 0, 0.75); }
[data-theme="dark"] .table-scroll.table-scroll--more-left.table-scroll--more-right {
  box-shadow: inset 16px 0 12px -12px rgba(0, 0, 0, 0.75), inset -16px 0 12px -12px rgba(0, 0, 0, 0.75);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .table-scroll.table-scroll--more-right { box-shadow: inset -16px 0 12px -12px rgba(0, 0, 0, 0.75); }
  :root:not([data-theme="light"]) .table-scroll.table-scroll--more-left { box-shadow: inset 16px 0 12px -12px rgba(0, 0, 0, 0.75); }
  :root:not([data-theme="light"]) .table-scroll.table-scroll--more-left.table-scroll--more-right {
    box-shadow: inset 16px 0 12px -12px rgba(0, 0, 0, 0.75), inset -16px 0 12px -12px rgba(0, 0, 0, 0.75);
  }
}
/* QA-T13 (R4-09): Admin's client roster has 10 columns; even with the wider
   content column it is wider than the box on a 1440 screen, and its one
   action ("View as", which opens the app as that client) was the column
   that fell off. The action column now rides the right edge of the box
   (sticky, on the table's own paper colour) so it is always in reach while
   the numbers scroll under it, and the client column keeps a sane measure
   instead of stretching to the longest description. Social Scheduler's row
   buttons get the same treatment (.sticky-act). */
.admin-master-table .admin-c-client { min-width: 12rem; max-width: 18rem; }
.kw-table .sticky-act {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--stock);
  white-space: nowrap;
}
.kw-table th.sticky-act { z-index: 3; }
.kw-table tr:hover td.sticky-act { background: var(--desk); }
/* On a phone the Social Scheduler's stacked row buttons would pin 40% of a
   290px box over the post text, so they scroll with the row there.
   position:relative, not static: measured, static widened the whole page to
   862px on a 390px phone in Chromium and WebKit (cause not isolated). */
/* W2 (QA R5-N4): the same opt-out up to 1439px. Below that width the table
   never fits its box (measured: 924px of table in a 518-774px box at 1024 and
   1280; it fits with nothing hidden at 1440), so the pinned stack of three
   buttons always sat over the Video and Post columns: "Render video" and
   "Post now" were covered and the held-post note was cut mid-word. The
   buttons now scroll with their row and the box's right-edge cue says there
   is more, so every column, those two buttons included, is reachable and
   nothing is drawn over another. Admin keeps its pin: one narrow "View as"
   over columns of numbers. */
/* R6-N3 (QA round 6): at 1440 the pin still assumed the table fits, but a row
   whose Post column held the wider "Confirm the offer first" made it overflow,
   and the pinned stack cut that button to "Confirm the of". A pin is only
   safe when it covers nothing, which is exactly when it is not needed, so the
   Social Scheduler's row buttons scroll with their row at EVERY width; the
   box's right-edge cue (table-scroll--more-right) says when there is more.
   That held button may wrap to two lines (below), so at 1440 the table fits. */
#soc-table .sticky-act { position: relative; right: auto; box-shadow: none; }
/* The pinned column is opaque, so it sits OVER the box's right-edge cue. It
   carries the cue itself instead: while columns are hidden (they pass under
   it), its left edge casts the shadow; when everything fits, no shadow. */
.table-scroll--more-right .kw-table .sticky-act { box-shadow: -14px 0 12px -10px rgba(42, 37, 34, 0.3); }
[data-theme="dark"] .table-scroll--more-right .kw-table .sticky-act { box-shadow: -14px 0 12px -10px rgba(0, 0, 0, 0.8); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .table-scroll--more-right .kw-table .sticky-act { box-shadow: -14px 0 12px -10px rgba(0, 0, 0, 0.8); }
}
/* W2 (QA R4-09): Local SEO's NAP table (four text inputs and a remove
   button per row) was 50px wider than its box at 1440, 210px at 1280 and
   370-466px on a tablet, because each input kept its own ~20-character
   width; the remove column sat behind a sideways scroll. From a tablet up
   the columns now share the box (fixed layout, inputs fill their column),
   so nothing is hidden. On a phone it stays a sideways scroll with the
   box's edge cue (four 40px-wide inputs would not be usable). */
#nap-table th, #nap-table td { padding-left: 0.4rem; padding-right: 0.4rem; }
@media (min-width: 601px) {
  #nap-table { table-layout: fixed; }
  #nap-table .nap-cell { width: 100%; min-width: 0; box-sizing: border-box; padding-left: 0.6rem; padding-right: 0.6rem; }
}
.kw-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 16px;
}
.kw-table thead {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 700;
  color: var(--text-muted);
}
.kw-table th {
  text-align: left;
  padding: 0.75rem 0.75rem;
  border-bottom: 2px solid var(--rule);
  font-weight: 700;
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--stock);
}
.kw-table td {
  padding: 0.85rem 0.75rem;
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: middle;
  transition: background 150ms ease;
}
.kw-table tr:hover td {
  background: var(--desk);
}
.kw-table .kw-actions {
  white-space: nowrap;
}
.kw-table .kw-actions button {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 0.4rem 0.85rem;
  min-height: 44px;
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-pill);
  background: none;
  cursor: pointer;
  color: var(--text-muted);
  transition: all 200ms var(--snap);
}
.kw-table .kw-actions button:hover {
  border-color: var(--text);
  color: var(--text);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}
.kw-table .kw-actions .kw-del:hover {
  border-color: var(--alert);
  color: var(--alert);
}

.kw-priority--high {
  color: var(--ink-reject);
  font-weight: 600;
}
.kw-priority--low {
  color: var(--ink-k-40);
}
.kw-status {
  display: inline-block;
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  padding: 0.3rem 0.65rem;
  border-radius: 99px;
}
/* U2: the pill prints the stored status word ("planned", "generated"); sentence case comes from here, not from
   shouting it in capitals. ::first-letter needs the inline-block above. */
.kw-status::first-letter { text-transform: uppercase; }
.kw-status--planned { background: var(--rule-soft); color: var(--text-muted); }
.kw-status--generated { background: var(--trust-bg); color: var(--trust); }
.kw-status--published { background: var(--growth-bg); color: var(--growth); }
.kw-status--failed { background: var(--alert-light); color: var(--alert); }

/* ----------------------------- local SEO --------------------------------- */

.lseo-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
  margin-bottom: 1.5rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--rule);
}
.lseo-controls .setup__field {
  flex: 1;
  min-width: 200px;
  margin: 0;
}

.lseo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1rem;
}
.lseo-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.15rem;
  cursor: pointer;
  transition: border-color 150ms ease, box-shadow 150ms ease, transform 150ms ease;
}
.lseo-card:hover {
  border-color: var(--action);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}
.lseo-card__area {
  font-weight: 700;
  font-size: 16px;
  margin: 0 0 0.25rem;
  color: var(--text);
}
.lseo-card__meta {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}
.lseo-card__snippet {
  margin: 0.5rem 0 0;
  font-size: 14px;
  color: var(--text-secondary);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.lseo-preview__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--ink-k);
}

/* ------------------------------ batch section --------------------------- */

.batch-section {
  margin-top: 2rem;
}
.batch-section h2 {
  font-size: 1.25rem;
  margin-bottom: 0.25rem;
}

.section-rule {
  border: none;
  border-top: 1px solid var(--rule);
  margin: 2.5rem 0;
}

.batch-kw-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  max-height: 20rem;
  overflow-y: auto;
  padding: 0.25rem 0;
}

.batch-kw-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
  transition: background 120ms ease;
}
.batch-kw-item:hover {
  background: var(--desk);
}
.batch-kw-item input[type="checkbox"] {
  accent-color: var(--action);
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}
.batch-kw-item__keyword {
  font-weight: 600;
}
.batch-kw-item__service {
  color: var(--ink-k-40);
  font-size: 0.85rem;
}

.batch-queue {
  margin-top: 1.5rem;
}
.batch-queue h3 {
  font-size: 1rem;
  margin-bottom: 0.5rem;
}

.batch-status--queued { color: var(--text-muted); }
.batch-status--running { color: var(--action); font-weight: 600; }
.batch-status--done { color: var(--growth); font-weight: 600; }
.batch-status--error { color: var(--alert); font-weight: 600; }

.btn--sm {
  padding: 0.4rem 0.75rem;
  font-size: 14px;
  min-height: 44px;
}

.rpt-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.rpt-summary {
  margin-bottom: 1.5rem;
}
.rpt-summary p {
  margin: 0.25rem 0;
}

.rpt-detail-body {
  margin-top: 1rem;
}

/* -------------------------------- scaffold ------------------------------ */
.scaffold-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.scaffold-card {
  background: var(--stock);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  box-shadow: var(--shadow-sm);
  transition: all 250ms var(--snap);
}
.scaffold-card:hover {
  border-color: var(--rule);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
.scaffold-card h3 {
  margin: 0 0 0.5rem;
  font-size: 1.1rem;
  color: var(--text);
}
.scaffold-card p {
  margin: 0 0 0.75rem;
  font-size: 15px;
  color: var(--text-secondary);
}

#gw-setup-guide {
  margin-top: 0.75rem;
  padding: 1rem 1.25rem;
  background: var(--stock-amber);
  border-left: 3px solid var(--action);
  border-radius: var(--radius);
  font-size: 15px;
  color: var(--text-secondary);
}

.settings-sections {
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
}

/* -------------------- Settings jump-nav (2026-08-30) ---------------------
   Settings is ~20 fieldsets deep with no way to jump straight to one -
   this sticks a compact chip row under the page's own sticky slug header
   (.slug, position:sticky top:0) so an operator always has one visible.
   --settings-chrome-h is set by initSettingsJumpNav() in app.js from the
   slug's real measured height (it wraps to more than one row below ~600px
   and on a long identity/tenant name) - a hardcoded top offset would drift
   out of sync with that and either gap or overlap. Same token language as
   .settings-group (var(--stock)/var(--rule)/var(--radius-lg)/var(--shadow-sm))
   and the sidebar's active-state pattern
   (.sidebar__item[aria-current="true"]), not a new visual system. */
.settings-jumpnav {
  position: sticky;
  top: var(--settings-chrome-h, 64px);
  z-index: 6;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.75rem 0.85rem;
  margin: 0 0 1.75rem;
  background: var(--stock-warm);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.settings-jumpnav__link {
  border: 1px solid var(--rule);
  background: var(--stock);
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.35rem 0.7rem;
  border-radius: 9999px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms var(--snap);
}
.settings-jumpnav__link:hover {
  color: var(--text);
  border-color: var(--action);
  transform: translateY(-1px);
}
.settings-jumpnav__link:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
.settings-jumpnav__link[aria-current="true"] {
  color: var(--action);
  background: var(--action-light);
  border-color: var(--action);
  font-weight: 700;
}
/* Jumped-to section gets a brief highlight + is the scroll-margin target,
   so a sticky jump-nav taller than one row never covers the section's own
   legend the instant it lands. */
.settings-group,
#settings-sec-kb {
  scroll-margin-top: calc(var(--settings-chrome-h, 64px) + 4.5rem);
}
.settings-group:focus-visible,
#settings-sec-kb:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .settings-jumped-flash {
    animation: settings-jump-flash 900ms ease;
  }
}
@keyframes settings-jump-flash {
  0% { box-shadow: 0 0 0 3px var(--action-light), var(--shadow-sm); }
  100% { box-shadow: var(--shadow-sm); }
}

/* -------------------- Admin Dashboard jump-nav (2026-08-30) --------------
   Same problem as Settings (~10 stacked card sections, no way to jump) so
   this copies that pattern's visual language and mechanics wholesale - see
   initAdminJumpNav() in app.js. A SEPARATE class/variable/keyframe from
   .settings-jumpnav on purpose: Settings' own jump-nav is explicitly out of
   scope to modify, and --admin-chrome-h is measured off the same shared
   .slug header independently so this can't drift if Settings' offset logic
   ever changes. */
.admin-jumpnav {
  position: sticky;
  top: var(--admin-chrome-h, 64px);
  z-index: 6;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.75rem 0.85rem;
  margin: 0 0 1.5rem;
  background: var(--stock-warm);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.admin-jumpnav__link {
  border: 1px solid var(--rule);
  background: var(--stock);
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.35rem 0.7rem;
  border-radius: 9999px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms var(--snap);
}
.admin-jumpnav__link:hover {
  color: var(--text);
  border-color: var(--action);
  transform: translateY(-1px);
}
.admin-jumpnav__link:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}
.admin-jumpnav__link[aria-current="true"] {
  color: var(--action);
  background: var(--action-light);
  border-color: var(--action);
  font-weight: 700;
}
/* Jumped-to card gets a brief highlight + is the scroll-margin target, so
   the sticky jump-nav never covers the card's own heading the instant it
   lands. .admin-card is exclusive to Admin Dashboard (see admin-grid),
   so this is safe to apply to the class broadly rather than a new marker
   on every target. */
/* W6B: the offset is the jump-nav's own measured height (app.js updateAdminChromeOffset sets --admin-jumpnav-h)
   plus a gap, so a taller nav (two-line labels on a phone) never covers the heading it jumped to. "Advanced" is a
   <details>, not an .admin-card, and had no offset at all. */
.admin-card,
#admin-sec-advanced {
  scroll-margin-top: calc(var(--admin-chrome-h, 64px) + var(--admin-jumpnav-h, 3.75rem) + 0.75rem);
}
.admin-card:focus-visible {
  outline: 2px solid var(--action);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .admin-jumped-flash {
    animation: admin-jump-flash 900ms ease;
  }
}
@keyframes admin-jump-flash {
  0% { box-shadow: 0 0 0 3px var(--action-light), var(--shadow-sm); }
  100% { box-shadow: var(--shadow-sm); }
}
.tier-grid {
  display: grid;
  grid-template-columns: 1fr repeat(3, 100px);
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.5rem;
}
.tier-grid--head {
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.25rem;
}
@media (max-width: 600px) {
  .tier-grid { grid-template-columns: 1fr repeat(3, 56px); gap: 0.35rem; }
}

.settings-group {
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  background: var(--stock);
  box-shadow: var(--shadow-sm);
  transition: box-shadow 200ms ease, transform 200ms ease;
  /* fieldset has a UA-default content-based minimum width that resists
     shrinking in a flex layout (.settings-sections is display:flex) —
     without this it held the whole page open past the viewport on mobile. */
  min-width: 0;
}
.settings-group:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}
.settings-group legend {
  font-weight: 700;
  font-size: 16px;
  padding: 0 0.5rem;
  color: var(--text);
}

.stream-status {
  margin-top: 0.75rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  font-size: 15px;
  font-weight: 500;
}
.stream-status[data-state="running"] {
  background: var(--action-light);
  border-left: 3px solid var(--action);
  color: var(--text-secondary);
}
.stream-status[data-state="done"] {
  background: var(--growth-light);
  border-left: 3px solid var(--growth);
  color: var(--growth);
}
.stream-status[data-state="error"] {
  background: var(--alert-light);
  border-left: 3px solid var(--alert);
  color: var(--alert);
}

.script-output {
  background: var(--stock-warm);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 1.5rem;
  white-space: pre-wrap;
  word-wrap: break-word;
  font-family: var(--manuscript);
  font-size: 0.925rem;
  line-height: 1.65;
  max-height: 600px;
  overflow-y: auto;
}

.page-preview {
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: 6px;
  overflow: hidden;
}
.page-preview iframe {
  width: 100%;
  height: 400px;
  border: 0;
}
.page-preview__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.6rem 1rem;
  background: var(--rule-soft);
  border-top: 1px solid var(--rule);
  font-size: 0.8rem;
}

/* ------------------------------ integration cards ----------------------- */

.scaffold-card .int-status {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  padding: 0.3rem 0.65rem;
  border-radius: 99px;
  display: inline-block;
  margin-top: 0.5rem;
}
.scaffold-card .int-status--connected {
  background: var(--growth-bg);
  color: var(--growth);
}
.scaffold-card .int-status--pending {
  background: var(--action-light);
  color: var(--action);
}

/* ----------------------------- warm helpers ----------------------------- */

.hint a {
  color: var(--action);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.hint a:hover {
  color: var(--action-hover);
}

.lede {
  color: var(--text-secondary);
  font-size: 1rem;
  line-height: 1.6;
}

/* ----------------------------- in-page dialog --------------------------- */

.app-dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(42, 37, 34, 0.45);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.app-dialog-overlay[hidden] {
  display: none;
}

.app-dialog {
  background: var(--stock);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: 2rem 2rem;
  max-width: 420px;
  width: 90vw;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  animation: dialog-pop 300ms var(--snap);
}
/* R6-N6: a dialog that replaces another one (app.js _openAppDialogLayer) appears at once: no frame of a half-faded dialog over the page. */
.app-dialog-overlay--swap .app-dialog { animation: none; }
@keyframes dialog-pop {
  from { opacity: 0; transform: scale(0.9) translateY(10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

.app-dialog__msg {
  font-size: 17px;
  line-height: 1.5;
  color: var(--text);
  margin: 0 0 1.25rem;
  white-space: pre-line; /* A1 (R4-05): a confirm written as short lines keeps them */
}

.app-dialog__input {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 15px;
  background: var(--surface);
  color: var(--text);
  margin-bottom: 1rem;
}

.app-dialog__actions {
  display: flex;
  flex-wrap: wrap; /* A1: an OK button named for its action wraps under Cancel on a phone */
  gap: 0.75rem;
  justify-content: flex-end;
}
.app-dialog__actions .btn { white-space: normal; max-width: 100%; }

/* Fix round 1, F3: offer list + required tick (appConfirmWithTick). */
.app-dialog__list {
  margin: 0 0 1rem;
  padding-left: 1.1rem;
  font-size: 15px;
  line-height: 1.45;
  color: var(--text);
}
.app-dialog__list li + li {
  margin-top: 0.4rem;
}
.app-dialog__list[hidden] {
  display: none;
}
.app-dialog__tick {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  margin: 0 0 1.25rem;
  font-size: 15px;
  line-height: 1.4;
  color: var(--text);
}
.app-dialog__tick input {
  margin-top: 0.2rem;
  flex: none;
}
.app-dialog__tick[hidden] {
  display: none;
}

/* ------------------------------ backlink pipeline ----------------------- */

.bl-pipeline {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.75rem;
  margin: 1.5rem 0;
}

.bl-pipeline__lane {
  background: var(--stock);
  border: 1.5px solid var(--rule-soft);
  border-radius: var(--radius);
  padding: 1rem;
  text-align: center;
}

.bl-pipeline__lane h3 {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-secondary);
  margin: 0;
}

.bl-count {
  display: inline-block;
  background: var(--action);
  color: var(--on-action);
  font-size: 13px;
  font-weight: 700;
  padding: 0.15em 0.55em;
  border-radius: 99px;
  margin-left: 0.35rem;
  vertical-align: middle;
}

.bl-query-list {
  list-style: none;
  padding: 0;
  margin: 0.75rem 0;
}

.bl-query-list li {
  font-family: var(--mono, 'SF Mono', 'Consolas', monospace);
  font-size: 0.8rem;
  padding: 0.5rem 0.75rem;
  border: 1px dashed var(--rule-soft, #e5e0db);
  border-radius: 4px;
  margin-bottom: 0.5rem;
  background: var(--stock-warm, #FAF8F5);
  cursor: pointer;
}

.bl-query-list li:hover {
  background: var(--stock, #fff);
}

@media (max-width: 719px) {
  .bl-pipeline {
    grid-template-columns: repeat(2, 1fr);
  }
}

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

.cal-timeline {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding: 1rem 0;
}

.cal-month {
  min-width: 150px;
  background: var(--stock);
  border: 1.5px solid var(--rule-soft);
  border-radius: var(--radius);
  padding: 1rem;
  text-align: center;
  flex-shrink: 0;
}

.cal-month__name {
  font-weight: 700;
  font-size: 15px;
  color: var(--text);
}

.cal-month__count {
  font-size: 1.75rem;
  font-weight: 700;
  color: var(--action);
}

.cal-month__label {
  font-size: 13px;
  color: var(--text-secondary);
}

/* ------------------------------ ranking chart --------------------------- */

.rank-summary {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 1rem 0;
}

.rank-card {
  background: var(--stock);
  border: 1.5px solid var(--rule-soft);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  text-align: center;
  flex: 1;
  min-width: 120px;
}

.rank-card b {
  display: block;
  font-size: 1.75rem;
  color: var(--action);
}

.rank-card span {
  font-size: 14px;
  color: var(--text-secondary);
}

/* Trend-count cards reuse the same growth/alert/trust palette as .pill so
   an "Improving" number and an "Improving" pill read as the same signal. */
.rank-card--growth b { color: var(--growth); }
.rank-card--alert b { color: var(--alert); }
.rank-card--trust b { color: var(--trust); }

.rank-crossed {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}
.rank-crossed:empty,
.rank-crossed[hidden] {
  display: none;
}

/* ------------------------- AI SEO recommendations ------------------------ */
/* One card per recommendation, not a table row — a table can't hold a
   hypothesis list + an actions list + evidence text legibly, and this is
   the PRD's own flagship feature (§8); display quality matters more here
   than density does. Reuses .rank-card's palette (--stock/--rule-soft) so
   it reads as the same design system, not a bolted-on new one. */
.rec-card {
  background: var(--stock);
  border: 1.5px solid var(--rule-soft);
  border-radius: var(--radius-lg);
  padding: 1.25rem 1.5rem;
  margin-bottom: 1rem;
}
.rec-card__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
}
.rec-card__problem {
  font-weight: 600;
  font-size: 1.05rem;
  margin: 0 0 0.25rem;
}
.rec-card__meta {
  color: var(--text-secondary);
  font-size: 13px;
  margin: 0 0 0.75rem;
}
.rec-card__section {
  margin-top: 0.75rem;
}
.rec-card__section h4 {
  margin: 0 0 0.35rem;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}
.rec-card__section ul {
  margin: 0;
  padding-left: 1.1rem;
}
.rec-card__actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 1rem;
}

/* ------------------------------ link map -------------------------------- */

.lm-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: 1rem 0;
}

.stat-card {
  background: var(--stock);
  border: 1.5px solid var(--rule-soft);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  text-align: center;
  flex: 1;
}

.stat-card b {
  display: block;
  font-size: 1.75rem;
  color: var(--action);
}

.stat-card span {
  font-size: 14px;
  color: var(--text-secondary);
}

/* ------------------------------ GHL workflow steps ---------------------- */

.ghlwf-steps {
  margin: 1rem 0;
}

.ghlwf-step {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  border-left: 3px solid var(--growth, #1A7A5C);
  margin-bottom: 0.5rem;
  background: var(--stock-warm, #FAF8F5);
  border-radius: 0 4px 4px 0;
}

.ghlwf-step__num {
  font-weight: 700;
  color: var(--growth, #1A7A5C);
  min-width: 1.5rem;
}

.ghlwf-step__type {
  font-family: var(--mono, 'SF Mono', monospace);
  font-size: 0.75rem;
  background: var(--stock, #fff);
  padding: 0.15em 0.5em;
  border-radius: 3px;
  border: 1px solid var(--rule-soft, #e5e0db);
}

.ghlwf-step__cfg {
  font-size: 0.75rem;
  color: var(--ink-k-60, #6B6460);
}

/* ------------------------------ tenant table extras --------------------- */

.tenant-color {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 3px;
  vertical-align: middle;
  margin-right: 0.35rem;
  border: 1px solid var(--rule-soft, #e5e0db);
}

.tenant-client-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--rule-soft, #e5e0db);
}

.td-truncate {
  max-width: 250px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.pill--growth { background: var(--growth-light); color: var(--growth); }
.pill--trust  { background: var(--trust-light);  color: var(--trust); }
.pill--action { background: var(--action-light); color: var(--action); }
.pill--alert  { background: var(--alert-light);  color: var(--alert); }
/* R6-P1: a social post that is not scheduled (public/social-row.js): quiet, outlined, never the queue's blue. */
.pill--draft  { background: transparent; color: var(--text-secondary); border: 1px dashed var(--rule); }

.setup__field--inline {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
  min-width: 0;
}
.setup__field--inline span { white-space: nowrap; }
/* QA-T13 (R4-11): app.js (attachProfileLink) puts a no-wrap "View full
   profile" link after the picker inside this same one-line row, so on a
   phone the picker shrank to "QA Rou". The row now wraps: the picker keeps
   a real width (12rem, or the whole row when narrower) and takes the free
   space, and the link drops under it when both do not fit. */
.setup__field--inline { flex-wrap: wrap; row-gap: 0.35rem; }
.setup__field--inline select { flex: 1 1 12rem; min-width: min(100%, 12rem); }

/* ------------------------------ reduced motion -------------------------- */

@media (prefers-reduced-motion: reduce) {
  .patch[data-state="done"] .patch__ink i { transition: none; }
  .patch[data-state="running"] .patch__ink::after { animation: none; display: none; }
  * { scroll-behavior: auto !important; }
  .sheet { animation: none !important; }
  /* Reduced-motion audit (2026-09-02): the next three were pointing at
     class names that don't exist anywhere in this file — .app-dialog__box
     (real class is .app-dialog, which carries dialog-pop), .cmdk__inner
     (the real animating element is .cmdk__panel, cmdk-in), .assist-panel
     (real class is .assist__panel, BEM double-underscore, assist-in). Each
     one silently covered nothing; the animation it was meant to stop kept
     running under reduced motion. Corrected to the real selectors. */
  .app-dialog { animation: none !important; }
  .cmdk__panel { animation: none !important; }
  /* Wave 2 (2026-09-02): the server-search loading row's icon pulse is
     motion with a real static fallback (the row + "Searching…" text stay
     fully visible without it), same reasoning as every other reduced-
     motion override in this block. */
  .cmdk__loading-row .cmdk__result-icon { animation: none !important; opacity: 1; }
  .assist__panel { animation: none !important; }
  /* Assistant bar: panel reveal, the busy/"thinking" dots, and the
     listening-state pulse on the mic button are all motion, not content —
     each has a real static fallback (panel just appears, dots sit still but
     stay visible via .assistant-bar__busy[hidden] toggling, mic still shows
     .is-listening's color change with no pulse). */
  .assistant-bar__panel { animation: none !important; }
  .assistant-bar__busy-dot { animation: none !important; }
  .assistant-bar__mic.is-listening { animation: none !important; }
  .confetti-particle { animation: none !important; }
  .spotlight-overlay { transition: none !important; }
  .progress-ring__fill { transition: none !important; }
  .setup-card__head::after { animation: none !important; }
  .onboard-overlay { animation: none !important; }
  .skeleton-bar { animation: none !important; }
  .view-active { transition: none !important; }
  .confetti-toast { transition: none !important; }
  /* Reduced-motion audit (2026-09-02) — real gaps found, none previously
     listed anywhere in this file: */
  /* the Setup tab's red "something needs attention" pulse (data-critical) */
  .slug__setup-tab[data-critical="true"] { animation: none !important; }
  /* feedback widget's own mic-listening pulse — same pattern already
     covered above for the assistant bar's mic, just a second, separate
     widget with its own class */
  .feedback-mic-btn.is-listening { animation: none !important; }
  /* button loading spinner — same "in-progress" reasoning as the assistant
     bar's busy dots above: the spin is motion, not the information itself
     (disabled state + opacity/color already convey "working" without it) */
  .btn--loading::after { animation: none !important; }
  /* Hover/state transform:scale effects — CSS keyframe animations aren't
     the only motion this app has; these are transition-driven scale-ups
     the task's own audit called out by name. Same fix already used on
     .capability below (transform:none, not transition:none, so the
     resting/active visual state itself is unaffected — only the animated
     growth to it is). */
  .sidebar__item:hover .sidebar__plate,
  .sidebar__item[aria-current="true"] .sidebar__plate,
  .cmdk__result:hover .cmdk__result-icon,
  .cmdk__result[aria-selected="true"] .cmdk__result-icon,
  .assist__toggle:hover,
  .onboard-dot.is-active,
  .scroll-top-fab:hover,
  .help-fab:hover,
  .feedback-fab:hover,
  .assistant-bar__toggle:hover {
    transform: none !important;
  }
}

/* ------------------------------ view transitions ------------------------- */

.view-enter {
  opacity: 0;
  transform: translateY(8px);
}
.view-active {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 200ms ease, transform 200ms ease;
}

/* ------------------------------ skeleton loading ------------------------- */

.skeleton-wrap { display: flex; flex-direction: column; gap: 0.75rem; padding: 1rem 0; }
.skeleton-bar {
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--rule-soft) 25%, var(--desk) 50%, var(--rule-soft) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
}
.skeleton-bar:nth-child(2) { width: 75%; }
.skeleton-bar:nth-child(3) { width: 60%; }
.skeleton-bar:nth-child(4) { width: 85%; }
.skeleton-bar:nth-child(5) { width: 45%; }
@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ------------------------------ scroll-to-top FAB ----------------------- */

.scroll-top-fab {
  position: fixed;
  /* Shell audit (2026-08-30): confirmed live at 375px — this button's own
     bottom:1.5rem sits fully inside the cookie-consent banner's box
     (bottom:1rem, up to ~150px tall while its 3-line notice text is
     wrapped), so on a first visit that's scrolled past 400px the banner
     renders its text right under this button. Reuses the same
     --cookie-banner-clearance custom property initCookieBanner() already
     sets/clears in app.js (mirrors the working --toast-stack-bottom fix
     for the exact same banner, see its own comment) rather than a second,
     divergent mechanism — falls back to the button's normal 1.5rem once
     the banner is dismissed or was never shown. */
  bottom: var(--cookie-banner-clearance, 1.5rem);
  left: 1.5rem;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--card, var(--stock));
  border: 1px solid var(--rule);
  color: var(--text);
  font-size: 1.2rem;
  cursor: pointer;
  box-shadow: var(--shadow-md);
  z-index: 900;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s, transform 0.25s, bottom 0.2s ease;
}
.scroll-top-fab.is-visible {
  opacity: 1;
  pointer-events: auto;
}
.scroll-top-fab:hover { transform: scale(1.1); }
/* QA-T12 (R3-1, round 3): bottom-left it sat on top of Review's sticky
   action bar ('Delete this month', WebKit 390x844). On phones it joins the
   right-hand FAB column instead, 12px above What's Next (.assist: bottom
   7.5rem, 44px; its count badge pokes ~6px above it), which stays right of the sheet's content edge, so it can
   never cover a field or a button. */
@media (max-width: 600px) {
  .scroll-top-fab {
    left: auto;
    right: 0.5rem;
    bottom: calc(7.5rem + 56px);
  }
}
/* Same collision wider up: at 768x1024 (drawer band) it sat on 'Save
   edits', and at 1440x900 on the persistent sidebar's last group heading
   (D14). Above phone width it tops the desktop FAB column instead, 12px
   above What's Next (.assist: bottom 13.25rem, right 1.5rem, 56px), which
   main's 8rem right gutter (16rem before QA-T13) keeps clear of the page. */
@media (min-width: 601px) {
  .scroll-top-fab {
    left: auto;
    right: calc(1.5rem + 6px);
    bottom: calc(13.25rem + 68px);
  }
}

/* ------------------------------ password toggle -------------------------- */

.field-toggle-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.field-toggle-wrap input { flex: 1; padding-right: 2.5rem; }
.field-toggle {
  position: absolute;
  right: 8px;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  color: var(--text-muted);
  padding: 4px;
  line-height: 1;
}

/* ------------------------------ button loading spinner ------------------- */

.btn--loading {
  pointer-events: none;
  opacity: 0.7;
  position: relative;
}
.btn--loading::after {
  content: '';
  position: absolute;
  right: 10px;
  top: 50%;
  width: 14px;
  height: 14px;
  margin-top: -7px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* ------------------------------ notification badges ---------------------- */

.sidebar__badge {
  background: var(--alert);
  color: var(--on-alert);
  font-size: 10px;
  font-weight: 700;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
  margin-left: auto;
  margin-right: 2px;
  flex-shrink: 0;
}

/* ------------------------------ char count ------------------------------- */

.char-count {
  font-size: 12px;
  color: var(--text-muted);
  text-align: right;
  margin-top: 2px;
}
.char-count.is-over { color: var(--alert); font-weight: 600; }

/* ------------------------------ breadcrumb ------------------------------- */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 13px;
  color: var(--text-muted);
  padding: 0.5rem 0;
  margin-bottom: 0.25rem;
}
.breadcrumb__sep { opacity: 0.5; }
.breadcrumb__current { color: var(--text); font-weight: 600; }

/* ------------------------------ toast stack ------------------------------- */

.toast-stack {
  position: fixed;
  bottom: var(--toast-stack-bottom, 5.5rem);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column-reverse;
  gap: 0.5rem;
  /* z-index audit (2026-09-02): was 10000, above every real modal dialog
     (.app-dialog-overlay etc. start at 9000) - a toast firing while a
     dialog was open could sit on top of its buttons. A toast is an
     informational aside, never something a person needs to act on before
     a blocking dialog; it should never be able to outrank one.
     QA-T12 fix: 5000 put it UNDER the open nav drawer (8110) and its
     backdrop (8100), so an error raised while the drawer was open was
     hidden. Above the drawer, still below every modal (9000+). */
  z-index: 8200;
  pointer-events: none;
}
.toast-stack .confetti-toast {
  position: relative;
  bottom: auto;
  left: auto;
  transform: none;
  /* QA-T12 (D11): a celebration is never in the way: taps go through it to
     the page; only its close button takes one. */
  pointer-events: none;
}
/* The base .confetti-toast.is-visible (later in this file, same
   specificity) re-applied translateX(-50%) inside the already-centred
   stack, pushing the toast half its width to the left. */
.toast-stack .confetti-toast.is-visible { transform: none; }
.confetti-toast__close {
  pointer-events: auto;
  min-width: 44px;
  min-height: 44px;
  margin: -0.6rem -1.1rem -0.6rem 0;
  border: none;
  background: none;
  color: var(--text-secondary);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
/* QA-T12 (D11): on phones the bottom of the screen holds the primary
   actions (Review's sticky send bar, the FAB column), so toasts appear at
   the top, just under the header, where they cover no button. */
@media (max-width: 600px) {
  .toast-stack {
    top: calc(var(--slug-h, 64px) + 0.5rem);
    bottom: auto;
    flex-direction: column;
    width: max-content;
    max-width: calc(100vw - 2rem);
  }
}
.toast {
  padding: 0.75rem 1.25rem;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 500;
  background: var(--stock);
  color: var(--ink);
  box-shadow: 0 4px 16px rgba(0,0,0,.15);
  pointer-events: auto;
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .25s, transform .25s;
}
.toast--visible { opacity: 1; transform: translateY(0); }
.toast--success { border-left: 3px solid var(--growth); }
.toast--error { border-left: 3px solid var(--alert); }
.toast--info { border-left: 3px solid var(--action); }

/* Backlog #18: retry affordance on an error toast (showToast's opts.retry) */
.toast__retry {
  display: block;
  margin-top: 0.5rem;
  padding: 0.3rem 0.7rem;
  border-radius: 6px;
  border: 1px solid currentColor;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.toast__retry:hover { background: rgba(127, 127, 127, 0.15); }
.toast__retry:disabled { opacity: 0.6; cursor: default; }

/* ------------------------------ inline field error ----------------------- */

.field-error {
  display: block;
  font-size: 12px;
  color: var(--alert);
  margin-top: 2px;
  font-weight: 500;
}
.field--error { border-color: var(--alert) !important; }
/* QA R2-2: a stored legacy value that fails today's checks; a note, not a blocking error. */
.field-error--warn { color: var(--text-muted); font-style: italic; }

/* ------------------------------ client avatar ---------------------------- */

.client-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  /* QA-T13 (R4-15): white initials on NO background were invisible in light
     mode. The disc takes the per-name hue app.js sets (--avatar-h, see
     clientAvatarHue); at 45% saturation and 30% lightness every hue keeps
     white text above 5:1, and the dark-theme disc below (75% lightness,
     near-black text) stays above 7:1. Both checked across all 360 hues in
     tests/qa-t13-layout.test.js. */
  background: hsl(var(--avatar-h, 210) 45% 30%);
  color: #fff;
  flex-shrink: 0;
}
[data-theme="dark"] .client-avatar { background: hsl(var(--avatar-h, 210) 50% 75%); color: #1a1a1a; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .client-avatar { background: hsl(var(--avatar-h, 210) 50% 75%); color: #1a1a1a; }
}

/* ------------------------------ table filter input ----------------------- */

.table-filter {
  display: block;
  width: 100%;
  max-width: 320px;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.75rem;
  font-size: 13px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--stock);
  color: var(--text);
  transition: border-color .15s;
}
.table-filter:focus { border-color: var(--action); outline: none; }
/* UX gauntlet (2026-08-30): `.table-filter { display: block }` above has
   equal specificity to the UA `[hidden] { display: none }` rule and wins
   on source order — same trap already documented at .inbox-thread[hidden]
   in this file. Needed once addTableFilter() started toggling `hidden` on
   this input to match its table's visibility. */
.table-filter[hidden] { display: none; }

/* ------------------------------ offline banner --------------------------- */

.offline-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  background: var(--alert);
  color: var(--on-alert);
  text-align: center;
  padding: 0.5rem;
  font-size: 13px;
  font-weight: 600;
  z-index: 99999;
}
.offline-banner[hidden] { display: none; }

/* Visual-appeal gauntlet (2026-08-30): this used to be centered (margin:0
   auto within a left:1rem/right:1rem box, effectively spanning up to
   640px in the middle of the screen) — confirmed live overlapping real
   buttons/forms on Quotes, Call Tracking, GoHighLevel, and Google
   Workspace on first visit. Anchored to the bottom-LEFT corner instead
   (narrower, capped width) so it never reaches into the main content
   column; --cookie-banner-clearance (already built for this exact corner
   — see .scroll-top-fab, also bottom-left) keeps it clear of that button
   too. */
.cookie-banner {
  position: fixed;
  left: 1rem; right: 1rem; bottom: 1rem;
  max-width: 420px;
  /* Bug hunt (2026-09-10): this deliberately inverts the page's own text
     color for a fixed dark high-contrast chip — a reasonable idea in
     light mode, where --text is dark (~#1a1a1a) and works with the
     hardcoded white text right below it. But --text is THEME-DEPENDENT —
     in dark mode it's a light cream (#e8e5dc, see the dark token blocks
     above), which flips this banner to a near-white background behind
     the same hardcoded white text, right at the edge of illegible. The
     chip is supposed to look the same regardless of page theme, so it
     needs a literal, not a variable that happens to invert along with
     everything else on the page.
   */
  background: #1a1a1a;
  color: #fff;
  border-radius: 10px;
  padding: 0.9rem 1.1rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 13px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
  /* Nightly sweep (2026-08-15): was z-index 99998, far above every
     .app-dialog-overlay (z-index 9000) — a first-run operator got the
     incident-notice dialog and this banner at once, and the banner's own
     293px-taller box sat on top of the dialog's "Got It" button, making it
     unclickable. A one-time dismiss toast must never be able to block a
     modal (aria-modal="true") underneath it, so this now sits below every
     overlay in the app (lowest is 9000) instead of above literally
     everything. */
  z-index: 500;
}
.cookie-banner a { color: #fff; text-decoration: underline; }
.cookie-banner__accept {
  margin-left: auto;
  white-space: nowrap;
  background: var(--action);
  color: var(--on-action);
  border: none;
}
/* Shell audit (2026-08-30): elementFromPoint at the "Got it" button's own
   center resolved to .chat-fab (retired 2026-09-01, replaced in the same
   stack slot by .assistant-bar__toggle — the geometry this note describes
   still applies to its successor), not the button underneath it — confirmed
   live at 375px, not a guess. The mobile FAB stack (.assist/.help-fab/
   .assistant-bar__toggle/.feedback-fab, all right:1rem, up to 56px wide,
   see their own mobile overrides below) sits at z-index >=900 while this banner is
   z-index 500 on purpose (see the note above — it must stay below every
   modal). Reserves the FAB column's real footprint on the banner itself,
   the same "footprint, not per-element" fix main already uses for the
   same stack, instead of fighting the deliberate z-index order. */
@media (max-width: 600px) {
  .cookie-banner { padding-right: 4.5rem; }
}
/* U3 (UI audit 2026-10-28, docs/ui-audit/2026-10-28/INDEX.md): the banner is only ever built for the signed-out
   screen (initCookieBanner in app.js), and that screen is #login-overlay at z-index 9900, so at 500 the banner sat
   underneath it: rendered, never seen, and "Got it" hit-tested to #login-overlay at 1440 and 390. One step above the
   sign-in overlay while that is the screen on top; the terms and two-step overlays (same .login-overlay layer, later
   in the DOM) still cover it, and every real modal and tour layer (9990+) stays above. The banner is appended to
   <body> after all three overlays, so the general-sibling selector reaches it with no script change. */
#login-overlay:not([hidden]) ~ .cookie-banner { z-index: 9901; }
#terms-overlay:not([hidden]) ~ .cookie-banner,
#totp-login-overlay:not([hidden]) ~ .cookie-banner { z-index: 500; }
/* Once visible, at 390x844 the banner covered the card's Terms of Service line. initCookieBanner already publishes
   the banner's real height as --cookie-banner-clearance (0 once dismissed), so the sign-in card centres in the
   space above it (card about 620px + clearance about 180px fits 844px). */
#login-overlay { padding-bottom: var(--cookie-banner-clearance, 0px); }
@media (max-width: 600px) {
  /* The 4.5rem right padding above reserves the signed-in FAB column, which the sign-in screen does not have. */
  #login-overlay:not([hidden]) ~ .cookie-banner { padding-right: 1.1rem; }
}

.token-expiry-banner {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 0.65rem 1rem;
  margin-bottom: 1rem;
  background: color-mix(in srgb, var(--alert) 12%, transparent);
  border: 1px solid var(--alert);
  border-radius: 6px;
  color: var(--ink-reject);
  font-size: 0.88rem;
  font-weight: 600;
}
.token-expiry-banner[hidden] { display: none; }
.token-expiry-banner .btn { flex: none; }

/* ------------------------------ print ------------------------------------ */

@media print {
  .slug, .sidebar, #sidebar-backdrop, .sidebar-toggle,
  .reg, .assistant-bar__toggle, .assistant-bar__panel, .help-fab, .help-panel,
  .assist-toggle, #assist-panel, .scroll-top-fab,
  .cmdk, .app-overlay, .onboard-overlay,
  .spotlight-overlay, .confetti-toast, .toast-stack { display: none !important; }
  main { margin: 0 !important; padding: 0 !important; }
  .sheet { box-shadow: none !important; border: none !important; margin: 0 !important; }
  body { background: #fff !important; background-image: none !important; }
  section.sheet[hidden] { display: none !important; }
}

/* ------------------------------ responsive tables → cards (mobile) ------- */
/* THE CANONICAL WIDE-TABLE PATTERN for this app: wrap any wide table in
   `<div class="table-scroll"><table class="kw-table">...`. Desktop/tablet
   get a horizontally-scrolling container that never pushes `main` sideways
   (`.table-scroll`'s own `overflow-x:auto; min-width:0` above, in the
   ------- keywords ------- section). Below 600px this block instead turns
   it into a stack of cards — no test screen should introduce its own
   table-scroll variant; widen this one instead.

   REQUIRED for the mobile card labels below: every <td> needs
   `data-label="Column Name"` matching its <thead> column, e.g.
   `<td data-label="Client">...`. Without it, `content: attr(data-label)`
   silently resolves to an empty string — the row still renders as a
   stack, just with no header text above each value, so it LOOKS like a
   bug in this shared rule when it's actually a missing attribute one
   level up in the table's own markup.

   Shell audit (2026-08-30): grepped every table row this app renders —
   of ~207 `<td>` sites across app.js, only the 6 in the Activity Log
   table (renderActivityLog) carry data-label. Every other table (Admin's
   "Master Library", Keywords, Rankings, Backlinks, Content Calendar,
   GHL Workflows, Google Workspace, ~40 more `.table-scroll` instances)
   renders as an unlabeled stack of bare values at 375px right now —
   confirmed live on the Admin table specifically (Client/Packages/
   Published/Videos/Packs/Platforms/Smart Mode/Approval/Last Activity
   all show as one column of numbers and dashes with no idea which is
   which). Not a horizontal-overflow bug (this rule already prevents
   that) and not fixable from here — each table's own render function
   owns its <td> markup, which is per-screen content outside the global
   shell. Flagged for the screen-owning agents rather than silently
   patched: add data-label to each <td> to match its <th>. */

/* Selector-drift + mobile-table sweep (2026-08-30): the row->card conversion
   below is now OPT-IN via `.kw-table.mobile-cards`, not applied to every
   table. Reason: it depends on every <td> carrying data-label (see the note
   above), but only a handful of this app's ~200 <td> render sites do — the
   rest were silently rendering as unlabeled stacks of bare values at 375px.
   Rather than chase data-label onto every cell (error-prone, and pointless
   for admin tables no operator opens on a phone), the SAFE DEFAULT is now
   horizontal scroll inside .table-scroll — always readable, never broken —
   and a table opts into the nicer labeled-card layout by adding both the
   `mobile-cards` class AND data-label on every <td>. Operator-facing loop
   tables (Rankings, Keywords, Activity Log, and the Integrations/Capture
   Leads screens the screen agents already labeled) opt in; everything else
   scrolls. This removes the broken unlabeled-stack state entirely. */
@media (max-width: 600px) {
  .table-scroll .kw-table.mobile-cards thead { display: none; }
  .table-scroll .kw-table.mobile-cards,
  .table-scroll .kw-table.mobile-cards tbody,
  .table-scroll .kw-table.mobile-cards tr,
  .table-scroll .kw-table.mobile-cards td {
    display: block;
    width: 100%;
  }
  .table-scroll .kw-table.mobile-cards tr {
    padding: 0.75rem;
    border-bottom: 1px solid var(--rule-soft);
    margin-bottom: 0.5rem;
    background: var(--stock);
    border-radius: var(--radius);
  }
  .table-scroll .kw-table.mobile-cards td {
    padding: 0.25rem 0;
    border-bottom: none;
    text-align: left;
    /* a long unbroken value (e.g. Activity Log's raw JSON details column)
       can't line-break on its own and forces this cell — and everything
       above it up to main — wider than the viewport. */
    overflow-wrap: break-word;
  }
  .table-scroll .kw-table.mobile-cards td::before {
    content: attr(data-label);
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    display: block;
    margin-bottom: 2px;
  }
  /* Card tables don't need their container to scroll (they're full-width
     blocks); every other table keeps overflow-x:auto so a wide table scrolls
     inside its own box instead of pushing the page body sideways. */
  .table-scroll:has(.kw-table.mobile-cards) { overflow-x: visible; }
}

/* ------------------------------ Cmd+K search ----------------------------- */

.cmdk {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 20vh;
}
.cmdk[hidden] { display: none; }
.cmdk__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.25);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.cmdk__panel {
  position: relative;
  width: min(560px, 90vw);
  background: var(--stock);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), 0 0 0 1px var(--rule);
  overflow: hidden;
  animation: cmdk-in 250ms var(--snap);
}
@keyframes cmdk-in {
  from { opacity: 0; transform: scale(0.92) translateY(-12px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
.cmdk__input-wrap {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1rem;
  border-bottom: 1.5px solid var(--rule-soft);
}
.cmdk__icon {
  flex: none;
  color: var(--text-muted);
}
.cmdk__input {
  flex: 1;
  border: none;
  background: none;
  font-family: var(--ui);
  font-size: 18px;
  color: var(--text);
  outline: none;
}
.cmdk__input::placeholder {
  color: var(--text-muted);
}
.cmdk__esc {
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--rule-soft);
  padding: 2px 6px;
  border-radius: 4px;
}
/* Visible, clickable close button — added 2026-08-04 (DUPLICATE-PALETTE-001
   incident review). Before this, closing #cmdk required knowing Esc or
   clicking the backdrop; neither is an obvious affordance for someone who
   didn't open the panel expecting a keyboard-driven UI. Sized well above
   the 44px touch-target floor since it's the one thing you need to be able
   to hit reliably when you feel stuck. */
.cmdk__close {
  flex: none;
  min-width: 40px;
  min-height: 40px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 1.3rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.cmdk__close:hover, .cmdk__close:focus-visible {
  background: var(--ink-reject, #c43c2e);
  color: var(--on-alert);
  border-color: var(--ink-reject, #c43c2e);
  /* QA-T4: no `outline: none` here: keyboard focus keeps the global ring. */
}
.cmdk__results {
  list-style: none;
  margin: 0;
  padding: 0.4rem;
  max-height: 320px;
  overflow-y: auto;
}
/* Wave 2 (2026-09-02): superseded by the real "No results for '<query>'"
   empty state rendered as an actual <li class="cmdk__empty"> (see
   cmdkEmptyStateHtml() in app.js and .cmdk__empty below) — the ul is never
   truly :empty anymore now that it can also hold the server-search loading
   row or that empty-state row, so this :empty::after rule is unreachable.
   Left removed rather than left dead: this file already has a documented
   history of competing empty-state visual systems drifting out of sync
   with each other; one live implementation per widget, not two. */
.cmdk__result {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.7rem 0.85rem;
  border-radius: 10px;
  cursor: pointer;
  font-size: 16px;
  color: var(--text);
  transition: all 150ms ease;
}
.cmdk__result[aria-selected="true"],
.cmdk__result:hover {
  background: var(--desk);
}
.cmdk__result-icon {
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 700;
  background: var(--action-light);
  color: var(--action);
  transition: transform 200ms var(--snap);
}
.cmdk__result:hover .cmdk__result-icon,
.cmdk__result[aria-selected="true"] .cmdk__result-icon {
  transform: scale(1.1);
}
.cmdk__result-icon--growth { background: var(--action-light); color: var(--action); }
.cmdk__result-icon--trust  { background: var(--trust-bg); color: var(--trust); }
.cmdk__result-icon--action { background: var(--action-light); color: var(--action); }
.cmdk__result-icon--alert  { background: var(--alert-light); color: var(--alert); }
/* Executable actions (Wave 2, 2026-09-02) — every page-group theme above
   (growth/trust/action/alert) is already spoken for by real sidebar
   groups, so a command result reusing one of them would read as "this is
   a page in that group" at a glance, which it isn't. --ink-m is this
   app's own real accent color (already used for the color-bar/patch
   motif, app.css search "--ink-m") sitting outside those four, giving an
   action result a genuinely distinct hue rather than a re-skinned one. */
.cmdk__result-icon--command { background: color-mix(in srgb, var(--ink-m) 12%, transparent); color: var(--ink-m); }
.cmdk__result-body {
  flex: 1;
  min-width: 0;
}
.cmdk__result-name {
  font-weight: 600;
}
.cmdk__result-group {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 1px;
}

/* Wave 2 (2026-09-02): server-backed search (keywords/content/tasks/
   recommendations, GET /api/search) is debounced and takes a real network
   round trip, unlike the instant client-side page/client results above.
   .cmdk__loading-row deliberately does NOT carry the .cmdk__result class —
   cmdkMove()/cmdkSelected()/the click and keyboard handlers all key off
   that exact class, so this row is inert by construction: unselectable,
   unclickable, skipped by arrow-key navigation, same as it visually reads
   (a status line, not an option). */
.cmdk__loading-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.85rem;
  color: var(--text-muted);
  font-size: 14px;
}
.cmdk__loading-row .cmdk__result-icon {
  animation: cmdk-loading-pulse 1.2s ease-in-out infinite;
}
@keyframes cmdk-loading-pulse {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 1; }
}

/* Real "no results anywhere" state (item 5 of the Wave 2 integration) —
   reuses this app's existing warm empty-state language (icon, sentence-
   case title, muted body — see .feature-empty__icon/__title/__body) scaled
   down for a dropdown-sized surface, instead of introducing a new visual
   style for empty states. Only ever rendered once BOTH the fast section
   and the debounced server section have actually settled — see
   cmdkPaint()'s comment in app.js for why that ordering matters. */
.cmdk__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 2rem 1.25rem;
  gap: 6px;
}
.cmdk__empty-icon {
  font-size: 1.4rem;
  margin-bottom: 4px;
  opacity: 0.6;
}
.cmdk__empty-title {
  font-family: var(--ui);
  font-size: 15px;
  font-weight: 700;
  color: var(--ink-k);
}
.cmdk__empty-body {
  font-size: 13px;
  color: var(--text-muted);
  max-width: 32rem;
  line-height: 1.5;
}

/* Higher-risk action confirm (Wave 2, 2026-09-02) — none of today's three
   shipped actions ever reach this (see cmdk-actions.js), but the gate is
   real and wired for whenever one does. flex-basis:100% on a flex-wrap
   parent (.cmdk__result, above) pushes it onto its own line under the
   name/group text rather than squeezing into the same row as the icon. */
.cmdk__action-confirm {
  flex-basis: 100%;
  padding-left: calc(34px + 0.75rem);
}
.cmdk__action-confirm-btn {
  width: 100%;
  text-align: left;
  white-space: normal;
}
.cmdk__footer {
  display: flex;
  gap: 1.2rem;
  padding: 0.55rem 1rem;
  border-top: 1.5px solid var(--rule-soft);
  font-size: 12px;
  color: var(--text-muted);
}
.cmdk__footer kbd {
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  background: var(--rule-soft);
  padding: 1px 5px;
  border-radius: 3px;
  margin-right: 2px;
}

/* Search palette layout pass (2026-09-02), small-phone touch-hint cleanup:
   .cmdk__esc (next to the input) and .cmdk__footer's "↑ ↓ navigate / ↵
   open" both name physical keys a touchscreen doesn't have — real wasted
   space on a 320-480px phone, not just an aesthetic nit, since it's height
   the results list could use instead. .cmdk__close (the visible X button)
   already covers "how do I dismiss this" on touch, and a tap doesn't need
   the arrow-key instructions a keyboard user does. */
@media (max-width: 480px) {
  .cmdk__esc,
  .cmdk__footer {
    display: none;
  }
}

/* Search palette layout pass (2026-09-02), large-desktop (1600px+): the
   panel stayed a fixed min(560px, 90vw) at every breakpoint from a phone
   up to a 4K monitor, so the extra horizontal room at this size just sat
   empty around a narrow, cramped-looking box. Widened the panel and gave
   each result row a real two-column reading — name left, group/category
   label right, using the same two fields cmdkRender() already renders
   (p.group / "Client" + city) rather than the previous inline run where
   the group text just trailed the name on the far left of all that extra
   width. Presentation only: no new data, no markup change, no touch to
   cmdkRender()'s matching/ranking logic. */
@media (min-width: 1600px) {
  .cmdk__panel {
    width: min(760px, 55vw);
  }
  .cmdk__results {
    max-height: 420px;
  }
  .cmdk__result {
    padding: 0.85rem 1.1rem;
    gap: 1rem;
  }
  .cmdk__result-icon {
    width: 38px;
    height: 38px;
    font-size: 16px;
  }
  .cmdk__result-body {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
  }
  .cmdk__result-name {
    font-size: 16.5px;
  }
  .cmdk__result-group {
    flex: none;
    margin-top: 0;
    text-align: right;
  }
}

/* ------------------------------ AI assistant ----------------------------- */

.assist {
  /* Stacked above .chat-fab (bottom:1.5rem, 48px — retired 2026-09-01,
     replaced in the same slot by .assistant-bar__toggle at the same
     geometry), .help-fab (bottom:5.5rem, 44px), and .feedback-fab
     (bottom:9.5rem, 44px pill) — this used to sit at bottom:9.5rem/
     right:1.5rem, identical to .feedback-fab's position (added later,
     2026-08-14, without checking this widget's own position), so this
     widget's z-index:8000 silently ate every click meant for the
     feedback button underneath it. Found live on production - Keeno hit it
     himself as a misclick, initially logged as unrelated. Same root cause
     this widget was already moved once before (2026-08-06) to stop
     colliding with .chat-fab (now .assistant-bar__toggle) - a
     fixed-position FAB tower needs every new entry checked against ALL
     the others already in it, not just the one directly below. */
  position: fixed;
  bottom: 13.25rem;
  right: 1.5rem;
  z-index: 8000;
}

/* Visual-appeal gauntlet (2026-08-30): 4 icon-only floating buttons
   stacked in one corner with no visible text told an operator nothing
   about which one to reach for (confirmed: 3 of the 4 — Assistant, tab
   help "?", AI chat — sound like the same kind of "help" from an icon
   alone). Merging them into one control would be a real feature change,
   not a visual fix, so instead: a text label reveals on hover/focus for
   each of the 3 icon-only ones (feedback already has a persistent label).
   Absolutely positioned relative to each button, so this adds zero new
   layout footprint and can't reintroduce the FAB-collision bug documented
   above (verified with scripts/layout-collision-sweep.mjs after). */
.fab-label {
  position: absolute;
  right: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  /* Bug hunt (2026-09-10): same root cause as .cookie-banner above —
     var(--text) is theme-dependent, this tooltip's white text is not. */
  background: #1a1a1a;
  color: #fff;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  padding: 5px 10px;
  border-radius: 6px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 150ms;
}
button:hover > .fab-label,
button:focus-visible > .fab-label {
  opacity: 1;
}
@media (max-width: 600px) {
  /* Hover doesn't exist on touch — labels would just never appear, so
     don't render them at all rather than leave dead unreachable markup. */
  .fab-label { display: none; }
}

.assist__toggle {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: none;
  background: var(--action);
  color: var(--on-action);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 8px rgba(229, 96, 26, 0.25);
  transition: all 250ms var(--snap);
  position: relative;
}
.assist__toggle:hover {
  transform: scale(1.08);
  box-shadow: 0 4px 16px rgba(229, 96, 26, 0.3);
}
.assist__toggle[aria-expanded="true"] {
  /* Bug hunt (2026-09-10): same root cause as .cookie-banner above — the
     base .assist__toggle rule's `color: #fff` is hardcoded, var(--text)
     is not. */
  background: #1a1a1a;
  color: #fff; /* QA-T4: the base is var(--on-action) now (dark text in dark mode) */
}
.assist__badge {
  position: absolute;
  top: -4px;
  right: -4px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--action);
  color: var(--on-action);
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.assist__badge[hidden] { display: none; }
.assist__panel {
  position: absolute;
  bottom: 68px;
  right: 0;
  width: min(380px, 85vw);
  background: var(--stock);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  border: 1.5px solid var(--rule-soft);
  overflow: hidden;
  animation: assist-in 300ms var(--snap);
}
.assist__panel[hidden] { display: none; }
@keyframes assist-in {
  from { opacity: 0; transform: translateY(16px) scale(0.9); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.assist__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  border-bottom: 1.5px solid var(--rule-soft);
  background: var(--stock-warm);
}
.assist__title {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}
.assist__close {
  width: 28px;
  height: 28px;
  border: none;
  background: none;
  font-size: 20px;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.assist__close:hover { background: var(--rule-soft); color: var(--text); }
.assist__nudges {
  list-style: none;
  margin: 0;
  padding: 0.4rem;
  max-height: 320px;
  overflow-y: auto;
}
.assist__nudge {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  padding: 0.7rem 0.65rem;
  border-radius: 6px;
  cursor: pointer;
  transition: background 100ms ease;
}
.assist__nudge:hover { background: var(--desk); }
.assist__nudge-icon {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
}
.assist__nudge-icon--growth  { background: var(--action-light); color: var(--action); }
.assist__nudge-icon--trust   { background: var(--trust-bg); color: var(--trust); }
.assist__nudge-icon--action  { background: var(--action-light); color: var(--action); }
.assist__nudge-body { flex: 1; min-width: 0; }
.assist__nudge-text {
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  line-height: 1.4;
}
.assist__nudge-detail {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 2px;
}
.assist__nudge-action {
  display: inline-block;
  margin-top: 0.35rem;
  font-size: 13px;
  font-weight: 600;
  color: var(--trust);
}

/* ------------------------------ celebrations ----------------------------- */

.celebrate {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
}
.celebrate[hidden] { display: none; }
.celebrate__particle {
  position: absolute;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  animation: confetti 800ms ease forwards;
}
@keyframes confetti {
  0%   { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
  100% { opacity: 0; transform: translateY(120px) rotate(360deg) scale(0.4); }
}

/* ========================== SETUP CHECKLIST TAB ========================= */

/* Visual-appeal gauntlet (2026-08-30): default is calm/informational —
   most missing connections are optional, so this shouldn't read as an
   alarm by default. data-critical="true" (WordPress specifically not
   connected — the one thing that actually blocks the core loop) is the
   only state that gets the red pulse; see the JS that sets it. */
.slug__setup-tab {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 18px;
  border: 2px solid var(--action);
  border-radius: var(--radius-pill);
  background: var(--action);
  color: var(--on-action);
  font-size: 13px;
  font-weight: 700;
  font-family: var(--ui);
  cursor: pointer;
  transition: all 200ms var(--snap);
  white-space: nowrap;
}
.slug__setup-tab[data-critical="true"] {
  border-color: var(--alert);
  background: var(--alert);
  color: var(--on-alert); /* QA-T4 J8: white on dark-mode red was 2.77:1 */
  animation: setup-pulse 2s ease-in-out infinite;
}
@keyframes setup-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.4); }
  50% { box-shadow: 0 0 0 8px rgba(220, 38, 38, 0); }
}
.slug__setup-tab:hover {
  filter: brightness(0.94);
  animation: none;
}
.slug__setup-tab[aria-pressed="true"] {
  background: var(--action);
  border-color: var(--action);
  color: var(--on-action);
  animation: none;
}
.slug__setup-label {
  letter-spacing: 0.02em;
}
.slug__setup-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 11px;
  background: var(--stock);
  color: var(--action);
  font-size: 12px;
  font-weight: 800;
}
/* Market-ready gauntlet item 6 (2026-09-06) — same [hidden]-specificity bug
   as .publish-confirm's own comment: app.js sets `badge.hidden = true` here
   once every setup connection is complete (real, reachable — this is the
   top-nav "Setup" pill's count badge visible on literally every screen).
   Without this override the badge could keep showing a stale leftover
   count (or "0") forever after setup finished, permanently misleading an
   operator into thinking something is still incomplete. */
.slug__setup-badge[hidden] {
  display: none;
}
.slug__setup-tab[data-critical="true"] .slug__setup-badge {
  color: var(--alert);
}

/* ---- Progress bar ---- */

.setup-progress {
  margin-bottom: 1.5rem;
}
.setup-progress__bar {
  height: 8px;
  border-radius: 4px;
  background: var(--rule-soft);
  overflow: hidden;
}
.setup-progress__fill {
  height: 100%;
  border-radius: 4px;
  background: var(--action);
  transform-origin: left;
  transition: transform 500ms var(--snap);
  transform: scaleX(0);
}
.setup-progress__text {
  margin: 0.5rem 0 0;
  font-size: 14px;
  color: var(--text-muted);
  font-weight: 500;
}

/* ---- Checklist cards ---- */

.setup-checklist {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.setup-card {
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius-lg);
  background: var(--stock);
  overflow: hidden;
  transition: box-shadow 200ms var(--snap), border-color 200ms var(--snap);
}
.setup-card:hover {
  box-shadow: var(--shadow-sm);
}
.setup-card.is-connected {
  border-color: var(--action);
}

.setup-card__header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem 1.25rem;
  cursor: pointer;
  user-select: none;
  /* Bug hunt (2026-08-31): at mobile widths, #main's 6rem right padding
     (reserved for the FAB stack, app.css ~line 781) leaves this row only
     ~200px to lay out icon + title + status pill + chevron. .setup-card__title
     is `flex: 1 1 0%` with min-width:0, so it collapsed to a real 0px box
     while its text kept painting — visually overlapping the status pill
     rather than wrapping under it. flex-wrap plus a real minimum on the
     title lets status+chevron drop to their own line instead of colliding. */
  flex-wrap: wrap;
}
.setup-card__icon {
  font-size: 28px;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  background: var(--stock-parchment);
}
.setup-card.is-connected .setup-card__icon {
  background: var(--action-light);
}
.setup-card__title {
  flex: 1 1 140px;
  min-width: 140px;
}
.setup-card__title h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}
.setup-card__title p {
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--text-muted);
}

.setup-card__status {
  padding: 4px 12px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  background: var(--action-light);
  color: var(--action);
  /* Wraps onto its own line with the chevron once title claims its
     140px minimum (see .setup-card__header) — pins it to the right
     instead of flush left under the icon. */
  margin-left: auto;
}
.setup-card.is-connected .setup-card__status {
  background: var(--growth-bg);
  color: var(--growth);
}

.setup-card__chevron {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform 250ms ease;
}
.setup-card.is-open .setup-card__chevron {
  transform: rotate(180deg);
}

.setup-card__body {
  display: none;
  padding: 0 1.25rem 1.25rem;
  border-top: 1px solid var(--rule-soft);
}
.setup-card.is-open .setup-card__body {
  display: block;
}
.setup-card__why {
  margin: 1rem 0;
  font-size: 14px;
  color: var(--text-secondary);
  line-height: 1.6;
}
.setup-card__fields {
  display: grid;
  gap: 1rem;
}
.setup-card__actions {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 1rem;
  /* Same narrow-column squeeze as .setup-card__header (see its comment) —
     Save + Test buttons were overflowing past the card's own right edge
     into the FAB stack's reserved space instead of wrapping. */
  flex-wrap: wrap;
}
/* 2026-08-12: was just a color change on plain inline text - no background,
   no border, easy to glance past. Jason hit exactly this: PUT /api/settings
   403'd four times over five minutes because his role wasn't admin, and
   the code technically DID throw and display the real error every time -
   but through THIS specific treatment (the Setup Checklist's setupResult()
   helper), not the better-designed one .stream-status[data-state="error"]
   already uses elsewhere in this same file (colored background block, left
   border) for the main Settings page and GHL forms. "The code shows the
   error" and "a person mid-task at 1am will actually register it" turned
   out to be two different claims - this brings both states up to the same
   visual weight as the treatment that already existed one scroll away. */
.setup-card__result {
  margin: 0.5rem 0 0;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 500;
}
.setup-card__result:empty {
  margin: 0;
  padding: 0;
}
.setup-card__result.is-ok {
  background: var(--action-light);
  border-left: 3px solid var(--action);
  color: var(--action);
}
.setup-card__result.is-err {
  background: var(--alert-light);
  border-left: 3px solid var(--alert);
  color: var(--alert);
}

@media (max-width: 600px) {
  .slug__setup-tab {
    padding: 7px 12px;
    font-size: 12px;
  }
  .sidebar-toggle__label {
    display: none;
  }
}

/* ----------------------------- dashboard -------------------------------- */

/* ============================================================================
   RETROFIT 2026-08-04 — ADMIN-UI-SLOP-001 REMEDIATION
   ----------------------------------------------------------------------------
   Everything below this comment was rewritten to bring the session's new
   admin/SaaS surfaces back inside the imposition-sheet visual world defined
   by DESIGN.md. Refs: reference/craft-floor.md; feedback_never_skip_taste
   _pass_on_admin_ui.md; audit finding ADMIN-UI-SLOP-001.
   Retired patterns: rounded cards (border-radius >6px + box-shadow), 4px
   side-tab colored border-left, gradient backgrounds, shimmer skeletons,
   pill-shaped chips, hover-translate motion.
   Adopted patterns: hairline rules, crop marks, plate patches, mono slug
   heads, growth-green as the action accent, contact shadow only.
   ============================================================================ */

/* Capability strip — four imposed cells across the top of the Dashboard.
   Same crop-mark grammar as .imposed on the Review sheet, so the operator's
   first screen already belongs to the world. */
.capability-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--crop-gap, 5px);
  margin-bottom: 24px;
}
.capability {
  position: relative;
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  gap: 8px;
  padding: 18px 16px 14px;
  background: var(--sheet);
  border: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
  /* Crop marks — four corner Ls, same primitive as .imposed */
  background-image:
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule));
  background-repeat: no-repeat;
  background-size: var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop),
    var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop);
  background-position:
    left top, left top, right top, right top,
    left bottom, left bottom, right bottom, right bottom;
}
.capability:hover, .capability:focus-visible {
  border-color: var(--growth);
  outline: 2px solid var(--growth);
  outline-offset: -1px;
}
.capability__icon { font-size: 1.5rem; line-height: 1; color: var(--ink-k-40); }
.capability__title {
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 700;
  color: var(--ink-k);
}
.capability__body {
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text-muted);
}
.capability__cta {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--growth);
}
@media (max-width: 900px) { .capability-strip { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .capability-strip { grid-template-columns: 1fr; } }

/* Empty-state teaser — an "unimposed cell" waiting for its first plate.
   Hairline dashed rule instead of full dashed panel; mono label mirrors the
   slug line pattern. */
/* U4 (2026-10-28): the block tier of the one empty-state system (see .empty): same card, same icon/headline/body
   sizes. It was a full-bleed band between two hairlines on --sheet, the only empty state with no edge of its own. */
.feature-empty {
  padding: 2rem 1.5rem;
  background: var(--empty-surface);
  border: var(--empty-rule);
  border-radius: var(--radius-lg);
  color: var(--empty-text);
  text-align: center;
  margin: 1rem 0 0.75rem;
}
/* Visual-appeal gauntlet (2026-08-30): this used to render its headline
   and step chips in mono font, all-caps, wide letter-spacing -- the one
   place in the whole app styled like a terminal instead of a product.
   Same layout (icon, headline, body, step chips), normal UI font and
   sentence case now, matching the "warm" empty-state pattern already used
   well elsewhere (Content Calendar, Website Builder, Video/Audio). */
.feature-empty__icon { font-size: var(--empty-icon-block); line-height: 1; display: block; margin-bottom: 0.6rem; color: var(--ink-k-40); }
.feature-empty__title {
  font-family: var(--ui);
  font-size: 16px;
  font-weight: 700;
  margin: 0 0 0.35rem;
  color: var(--text);
}
.feature-empty__body {
  font-size: 14px;
  color: var(--empty-text);
  max-width: 62ch;
  margin: 0 auto 1rem;
  line-height: 1.55;
}
.feature-empty__steps {
  display: inline-flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  font-family: var(--ui);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-k);
  margin-bottom: 14px;
}
.feature-empty__step {
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--rule);
  background: var(--stock-warm);
  color: var(--ink-k);
  font-weight: 600;
}
.feature-empty__arrow { color: var(--ink-k-40); }

.dash-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  margin-bottom: 24px;
}
.dash-stat {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 16px;
  text-align: center;
}
.dash-stat__value {
  display: block;
  font-size: 2rem;
  font-weight: 700;
  color: var(--growth);
  font-variant-numeric: tabular-nums;
}
.dash-stat__label {
  /* W4C (defect 15): sentence case like every other label. */
  display: block;
  font-size: 0.8rem;
  color: var(--slug);
  margin-top: 4px;
}
.dash-trend-pills { display: flex; flex-wrap: wrap; gap: 8px; }
.dash-section { margin-bottom: 24px; }
.dash-section h2 { font-size: 1rem; margin-bottom: 12px; }
.dash-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.dash-activity { list-style: none; padding: 0; }
.dash-activity__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.875rem;
}
.dash-activity__icon { font-size: 1.1rem; flex-shrink: 0; }
.dash-activity__text { flex: 1; min-width: 0; }
.dash-activity__date { color: var(--slug); font-size: 0.75rem; white-space: nowrap; }
.dash-activity__item--link { cursor: pointer; }
.dash-activity__item--link:hover { background: var(--desk); }

@media (max-width: 768px) {
  .dash-stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .dash-stats { grid-template-columns: 1fr; }
  .dash-stat__value { font-size: 1.5rem; }
}

/* ----------------------------- delivery board -------------------------- */
/* The operator home. One card per client; the monthly cycle as a row of
   stage pills; the first not-done stage is a real button ("Do this"). Built
   to read at a glance for a non-technical operator and to reflow cleanly from
   375px to desktop (stages wrap; card padding tightens). */
.db-legend {
  display: flex; flex-wrap: wrap; gap: 14px;
  font-size: 12.5px; color: var(--text-secondary, #6b6257);
  margin: 0 0 16px;
}
.db-legend__item { display: inline-flex; align-items: center; gap: 6px; }
.db-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; display: inline-block; }
.db-dot--done { background: var(--growth, #3f9b52); }
.db-dot--next { background: var(--action, #c2542a); }
.db-dot--wait { background: var(--rule, #d8cfc0); }
.db-flag, .db-card__flag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--alert, #c23a2a); color: var(--on-alert);
  font-weight: 700; font-size: 12px; flex: none;
}
.db-loading, .db-empty { padding: 28px 4px; color: var(--text-secondary, #6b6257); }
.db-empty { text-align: center; }
.db-empty .btn { margin-top: 12px; }

.db-card {
  border: 1px solid var(--rule, #e4dccd);
  background: var(--stock, #fdfbf7);
  border-radius: 10px;
  padding: 16px 18px;
  margin-bottom: 14px;
}
.db-card--flagged { border-color: var(--alert, #c23a2a); box-shadow: 0 0 0 1px var(--alert, #c23a2a) inset; }
.db-card__head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.db-card__name {
  font-weight: 650; font-size: 16px; color: var(--ink, #221d16);
  background: none; border: none; padding: 0; cursor: pointer; text-align: left;
  min-width: 0; overflow-wrap: anywhere; /* QA-T4 D6: a long name wraps, never widens the page */
}
.db-card__name:hover { text-decoration: underline; }
.db-card__loc { font-size: 12.5px; color: var(--text-secondary, #6b6257); min-width: 0; overflow-wrap: anywhere; }
.db-card__progress { margin-left: auto; font-size: 12.5px; color: var(--text-secondary, #6b6257); white-space: nowrap; }

.db-card__stages { display: flex; flex-wrap: wrap; gap: 8px; }
.db-stage {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 11px; border-radius: 999px;
  border: 1px solid var(--rule, #e4dccd); background: var(--stock-warm, #f7f1e6);
  font-size: 13px; color: var(--ink, #221d16);
}
.db-stage--done { color: var(--text-secondary, #6b6257); border-style: solid; }
.db-stage--wait { opacity: 0.72; }
.db-stage--next {
  cursor: pointer; border-color: var(--action, #c2542a);
  background: var(--action-light, #f6e7df); font-weight: 600;
}
.db-stage--next:hover { background: var(--action, #c2542a); color: var(--on-action); }
.db-stage--next:hover .db-dot--next { background: var(--on-action); }
.db-stage__go { font-size: 12px; opacity: 0.85; }
.db-stage__label { white-space: nowrap; }

.db-card__goals { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--rule, #e4dccd); }
.db-goal { font-size: 12px; padding: 4px 9px; border-radius: 6px; background: var(--stock-warm, #f7f1e6); color: var(--text-secondary, #6b6257); }
.db-goal--met { background: var(--growth-bg, #e6f2e9); color: var(--growth, #2e7d40); }
.db-goal--behind { background: var(--action-light, #f6e7df); color: var(--action, #b4491f); }
.db-goal--pending { font-style: italic; }

@media (max-width: 640px) {
  .db-card { padding: 14px; }
  .db-card__progress { margin-left: 0; width: 100%; order: 3; }
  .db-stage { flex: 1 1 auto; justify-content: center; }
}

/* ------------- Easy Navigation Phase 8: compact work strip -------------- */
/* Same data/tokens as .db-card above, collapsed to one line per client so it
   costs nothing to show above the chat dock on the default landing view. */
.cc-strip {
  border: 1px solid var(--rule, #e4dccd);
  background: var(--stock, #fdfbf7);
  border-radius: 10px;
  margin-bottom: 18px;
  overflow: hidden;
}
.cc-strip__loading { padding: 14px 16px; color: var(--text-secondary, #6b6257); font-size: 13px; }
.cc-strip__rows { display: flex; flex-direction: column; }
.cc-strip__row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--rule, #e4dccd);
  font-size: 13.5px;
}
.cc-strip__row:last-child { border-bottom: none; }
.cc-strip__name {
  font-weight: 650; color: var(--ink, #221d16);
  background: none; border: none; padding: 0; cursor: pointer; text-align: left;
}
.cc-strip__name:hover { text-decoration: underline; }
.cc-strip__flag-group { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.cc-strip__flag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--alert, #c23a2a); color: var(--on-alert);
  font-weight: 700; font-size: 11px; flex: none;
}
/* Punchlist #21 (2026-09-04): a real inline label, not a hover-only
   title="" — tooltips don't exist on touch. Truncates gracefully rather
   than pushing the row's other controls off — the full reason is still
   in the title attribute below it for anyone who does hover. */
.cc-strip__flag-label {
  color: var(--alert, #c23a2a); font-size: 12px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px;
}
.cc-strip__progress { color: var(--text-secondary, #6b6257); font-size: 12px; white-space: nowrap; }
.cc-strip__done { margin-left: auto; color: var(--growth, #2e7d40); font-size: 12.5px; white-space: nowrap; }

/* Punchlist #6/#7 (2026-09-04): visual weight now tracks real urgency —
   a flagged row is bigger/bolder, a fully-done row shrinks and mutes,
   instead of every row looking identical regardless of whether it
   actually needs her. */
.cc-strip__row--urgent {
  font-size: 15px;
  background: var(--action-light, #f6e7df);
  border-left: 3px solid var(--alert, #c23a2a);
  padding-left: 13px;
}
.cc-strip__row--urgent .cc-strip__name { font-weight: 750; }
.cc-strip__row--urgent { flex-wrap: wrap; }
/* Punchlist #20 (2026-09-04): always-visible on the one kind of row where
   it matters most (urgent), not a title="" tooltip that doesn't exist on
   touch — full-width so it wraps to its own line under the rest. */
.cc-strip__detail {
  width: 100%;
  font-size: 12px;
  color: var(--text-secondary, #6b6257);
  margin-top: 2px;
}
/* Punchlist #22 (2026-09-04): a gentle pulse on the single most urgent row
   only — draws her eye without needing her to read every row. Respects
   reduced-motion (same guard convention already used elsewhere in this
   file) — a static --urgent row is already visually distinct on its own
   via #6/#7's bigger/bolder treatment, so reduced-motion loses nothing. */
@media (prefers-reduced-motion: no-preference) {
  .cc-strip__row--pulse { animation: cc-strip-pulse 2.2s ease-in-out infinite; }
  @keyframes cc-strip-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(194, 58, 42, 0.25); }
    50% { box-shadow: 0 0 0 5px rgba(194, 58, 42, 0); }
  }
}
.cc-strip__row--done {
  font-size: 12.5px;
  opacity: 0.68;
}
.cc-strip__row--done .cc-strip__name { font-weight: 550; }
.cc-strip__go {
  margin-left: auto; cursor: pointer; border: 1px solid var(--action, #c2542a);
  background: var(--action-light, #f6e7df); color: var(--action, #b4491f);
  border-radius: 999px; padding: 5px 12px; font-size: 12.5px; font-weight: 600;
  white-space: nowrap;
}
.cc-strip__go:hover { background: var(--action, #c2542a); color: var(--on-action); }
/* Punchlist #15 (2026-09-04): same 4-theme palette as RESULT_TYPE_META
   (cmdk-nav.js) — trust/action/growth already have real tokens defined
   app-wide; overrides the default action-orange base above. */
.cc-strip__go--trust { border-color: var(--trust); background: var(--trust-bg); color: var(--trust); }
.cc-strip__go--trust:hover { background: var(--trust); color: var(--on-trust); }
.cc-strip__go--growth { border-color: var(--growth, #2e7d40); background: var(--growth-bg, #e6f2e9); color: var(--growth, #2e7d40); }
.cc-strip__go--growth:hover { background: var(--growth, #2e7d40); color: var(--on-growth); }
.cc-strip__footer {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 9px; border-top: 1px dashed var(--rule, #e4dccd);
}
.cc-strip__more { font-size: 12.5px; color: var(--action, #b4491f); }
/* Punchlist #29 (2026-09-04) */
.cc-strip__synced { font-size: 11px; color: var(--text-secondary, #6b6257); }
.cc-strip__synced::before { content: '·'; margin-right: 10px; }
@media (max-width: 480px) {
  .cc-strip__footer { flex-direction: column; gap: 2px; }
  .cc-strip__synced::before { content: none; margin: 0; }
}
@media (max-width: 640px) {
  .cc-strip__row { flex-wrap: wrap; }
  .cc-strip__go, .cc-strip__done { margin-left: 0; width: 100%; order: 3; text-align: center; }
  .cc-strip__flag-label { max-width: 100%; white-space: normal; }
}
/* QA-T12 (R3-2, round 3, WebKit 390x844): "Let's get <client> going — start
   with Keywords →" kept the base white-space:nowrap, so in its full-width
   256px box the label ran 320-940px wide and spilled out of the pill and
   the strip. Every row item may shrink (min-width:0 here; the touch block's
   44px floor still wins on phones and only sets the tap size) and long
   names / labels wrap, at every width: on desktop a 60-character client
   name was squeezed into one word per line by its nowrap neighbour. */
.cc-strip__row > * { min-width: 0; }
.cc-strip__name,
.cc-strip__go,
.cc-strip__flag-group {
  max-width: 100%;
  overflow-wrap: anywhere;
}
.cc-strip__go { white-space: normal; }
.cc-strip__name { flex: 0 1 auto; }

/* ----------------------------- smart-mode + feedback -------------------- */
/* Smart-mode badge: a plate-style status strip. Left is the ink patch (state
   color) + mono state word; right is the message. No gradient, no side-tab,
   no rounded card — this reads as a plate on the sheet, not a notification. */
.smart-badge {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px;
  align-items: center;
  padding: 8px 12px;
  margin-top: 10px;
  background: var(--sheet);
  border: 1px solid var(--rule);
  font-size: 0.88rem;
}
/* `display: grid` above is author CSS with equal specificity to the
   browser's UA rule `[hidden] { display: none }` — author rules win over
   UA rules on equal specificity, so JS setting the hidden attribute was
   silently ignored and the badge could render "Smart mode: on" regardless
   of actual state. Same bug class as `.inbox-thread[hidden]` above; same
   fix, scoped to this one class only. */
.smart-badge[hidden] {
  display: none;
}
.smart-badge__icon {
  font-size: 1.1rem; line-height: 1;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--growth-bg, var(--action-light));
  border: 1px solid var(--growth);
  color: var(--growth);
}
.smart-badge__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.smart-badge__body strong {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-k);
}
.smart-badge__body .hint { font-size: 0.82rem; color: var(--text-muted); }

/* Review-feedback: horizontal strip with hairline top-border, mono label,
   growth/reject sized ratings, chip row for tags. No dashed panel. */
.review-feedback {
  margin-top: 12px;
  padding: 12px 0 8px;
  border-top: 1px solid var(--rule);
  font-size: 0.88rem;
}
.review-feedback__row {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  min-height: 44px; /* Touch-target floor for the button row */
}
.review-feedback__tags {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--rule);
}
/* Market-ready gauntlet item 6 (2026-09-06) — same [hidden]-specificity bug
   as .publish-confirm's own comment: app.js sets `.hidden = rating !==
   'down'` here, real and reachable on every thumbs-up/down click, but
   without this override a positive ("up") rating left the "what was off"
   tag picker visibly on screen instead of hiding it. */
.review-feedback__tags[hidden] {
  display: none;
}
.review-feedback__label {
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-k-60, var(--slug));
}
.review-feedback [data-feedback], .review-feedback [data-tag] {
  min-height: 44px; min-width: 44px;
}
.review-feedback [data-tag][data-picked] {
  background: var(--growth); color: var(--on-growth); border-color: var(--growth);
}

/* ----------------------------- toasts + skeletons + palette ------------- */

/* Toast rack — reject slips + growth slips. Sheet-like paper: hairline
   border, contact + soft-cast shadow (short, not floating), plate patch on
   the head to name the state. Retires the 4px colored side-tab (the "most
   recognisable tell of AI-generated UIs" per the detector). */
.toast-rack {
  position: fixed;
  top: 16px;
  right: 16px;
  /* QA-T12 fix: was 200, under the open nav drawer (8110) and its backdrop
     (8100), so window.toast() errors hid behind them. Same tier as
     .toast-stack: above the drawer, below every modal (9000+). */
  z-index: 8200;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
  max-width: min(400px, calc(100vw - 32px));
}
.toast {
  pointer-events: auto;
  background: var(--sheet);
  border: 1px solid var(--rule);
  padding: 10px 12px 12px;
  /* Contact + short cast — paper on desk, not floating card */
  box-shadow: 0 1px 0 var(--rule), 0 6px 12px rgba(0, 0, 0, 0.08);
  font-size: 0.9rem;
  color: var(--ink-k);
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: start;
}
.toast::before {
  content: "Note";
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 700;
  padding: 3px 6px;
  color: var(--ink-k);
  background: var(--sheet);
  border: 1px solid var(--ink-k-40, var(--rule));
  align-self: start;
}
/* U2: the press-room words ("REJECT", "OK") read as a system log; a person reads "Error" and "Done". */
.toast--success::before { content: "Done"; color: var(--on-growth); background: var(--growth); border-color: var(--growth); }
.toast--error::before   { content: "Error"; color: var(--on-alert); background: var(--ink-reject); border-color: var(--ink-reject); }
.toast--info::before    { content: "Note"; }
.toast__body { flex: 1; min-width: 0; word-wrap: break-word; line-height: 1.4; }
.toast__close {
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  opacity: 0.55;
  min-width: 44px;
  min-height: 44px;
  font-size: 1.2rem;
  line-height: 1;
  align-self: center;
}
.toast__close:hover { opacity: 1; }

/* Skeleton — hairline strips. Static by default; no shimmer. The layout is
   the message ("data is loading here"); the shape doesn't need to shine to
   say it. Motion-friendly to prefers-reduced-motion by construction. */
.skeleton {
  background: var(--rule-soft, var(--rule));
  color: transparent;
}
.skel-line  { height: 10px; margin: 6px 0; border-top: 1px dashed var(--rule); background: transparent; }
.skel-line--wide  { width: 100%; }
.skel-line--mid   { width: 70%; }
.skel-line--short { width: 40%; }
.skel-block { height: 80px; margin: 8px 0; border: 1px dashed var(--rule); background: transparent; }

/* DUPLICATE-PALETTE-001 (2026-08-04): a second, redundant command-palette
   system (.palette*) was built this session without checking one already
   existed at #cmdk. Both bound to Cmd+K, racing each other; only #cmdk was
   ever reachable from the visible search button. All .palette* CSS removed
   — see #cmdk rules (~line 2755) for the real, now-hardened implementation,
   and .cmdk__close below for the visible close button this incident showed
   #cmdk itself was missing. */

/* ----------------------------- identity badge (top bar) ------------------ */
/* Persistent slug-style badge in the top bar. Answers the question "am I
   admin? which tenant am I viewing as?" — mono uppercase, plate patch,
   growth-green in admin state, reject-band while impersonating.
   Clicking opens a small menu that lets admin flip tenants without
   navigating to the Admin Dashboard first. */
/* 2026-08-12: Keeno's own live feedback — the account control lived
   between "Action Required" and Search, read as a status pill ("ADMIN"),
   not as an account menu, and didn't match where every market-standard
   SaaS product puts this (the true top-right corner, as a person avatar).
   `order` pushes it past search/theme-toggle/backend-readout without
   reordering the DOM (identity-menu's own JS doesn't assume a DOM
   position, only ids, so this is safe) - margin-left:auto moved to
   .slug__search below, since that's now visually first-of-the-right-group. */
.slug__identity {
  position: relative;
  order: 20;
  margin-right: 4px;
}
.slug__identity-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 4px 12px 4px 4px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--sheet);
  color: var(--ink-k);
  font-family: var(--ui);
  cursor: pointer;
}
.slug__identity-btn:hover, .slug__identity-btn:focus-visible {
  border-color: var(--growth);
  /* QA-T4 (J-focus): `outline: none` used to sit here, so keyboard focus on
     the account button was only a 1px border hue change. The global
     :focus-visible ring now applies like every other control. */
}
/* The avatar carries the same role/impersonating color cue the old
   tiny square dot did, just on something that actually reads as "this
   is your account" at a glance - initial letter set by renderIdentityBadge(). */
.slug__identity-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--growth);
  color: var(--stock-warm);
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 700;
  flex-shrink: 0;
}
.slug__identity[data-role="impersonating"] .slug__identity-avatar {
  background: var(--ink-reject);
}
.slug__identity[data-role="impersonating"] .slug__identity-btn {
  border-color: var(--ink-reject);
  color: var(--ink-k);
}
.slug__identity-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.2;
}
.slug__identity-name {
  color: var(--ink-k);
  font-size: 12.5px;
  font-weight: 600;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.slug__identity-name:empty { display: none; }
/* U2: still capitals (the role words are written that way in app.js: ADMIN / OPERATOR), now in the UI face. */
.slug__identity-role {
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--growth);
}
.slug__identity[data-role="impersonating"] .slug__identity-role {
  color: var(--ink-reject);
}
/* ID selector, not another class: .slug__identity-menu-head (defined
   below) sets text-transform: uppercase at equal class specificity, and
   being later in the file it was winning regardless of declaration order
   here — confirmed live, the email was rendering all-caps. */
#identity-menu-whoami {
  color: var(--ink-k);
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.78rem;
}
.slug__identity-tenant {
  padding-left: 6px;
  border-left: 1px solid var(--rule);
  margin-left: 2px;
}
.slug__identity-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 260px;
  /* T1 review (2026-10-16): capped so an anchored-right menu can never
     run off the left edge of a 481-1023px header (it reached x=-9). */
  max-width: min(360px, calc(100vw - 24px));
  z-index: 50;
  /* 2026-08-12: this was var(--sheet), a token that was never actually
     defined anywhere in this file — confirmed by grepping the whole
     stylesheet for "--sheet:". Silently fell back to fully transparent
     forever; only readable-by-accident on desktop against whatever
     happened to be behind it. Found live on a real mobile viewport where
     the page content behind it made the menu genuinely illegible. */
  background: var(--stock);
  border: 1px solid var(--rule);
  /* Same contact+short-cast paper-on-desk logic as palette + modal */
  box-shadow: 0 1px 0 var(--rule), 0 12px 24px rgba(0, 0, 0, 0.14);
  padding: 6px 0;
}
/* 2026-08-12: `right: 0` anchors to .slug__identity's OWN box, not the
   viewport edge — on a wrapped mobile header the badge doesn't sit at the
   screen's right edge, so the menu (min-width: 260px) rendered mostly or
   fully off-screen to the left. Confirmed live: getBoundingClientRect()
   showed the menu starting at x=-92px on a 375px-wide viewport. Anchoring
   to the viewport itself below this width fixes it regardless of where
   the badge happens to sit in the wrapped layout. */
@media (max-width: 480px) {
  .slug__identity-menu {
    position: fixed;
    left: 12px;
    right: 12px;
    min-width: 0;
    width: auto;
  }
}
.slug__identity-menu-head {
  margin: 0;
  padding: 6px 14px;
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-k-60, var(--slug));
  border-bottom: 1px solid var(--rule);
}
.slug__identity-menu-item {
  display: block;
  width: 100%;
  padding: 10px 14px;
  min-height: 44px;
  border: 0;
  background: transparent;
  color: var(--ink-k);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  font: inherit;
  font-size: 0.88rem;
}
.slug__identity-menu-item:hover, .slug__identity-menu-item:focus-visible {
  background: var(--rule-soft, var(--rule));
}
/* QA-T4: keep the focus ring (was outline:none), drawn inside the menu row. */
.slug__identity-menu-item:focus-visible { outline-offset: -3px; }
/* QA round 1 (2026-10-16, T1 review): a long single-word city (the QA seed
   has a 40-letter one) was an unbreakable flex item, so the tenant name
   beside it was squeezed to 13-59px and wrapped one letter per line: the
   menu grew to 2,259px in the phone drawer and to 542px wide (9px off the
   left edge, Sign out unreachable) in the 700px header. The row now wraps
   (city drops under the name when both don't fit), both spans may break
   inside a word, and the name keeps a 10rem basis so it can't be crushed. */
.slug__identity-menu-item[data-tenant] { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 10px; row-gap: 2px; }
.slug__identity-menu-item[data-tenant] .slug__identity-menu-tenant-name {
  flex: 1 1 10rem;
  min-width: 0;
  overflow-wrap: anywhere;
  /* Two lines at most; the full name + city is in the row's title. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.slug__identity-menu-item[data-tenant] .hint {
  min-width: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The tenant list scrolls inside its own box so the menu stays a bounded
   size however many tenants exist and My Profile / Admin / Sign out below
   it are always on screen; the whole menu is also capped to the viewport
   (it hangs off the sticky header, so a taller menu could never be
   scrolled into view). */
#identity-menu-items { display: block; max-height: 148px; overflow-y: auto; overscroll-behavior: contain; }
.slug__identity-menu { max-height: calc(100vh - 80px); max-height: calc(100dvh - 80px); overflow-y: auto; }
.slug__identity-menu-item[data-tenant][data-active] {
  background: var(--growth-bg, var(--action-light));
  color: var(--growth);
  font-weight: 700;
}
#identity-stop-impersonate {
  color: var(--ink-reject);
  font-weight: 700;
  border-top: 1px solid var(--rule);
}

/* ----------------------------- admin panel ------------------------------- */
/* Admin surfaces adopt the imposition-sheet grammar: crop-mark cells for the
   totals bar, slug head strips for section titles, plate patches for state,
   hairline dividers for lists. No shadowed cards. */

.admin-totals {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--crop-gap, 5px);
  margin-bottom: 24px;
}
.admin-total {
  background: var(--sheet);
  border: 1px solid var(--rule);
  padding: 14px 14px 12px;
  text-align: left;
  /* Same crop-mark corners as .capability and .imposed */
  background-image:
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule));
  background-repeat: no-repeat;
  background-size: var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop),
    var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop);
  background-position:
    left top, left top, right top, right top,
    left bottom, left bottom, right bottom, right bottom;
}
.admin-total__value {
  display: block;
  font-size: 1.65rem;
  font-weight: 700;
  color: var(--growth);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.admin-total__label {
  /* W4C (defect 15): sentence case in the interface face, readable at a glance. */
  display: block;
  font-size: 0.8rem;
  color: var(--ink-k-60, var(--slug));
  margin-top: 6px;
}

.admin-grid { display: flex; flex-direction: column; gap: 20px; }

.admin-card {
  background: var(--sheet);
  border: 1px solid var(--rule);
  padding: 0 0 12px;
}
.admin-card__head {
  padding: 12px 16px 10px;
  border-bottom: 1px solid var(--rule);
  background: var(--desk, var(--sheet));
}
.admin-card__head h2 {
  /* W4C (defect 15): a section title in sentence case, not tracked mono capitals. */
  margin: 0 0 4px;
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink-k);
}
.admin-card__head .hint {
  margin: 0;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.admin-card > *:not(.admin-card__head) { padding-left: 16px; padding-right: 16px; }
/* W2 (QA R5-N2): a sideways-scrolling table box keeps the 16px inset as
   MARGIN, not padding. Inside its own padding a scroller lets the scrolled
   cells show in that 16px strip, and a sticky right:0 column stops at the
   padding edge, so the pinned "View as" sat 16px short of the edge with
   fragments of hidden columns beside it. */
.admin-card > .table-scroll { padding-left: 0; padding-right: 0; margin-left: 16px; margin-right: 16px; }

/* Signal list: table-row-like with hairline dividers + tabular numbers */
.admin-signals { padding: 12px 16px 0; }
.admin-signals__list {
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: 1px solid var(--rule);
}
.admin-signals__list li {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 8px 4px;
  border-bottom: 1px solid var(--rule);
  font-size: 0.9rem;
}
.admin-signals__type {
  font-family: var(--ui);
  font-size: 13px;
  color: var(--ink-k);
  overflow: hidden;
  text-overflow: ellipsis;
}
.admin-signals__count {
  font-family: var(--ui);
  font-weight: 700;
  color: var(--growth);
  font-variant-numeric: tabular-nums;
  font-size: 0.9rem;
}

/* Impersonation banner: the reject-band pattern at the bottom-right, mono
   uppercase state. Named "IMPERSONATING · X" so state is never color-only. */
.admin-impersonate {
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 40;
  background: var(--sheet);
  border: 1px solid var(--ink-reject);
  border-top: 3px solid var(--ink-reject);
  padding: 8px 14px;
  display: flex;
  gap: 12px;
  align-items: center;
  /* Contact + short cast, not a heavy 20px halo */
  box-shadow: 0 1px 0 var(--rule), 0 6px 12px rgba(196, 60, 46, 0.15);
  /* U2: UI face, sentence case. The state stays named in words and the red band, so it is still never colour-only. */
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-k);
  min-height: 44px;
}
.admin-impersonate .btn { min-height: 36px; }
/* Same [hidden]-vs-author-display bug this codebase keeps hitting (see
   .onboard-modal, .login-card .setup__field elsewhere in this file): the
   class's own `display: flex` outranks the browser's built-in [hidden]
   rule regardless of specificity, because author styles always beat the
   user-agent sheet. Found live 2026-08-12 testing the NEW operator-
   impersonation banner (public/index.html's #operator-impersonate-banner,
   a sibling of every view, not nested inside one) - it rendered with an
   empty label at top-left despite hidden=true. The ORIGINAL tenant-
   impersonation banner (#admin-impersonate-banner) has this exact same
   bug, just masked until now: it's nested inside #view-admin, so its
   parent's own correctly-working hidden state blocked it from ever being
   visible on any OTHER view - but while genuinely on the Admin Dashboard
   and NOT impersonating, it would show empty too. One rule fixes both. */
.admin-impersonate[hidden] { display: none; }

/* Operator impersonation (2026-08-12): same reject-band visual language,
   but positioned top-center and colored with --action (orange) instead of
   --ink-reject (red) - deliberately different placement AND color from the
   tenant-impersonation banner so the two are never confusable at a glance,
   even though the server-side nesting rule means only one can ever be
   active at once. Top-center rather than bottom-right: assuming another
   admin's full identity is higher-stakes than a narrowed client view, and
   this needs to be the kind of thing you can't scroll past or ignore. */
.admin-impersonate--operator {
  position: fixed;
  top: 0;
  left: 50%;
  bottom: auto;
  right: auto;
  transform: translateX(-50%);
  border-color: var(--action);
  border-top-color: var(--action);
  border-top-width: 0;
  border-bottom: 3px solid var(--action);
  z-index: 60;
}

@media (max-width: 900px) { .admin-totals { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .admin-totals { grid-template-columns: 1fr; } }

/* ----------------------------- onboarding modal ------------------------- */
/* Modal as an envelope: hairline frame, contact shadow only. Slug-head strip
   for the title so it belongs to the same world as the sheets it opens over. */
/* Same [hidden]-vs-display:flex bug as .palette-backdrop above — fixed
   the same way. */
.onboard-modal[hidden] { display: none; }
.onboard-modal {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 6vh 16px 16px;
  background: rgba(24, 20, 16, 0.42);
  overflow-y: auto;
}
.onboard-modal__panel {
  width: 100%;
  max-width: 560px;
  background: var(--sheet);
  color: var(--ink-k);
  padding: 0 22px 22px;
  /* Same contact + short cast as toast + palette — paper on desk */
  box-shadow: 0 1px 0 var(--rule), 0 12px 24px rgba(0, 0, 0, 0.14);
  border: 1px solid var(--rule);
}
.onboard-modal__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0 -22px 14px;
  padding: 12px 22px 10px;
  border-bottom: 1px solid var(--rule);
  background: var(--desk, var(--sheet));
}
.onboard-modal__head h3 {
  margin: 0;
  font-family: var(--ui);
  font-size: 16px;
  font-weight: 700;
  color: var(--ink-k);
}
.onboard-modal__head .btn { min-height: 36px; min-width: 36px; }
.onboard-modal label { display: block; }
.onboard-modal label span {
  display: block;
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-k-60, var(--slug));
  margin-bottom: 4px;
}
.onboard-modal input,
.onboard-modal textarea,
.onboard-modal select {
  width: 100%;
  padding: 10px;
  min-height: 40px;
  font-size: 0.9rem;
  border: 1px solid var(--rule);
  background: transparent;
  color: inherit;
}
.onboard-modal input:focus,
.onboard-modal textarea:focus,
.onboard-modal select:focus {
  outline: 2px solid var(--growth);
  outline-offset: 0;
  border-color: var(--growth);
}

/* Playbook alert rows in Master Library — plate patch + hairline top border
   between rows. No side-tab, no colored side border. */
.playbook-alert {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px;
  padding: 10px 4px;
  border-top: 1px solid var(--rule);
  align-items: start;
}
.playbook-alert:first-child { border-top: 0; }
.playbook-alert__patch {
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 3px 6px;
  border: 1px solid var(--ink-k-40);
  color: var(--ink-k);
  background: var(--sheet);
  align-self: start;
}
.playbook-alert__patch--high { color: var(--on-alert); background: var(--ink-reject); border-color: var(--ink-reject); }
/* QA-T4: --ink-k flips light in dark mode but --ink-y does not (1.89:1); dark ink in both themes (6.35:1). */
.playbook-alert__patch--med  { color: #2c2520; background: var(--ink-y); border-color: var(--ink-y); }
.playbook-alert__body { min-width: 0; }
.playbook-alert__title { margin: 0 0 4px; font-size: 0.9rem; color: var(--ink-k); }
.playbook-alert__title code {
  font-family: var(--mono);
  font-size: 0.85rem;
  background: var(--rule-soft, var(--rule));
  padding: 1px 5px;
}

/* ----------------------------- prefers-reduced-motion -------------------- */
/* Extend the existing block (~line 2438) — new animations added this session
   (skel-shimmer retired; palette + toast entrance kept motionless; capability
   tile no longer translates). This block is defense-in-depth against future
   drift. */
@media (prefers-reduced-motion: reduce) {
  .capability { transition: none !important; }
  .capability:hover, .capability:focus-visible { transform: none !important; }
  .toast, .cmdk, .onboard-modal__panel { animation: none !important; }
  .skeleton { animation: none !important; }
}

/* ----------------------------- billing ---------------------------------- */

.plan-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-bottom: 24px;
}
.plan-card {
  background: var(--sheet);
  border: 2px solid var(--rule);
  border-radius: 10px;
  padding: 24px;
  text-align: center;
}
.plan-card--featured { border-color: var(--growth); }
/* W4C: the classes the plan cards actually carry (the BEM rules below match nothing in index.html). */
.plan-card h3 { margin: 0 0 4px; font-size: 1.05rem; }
.plan-price { font-size: 1.6rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.plan-price span { font-size: 0.95rem; font-weight: 400; color: var(--text-secondary); }
.plan-features { list-style: none; margin: 12px 0 16px; padding: 0; font-size: 14px; color: var(--text-secondary); line-height: 1.5; }
.plan-features li { padding: 2px 0; }
.plan-card__name { font-size: 1.05rem; font-weight: 700; margin-bottom: 8px; } /* U2/U8: plan names in their own case, not capitals */
.plan-card__price { font-size: 2.5rem; font-weight: 800; color: var(--growth); }
.plan-card__price span { font-size: 1rem; font-weight: 400; color: var(--slug); }
.plan-card__features { list-style: none; padding: 0; margin: 16px 0; text-align: left; font-size: 0.875rem; }
.plan-card__features li { padding: 4px 0; border-bottom: 1px solid var(--rule); }
.plan-card__features li::before { content: '✓ '; color: var(--growth); font-weight: 700; }
.plan-card__btn { width: 100%; margin-top: 12px; }

/* W1C: the admin's monthly estimate, the plan's end date and actions, the receipts list. */
.billing-mrr { margin: 0 0 24px; padding: 16px 20px; background: var(--sheet); border: 1px solid var(--rule); border-radius: var(--radius); }
.billing-mrr__h { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 4px; font-size: 1rem; }
.billing-mrr__amount { margin: 0; font-size: 2rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.billing-mrr__line { margin: 2px 0 0; font-size: 14px; }
.billing-ending { margin: .75rem 0 0; font-weight: 600; }
/* W1C skeptic LOW 6 / MED 4: the plan card's edge says its state; the default ('ok') stays plain. */
.billing-plan { padding: 16px 20px; background: var(--sheet); border: 1px solid var(--rule); border-left: 3px solid var(--growth); border-radius: var(--radius); } /* W4C: the calm "Your plan" card */
.billing-plan__h { margin: 0 0 4px; font-size: 0.95rem; font-weight: 600; color: var(--text-secondary); }
.billing-plan__name { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 1.35rem; font-weight: 700; }
.billing-plan__name .pill { font-size: 13px; }
.billing-plan__date { margin: 4px 0 0; font-size: 14px; color: var(--text-secondary); }
#billing-plan-card[data-kind='ending'] { border-left-color: var(--text-secondary); }
#billing-plan-card[data-kind='past-due'] { border-left-color: var(--alert); }
.billing-alert { margin: .75rem 0 0; color: var(--alert); font-weight: 600; }
.billing-message:focus { outline: none; }
.billing-message:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; }
.billing-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: .75rem; }
/* hidden billing buttons: the global [hidden] guard (above) already hides them; no per-button rule is needed */
.billing-actions .btn { min-height: 44px; }
.billing-message:empty { display: none; }
.billing-message { margin: .75rem 0 0; }
/* LOOK pass: the plan block and the receipts sat flush against what follows them at 1280 and 390. */
#billing-current { margin-bottom: 24px; }
.billing-receipts-wrap { margin: 24px 0; }
/* LOOK pass: the default browser blue read as foreign; the app's links are --action. A row's own link, not inline
   text, so it keeps a 44px tap height (tests/phone-layout-floor.test.js exempts only links inside sentences). */
.billing-receipt a { display: inline-flex; align-items: center; min-height: 44px; color: var(--action); text-decoration: underline; text-underline-offset: 2px; }
.billing-receipt a:hover { color: var(--action-hover); }
.billing-receipts-wrap__h { margin: 0 0 .5rem; font-size: 1.1rem; }
.billing-receipts { list-style: none; margin: 0 0 .5rem; padding: 0; }
.billing-receipt { display: grid; grid-template-columns: minmax(9rem, 1fr) auto auto auto; gap: 4px 16px; align-items: center; padding: 2px 0; border-bottom: 1px solid var(--rule); font-size: 14px; }
.billing-receipt__amount { font-variant-numeric: tabular-nums; text-align: right; }
.billing-receipt[data-status="failed"] .billing-receipt__status { color: var(--alert); font-weight: 600; }
.billing-receipt__none { color: var(--text-secondary); }
@media (max-width: 560px) {
  .billing-receipt { grid-template-columns: 1fr auto; }
  .billing-receipt__status, .billing-receipt a, .billing-receipt__none { grid-column: span 1; }
}

@media (max-width: 768px) {
  .plan-grid { grid-template-columns: 1fr; }
}

/* ----------------------------- client portal ----------------------------- */

.cp-header {
  margin-bottom: 20px;
  padding-bottom: 16px;
  border-bottom: 2px solid var(--rule);
}
.cp-header h2 { margin-bottom: 4px; }
.cp-header p { color: var(--slug); font-size: 0.875rem; }
.cp-section { margin-bottom: 20px; }
.cp-section h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 8px; color: var(--slug); }
.cp-section ul { list-style: none; padding: 0; }
.cp-section li { padding: 6px 0; border-bottom: 1px solid var(--rule); font-size: 0.875rem; }

/* ----------------------------- rank chart -------------------------------- */

#rank-canvas {
  width: 100%;
  height: 200px;
  margin-bottom: 16px;
  border: 1px solid var(--rule);
  border-radius: 6px;
}

/* ----------------------------- onboarding wizard -------------------------- */

.login-overlay {
  position: fixed;
  inset: 0;
  z-index: 9900;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--desk);
}
.login-overlay[hidden] { display: none; }
.login-card {
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 80px rgba(0,0,0,0.18);
  max-width: 380px;
  width: 90vw;
  padding: 2.25rem 2rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.login-card h1 { font-size: 1.4rem; margin: 0; }
.login-card .lede { margin: 0 0 0.25rem; color: var(--text-secondary); }
.login-card__error {
  color: var(--ink-reject);
  font-size: 0.88rem;
  margin: 0;
}
.login-card__error[hidden] { display: none; }
/* 2026-08-12: needs to actually be noticed before anyone types anything -
   same "a real background/border block reads as important, plain text
   gets glanced past" lesson from the settings-save error-visibility fix
   earlier this same session (.setup-card__result). This is the opposite
   of an error, but the same principle: informational text that matters
   shouldn't look like a throwaway caption. */
.login-card__hint {
  /* W4C (Monday plan defect 12): one short plain line under the title. The tinted block with a coloured edge read
     as a warning on the first screen anyone sees; the sentence alone does the job. */
  margin: 0 0 0.25rem;
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.4;
}

/* 2026-08-12: Operator Portal migration-status banner. Same "real
   background/border block, not a throwaway caption" treatment as
   .login-card__hint above, but alert-toned since this one names an
   action the admin actually needs to take (or confirms none is needed). */
.notice-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0 0 0.85rem;
  padding: 0.65rem 0.85rem;
  border-radius: var(--radius);
  background: var(--alert-light);
  border-left: 3px solid var(--alert);
  color: var(--text-primary);
  font-size: 0.85rem;
  line-height: 1.4;
}
.notice-banner[hidden] { display: none; }
/* Same [hidden]-vs-author-display bug this codebase keeps hitting (see
   .onboard-modal above): a bare `label { display: block }` author rule
   outranks the browser's built-in [hidden] regardless of specificity,
   because author styles always beat the user-agent sheet. Without this the
   "Confirm password" field is visible on the normal sign-in screen. Found
   by actually rendering it, not by reading the markup - it looks correct in
   the HTML. Deliberately scoped rather than a global [hidden] override:
   .onboard-wizard__nav > [hidden] relies on hidden elements STILL taking up
   layout space, so a blanket display:none would silently break that nav.
   2026-10-16: a global guard now exists near the top of this file and
   excludes exactly that nav; this rule is a redundant backstop. */
.login-card .setup__field[hidden] { display: none; }
.login-card #login-submit { width: 100%; }
/* A: the "Remember this device" tick, under the password. */
.login-card__remember { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: var(--text-secondary); cursor: pointer; }
.login-card__remember[hidden] { display: none; }
.login-card__remember input { width: auto; margin: 0; }
/* A: Profile > Remembered devices. */
.device-list { list-style: none; margin: 0 0 0.75rem; padding: 0; }
.device-list__item { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
.device-list__item:last-child { border-bottom: 0; }
.login-card__legal {
  margin: 0;
  text-align: center;
  font-size: 0.78rem;
  color: var(--text-secondary);
}
/* Q7: "Forgot password?", Terms and Privacy were 15 px tall: a business owner signs in on a phone. */
.login-card__legal a { color: inherit; text-decoration: underline; display: inline-flex; align-items: center; min-height: 44px; padding: 0 6px; }
.login-card__legal a:hover { color: var(--text-primary); }
.login-card .setup__field--checkbox {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 0.6rem;
  font-size: 0.88rem;
}
.login-card .setup__field--checkbox input[type="checkbox"] {
  margin-top: 0.2rem;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}
.login-card .setup__field--checkbox a { color: inherit; text-decoration: underline; }

/* 2026-08-13, Jason's WordPress-preview request. */
.preview-trigger {
  margin-bottom: 1rem;
}
.blog-preview-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(4px);
  padding: 2rem 1rem;
}
/* Same recurring bug this codebase keeps hitting: an author display
   rule on the same element outranks the UA [hidden] stylesheet rule
   regardless of specificity — explicit override, not an assumption. */
.blog-preview-overlay[hidden] { display: none; }
.blog-preview {
  background: var(--stock);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 80px rgba(0,0,0,0.25);
  max-width: 760px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  padding: 1.5rem 2rem 2rem;
}
.blog-preview__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.75rem;
  margin-bottom: 0.5rem;
}
.blog-preview__head h2 {
  margin: 0;
  font-size: 1.1rem;
}
.blog-preview__note {
  margin: 0.5rem 0 1rem;
}
.blog-preview__tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}
.blog-preview__tabs button[data-preview-tab].is-active {
  background: var(--action);
  color: var(--stock-warm);
  border-color: var(--action);
}
/* The actual point of this feature: real blog typography, not app UI
   chrome — generous measure and line-height so grammar/spelling/format
   issues read exactly the way they would on the client's real site. */
.blog-preview__article {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 1.05rem;
  line-height: 1.75;
  color: var(--ink-k);
  max-width: 640px;
  margin: 0 auto;
}
.blog-preview__article h1 {
  font-family: var(--ui);
  font-size: 1.8rem;
  line-height: 1.25;
  margin: 0 0 1.25rem;
}
.blog-preview__article h2 {
  font-family: var(--ui);
  font-size: 1.35rem;
  margin: 2rem 0 0.75rem;
}
.blog-preview__article h3 {
  font-family: var(--ui);
  font-size: 1.1rem;
  margin: 1.5rem 0 0.5rem;
}
.blog-preview__article p {
  margin: 0 0 1.25rem;
}
.blog-preview__article ul,
.blog-preview__article ol {
  margin: 0 0 1.25rem;
  padding-left: 1.5rem;
}
.blog-preview__article li {
  margin-bottom: 0.5rem;
}
.blog-preview__article a {
  color: var(--action);
}
.blog-preview__point {
  padding-bottom: 1.5rem;
  margin-bottom: 1.5rem;
  border-bottom: 1px dashed var(--rule);
}
.blog-preview__point:last-child {
  border-bottom: none;
}
/* Deliberately NOT serif/article-styled — this is app chrome pointing
   at the social post, not part of the article itself, and needs to
   read as clearly separate at a glance. */
.blog-preview__social {
  font-family: var(--ui);
  background: var(--action-light);
  border-left: 3px solid var(--action);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin-top: 0.5rem;
}
.blog-preview__social-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--action);
  margin-bottom: 0.35rem;
}
.blog-preview__social p {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.5;
  white-space: pre-wrap;
}
/* A flagged pairing (missing or over-length social post) needs to draw
   the eye first — that's the whole point of the flag, so it borrows
   the same reject-red the rest of the app already uses for "this
   needs your attention," not a new color. */
.blog-preview__social--flag {
  background: var(--stock-amber);
  border-left-color: var(--ink-reject);
}
.blog-preview__social--flag .blog-preview__social-label {
  color: var(--ink-reject);
}
.blog-preview__social-flag-note {
  margin: 0.35rem 0 0 !important;
  font-size: 0.8rem !important;
  color: var(--ink-reject);
}
/* Fake address bar — makes "as if it was posted... on a client's
   blog" (Jason's own words) concrete without fetching the client's
   real WordPress theme. */
.blog-preview__site-line {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 0.8rem;
  color: var(--ink-m);
  background: var(--stock-warm-2, var(--stock-warm));
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.4rem 0.75rem;
  margin: 0 0 1.5rem;
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
}
.blog-preview__foot {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
}

.onboard-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(4px);
  animation: fadeIn 300ms ease;
}
.onboard-overlay[hidden] { display: none; }
.onboard-wizard {
  background: var(--stock);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 80px rgba(0,0,0,0.25);
  max-width: 560px;
  width: 92vw;
  max-height: 85vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.onboard-wizard__steps {
  padding: 2rem 2rem 1rem;
  flex: 1;
}
.onboard-step__icon {
  font-size: 48px;
  margin-bottom: 0.5rem;
}
.onboard-step h2 {
  margin: 0 0 0.75rem;
  font-size: 1.5rem;
  color: var(--text);
}
.onboard-step h3 {
  margin: 1rem 0 0.5rem;
  font-size: 0.95rem;
  color: var(--text-muted);
}
.onboard-step p {
  margin: 0 0 0.75rem;
  line-height: 1.65;
  color: var(--text-secondary);
  font-size: 0.95rem;
}
.onboard-step ol {
  padding-left: 1.25rem;
  margin: 0 0 1rem;
  line-height: 1.8;
  color: var(--text-secondary);
  font-size: 0.95rem;
}
.onboard-step ol li { padding: 2px 0; }
.onboard-step__tip {
  background: var(--action-light);
  border-left: 3px solid var(--action);
  padding: 0.75rem 1rem;
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: 0.875rem;
}
.onboard-step__cards {
  display: grid;
  gap: 0.5rem;
  margin-top: 1rem;
}
.onboard-mini {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.625rem 0.875rem;
  border-radius: var(--radius);
  background: var(--desk);
  font-size: 0.875rem;
  color: var(--text-secondary);
}
.onboard-mini span { font-size: 1.1rem; flex-shrink: 0; }

/* Visual-appeal gauntlet (2026-08-30): a real, visible way out of the
   wizard besides clicking the dark backdrop (which nothing on screen
   hints exists). Small and unobtrusive on purpose — it's an escape hatch,
   not something to compete with "Let's go" for attention. */
.onboard-wizard__skip {
  display: block;
  margin: 0 auto;
  padding: 0.4rem 0;
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 0.8rem;
  text-decoration: underline;
  cursor: pointer;
}
.onboard-wizard__skip:hover { color: var(--text-secondary); }

.onboard-wizard__nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem 1.5rem;
  gap: 1rem;
}
/* The one intentional exception to the global `[hidden]` display:none
   guard near the top of this file (that rule excludes
   `.onboard-wizard__nav > *`): a hidden nav child (the step-1 Back button)
   keeps its layout box, invisible, so the dots and Next stay put. */
.onboard-wizard__nav > [hidden] {
  visibility: hidden;
}
.onboard-wizard__dots {
  display: flex;
  gap: 6px;
  justify-content: center;
  flex: 1;
}
.onboard-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--rule);
  transition: background 200ms ease, transform 200ms ease;
}
.onboard-dot.is-active {
  background: var(--action);
  transform: scale(1.3);
}

/* ----------------------------- setup step badges -------------------------- */

.setup-card[data-step]::before {
  content: "Step " attr(data-step);
  position: absolute;
  top: -10px;
  left: 16px;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 700;
  z-index: 1;
}
.setup-card[data-step] {
  position: relative;
  margin-top: 1.25rem;
}
.setup-card[data-priority="essential"]::before {
  background: var(--growth);
  color: var(--on-growth);
}
.setup-card[data-priority="recommended"]::before {
  background: var(--trust);
  color: var(--on-trust);
}
.setup-card[data-priority="optional"]::before {
  /* Bug hunt (2026-09-10): same root cause as .cookie-banner above —
     var(--text-muted) is theme-dependent (dark grey in light mode, light
     grey in dark mode), this badge's white text is not; dark mode reduces
     this to poor, near-failing contrast instead of full white-on-white,
     but it's the same underlying bug. */
  background: #5a5550;
  color: #fff;
}

.setup-card__next {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule-soft);
}
.setup-card__next button {
  margin-left: auto;
}

/* guided-mode highlight */
.setup-card.is-guided {
  box-shadow: 0 0 0 3px var(--action), var(--shadow-lg);
  border-color: var(--action);
}

/* "What's new" gauntlet (2026-08-13): same ring-highlight idea as
   .setup-card.is-guided above, generalized to any element (a button, an
   h1) rather than just a setup card - the tour spotlights real elements
   on real screens, not a fixed shape. */
.tour-highlight {
  outline: 3px solid var(--action);
  outline-offset: 4px;
  border-radius: var(--radius);
  animation: tour-pulse 1.6s ease-in-out infinite;
}
@keyframes tour-pulse {
  0%, 100% { outline-color: var(--action); }
  50% { outline-color: color-mix(in srgb, var(--action) 40%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .tour-highlight { animation: none; }
}

/* start button in progress bar area */
.setup-start-btn {
  margin-top: 0.75rem;
}

@media (max-width: 600px) {
  .onboard-wizard__steps { padding: 1.5rem 1.25rem 0.75rem; }
  .onboard-wizard__nav { padding: 0.75rem 1.25rem 1.25rem; }
  .onboard-step h2 { font-size: 1.25rem; }
  .onboard-step__icon { font-size: 36px; }
}

/* ───────────────── sidebar info tooltips ───────────────── */

/* Visual-appeal gauntlet (2026-08-30): 24 of these ran down the sidebar at
   full, always-visible weight, in a monospace font that (like the rest of
   this pass) read more like a code comment marker than a UI hint. Dimmed
   at rest, full opacity on hover of the item row (still discoverable, no
   longer shouting on every single line) and switched to the app's normal
   UI font. */
.sidebar__info {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  margin-left: auto;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--sidebar-rule);
  background: none;
  color: var(--text-muted);
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  cursor: help;
  opacity: 0.45;
  transition: border-color 200ms ease, color 200ms ease, background 200ms ease, opacity 200ms ease;
  position: relative;
}
.sidebar__item:hover .sidebar__info,
.sidebar__info:hover,
.sidebar__info:focus-visible {
  opacity: 1;
}
.sidebar__info:hover,
.sidebar__info:focus-visible {
  border-color: var(--action);
  color: var(--action);
  background: var(--action-light);
}
.sidebar__info-pop {
  display: none;
  position: fixed;
  width: 240px;
  padding: 0.65rem 0.85rem;
  background: var(--stock);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  color: var(--text);
  font: 400 13px/1.45 var(--sans);
  text-align: left;
  z-index: 9999;
  pointer-events: none;
}
.sidebar__info-pop.is-visible {
  display: block;
}

/* R4-20 follow-up (QA round 6): on a phone the preamble before the first control (lede, "Why this
   matters", "How this screen works", Review's AI note) is one short line each. The blocks start
   folded there (public/preamble-memory.js), and the lede is left out after the first visit to that
   screen (app.js initPreambleMemory adds .sheet--lede-seen). A folded block keeps a 44px tap row.
   !important only to beat the watermark note's inline padding/margin (index.html). */
@media (max-width: 600px) {
  .sheet--lede-seen > .sheet__head > .lede { display: none; }
  .help-section:not([open]) > summary { padding-top: 0.6rem; padding-bottom: 0.6rem; }
  .help-section:not([open]) { margin-bottom: 0.6rem; }
  .sheet__head { padding-bottom: 0.75rem; margin-bottom: 1rem; }
}

/* ───────────────── spotlight walkthroughs ───────────────── */

.spotlight-overlay {
  position: fixed;
  inset: 0;
  z-index: 9990;
  background: rgba(0, 0, 0, 0.55);
  mask-image: radial-gradient(
    ellipse var(--spot-w, 200px) var(--spot-h, 60px)
    at var(--spot-x, 50%) var(--spot-y, 50%),
    transparent 48%, black 52%
  );
  -webkit-mask-image: radial-gradient(
    ellipse var(--spot-w, 200px) var(--spot-h, 60px)
    at var(--spot-x, 50%) var(--spot-y, 50%),
    transparent 48%, black 52%
  );
  transition: --spot-x 400ms ease, --spot-y 400ms ease, --spot-w 400ms ease, --spot-h 400ms ease;
}

.spotlight-tip {
  position: fixed;
  z-index: 9991;
  width: 300px;
  max-width: calc(100vw - 32px);
  padding: 1rem 1.25rem;
  background: var(--stock);
  border: 1.5px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  color: var(--text);
  font: 400 14px/1.5 var(--sans);
}
.spotlight-tip__counter {
  display: block;
  font-size: 12px;
  font-weight: 700;
  color: var(--action);
  margin-bottom: 0.35rem;
}
.spotlight-tip__msg {
  margin: 0 0 1rem;
  font-size: 14px;
  line-height: 1.5;
}
.spotlight-tip__nav {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
}

/* Assistant execution engine (2026-09-01): the proposal a `fill` step shows
   inside .spotlight-tip — a live current-vs-proposed comparison, an
   explicit Apply the human has to click, and (2026-09-01 product audit) an
   Undo that appears once Applied, plus a visible failure state when a
   select's proposed value matches no real <option>. Never auto-applied; see
   assistApplyFill()/assistReadFieldValue() in app.js for the hard rules
   this backs. New scoped classes under .spotlight-tip only, no changes to
   the tour/nav rules above. */
.spotlight-tip__fill {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin: 0 0 1rem;
  padding: 0.5rem 0.65rem;
  background: var(--action-light);
  border-radius: var(--radius);
  font-size: 13px;
}
.spotlight-tip__fill[hidden] { display: none; }
.spotlight-tip__fill-compare {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.spotlight-tip__fill-label {
  font-weight: 600;
  color: var(--text-muted);
  flex: none;
}
.spotlight-tip__fill-value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  color: var(--text);
}
.spotlight-tip__fill-arrow {
  flex: none;
  color: var(--text-muted);
}
/* The "you're about to overwrite something real" signal — a single click
   still applies it, this just makes that unmistakable rather than gating it
   behind an extra confirm step. */
.spotlight-tip__fill-compare--overwrite .spotlight-tip__fill-value--current {
  color: var(--alert);
  font-weight: 700;
}
.spotlight-tip__fill-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.spotlight-tip__fill-apply,
.spotlight-tip__fill-undo {
  flex: none;
}
/* .btn's own `display: inline-flex` (an author rule) beats the UA
   stylesheet's `[hidden] { display: none }` — confirmed live: without this,
   toggling .hidden on either button left it fully visible, just inert.
   Same fix already applied to .spotlight-tip__fill and
   .spotlight-tip__fill-error below/above. */
.spotlight-tip__fill-apply[hidden],
.spotlight-tip__fill-undo[hidden] {
  display: none;
}
.spotlight-tip__fill-error {
  margin: 0;
  color: var(--alert);
  font-size: 12.5px;
  line-height: 1.4;
}
.spotlight-tip__fill-error[hidden] { display: none; }

/* Risk gate (2026-09-01): shown instead of click-to-advance for a step
   classified 'external'/'destructive' (assistant-act.js's
   classifyTargetRisk) — a human must read and click this before the target
   itself can advance the tour. Reuses .btn--reject, this app's own real
   "dangerous action" button style (see delete-client / totp-disable). */
.spotlight-tip__confirm {
  margin: 0 0 1rem;
}
.spotlight-tip__confirm[hidden] { display: none; }
.spotlight-tip__confirm-btn {
  width: 100%;
}

/* ───────────────── progress ring on dashboard ───────────────── */

.progress-ring-card {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin-top: 1rem;
  padding: 1.25rem 1.5rem;
  background: linear-gradient(135deg, var(--growth-light) 0%, var(--trust-light) 100%);
  border: 1.5px solid var(--growth);
  border-radius: var(--radius-lg);
}
.progress-ring-card__ring {
  position: relative;
  flex: none;
  width: 96px;
  height: 96px;
}
.progress-ring-svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.progress-ring__track {
  fill: none;
  stroke: var(--rule);
  stroke-width: 8;
}
.progress-ring__fill {
  fill: none;
  stroke: var(--growth);
  stroke-width: 8;
  stroke-linecap: round;
  transition: stroke-dashoffset 800ms ease;
}
.progress-ring__pct {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 22px;
  font-weight: 800;
  color: var(--growth);
}
.progress-ring-card__info {
  flex: 1;
  min-width: 0;
}
.progress-ring-card__title {
  margin: 0 0 0.25rem;
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}
.progress-ring-card__sub {
  margin: 0 0 0.65rem;
  font-size: 13px;
  color: var(--text-muted);
}
.progress-ring-card__actions {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.progress-ring-card__link {
  background: none;
  border: none;
  padding: 0;
  color: var(--action);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.progress-ring-card__link::before {
  content: '→ ';
}
.progress-ring-card__link:hover {
  text-decoration: underline;
}
.progress-ring-card__more {
  color: var(--text-muted);
  font-weight: 500;
}
.progress-ring-card__more::before {
  content: none;
}
.progress-ring-card__done {
  font-size: 13.5px;
  color: var(--growth);
  font-weight: 600;
}

@media (max-width: 600px) {
  .progress-ring-card {
    flex-direction: column;
    text-align: center;
    padding: 1.25rem 1rem;
  }
  .progress-ring-card__actions {
    align-items: center;
  }
  .progress-ring-card__link {
    text-align: center;
  }
  .spotlight-tip {
    width: calc(100vw - 32px);
    left: 16px !important;
  }
}

/* ──────────── sidebar status dots ──────────── */

.sidebar__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-muted);
  opacity: 0.35;
  flex-shrink: 0;
  margin-left: auto;
  margin-right: 4px;
  transition: background 0.3s, opacity 0.3s;
}
.sidebar__dot.is-active {
  background: var(--growth);
  opacity: 1;
}

/* ──────────── confetti toasts ──────────── */

.confetti-toast {
  position: fixed;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: var(--card);
  border: 1.5px solid var(--growth);
  border-radius: 12px;
  padding: 0.75rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  box-shadow: 0 8px 32px rgba(0,0,0,0.18);
  opacity: 0;
  transition: opacity 0.35s, transform 0.35s;
  /* z-index audit (2026-09-02): matches .toast-stack's own tier (5000) —
     see that rule's comment. Same reasoning: below every real modal.
     QA-T12 fix: raised with it, above the open drawer (8110). */
  z-index: 8200;
  overflow: visible;
  font-weight: 600;
  font-size: 0.95rem;
}
.confetti-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.confetti-toast__icon { font-size: 1.3rem; }
.confetti-toast__msg { color: var(--text); }

.confetti-particle {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 2px;
  top: 50%;
  left: 50%;
  pointer-events: none;
  animation: confetti-burst var(--d) ease-out forwards;
}
@keyframes confetti-burst {
  0% { transform: translate(0, 0) rotate(0); opacity: 1; }
  100% { transform: translate(var(--x), var(--y)) rotate(var(--r)); opacity: 0; }
}

/* ──────────── contextual help panel ──────────── */

.help-fab {
  position: fixed;
  bottom: 5.5rem;
  right: 1.5rem;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--trust);
  color: var(--on-trust);
  border: none;
  font-size: 1.2rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  z-index: 900;
  transition: background 0.2s, transform 0.2s;
}
.help-fab:hover { transform: scale(1.1); }
/* Bug hunt (2026-09-10): same root cause as .cookie-banner above — base
   .help-fab's `color: #fff` is hardcoded, var(--text-muted) is not. */
.help-fab.is-open { background: #5a5550; color: #fff; }
/* Punchlist #19 (2026-09-04): hidden until "More" reveals it, in its exact
   existing slot — see setupFabMoreToggle() in app.js for why this is a
   pure visibility toggle rather than a reposition. */
.help-fab { display: none; }
.help-fab.fab-more-item--visible { display: block; }

.help-panel {
  position: fixed;
  bottom: 7rem;
  right: 1.5rem;
  width: 300px;
  max-height: 340px;
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.18);
  z-index: 901;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.help-panel[hidden] { display: none !important; }
.help-panel__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}
.help-panel__title {
  font-size: 0.9rem;
  font-weight: 600;
  flex: 1;
  margin: 0;
}
.help-panel__close {
  background: none;
  border: none;
  font-size: 1.3rem;
  cursor: pointer;
  color: var(--text-muted);
  padding: 0;
  line-height: 1;
}
.help-panel__tips {
  list-style: none;
  margin: 0;
  padding: 0.75rem 1rem;
  overflow-y: auto;
  flex: 1;
}
.help-panel__tip {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text);
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--border);
}
.help-panel__tip:last-child { border-bottom: none; }

/* ──────────── Feedback widget gauntlet (2026-08-14) ──────────── */
/* Same FAB+panel shape as .help-fab/.assistant-bar__toggle above (the
   latter was .chat-fab until 2026-09-01), stacked a third
   level up (help-fab sits at bottom:5.5rem ~44px tall, so 9.5rem clears
   it with room). Pill-shaped with a label rather than a bare icon circle
   — Keeno's own word was "larger," and this is meant to be found and
   used without first having to guess what an unlabeled icon does. */
.feedback-fab {
  position: fixed;
  bottom: 9.5rem;
  right: 1.5rem;
  height: 44px;
  padding: 0 1rem;
  border-radius: 999px;
  background: var(--growth);
  color: var(--on-growth);
  border: none;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
  box-shadow: 0 4px 16px rgba(0,0,0,0.22);
  z-index: 900;
  transition: background 0.2s, transform 0.2s;
}
.feedback-fab:hover { transform: scale(1.05); }
/* Bug hunt (2026-09-10): same root cause as .cookie-banner above. */
.feedback-fab.is-open { background: #5a5550; color: #fff; }
/* Punchlist #19 (2026-09-04): same visibility-toggle reasoning as .help-fab
   above — kept in its exact existing slot, just hidden until revealed. */
.feedback-fab { display: none; }
.feedback-fab.fab-more-item--visible { display: flex; }

/* The new consolidated toggle — takes .help-fab's old always-visible slot
   (bottom: 5.5rem), since that's now the entry point for both. */
.fab-more {
  position: fixed;
  bottom: 5.5rem;
  right: 1.5rem;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--trust);
  color: var(--on-trust);
  border: none;
  font-size: 1.3rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  z-index: 900;
  transition: background 0.2s, transform 0.2s;
}
.fab-more:hover { transform: scale(1.1); }
/* Bug hunt (2026-09-10): same root cause as .cookie-banner above. */
.fab-more.is-open { background: #5a5550; color: #fff; }
/* F1: the More sheet is a phone-only surface (the phone block below shows it). */
.fab-sheet { display: none; z-index: 900; }
/* R6-P2 (QA round 6): above phone width More reveals its two items IN PLACE,
   and "Tab tips" (.help-fab) was revealed in More's own slot (both
   bottom:5.5rem; right:1.5rem): the hit-test at its centre landed on More,
   so only "Suggest an improvement" could be pressed. The revealed items now
   open in a row to More's LEFT on More's line (the phone D9 pattern below):
   [Suggest an improvement] [?] [More], 8px apart, nothing on top of the
   trigger, the column above (.assist at 13.25rem) untouched. The hover
   labels that would print sideways across the row go above their button,
   More's own label is not shown while it is open (it is the open control),
   and the tips panel opens above the row instead of over it (bottom 7rem
   used to cover More's top 20px). The phone block below still hides both
   (its sheet lists them). Click test: scripts/qa/fab-more-check.mjs. */
.help-fab.fab-more-item--visible { bottom: 5.5rem; right: calc(1.5rem + 44px + 8px); }
.feedback-fab.fab-more-item--visible { bottom: 5.5rem; right: calc(1.5rem + 2 * (44px + 8px)); }
.help-fab.fab-more-item--visible > .fab-label { right: auto; left: 50%; top: auto; bottom: calc(100% + 8px); transform: translateX(-50%); }
.fab-more.is-open > .fab-label { display: none; }
.help-panel { bottom: calc(5.5rem + 44px + 12px); }
/* R6-P2: an open tips or suggestion panel is the active surface: it sits above the idle FAB tower
   (.assist is z-index 8000 and printed its button over the tips text), below every real modal
   (9000+). The same 8050 tier the phone takeover and .assistant-bar__panel already use. */
body .help-panel, body .feedback-panel { z-index: 8050; } /* body: beats the later .feedback-panel base rule */

.feedback-panel {
  position: fixed;
  bottom: 12.5rem;
  right: 1.5rem;
  width: 340px;
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.22);
  z-index: 901;
  overflow: hidden;
}
.feedback-panel[hidden] { display: none !important; }
.feedback-panel__head {
  padding: 0.85rem 1rem 0.6rem;
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.feedback-panel__title { font-size: 0.95rem; font-weight: 700; margin: 0; flex: 1; }
.feedback-panel__sub { display: block; font-size: 0.78rem; color: var(--text-muted); margin-top: 0.15rem; font-weight: 400; }
.feedback-panel__close { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--text-muted); padding: 0; line-height: 1; }
.feedback-panel__what-btn {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: none;
  color: var(--text-muted);
  font-size: 0.7rem;
  font-weight: 700;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin-top: 0.1rem;
}
.feedback-panel__what-btn:hover { border-color: var(--action); color: var(--action); }
.feedback-panel__body { padding: 0.85rem 1rem 1rem; }
.feedback-panel__body textarea {
  width: 100%;
  min-height: 90px;
  resize: vertical;
  font: inherit;
  padding: 0.6rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--stock-warm, var(--stock));
  color: inherit;
}
.feedback-panel__context { font-size: 0.75rem; color: var(--text-muted); margin: 0.5rem 0 0; }
.feedback-panel__actions { display: flex; justify-content: flex-end; margin-top: 0.75rem; }
.feedback-panel__confirm { padding: 1.25rem 1rem; text-align: center; font-size: 0.9rem; }

.feedback-mic-btn {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  width: 100%;
  justify-content: center;
  margin-top: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: 1px dashed var(--rule);
  border-radius: 8px;
  background: var(--stock-warm, var(--stock));
  color: var(--text);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.feedback-mic-btn:hover { border-color: var(--action); }
.feedback-mic-btn.is-listening {
  border: 1px solid #dc2626;
  background: #fef2f2;
  color: #991b1b;
  animation: feedback-mic-pulse 1.4s ease-in-out infinite;
}
[data-theme="dark"] .feedback-mic-btn.is-listening { background: #450a0a; color: #fca5a5; }
@keyframes feedback-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.35); }
  50% { box-shadow: 0 0 0 6px rgba(220, 38, 38, 0); }
}
.feedback-panel__mic-hint {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-style: italic;
  margin: 0.4rem 0 0;
}

@media (max-width: 600px) {
  /* Bug hunt (2026-08-19, live craft pass): confirmed live at 375px width
     — the full-text pill overlapped a milestone card's status text AND a
     real clickable link underneath it. The old fix here only repositioned
     the pill, never shrank it, so the collision remained. Icon-only on
     mobile, matching the other 3 FABs in the same stack (.help-fab/
     .assistant-bar__toggle/.assist are all icon-only circles already) — the label text
     stays in the aria-label for screen readers. */
  .feedback-fab { bottom: 7.75rem; right: 1rem; width: 44px; padding: 0; justify-content: center; }
  .feedback-fab__label { display: none; }
  .feedback-panel { right: 1rem; left: 1rem; width: auto; }
}

/* ──────────── Assistant bar (2026-09-01) ──────────── */
/* Replaces the old "Ask AI" widget (.chat-fab/.chat-panel/.chat-msg--bot/
   --user/--thinking, all removed here) — Keeno's decision: ONE persistent
   floating bar for navigation help, guidance, AND plain questions, not a
   separate chat-only entry point living apart from the rest of the FAB
   stack. Takes over the exact position .chat-fab used to hold
   (bottom:1.5rem/right:1.5rem desktop, bottom:1rem/right:1rem mobile,
   z-index:900) so .help-fab/.feedback-fab/.assist above it in the tower
   need no repositioning — the FAB-collision history already documented on
   .assist above established that every new entry has to be checked
   against the whole stack; swapping into a slot a removed entry vacates is
   the version of that check that requires no other widget to move.
   Pill shape with a persistent label (not a bare icon circle), the same
   shape choice Keeno made for .feedback-fab ("larger... found and used
   without first guessing what an unlabeled icon does") — this is meant to
   be the ONE place people go, so it gets the same found-without-guessing
   treatment the lower-traffic icon-only widgets don't need. */
.assistant-bar__toggle {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  height: 48px;
  padding: 0 1.15rem;
  border-radius: 999px;
  background: var(--action);
  color: var(--on-action);
  border: none;
  font-family: var(--ui);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  white-space: nowrap;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  z-index: 900;
  transition: background 0.2s, transform 0.2s;
}
.assistant-bar__toggle:hover { transform: scale(1.05); }
/* Companion-home audit (2026-09-12): confirmed live — dockAssistantPanel()
   sets toggle.hidden = true when Command Center docks this same panel, but
   the base rule above sets `display: flex` unconditionally, and an author
   stylesheet rule always beats the UA [hidden] rule regardless of
   specificity or source order. Same bug class as .assistant-bar__panel
   already guards against two rules below (`[hidden] { display: none }`)
   — this button never got the matching guard. Result: the floating "Ask"
   pill stayed visible and clickable, floating over the exact page that IS
   this same feature, docked. */
.assistant-bar__toggle[hidden] { display: none !important; }
/* Bug hunt (2026-09-10): same root cause as .cookie-banner above. */
.assistant-bar__toggle.is-open { background: #5a5550; color: #fff; }
.assistant-bar__toggle svg { flex: none; }

.assistant-bar__panel {
  position: fixed;
  bottom: 4.5rem;
  right: 1.5rem;
  width: min(380px, 85vw);
  max-height: 560px;
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.22);
  /* Confirmed live (craft-pass screenshot, 2026-09-01): at this bottom/
     right this panel's real rendered height — even with just the opening
     line in the transcript — reaches up into .assist's fixed icon slot
     (bottom:13.25rem, z-index:8000, deliberately the topmost FAB per its
     own incident history). At z-index:901 that let the small "What's
     Next" badge float visibly on top of this panel's transcript, which
     only gets worse as more turns arrive and the panel grows toward its
     560px cap. Placed above .assist (8000) but still below every real
     modal (.app-dialog-overlay etc. start at 9000) — an open conversation
     panel should outrank an idle nudge badge sitting behind it, the same
     ordering a real modal already gets over the whole FAB stack. */
  z-index: 8050;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: assistant-bar-in 250ms var(--snap);
}
.assistant-bar__panel[hidden] { display: none !important; }

/* Command Center (2026-09-03) — the SAME panel, reparented into
   #command-center-dock instead of floating fixed in the corner. Every
   fixed/positioning/sizing rule above is for the floating case and gets
   overridden here, not duplicated into a second component. */
.assistant-bar__panel--docked {
  position: static;
  width: 100%;
  max-height: none;
  height: min(720px, calc(100vh - 220px));
  border-radius: 12px;
  box-shadow: none;
  animation: none;
  z-index: auto;
}
.assistant-bar__panel--docked .assistant-bar__transcript { max-height: none; }
/* QA round 1 (2026-10-16, D1 follow-up, seen live at 1440x900 and 390x844):
   the docked panel is flex-filled to whatever height the view leaves it
   (about 390px at 1440x900), but its children could not shrink to fit: the
   expanded "What can I ask?" block, plus the transcript's 200px floor, pushed
   the input row past the panel's bottom edge where overflow:hidden clipped it,
   so Command Center (the sign-in landing) showed no text box at all. Here the
   capabilities block shrinks and scrolls first, the transcript keeps a
   smaller floor, and the input row never shrinks. */
.assistant-bar__panel--docked .assistant-bar__capabilities { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
.assistant-bar__panel--docked .assistant-bar__transcript { min-height: 96px; }
.assistant-bar__panel--docked .assistant-bar__form { flex: none; }
/* Companion-home rework (2026-09-12): the fixed min(720px, calc(100vh -
   220px)) height above assumed a budget for chrome ABOVE the panel that
   never accounted for this view's own <h1>/lede — confirmed live, the
   real input sat 214px below the fold on a plain 768px-tall viewport, so
   "just a search bar" required scrolling before you could type anything.
   Flex-filling the view itself (below) so the dock takes whatever space is
   actually left, instead of guessing a magic number, fixes it regardless
   of how tall the head/strip end up being on any given screen. */
/* Live-measured iteration (2026-09-12): a pure vh-budget on the view
   overflowed because the chrome above it (dismissible banner, breadcrumb,
   "What's new") is variable, not fixed — but abandoning the flex-fill
   approach entirely made it WORSE (reverted to the un-adaptive
   min(720px, calc(100vh-220px)) base rule, which assumes even less real
   chrome than the vh version did). The right fix keeps the flex-fill
   shape but with a floor small enough to actually fit alongside the head
   and a capped strip: measured live, head+capped-strip+this floor fits
   inside calc(100vh-140px) with real margin, where the earlier 420px
   floor did not. */
#view-command-center { display: flex; flex-direction: column; height: calc(100vh - 140px); }
/* No general-media `.sheet[hidden]` guard exists (only inside @media
   print) — `display:flex` above is an author rule that would otherwise
   beat the UA `[hidden]{display:none}` default the same way
   `.assistant-bar__toggle` just did (see that rule's own comment). */
#view-command-center[hidden] { display: none !important; }
#view-command-center .sheet__head { flex: none; margin-bottom: 0.75rem; }
#view-command-center .sheet__head .lede { margin: 0.25rem 0 0; }
#view-command-center #command-center-dock { flex: 1 1 auto; display: flex; min-height: 0; }
/* Capped, not just de-prioritized: without this, a long strip (already
   capped at CC_STRIP_VISIBLE_CAP rows in JS, but still real height on
   small screens) would fight the dock for the same fixed-height column
   and push the input back below the fold — the exact problem this whole
   rework exists to fix. Its own internal scroll keeps every row reachable
   without ever costing the dock its space. */
#view-command-center #cc-work-strip { flex: none; max-height: 150px; overflow-y: auto; }
#view-command-center .assistant-bar__panel--docked { flex: 1 1 auto; height: auto; min-height: 300px; }
/* Mobile's own header stacks taller (hamburger row + wrapped banner text +
   breadcrumb take real vertical space a single-line desktop header
   doesn't) — live-measured at 380px of chrome above this view on a
   375-wide viewport with the AI-mode banner showing, vs. desktop's 243px.
   One reserve can't fit both; Keeno's own priority here is mobile
   specifically ("if this was a mobile app, everyone would love that
   more"), so this gets its own tuned number rather than splitting the
   difference and shortchanging the viewport that matters most. */
@media (max-width: 719px) {
  /* QA round 1 (2026-10-16, D1 follow-up): with the docked panel now
     actually docked on phones (it used to be forced full-screen, see the
     D1 note in the 600px block), a FIXED view height made the column
     overflow: the panel's min-height pushed past the view box and the work
     strip below painted over its input. The view sizes to its content here
     and the panel gets the height instead, so nothing overlaps and the page
     scrolls normally. */
  #view-command-center { height: auto; }
  #view-command-center #cc-work-strip { max-height: 110px; }
  /* Deliberately under the 812px-emulation measurement's exact fit — a
     real phone browser's address bar eats further into the visual
     viewport than this emulation reflects, so a floor tuned to land
     exactly at the edge in testing would land past it on a real device. */
  #view-command-center .assistant-bar__panel--docked {
    flex: none;
    height: max(360px, calc(100vh - 380px));
    height: max(360px, calc(100dvh - 380px));
    min-height: 220px;
  }
  /* QA-T13 (R4-29): at the 96px floor the transcript showed three lines of
     the four-line greeting on a 390px phone; the last line sat clipped at the
     input row. 9.5rem holds the whole greeting bubble; the "What can I ask?"
     block above (flex-shrink, scrolls) gives up the difference. */
  #view-command-center .assistant-bar__panel--docked .assistant-bar__transcript { min-height: 9.5rem; }
  /* W4C (Monday plan defect 14): at 390 wide the fixed panel height left the "What can I ask?" block a sliver, so
     it was cut mid-line under the greeting: "Try asking:" looked covered and its chips were scrolled out of
     sight. On a phone the panel sizes to its content instead (the view already scrolls here), so the block shows
     whole. It is open only on the first visit (setCapabilitiesOpen), so later visits stay compact. */
  #view-command-center .assistant-bar__panel--docked { height: auto; min-height: 0; }
  #view-command-center .assistant-bar__panel--docked .assistant-bar__capabilities { flex: none; overflow: visible; }
}
/* Redundant with the view's own lede ("Say what you need...") immediately
   above it — same reasoning .assistant-bar__title's docked hide already
   uses, freeing real vertical space instead of repeating the same
   sentence twice before the input is ever reachable. */
.assistant-bar__panel--docked .assistant-bar__sub { display: none; }
/* Docked mode has nothing to "close" to (it's the landing view itself, not
   an overlay) — the close button's own click handler still calls setOpen()
   directly, not _setDocked(), so leaving it visible/clickable here would
   hide the panel while the docked/toggle-hidden state stayed stuck. Leaving
   this view happens by navigating elsewhere, which already undocks it. */
.assistant-bar__panel--docked .assistant-bar__close { display: none; }
/* The view's own <h1> ("Easy Navigation") already establishes the heading
   here — the panel's internal <h3> title would just repeat it directly
   underneath. Sub-description stays; it's real functional copy, not a
   second name for the same thing. */
.assistant-bar__panel--docked .assistant-bar__title { display: none; }
/* QA-T12 (R3-2, round 3): with its title, sub-line and close button all
   hidden by the three rules above, the docked head was an empty bordered
   strip across the top of the panel (both viewports). Nothing is left in
   it to show, so the row itself goes. */
.assistant-bar__panel--docked .assistant-bar__head { display: none; }

@keyframes assistant-bar-in {
  from { opacity: 0; transform: translateY(16px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.assistant-bar__head {
  padding: 0.85rem 1rem 0.6rem;
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0 0.5rem;
}
.assistant-bar__title {
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
  flex: 1;
  color: var(--text);
}
.assistant-bar__sub {
  font-size: 0.75rem;
  color: var(--text-muted);
  width: 100%;
  margin-top: 0.15rem;
}
.assistant-bar__close {
  background: none;
  border: none;
  font-size: 1.4rem;
  cursor: pointer;
  color: var(--text-muted);
  padding: 0;
  line-height: 1;
  /* Touch-target audit (2026-09-02): the visible glyph stays small, but
     the real tappable area now meets this app's own 44x44px minimum
     (same convention as .sidebar__label etc.) instead of hugging the
     glyph's own tiny font-box. */
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.assistant-bar__close:hover { color: var(--text); }
/* U11 (UI audit 2026-10-28, both viewports): the close button comes after the 100%-wide subtitle in the DOM
   (app.js builds title, sub, close), so it wrapped onto a third row of its own, under the subtitle, instead of
   sitting beside the title as the head's flex-wrap was meant to place it. Visual order only; the DOM (and so the
   Tab order: title, then close) is untouched. */
.assistant-bar__title { order: -2; }
.assistant-bar__close { order: -1; }

/* Resilience pass (2026-09-01) — same "own block-level row directly below
   .assistant-bar__head" shape as .assistant-bar__capabilities-link just
   below, for the identical paint-bug reason (see that rule's own comment
   and the button's markup comment in app.js). Muted + right-aligned rather
   than styled like the capabilities link: this is a reset action, not
   something to invite exploration of on every open. */
.assistant-bar__reset {
  display: block;
  width: 100%;
  padding: 0.35rem 1rem;
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule);
  font-family: var(--ui);
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
  text-align: right;
  cursor: pointer;
}
.assistant-bar__reset:hover { color: var(--text); }

/* Always-reachable half of the capability disclosure (2026-09-01 product
   audit) — its own block-level row directly below .assistant-bar__head,
   deliberately NOT a 4th flex-wrap item inside .assistant-bar__head itself
   (see the app.js comment at this button's markup for the rendering bug
   that shape caused). */
.assistant-bar__capabilities-link {
  display: block;
  width: 100%;
  padding: 0.4rem 1rem;
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule);
  font-family: var(--ui);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--action);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  text-align: left;
}
.assistant-bar__capabilities-link:hover { color: var(--text); }

/* The disclosure itself — shown expanded once automatically (see
   setCapabilitiesOpen() in app.js), collapsed on every open after that and
   reachable only via the link above. --stock-warm/--rule-soft match the
   assistant's own reply bubbles just below it so this reads as "part of the
   assistant," not a separate system banner. */
.assistant-bar__capabilities {
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--rule);
  background: var(--stock-warm);
}
.assistant-bar__capabilities[hidden] { display: none; }
.assistant-bar__capabilities-text {
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text);
}
.assistant-bar__chips-label {
  margin: 0 0 0.4rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
}
.assistant-bar__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.assistant-bar__chip {
  background: var(--stock);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.35rem 0.7rem;
  font-family: var(--ui);
  font-size: 0.72rem;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.assistant-bar__chip:hover { background: var(--rule-soft); border-color: var(--action); }
/* Punchlist #32 (2026-09-04): a subtle visual distinction — "something you
   actually asked" reads differently from "a suggested example" — dashed
   border, same otherwise. */
.assistant-bar__chip--recent { border-style: dashed; }

/* Punchlist #36 (2026-09-05) — a plain-text link, not a chip or a
   destructive-styled button: clearing localStorage memory is reversible
   (it just starts accumulating again) and low-stakes, so it doesn't need
   alert styling, just a reachable, honest label. */
.assistant-bar__clear-memory {
  display: block;
  margin-top: 0.6rem;
  padding: 0;
  background: none;
  border: none;
  font-family: var(--ui);
  font-size: 0.68rem;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.assistant-bar__clear-memory:hover { color: var(--text); }

/* The transcript is the multi-turn UI (spec D) — every message either side
   has sent, newest at bottom, scrollable, and never reset on navigation. */
.assistant-bar__transcript {
  flex: 1;
  overflow-y: auto;
  padding: 0.75rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-height: 200px;
  max-height: 380px;
}
/* U11 (UI audit 2026-10-28, 1440x900): with "What can I ask?" open, head + capabilities + the 200px transcript floor
   above overflowed the 560px panel, and its overflow:hidden cut the text box and Send off entirely. On a desk the
   capabilities now scroll inside 200px and the transcript keeps 120px, so the box and Send always fit (about
   58 + 34 + 30 + 180 + 120 + 120 = 542px). Placed after the base rules so it wins; the phone block further down
   (transcript min-height 0, capabilities 45vh) still comes later and still wins on a phone. */
.assistant-bar__capabilities { max-height: 180px; overflow-y: auto; }
.assistant-bar__transcript { min-height: 120px; }
/* Once the form was back on screen, the labelled Attach and Talk buttons squeezed the text box to about 40px wide
   in the 380px floating panel: the same squeeze the phone block below already fixes (QA round 1, 2026-10-16) by
   giving the box and Send their own row. Same rule for the floating panel at every width; the docked Command
   Center panel keeps its own one-row form. */
.assistant-bar__panel:not(.assistant-bar__panel--docked) .assistant-bar__form { flex-wrap: wrap; }
.assistant-bar__panel:not(.assistant-bar__panel--docked) .assistant-bar__field { order: -2; flex: 1 1 calc(100% - 44px); }
.assistant-bar__panel:not(.assistant-bar__panel--docked) .assistant-bar__send { order: -1; }
.assistant-bar__msg p {
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre-wrap;
}
/* Punchlist #25 (2026-09-04): the assistant's own replies are the one place
   she reads actual prose rather than a short label/query — a touch more
   breathing room than the app's general 1.5 (tuned for buttons/labels),
   scoped to assistant bubbles only so her own short typed messages don't
   change. */
.assistant-bar__msg--assistant p { line-height: 1.65; }
.assistant-bar__msg--user {
  align-self: flex-end;
  background: var(--action);
  color: var(--on-action);
  border-radius: 12px 12px 2px 12px;
  padding: 0.5rem 0.75rem;
  max-width: 80%;
}
/* Phase 10 (2026-09-04) — sits inside the same bubble, on its own line below
   the text (the row is a plain block, so a second child flows underneath
   the <p> for free); styled as a subtle link rather than inheriting the
   bubble's own bold action-color treatment, since this isn't the message
   itself, just an action on it. */
.assistant-bar__edit {
  display: block;
  margin-top: 0.3rem;
  padding: 0;
  border: none;
  background: none;
  color: rgba(255, 255, 255, 0.78);
  font-family: var(--ui);
  font-size: 0.7rem;
  text-decoration: underline;
  text-align: right;
  width: 100%;
  cursor: pointer;
}
.assistant-bar__edit:hover { color: #fff; }

/* Punchlist #30 (2026-09-04): briefly highlights the words that changed
   after Edit & resend, fading out rather than staying marked forever —
   "briefly", not a permanent diff view. Light-on-dark since it sits inside
   the action-colored user bubble. Respects reduced-motion by skipping the
   fade and just showing the highlight statically (still real information,
   not decoration). */
.assistant-bar__edit-highlight {
  background: rgba(255, 255, 255, 0.85);
  color: var(--action, #b4491f);
  border-radius: 3px;
  padding: 0 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .assistant-bar__edit-highlight { animation: assistant-bar-edit-fade 3s ease-out forwards; }
  @keyframes assistant-bar-edit-fade {
    0%, 40% { background: rgba(255, 255, 255, 0.85); color: var(--action, #b4491f); }
    100% { background: transparent; color: inherit; }
  }
}
.assistant-bar__msg--assistant {
  align-self: flex-start;
  background: var(--stock-warm);
  border: 1px solid var(--rule-soft);
  border-radius: 12px 12px 12px 2px;
  padding: 0.5rem 0.75rem;
  max-width: 85%;
  color: var(--text);
}
/* Resilience pass (2026-09-01) — a genuine failure (timeout, network,
   rate-limit, AI backend down) gets a visually distinct bubble from a
   normal reply, using this app's own --alert token (already theme-aware —
   see :root vs [data-theme="dark"]) rather than a hardcoded red, same as
   every other error surface in this app. */
.assistant-bar__msg--error {
  border-color: var(--alert);
  background: var(--stock);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
}
.assistant-bar__msg--error p { color: var(--alert); }
.assistant-bar__retry {
  flex: none;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--alert);
  border-radius: 999px;
  background: none;
  color: var(--alert);
  font-family: var(--ui);
  font-size: 0.72rem;
  font-weight: 700;
  cursor: pointer;
}
.assistant-bar__retry:hover:not(:disabled) { background: var(--alert); color: var(--on-alert); }
.assistant-bar__retry:disabled { opacity: 0.6; cursor: default; }

/* Real disambiguation choices (product-audit finding, 2026-09-01) — a
   stack of real buttons, not the pill-shaped .assistant-bar__chip row above
   (those fill the input for editing; these commit a real pick immediately). */
.assistant-bar__choices {
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-width: 85%;
}
.assistant-bar__choice {
  background: var(--stock-warm);
  border: 1px solid var(--rule-soft);
  border-radius: 10px;
  padding: 0.5rem 0.75rem;
  font-family: var(--ui);
  font-size: 0.82rem;
  text-align: left;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.assistant-bar__choice:hover:not(:disabled) { background: var(--rule-soft); border-color: var(--action); }
.assistant-bar__choice:disabled { opacity: 0.55; cursor: default; }
/* Punchlist #11 (2026-09-04): the model's own best guess (when it has one
   — see buildSystemPrompt's ordering rule) renders slightly larger than
   the other options, instead of every choice reading as equally likely. */
.assistant-bar__choice--lean {
  font-size: 0.88rem;
  font-weight: 650;
  border-width: 1.5px;
  border-color: var(--action);
}

/* Easy Navigation Phase 6 (2026-09-03) — real search-result cards in the
   transcript. Same card treatment as .assistant-bar__choice (rounded,
   bordered, hoverable button) combined with the icon+body layout from
   .cmdk__result — this is the same underlying data (GET /api/search) and
   the same real navigation (cmdkNavigateToServerResult), so it should
   read as a variant of both, not a third unrelated visual language. */
.assistant-bar__results {
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-width: 85%;
}
.assistant-bar__result {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--stock-warm);
  border: 1px solid var(--rule-soft);
  border-radius: 10px;
  padding: 0.45rem 0.7rem;
  font-family: var(--ui);
  text-align: left;
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.assistant-bar__result:hover:not(:disabled) { background: var(--rule-soft); border-color: var(--action); }
.assistant-bar__result:disabled { opacity: 0.55; cursor: default; }
/* Punchlist #10 (2026-09-04): the single unambiguously-best match stands
   out from the rest instead of all results reading as equally likely. */
.assistant-bar__result--best {
  padding: 0.6rem 0.85rem;
  border: 1.5px solid var(--action);
  background: var(--action-light);
}
.assistant-bar__result--best .assistant-bar__result-name { font-size: 0.9rem; }
.assistant-bar__result--best:hover:not(:disabled) { background: var(--action); }
.assistant-bar__result--best:hover:not(:disabled) .assistant-bar__result-name,
.assistant-bar__result--best:hover:not(:disabled) .assistant-bar__result-group { color: #fff; }
.assistant-bar__result-icon {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  background: var(--action-light);
  color: var(--action);
}
.assistant-bar__result-icon--trust  { background: var(--trust-bg); color: var(--trust); }
.assistant-bar__result-icon--action { background: var(--action-light); color: var(--action); }
.assistant-bar__result-icon--growth { background: var(--action-light); color: var(--action); }
.assistant-bar__result-icon--alert  { background: var(--alert-light); color: var(--alert); }
.assistant-bar__result-body { flex: 1; min-width: 0; }
.assistant-bar__result-name { font-weight: 600; font-size: 0.82rem; }
.assistant-bar__result-group { font-size: 0.72rem; color: var(--text-muted); margin-top: 1px; }

/* Functional, not decorative — the same reasoning DESIGN.md gives the run
   clock: without a visible "still working" signal, a slow reply and a
   stalled one look identical, and a non-technical operator's only
   available move is to assume it's broken. */
.assistant-bar__busy {
  align-self: flex-start;
  display: flex;
  gap: 0.3rem;
  margin: 0 1rem 0.6rem;
  padding: 0.6rem 0.8rem;
  background: var(--stock-warm);
  border: 1px solid var(--rule-soft);
  border-radius: 12px 12px 12px 2px;
}
.assistant-bar__busy[hidden] { display: none; }
.assistant-bar__busy-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
  animation: assistant-bar-bounce 1.1s ease-in-out infinite;
}
.assistant-bar__busy-dot:nth-child(2) { animation-delay: 0.15s; }
.assistant-bar__busy-dot:nth-child(3) { animation-delay: 0.3s; }
/* Punchlist #26 (2026-09-04) */
.assistant-bar__busy-label {
  margin-left: 0.3rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}
/* Resilience pass (2026-09-01) — lives in the same row as the busy dots
   (both are children of .assistant-bar__busy, which is what setBusy()
   actually toggles), so Cancel is only ever on screen exactly when there's
   something to cancel. [hidden] needs no explicit display:none override
   here — unlike .btn (used elsewhere in this app), this class doesn't set
   its own `display`, so the UA stylesheet's [hidden] rule already wins. */
.assistant-bar__cancel {
  margin-left: auto;
  flex: none;
  align-self: center;
  padding: 0.3rem 0.65rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--stock);
  color: var(--text-muted);
  font-family: var(--ui);
  font-size: 0.7rem;
  font-weight: 600;
  cursor: pointer;
}
.assistant-bar__cancel:hover { background: var(--rule-soft); color: var(--text); border-color: var(--action); }
@keyframes assistant-bar-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-4px); opacity: 1; }
}

/* Voice status live region (2026-09-01 product audit) — was a static
   always-same-text hint before this pass; now doubles as the "Listening…"
   label AND the permission-denied/recognition-error message, both written
   in by setMicStatus() in app.js. Default (listening) styling stays the
   same quiet italic muted tone the old hint used; the error modifier below
   switches it to this app's real --alert token, same as every other error
   surface in this file (.assistant-bar__msg--error, .assistant-bar__retry)
   — never a hardcoded red. */
.assistant-bar__mic-status {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-style: italic;
  margin: 0;
  padding: 0 1rem 0.5rem;
}
.assistant-bar__mic-status[hidden] { display: none; }
.assistant-bar__mic-status--error {
  color: var(--alert);
  font-style: normal;
}
.assistant-bar__mic-retry {
  margin-left: 0.3rem;
  padding: 0.1rem 0.55rem;
  border: 1px solid var(--alert);
  border-radius: 999px;
  background: none;
  color: var(--alert);
  font-family: var(--ui);
  font-size: 0.68rem;
  font-weight: 700;
  cursor: pointer;
}
.assistant-bar__mic-retry:hover { background: var(--alert); color: var(--on-alert); }

.assistant-bar__form {
  display: flex;
  align-items: center;
  gap: 0;
  border-top: 1px solid var(--rule);
}
.assistant-bar__mic {
  flex: none;
  width: 40px;
  height: 40px;
  margin-left: 0.5rem;
  border: none;
  background: none;
  border-radius: 50%;
  font-size: 1.05rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  transition: background 0.15s, color 0.15s;
}
.assistant-bar__mic:hover { background: var(--rule-soft); color: var(--text); }
.assistant-bar__mic.is-listening {
  color: #dc2626;
  animation: assistant-bar-mic-pulse 1.4s ease-in-out infinite;
}
[data-theme="dark"] .assistant-bar__mic.is-listening { color: #fca5a5; }
@keyframes assistant-bar-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.35); }
  50% { box-shadow: 0 0 0 6px rgba(220, 38, 38, 0); }
}
.assistant-bar__field {
  flex: 1;
  min-width: 0;
  border: none;
  padding: 0.7rem 0.85rem;
  font-size: 0.85rem;
  background: var(--stock);
  color: var(--text);
  outline: none;
}
.assistant-bar__send {
  flex: none;
  align-self: stretch;
  width: 44px;
  background: var(--action);
  color: var(--on-action);
  border: none;
  cursor: pointer;
  font-size: 1.1rem;
}
.assistant-bar__send:hover { opacity: 0.85; }

/* --------------- Easy Navigation Phase 12: attach a client --------------- */
/* Same size/shape convention as .assistant-bar__mic right next to it. */
.assistant-bar__attach {
  flex: none;
  width: 40px;
  height: 40px;
  margin-left: 0.5rem;
  border: none;
  background: none;
  border-radius: 50%;
  font-size: 1.15rem;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  transition: background 0.15s, color 0.15s;
}
.assistant-bar__attach:hover,
.assistant-bar__attach[aria-expanded="true"] { background: var(--rule-soft); color: var(--text); }

/* Punchlist #16 (2026-09-04): a text label for her first few real uses of
   the "+"/mic buttons, fading to icon-only once she's used it enough.
   Hidden by default (icon-only is the steady state); --labeled widens the
   button and reveals real text instead of relying on hover (which doesn't
   exist on touch anyway). */
.assistant-bar__first-use-label { display: none; }
.assistant-bar__icon-btn--labeled { width: auto; border-radius: 999px; padding: 0 0.7rem; gap: 0.35rem; }
.assistant-bar__icon-btn--labeled .assistant-bar__first-use-label {
  display: inline; font-size: 0.72rem; font-weight: 600; white-space: nowrap;
}

.assistant-bar__attach-chip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.5rem 0.75rem 0;
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  background: var(--stock-warm);
  border: 1px solid var(--rule-soft);
  font-size: 0.75rem;
  color: var(--text);
  align-self: flex-start;
}
.assistant-bar__attach-chip[hidden] { display: none; }
.assistant-bar__attach-chip-clear {
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0;
}
.assistant-bar__attach-chip-clear:hover { color: var(--alert); }

.assistant-bar__attach-picker {
  position: relative;
  margin: 0.5rem 0.75rem 0;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--stock);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
  overflow: hidden;
  z-index: 5;
}
.assistant-bar__attach-picker[hidden] { display: none; }
.assistant-bar__attach-filter {
  width: 100%;
  border: none;
  border-bottom: 1px solid var(--rule);
  padding: 0.55rem 0.7rem;
  font-size: 0.8rem;
  background: var(--stock);
  color: var(--text);
  outline: none;
}
.assistant-bar__attach-list { max-height: 180px; overflow-y: auto; }
.assistant-bar__attach-option {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  width: 100%;
  text-align: left;
  border: none;
  background: none;
  padding: 0.5rem 0.7rem;
  font-size: 0.8rem;
  color: var(--text);
  cursor: pointer;
}
.assistant-bar__attach-option:hover { background: var(--stock-warm); }
.assistant-bar__attach-option-name { font-weight: 600; }
.assistant-bar__attach-option-loc { color: var(--text-muted); font-size: 0.72rem; }
/* Punchlist #14 (2026-09-04): a client needing attention stands out in
   the attach picker too, same visual language as the work strip. */
.assistant-bar__attach-option--flagged { background: var(--action-light); }
.assistant-bar__attach-option-flag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 50%; margin-left: auto;
  background: var(--alert); color: var(--on-alert); font-weight: 700; font-size: 10px; flex: none;
}
.assistant-bar__attach-empty { padding: 0.6rem 0.7rem; font-size: 0.78rem; color: var(--text-muted); }

@media (max-width: 600px) {
  /* z-index audit (2026-09-02): this used to jump both panels to 10000 on
     mobile — above every real modal dialog (.app-dialog-overlay/.cmdk/
     .login-overlay/.onboard-overlay start at 9000+), so a dialog opened
     while either panel was showing could have its own buttons covered and
     unreachable. Dropped to match .assistant-bar__panel's own desktop tier
     (8050, see that rule's comment above) — same "below every real modal,
     above the idle FAB tower" ordering desktop already gets, now true on
     mobile too. .help-panel's desktop base (901) is lower than this, but
     sharing the higher tier here is correct: a full-screen mobile takeover
     of either is the "active" surface and should behave like the more
     important one, not silently regress below it. */
  .assistant-bar__panel, .help-panel {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    width: 100%;
    max-height: 100vh;
    max-height: 100dvh;
    border-radius: 0;
    z-index: 8050;
  }
  /* QA round 1 (2026-10-16, D1): the full-screen rule just above has the
     same specificity as .assistant-bar__panel--docked (defined earlier,
     outside any media query) and comes later, so on phones it won: the
     Command Center's docked chat became a fixed full-screen layer at
     z-index 8050 with its close button hidden (docked rules), covering
     Menu, Search, Setup and the account badge with no way out, right
     after sign-in. Re-asserting the docked geometry here, after the
     overlay rule, keeps it in the page on phones too. */
  .assistant-bar__panel--docked {
    position: static;
    inset: auto;
    width: 100%;
    max-height: none;
    border-radius: 12px;
    z-index: auto;
  }
  .assistant-bar__transcript { max-height: calc(100vh - 190px); max-height: calc(100dvh - 190px); }
  /* min-height:200px (desktop, above) is a floor flexbox won't shrink below
     — fine in a 560px-tall floating panel, but on a short phone with the
     capabilities block expanded (head + disclosure text + 4 wrapped chips)
     that floor is tall enough to push .assistant-bar__form off the bottom
     of the fixed full-screen panel, whose overflow:hidden would then clip
     the send button entirely. Dropping the floor here lets the transcript
     shrink as needed; it keeps its own overflow-y:auto scrollbar either way. */
  .assistant-bar__transcript { min-height: 0; }
  .assistant-bar__capabilities { max-height: 45vh; overflow-y: auto; }
  /* Safe-area audit (2026-09-02): with viewport-fit=cover now set
     (index.html), a fixed full-screen panel like this one can render its
     own bottom edge underneath an iPhone's home-indicator bar unless it
     accounts for the inset itself. env() with a 0px fallback is a no-op
     on any device without one. */
  .assistant-bar__form { padding-bottom: env(safe-area-inset-bottom, 0px); }
  /* QA round 1 (2026-10-16): with the labeled Attach and Talk buttons in
     the same row, the text box itself was squeezed to about 34px wide on a
     390px phone. Phones put the text box and Send on their own full-width
     row and the two helper buttons on the row below. */
  .assistant-bar__form { flex-wrap: wrap; }
  .assistant-bar__field { order: -2; flex: 1 1 calc(100% - 44px); }
  .assistant-bar__send { order: -1; }
  /* Icon-only on mobile, matching the rest of the FAB stack's own mobile
     treatment (see .feedback-fab's mobile note above) — the label stays in
     the button's aria-label for screen readers. */
  .assistant-bar__toggle { bottom: 1rem; right: 0.5rem; width: 44px; height: 44px; padding: 0; justify-content: center; }
  .assistant-bar__toggle-label { display: none; }
  /* QA round 1 (2026-10-16, D9): one tidy column, bottom-up with an 8px
     gap, all right:0.5rem and 44px wide: Ask (1rem), More (4.25rem),
     What's Next (7.5rem, toggle shrunk from 56px) = 164px tall at rest.
     That column stays right of main's phone content edge (see main's phone
     rule), and main's bottom padding clears its top. The two items More
     reveals open in a row to More's left (only while the tray is open),
     which keeps them below .feedback-panel's phone bottom (12.5rem).
     Before this, More (5.5rem, no phone rule) sat on top of the revealed
     help button (4.5rem) and overlapped feedback (7.75rem), so neither
     was reliably tappable. */
  .fab-more { bottom: 4.25rem; right: 0.5rem; }
  .assist { bottom: 7.5rem; right: 0.5rem; }
  .assist__toggle { width: 44px; height: 44px; }
  .help-fab { bottom: 4.25rem; right: calc(0.5rem + 52px); }
  .feedback-fab { bottom: 4.25rem; right: calc(0.5rem + 104px); }
  /* F1 (demo usability, 2026-10-17): the column above was still four
     floating buttons on a phone (Back to top, What's Next, More, Ask).
     Now More is the ONE resting button, in Ask's old slot; the others rest
     hidden and More opens .fab-sheet, which lists all five with words
     (setupFabMoreToggle in app.js presses the real button). The two items
     More reveals in place on a desktop stay hidden here: the sheet has them.
     What's Next keeps its slot for its panel, which now opens right above
     More. While the chat is docked (Easy Navigation) More steps aside (see
     below), so What's Next and Back to top come back as their own buttons
     there, alone in the column. */
  .fab-more { bottom: 1rem; }
  .assistant-bar__toggle,
  .assist__toggle,
  .scroll-top-fab,
  .help-fab.fab-more-item--visible,
  .feedback-fab.fab-more-item--visible { display: none; }
  .assist { bottom: 1rem; }
  body.cc-docked .assist__toggle { display: flex; }
  body.cc-docked .scroll-top-fab { display: block; }
  .fab-sheet {
    display: grid;
    position: fixed;
    right: 0.5rem;
    bottom: calc(1rem + 52px);
    width: min(16rem, calc(100vw - 1rem));
    padding: 0.35rem;
    gap: 0.15rem;
    background: var(--stock);
    border: 1px solid var(--rule);
    border-radius: 12px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
    z-index: 900;
  }
  .fab-sheet__item {
    min-height: 44px;
    min-width: 44px;
    padding: 0 0.85rem;
    border: 0;
    border-radius: 8px;
    background: none;
    color: var(--text);
    font: 600 15px / 1.3 var(--ui);
    text-align: left;
    cursor: pointer;
  }
  .fab-sheet__item:focus-visible { outline: 2px solid var(--action); outline-offset: -2px; }
  /* QA-T12 (R3-2, round 3): on Easy Navigation the chat is docked in the
     page and IS the help; More sat on the panel's edge right beside Send
     (WebKit 390x844), one slip from the wrong button. It and the two
     buttons it reveals step aside while the chat is docked (app.js sets
     body.cc-docked in _setDocked). "Suggest an improvement" is NOT in the
     menu (the sidebar's Feedback is the admin inbox), so the view carries
     its own entry to the same form instead (#cc-suggest, index.html). */
  body.cc-docked .fab-more,
  body.cc-docked .fab-sheet,
  body.cc-docked .help-fab,
  body.cc-docked .feedback-fab { display: none; }
  body.cc-docked .cc-suggest-row { display: block; }
}
.cc-suggest-row { display: none; margin: 0.75rem 0 0; text-align: center; }

/* -------------------- item 14: sortable column headers ------------------- */
th[aria-sort] { position: relative; padding-right: 1.4em; }
th[aria-sort]::after {
  content: '⇅';
  position: absolute;
  right: 0.4em;
  opacity: 0.3;
  font-size: 0.75em;
}
th[aria-sort="ascending"]::after  { content: '↑'; opacity: 0.8; }
th[aria-sort="descending"]::after { content: '↓'; opacity: 0.8; }

/* -------------------- item 15: undo button in toast ---------------------- */
.undo-btn {
  background: none;
  border: 1px solid currentColor;
  color: inherit;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 0.5rem;
  font-size: 0.85rem;
}
.undo-btn:hover { opacity: 0.8; }

/* -------------------- item 18: retry error block ------------------------- */
.retry-error {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: #fef2f2;
  border: 1px solid #fca5a5;
  border-radius: 8px;
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  color: #991b1b;
  font-size: 0.9rem;
}
[data-theme="dark"] .retry-error {
  background: #450a0a;
  border-color: #7f1d1d;
  color: #fca5a5;
}

/* -------------------- item 21: pagination bar ---------------------------- */
.pagination-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 0.75rem 0;
}
.pagination-info {
  font-size: 0.85rem;
  color: var(--text-muted);
}

/* -------------------- item 25: bulk actions bar -------------------------- */
.bulk-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem;
  background: var(--stock-warm);
  border: 1px solid var(--rule);
  border-radius: 8px;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
}
.bulk-bar[hidden] { display: none !important; }
.bulk-count { font-weight: 600; color: var(--action); }
.bulk-check, .bulk-all { width: 1rem; height: 1rem; cursor: pointer; accent-color: var(--action); }

/* -------------------- item 33: button loading spinner -------------------- */
.btn--loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.btn--loading::after {
  content: '';
  position: absolute;
  width: 1em;
  height: 1em;
  top: 50%;
  left: 50%;
  margin: -0.5em 0 0 -0.5em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  color: var(--text);
}
@keyframes spin { to { transform: rotate(360deg); } }

/* -------------------- item 37: breadcrumb bar ---------------------------- */
.breadcrumb-bar {
  padding: 0.5rem 1.5rem;
  font-size: 0.8rem;
  color: var(--text-muted);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0;
  row-gap: 0.3rem;
}
.breadcrumb-bar a {
  color: var(--action);
  text-decoration: none;
}
.breadcrumb-bar a:hover { text-decoration: underline; }
.breadcrumb-sep { margin: 0 0.4rem; opacity: 0.5; }
.breadcrumb-bar [aria-current="page"] { font-weight: 600; color: var(--text); }
/* Shell audit (2026-08-30): a live measurement at 375px on a 3-level crumb
   ("Dashboard › Client › Track Growth › Rankings") found the "Track
   Growth" span rendered 43px wide × 2 lines tall — the container is only
   279px here (main reserves 6rem on the right for the FAB stack even on
   this view, see main's own comment above), so the row's nowrap flex items
   genuinely don't all fit on one line. Without flex-wrap the browser's only
   remaining move was shrinking that flex item down to its own min-content,
   which for text is the width of its LONGEST WORD — so "Track Growth"
   broke into "Track" / "Growth" on separate lines mid-phrase. flex-wrap
   lets the whole crumb-plus-separator drop to a second line instead, and
   nowrap on each crumb (below) stops any single crumb from breaking
   internally the way main-axis shrinking did. */
.breadcrumb-bar a,
.breadcrumb-bar > span:not(.breadcrumb-sep) {
  white-space: nowrap;
  /* QA-T4 D6 (2026-10-16): nowrap with no cap let a long client name crumb
     run to 692px on a 390px phone. Capped at the bar's width and
     ellipsised; the client crumb carries the full name in its title. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* QA-T13 (R4-21): on a phone a long client name wrapped the trail onto 3
   lines. One line now: nothing wraps, the client crumb is the only one that
   gives up width (ellipsis, full name in its title), the page name keeps its
   own, and with a client shown the section crumb is dropped (Home › client ›
   page). breadcrumb.js puts the section before the client. */
@media (max-width: 600px) {
  .breadcrumb-bar { flex-wrap: nowrap; min-width: 0; }
  .breadcrumb-bar > * { flex: none; }
  .breadcrumb-bar > [data-crumb-client] { flex: 0 1 auto; min-width: 3rem; }
  /* W2 (QA R5-N3): the page name never shrinks (it shrank with the client
     crumb, so "Recommendations" read "Recommendat…" with room to spare);
     the client crumb, or the section crumb when no client is shown, gives
     up the width. max-width:100% (above) still ellipsises a name wider
     than the whole bar. */
  .breadcrumb-bar > [aria-current="page"] { flex: none; }
  .breadcrumb-bar > .breadcrumb-group { flex: 0 1 auto; min-width: 3rem; }
  .breadcrumb-bar--client .breadcrumb-group,
  .breadcrumb-bar--client .breadcrumb-group + .breadcrumb-sep { display: none; }
}

/* -------------------- item 39: settings progressive disclosure ----------- */
.settings-advanced {
  margin-top: 1rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 0;
}
.settings-advanced__toggle {
  cursor: pointer;
  padding: 0.75rem 1rem;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--action);
  list-style: none;
}
.settings-advanced__toggle::-webkit-details-marker { display: none; }
.settings-advanced__toggle::before { content: '▸ '; }
.settings-advanced[open] > .settings-advanced__toggle::before { content: '▾ '; }

/* Z1 (2026-10-29): the client form's collapsed "Legacy tools" group (WordPress, GoHighLevel and their switches). The
   summary is a 44px target; the triangle is drawn here because the browser's own is hidden for a consistent look. */
.legacy-tools { margin: 1.5rem 0; border: 1px solid var(--rule); border-radius: var(--radius); padding: 0 1.25rem; }
.legacy-tools[open] { padding-bottom: 0.25rem; }
.legacy-tools__summary { display: flex; align-items: center; min-height: 44px; cursor: pointer; list-style: none; font-family: var(--ui); font-size: 14px; font-weight: 600; letter-spacing: 0.02em; color: var(--text-secondary); }
.legacy-tools__summary::-webkit-details-marker { display: none; }
.legacy-tools__summary::before { content: '\25B8'; width: 1em; }
.legacy-tools[open] > .legacy-tools__summary::before { content: '\25BE'; }
.legacy-tools__summary:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; border-radius: 4px; }
.legacy-tools fieldset.wp { margin: 1rem 0; }

/* -------------------- item 50: consistent status tags -------------------- */
.status-tag {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: capitalize;
}
.status-tag--planned { background: #fef3c7; color: #92400e; }
.status-tag--active  { background: #dbeafe; color: #1e40af; }
.status-tag--done    { background: #d1fae5; color: #065f46; }
.status-tag--failed  { background: #fee2e2; color: #991b1b; }
[data-theme="dark"] .status-tag--planned { background: #451a03; color: #fcd34d; }
[data-theme="dark"] .status-tag--active  { background: #1e3a5f; color: #93c5fd; }
[data-theme="dark"] .status-tag--done    { background: #064e3b; color: #6ee7b7; }
[data-theme="dark"] .status-tag--failed  { background: #450a0a; color: #fca5a5; }

/* -------------------- item 49: relaunch onboard button ------------------- */
.relaunch-onboard {
  display: block;
  width: 100%;
  text-align: center;
}

/* ==================== CAPTURE LEADS — Call Center / Quotes =============== */

.cc-section-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.75rem;
  color: var(--text);
}

.empty-row {
  text-align: center;
  color: var(--text-muted);
  padding: 2rem 1rem;
  font-size: 0.9rem;
}

/* Warm icon+sentence empty state for a table cell (visual-appeal gauntlet,
   2026-08-30, fixes #5/#7) - same language as the .empty card (Tenants,
   Approval Center: icon, reassuring full sentence, no bare fragment) sized
   to sit inside a single <td> instead of a standalone block, since Client
   Portal's rankings/social tables and the Feedback table don't have a
   sibling container to swap in a full .empty card without restructuring
   the table itself. */
.empty-row--warm {
  padding: 2rem 1rem;
  background: var(--empty-surface);
}
.empty-row--warm .empty-row__icon {
  display: block;
  font-size: var(--empty-icon-block);
  margin-bottom: 0.6rem;
  line-height: 1;
}
.empty-row--warm .empty-row__text {
  font-size: 14px;
  color: var(--empty-text);
}

/* Market-ready gauntlet item 4 (2026-09-06) — .empty-state (a <li>/<p> used
   for a couple of "nothing here yet" states outside a table, e.g. the
   dashboard's recent-activity list and the Social Scheduler's account
   list) had no CSS rule at all, rendering as unstyled default browser
   text — the plainest, coldest of the several empty-state treatments
   found still coexisting across the app. Same restrained warmth as
   .empty-row--warm (a small icon, muted secondary text), sized for a
   single line rather than a full block since both real usages are one
   sentence, not a whole panel. */
.empty-state {
  text-align: center;
  color: var(--empty-text);
  background: var(--empty-surface);
  border: var(--empty-rule);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  font-size: 14px;
  list-style: none;
}
.empty-state__icon {
  font-size: var(--empty-icon-inline);
  vertical-align: -0.15em;
  margin-right: 0.35rem;
}

/* Activity Log: de-emphasise (not hide) the highest-volume, lowest-stakes
   row type - fix #8, "give page_view lower visual weight" per the gauntlet
   spec. Still fully visible and readable, just quieter than a real
   sensitive-action row (login, password reset, client delete). A literal
   token (var(--text-muted), already the app's vetted-AA muted-text color
   everywhere else - .hint, .empty-row) rather than opacity: a faded token
   can silently drop below the contrast it reads as at full strength,
   where a swapped color stays checkable. */
.activity-row--quiet td {
  color: var(--text-muted);
}

.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.chat-msg--inbound,
.chat-msg--outbound {
  padding: 0.75rem;
  border-radius: 0.5rem;
  max-width: 90%;
}

.chat-msg--inbound {
  background: var(--card);
  border: 1px solid var(--rule);
  align-self: flex-start;
}

.chat-msg--outbound {
  background: var(--growth-light);
  align-self: flex-end;
}

.chat-msg__label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

.chat-msg__time {
  display: block;
  font-size: 0.7rem;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

.chat-msg__body {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.4;
}

.qt-details summary {
  cursor: pointer;
}

/* UX gauntlet (2026-08-30): the Meta-compliance account-security disclosure
   (Social Scheduler) still needs to exist and stay reachable, but shouldn't
   read as the same weight as the product UI around it — closed by default,
   quieter type when open. */
.soc-security-note {
  margin: 0.75rem 0 1rem;
}
.soc-security-note summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--text-muted);
}
.soc-security-note[open] summary {
  margin-bottom: 0.4rem;
}
.soc-security-note .hint {
  font-size: 13px;
  color: var(--text-muted);
}
/* QA-T13 (R4-13/R4-09): the post table's dates, times, platform names and
   row buttons wrapped letter-group by letter-group ("2026-/10-04", "Mar /
   post", "Dele"). They keep one line each now; the row buttons stack, each
   whole, and the held-post note keeps a readable measure. */
#soc-table .soc-c-platform,
#soc-table .soc-c-when,
#soc-table td:not(.soc-video-cell) .btn { white-space: nowrap; }
#soc-table .soc-c-actions .btn { display: flex; width: max-content; margin: 0.15rem 0; }
#soc-table .td-truncate { max-width: 14rem; }
#soc-table .soc-offer-held { display: inline-block; min-width: 10rem; }
/* R6-N3: the held row's action wraps (two short lines) instead of widening the Post column past the box. */
#soc-table td .btn.soc-confirm-offer { white-space: normal; max-width: 8.5rem; text-align: left; line-height: 1.25; }
#soc-table th, #soc-table td { padding-left: 0.6rem; padding-right: 0.6rem; }
/* R6-N10 (QA round 6): at tablet width (820) the table was 924px in a 666px box, so the row's first
   action (Post now / Schedule / Confirm the offer first) sat past the right edge. The post text
   column (full text in its title) and the cell padding give way there, so the first action is in
   the box; Mark posted / Edit / Delete follow, and the box's right-edge cue says there is more. */
@media (min-width: 601px) and (max-width: 1023px) {
  #soc-table .td-truncate { max-width: 5.5rem; }
  #soc-table .soc-offer-held { min-width: 8.5rem; }
  #soc-table th, #soc-table td { padding-left: 0.4rem; padding-right: 0.4rem; }
}

.btn--sm {
  padding: 0.25rem 0.5rem;
  font-size: 0.8rem;
}

/* ── Inbox layout ── */
.inbox-layout {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: 1rem;
  min-height: 400px;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  overflow: hidden;
}
@media (max-width: 768px) {
  .inbox-layout { grid-template-columns: 1fr; }
}
.inbox-list {
  border-right: 1px solid var(--rule);
  overflow-y: auto;
  max-height: 70vh;
}
.inbox-list-item {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  transition: background 0.15s;
}
.inbox-list-item:hover { background: var(--bg-secondary); }
.inbox-list-item.active { background: var(--growth-light); }
.inbox-list-item__phone {
  font-weight: 600;
  font-size: 0.9rem;
}
.inbox-list-item__preview {
  font-size: 0.8rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.inbox-list-item__meta {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.inbox-unread-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--action);
  flex-shrink: 0;
}
.inbox-thread {
  display: flex;
  flex-direction: column;
  padding: 0;
}
/* UX gauntlet (2026-08-30): fills the right grid track's ~400px of dead
   space when no thread is selected — see the markup comment above
   #inbox-placeholder in index.html. Same [hidden]-vs-author-rule
   specificity trap as .inbox-thread[hidden] just below, so it gets the
   same explicit override rather than relying on the UA default. */
.inbox-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 2rem;
  text-align: center;
  color: var(--empty-text);
}
/* U4: the pane tier of the empty-state system (see .empty). The icon was faded with opacity: 0.6, the one empty
   state whose icon read as disabled rather than as the same family. */
.inbox-placeholder__icon {
  font-size: var(--empty-icon-block);
  line-height: 1;
}
.inbox-placeholder p {
  margin: 0;
  font-size: 14px;
}
.inbox-placeholder[hidden] {
  display: none;
}
.inbox-thread__header {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--rule);
  font-weight: 600;
}
.inbox-thread .chat-thread {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
  max-height: 50vh;
}
.inbox-reply {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--rule);
}
.inbox-reply input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.9rem;
  background: var(--bg);
  color: var(--text);
}

/* ── Email campaign step cards ── */
.ecamp-step {
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  padding: 1rem;
  margin-top: 0.75rem;
  background: var(--bg-secondary);
}
.ecamp-step__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
}
.ecamp-step__header strong {
  font-size: 0.85rem;
}
.ecamp-step label span {
  font-size: 0.8rem;
  color: var(--text-muted);
}
.ecamp-step textarea {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.85rem;
  resize: vertical;
  background: var(--bg);
  color: var(--text);
}
.ecamp-step input[type="number"] {
  width: 80px;
  padding: 0.35rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-family: inherit;
  background: var(--bg);
  color: var(--text);
}

/* UX maturity gauntlet (2026-08-10): shared grid for the Images library
   and, later, the unified Client Profile / Asset Library view — not
   duplicated per view. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 0.75rem;
  margin-top: 0.75rem;
}
.gallery-card {
  border: 1px solid var(--rule);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}
.gallery-card__thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--bg-alt, rgba(127, 127, 127, 0.1));
  display: block;
}
.gallery-card__thumb--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  color: var(--text-muted, #888);
  text-align: center;
  padding: 0.5rem;
}
.gallery-card__meta {
  padding: 0.5rem;
  font-size: 0.8rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.gallery-card__title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gallery-card__actions {
  display: flex;
  gap: 0.4rem;
}
.gallery-card__actions button {
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
}

/* UX maturity gauntlet (2026-08-10), Task #27: Client Profile / Asset Library. */
.cprofile-section {
  margin-top: 1.75rem;
}
.cprofile-section h2 {
  font-size: 1rem;
  margin-bottom: 0.25rem;
}
.cprofile-section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.cprofile-section__head h2 { margin-bottom: 0; }
.gallery-card__thumb--placeholder {
  font-size: 1.75rem;
}
video.gallery-card__thumb {
  background: #000;
}
.client-list__profile-btn {
  flex: none;
  width: auto !important;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 1rem;
  padding: 0.25rem 0.5rem;
  opacity: 0.6;
}
.client-list__profile-btn:hover {
  opacity: 1;
}
#client-list li {
  display: flex;
  align-items: center;
}
#client-list li button[data-id] {
  flex: 1;
  /* Bug hunt (2026-08-30, "don't embarrass me" pass): this button's
     .t-name/.t-where spans reuse class names styled under .tickets
     elsewhere in this file, but this button was never inside a .tickets
     container — so none of that styling ever applied. With no internal
     layout at all, a business name longer than a couple words (e.g.
     "ARBA · The Ember Hour") wrapped across 3-4 cramped lines instead of
     sitting on one line next to the location. min-width: 0 is required
     here — without it, a flex item won't shrink below its content's
     natural width, which is exactly what defeats the child's
     text-overflow: ellipsis below.
  */
  /* QA-T13 (R4-16): one row of avatar + name + city + folder left the name
     about 6-8 letters ("QA Rout...", "Zzzqorvex ..." twice) in the 230px
     desktop column and on a phone, so two clients could not be told apart.
     Now two lines: the name in full on the first (wrapping, never
     ellipsised), the city on the second under it. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 0.6rem;
  row-gap: 0.1rem;
  align-items: center;
  min-width: 0;
}
#client-list li button[data-id] .client-avatar { grid-row: 1 / span 2; }
#client-list li button[data-id] .t-name {
  grid-column: 2;
  min-width: 0;
  white-space: normal;
  overflow-wrap: break-word; /* breaks a word only when it alone is wider than the column */
  line-height: 1.3;
}
#client-list li button[data-id] .t-where {
  grid-column: 2;
  justify-self: start;
  margin-left: 0;
  /* QA-T4 D6 (2026-10-16): was flex-shrink:0, so a 60-letter town
     ("Llanfairpwll...") kept its full 507px, pushed the row to 608px on a
     390px phone and squeezed .t-name to nothing: the selected row showed
     the city and NOT the client's name. It now shrinks and ellipsises,
     capped so the name keeps the rest of the row (the full name is in the
     button's title). 60% here keeps a normal "CANONSBURG PA" whole on
     desktop exactly as before; the phone block below tightens it to 34%.
     QA-T13 (R4-16): it now has its own line under the name, so it may use
     the whole line (100%) and still ellipsises past that. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  color: var(--text-muted);
}

/* UX maturity gauntlet (2026-08-10), Task #29. */
.master-lib-row {
  cursor: pointer;
}
.master-lib-row:hover,
.master-lib-row:focus-visible {
  background: var(--desk);
}

/* UX maturity gauntlet (2026-08-10), Task #34. */
.module-profile-link {
  display: inline-block;
  flex: none; /* QA-T13 (R4-11): never squeezes the picker beside it, see .setup__field--inline */
  margin-left: 0.5rem;
  font-size: 0.82rem;
  white-space: nowrap;
  color: var(--action, #e8a63c);
  text-decoration: none;
}
.module-profile-link:hover {
  text-decoration: underline;
}

/* ================================================================
   UX gauntlet (2026-08-30), screen group: call-center / inbox /
   quotes / appointments / review-mgmt / reactivation.
   Screen-scoped fixes only — see each comment for the screen it
   belongs to. Nothing below touches global shell CSS.
   ================================================================ */

/* Inbox: #inbox-thread carries `hidden` in the markup until a thread
   is opened (see loadInbox()/openInboxThread() in app.js), but
   `.inbox-thread { display: flex }` above (line ~6091) is author CSS
   with equal specificity to the browser's UA rule `[hidden] {
   display: none }` — author rules always win over UA rules, so the
   `hidden` attribute was silently ignored. Confirmed live via
   Playwright: hasAttribute('hidden') === true while
   getComputedStyle(...).display === 'flex', occupying real layout
   space (468x398 at 1440px). Result: an empty message thread panel
   with a live, empty reply form rendered on Inbox before any
   conversation was ever selected, at both viewports. This is the
   standard `[hidden]` override fix — scoped to this one class only,
   doesn't touch the base .inbox-thread rule's declarations. */
.inbox-thread[hidden] {
  display: none;
}

/* ----------------------- review: before you approve ----------------------- */
/* Wave 1C (2026-10-09). The SEO checklist, the six SEO tags and the
   confirm-offer box on Review & Polish (index.html #review-approve). Status is
   always a word plus a mark, never colour alone. Every class here that sets
   display:flex/grid and is toggled with .hidden in app.js carries its own
   [hidden] override (see .publish-confirm[hidden] for why). */
.approve {
  margin: 0 0 1.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--stock-warm);
  display: grid;
  gap: 1.25rem;
}
.approve[hidden] {
  display: none;
}
.approve__title {
  margin: 0;
  font-size: 1.05rem;
}
.approve__subtitle {
  margin: 0 0 0.4rem;
  font: 700 14px / 1.4 var(--ui);
  color: var(--text);
}

.approve-offers {
  padding: 0.85rem 1rem;
  border: 1px solid var(--ink-reject);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--stock);
  display: grid;
  gap: 0.6rem;
}
.approve-offers[hidden] {
  display: none;
}
/* W4C (Monday plan defect 14): once the owner's confirmation is ticked the box has done its job, so it calms down
   (the red edge and title are for a claim nobody has checked yet). Review and Month share the box. */
.approve-offers:has(.approve-offers__confirm input:checked) {
  border-color: var(--rule);
  border-left-color: var(--growth);
}
.approve-offers:has(.approve-offers__confirm input:checked) .approve-offers__title {
  color: var(--text);
}
.approve-offers__title {
  margin: 0;
  font-size: 1rem;
  color: var(--ink-reject);
}
.approve-offers__lede {
  margin: 0;
  font-size: 14px;
  color: var(--text-secondary);
}
.approve-offers__list {
  margin: 0;
  padding-left: 1.1rem;
  display: grid;
  gap: 0.4rem;
  font-size: 14px;
}
.approve-offers__where {
  font: 600 12px / 1.4 var(--ui);
  color: var(--text-muted);
  margin-right: 0.35rem;
}
.approve-offers__context {
  display: block;
  color: var(--text-muted);
  font-size: 13px;
}
.approve-offers__confirm {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}
.approve-offers__confirm input {
  margin-top: 0.2rem;
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
}

.approve-tags__list {
  margin: 0 0 0.4rem;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.approve-tags__list[hidden] {
  display: none;
}
.approve-tags__tag {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--stock);
  font-size: 13px;
}
.approve-tags__actions {
  margin: 0.4rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.approve-tags__actions[hidden] {
  display: none;
}
#review-tags-note {
  margin: 0;
}

.seo-board__notice {
  margin: 0 0 0.75rem;
  font-size: 13px;
  color: var(--text-muted);
  max-width: 72ch;
}
.seo-board__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
}
.seo-col__title {
  margin: 0;
  font-size: 15px;
}
.seo-col__tally,
.seo-col__note {
  margin: 0.15rem 0 0.5rem;
  font-size: 13px;
  color: var(--text-muted);
}
.seo-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.35rem;
}
.seo-item {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 0.6rem;
  align-items: start;
  font-size: 14px;
}
.seo-item__badge {
  padding: 0.1rem 0.5rem;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  justify-self: start;
}
.seo-item[data-status="good"] .seo-item__badge {
  color: var(--growth);
  background: var(--growth-bg);
}
.seo-item[data-status="ok"] .seo-item__badge {
  color: var(--ink-y-dark);
  background: var(--stock-amber);
}
.seo-item[data-status="bad"] .seo-item__badge {
  color: var(--ink-reject);
  background: var(--stock-rose);
}
/* W4C (defect 6): not set up yet is neither fine nor a failure. */
.seo-item[data-status="neutral"] .seo-item__badge {
  color: var(--text-secondary);
  background: transparent;
  border: 1px dashed var(--rule);
}
.seo-item__text {
  display: grid;
  min-width: 0;
}
.seo-item__label {
  font-weight: 600;
}
.seo-item__detail {
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}
.seo-item__rule {
  font-size: 12px;
  color: var(--text-muted);
}

/* Each social post's blog link, under its word count. */
.social-link {
  margin: 0.3rem 0 0;
  font-size: 13px;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
.social-link__note {
  color: var(--ink-k-40);
}
@media (min-width: 1024px) {
  /* The social half's own row 5, beside the article half's regen row (the
     fold places every part explicitly, see .fold .count). */
  .fold .social-link {
    grid-row: 5;
  }
}

/* A sent draft whose Yoast fields did not stick, and a bulk-schedule warning
   (no connected account): both need reading, neither is a failure. */
.ship-links li[data-state="warning"] {
  color: var(--ink-reject);
}
#review-schedule-social-status[data-state="warning"] {
  color: var(--ink-reject);
}

@media (max-width: 719px) {
  .approve {
    padding: 0.85rem 0.9rem;
  }
  .seo-board__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .seo-item {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.15rem;
  }
  .approve-tags__actions {
    flex-wrap: wrap;
  }
}

/* B10 Month screen (public/month.js). Tokens only, so dark mode follows. */
.month-root { display: grid; gap: 1.25rem; max-width: 72rem; }
.month-root[aria-busy='true'] { opacity: 0.6; }
.month-banner { padding: 0.75rem 1rem; border: 1px solid var(--rule); border-left-width: 4px; border-radius: var(--radius); background: var(--stock); }
.month-banner[data-kind='ready'] { border-left-color: var(--growth); }
.month-banner[data-kind='blocked'] { border-left-color: var(--ink-reject); }
/* D4: a delivered month: ONE calm panel (green check), no Approve and send. */
.month-banner[data-kind='delivered'] { display: grid; gap: 0.6rem; border-left-color: var(--growth); overflow-wrap: anywhere; }
/* W1a (MED 7): delivered, but the social posts were only saved: a partial outcome, amber, never the green tick. */
.month-banner[data-kind='partial'] { display: grid; gap: 0.6rem; border-left-color: var(--ink-y-mid); overflow-wrap: anywhere; }
.month-banner[data-kind='partial'] .month-delivered__check { background: var(--stock-amber); color: var(--ink-y-dark); box-shadow: inset 0 0 0 1px var(--ink-y-mid); font-weight: 700; }
.month-delivered__title { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 16px; }
.month-delivered__check { display: inline-grid; place-items: center; flex: none; width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--growth); color: var(--on-growth); font-size: 13px; line-height: 1; }
.month-delivered__line { margin: 0; font-size: 14px; color: var(--text-secondary); }
.month-delivered__sub { margin-top: -0.35rem; padding-left: 2rem; } /* F1: the schedule line is the title's subtitle (lined up past the check) */
.month-delivered__actions, .month-send__done { margin: 0; }
.month-delivered .btn { white-space: normal; text-align: left; max-width: 100%; }
.month-send__done { font-size: 14px; color: var(--text-secondary); }
.month-banner__title { margin: 0 0 0.35rem; font-weight: 600; }
.month-banner ul { margin: 0 0 0.35rem; padding-left: 1.1rem; font-size: 14px; }
.month-banner__note { margin: 0.25rem 0 0; font-size: 13.5px; color: var(--text-secondary); }
.month-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1rem; }
.month-card { display: grid; gap: 0.5rem; align-content: start; padding: 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--stock); }
.month-card__title, .month-h { margin: 0; font-size: 1rem; }
.month-card__meta, .month-card__status, .month-card__empty { margin: 0; font-size: 14px; color: var(--text-secondary); }
.month-card__articles { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.6rem; }
.month-card__article { display: grid; gap: 0.15rem; font-size: 14px; }
/* W1A: sentence case in the body face ("Top-10 · post 1 · Not sent yet"), not monospace small caps: a label to read, not a code. */
.month-card__kind, .month-post__where { font-size: 13px; font-weight: 600; line-height: 1.4; color: var(--text-secondary); }
.month-card__name { font-weight: 600; overflow-wrap: anywhere; }
.month-card__seo, .month-card__sent, .month-card__fix { color: var(--text-secondary); font-size: 13px; }
.month-card .btn { justify-self: start; }
.month-posts { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.75rem; }
.month-post { display: grid; grid-template-columns: 96px 1fr; gap: 0.75rem; align-items: start; }
.month-post__img { width: 96px; height: 96px; object-fit: cover; border-radius: var(--radius); background: var(--stock-warm); }
.month-post__img--none { display: grid; place-items: center; font-size: 12px; color: var(--text-muted); border: 1px dashed var(--rule); }
.month-post__body { display: grid; gap: 0.2rem; min-width: 0; }
.month-post__text { margin: 0; font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
.month-post__link, .month-post__claims { font-size: 13px; color: var(--text-secondary); overflow-wrap: anywhere; }
.month-post__claims { color: var(--ink-reject); }
/* U4: the line tier of the empty-state system (see .empty): month.js uses it for status lines inside a Month card
   ("Already delivered: nothing left to send", the hub line), so it shares the text pair only, never a card. */
.month-empty { margin: 0; font-size: 14px; color: var(--empty-text); }
.month-proof { margin: 0; padding-left: 1.1rem; font-size: 14px; overflow-wrap: anywhere; }
/* R6-N4 (QA round 6): the month backup (public/month.js renderBackupHtml) had no rules at all, so its
   help ran on at body size beside the button and "Backup downloaded..." was appended to the same
   sentence. Now: the quiet button, then its help and its status each on their own line under it,
   at the proof list's size and colour; the status line takes no room until it says something. */
.month-backup { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.75rem; margin: 0.25rem 0 0; }
.month-backup__help, .month-backup__status { flex: 1 1 100%; margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-secondary); }
.month-backup__status { font-weight: 600; }
/* W2A: "Post it yourself" under a post (public/posting-kit.js). Folded by default, because a month has twenty posts
   and an open form under each would bury the list; the folded row still says when it is due. The summary is a
   44px target (same as .month-withdraw__summary); no border box, only a hairline, so the post stays the main thing. */
.post-kit { margin-top: 0.35rem; border-top: 1px solid var(--rule-soft); }
.post-kit__summary { display: flex; flex-wrap: wrap; align-items: center; gap: 0.15rem 0.6rem; min-height: 44px; cursor: pointer; font-size: 14px; }
.post-kit__summary:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; border-radius: 4px; }
/* display:flex drops the browser's own disclosure triangle, so the row says it opens (no animation: nothing to reduce). */
.post-kit__summary::-webkit-details-marker { display: none; }
.post-kit__summary::before { content: '\25B8'; width: 1em; color: var(--text-secondary); }
.post-kit[open] > .post-kit__summary::before { content: '\25BE'; }
.post-kit__title { font-weight: 600; color: var(--text); }
.post-kit__when { font-size: 13px; color: var(--text-secondary); }
.post-kit[data-kit-state="posted"] .post-kit__title { color: var(--growth-hover); } /* --growth-hover: 5.6:1 on white; --growth (4.35:1) is under 4.5:1 at 14px. No hex here: tests/review-approve-ui.test.js forbids hard-coded colours below the Review section, comments included */
.post-kit__body { display: grid; gap: 0.6rem; padding: 0.15rem 0 0.6rem; }
.post-kit__tools, .post-kit__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.post-kit__tools a.btn { text-decoration: none; } /* reads as the buttons beside it */
.post-kit__mark { display: grid; gap: 0.3rem; margin: 0; }
.post-kit__label { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.post-kit__url { flex: 1 1 16rem; min-width: 0; min-height: 44px; }
.post-kit__url[aria-invalid="true"] { border-color: var(--alert); }
.post-kit__hint, .post-kit__note, .post-kit__done { margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-secondary); overflow-wrap: anywhere; }
.post-kit__done { color: var(--text); }
.post-kit__msg { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.post-kit__msg:empty { display: none; }
.post-kit__msg[data-kind="error"] { color: var(--alert); }
.month-backup__status:empty { display: none; }
@media (max-width: 600px) {
  .month-backup__link { min-height: 44px; display: inline-flex; align-items: center; }
}
.month-send { display: grid; gap: 0.4rem; justify-items: start; padding-top: 0.5rem; border-top: 1px solid var(--rule-soft); }
.month-send__status { margin: 0; font-size: 14px; }
.month-send__status[data-kind='error'] { color: var(--ink-reject); }
.month-send__status[data-kind='progress'] { font-weight: 600; overflow-wrap: anywhere; } /* F1: what the send is doing */
/* W1A: the send's four steps (public/month.js renderSendProgressHtml): a tick when done, a spinner on the current one. */
.month-progress { display: grid; gap: 0.45rem; margin: 0.25rem 0 0; padding: 0; list-style: none; font-weight: 400; }
.month-progress__step { display: flex; align-items: center; gap: 0.6rem; min-height: 1.75rem; font-size: 14px; color: var(--text-secondary); }
.month-progress__step[data-state='current'] { color: inherit; font-weight: 600; }
.month-progress__step[data-state='done'], .month-progress__step[data-state='warned'], .month-progress__step[data-state='saved'] { color: inherit; }
.month-progress__mark { display: inline-grid; place-items: center; flex: none; width: 1.25rem; height: 1.25rem; border-radius: 50%; border: 1.5px solid var(--rule); font-size: 12px; font-weight: 700; line-height: 1; }
.month-progress__step[data-state='done'] .month-progress__mark { border-color: var(--growth); background: var(--growth); color: var(--on-growth); }
.month-progress__step[data-state='warned'] .month-progress__mark, .month-progress__step[data-state='saved'] .month-progress__mark { border-color: var(--ink-y-mid); background: var(--stock-amber); color: var(--ink-y-dark); }
.month-progress__step[data-state='failed'] .month-progress__mark, .month-progress__step[data-state='unknown'] .month-progress__mark { border-color: var(--ink-reject); color: var(--ink-reject); }
.month-progress__step[data-state='current'] .month-progress__mark { border-color: var(--action); border-top-color: transparent; animation: btn-spin 0.8s linear infinite; }
.month-progress__word { color: var(--text-secondary); font-weight: 400; }
.month-progress__word::before { content: '· '; }
@media (prefers-reduced-motion: reduce) {
  .month-progress__step[data-state='current'] .month-progress__mark { animation: none; border-top-color: var(--action); background: var(--stock-warm); }
}
/* O1 (A): "Take this month back": a quiet, folded secondary control at the end of the Month screen (public/month.js renderWithdrawHtml). */
.month-banner[data-kind='withdrawn'] { border-left-color: var(--trust); overflow-wrap: anywhere; }
.month-withdraw-wrap { display: grid; gap: 0.4rem; justify-self: stretch; margin-top: 0.75rem; padding-top: 0.5rem; border-top: 1px dashed var(--rule-soft); }
.month-withdraw__summary { display: inline-flex; align-items: center; min-height: 44px; cursor: pointer; font-size: 14px; color: var(--text-secondary); text-decoration: underline; text-underline-offset: 3px; }
.month-withdraw__summary:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; border-radius: 4px; }
.month-withdraw__body { display: grid; gap: 0.5rem; justify-items: start; padding: 0.75rem 1rem; border: 1px solid var(--rule); border-left: 4px solid var(--ink-reject); border-radius: var(--radius); background: var(--stock); font-size: 14px; overflow-wrap: anywhere; }
.month-withdraw__lede, .month-withdraw__status { margin: 0; }
.month-withdraw__list { margin: 0; padding-left: 1.1rem; display: grid; gap: 0.25rem; }
.month-withdraw__confirm { display: flex; align-items: flex-start; gap: 0.5rem; min-height: 44px; }
.month-withdraw__confirm input { margin-top: 0.2rem; flex: none; }
.month-withdraw__go { white-space: normal; text-align: left; max-width: 100%; }
.month-withdraw__status { font-size: 14px; overflow-wrap: anywhere; }
.month-withdraw__status[data-kind='error'] { color: var(--ink-reject); }
.month-withdraw__status[data-kind='progress'] { font-weight: 600; }
/* N2 connection pre-flight above Approve and send; N4 recheck; N5 client summary. Long labels wrap at 375px. */
.month-preflight { display: grid; gap: 0.35rem; justify-self: stretch; padding: 0.6rem 0.85rem; border: 1px solid var(--rule); border-left-width: 4px; border-left-color: var(--ink-reject); border-radius: var(--radius); background: var(--stock); font-size: 14px; overflow-wrap: anywhere; }
.month-preflight[data-kind='warn'] { border-left-color: var(--rule); }
.month-preflight__msg, .month-preflight__act { margin: 0; }
.month-preflight__msg { font-weight: 600; }
.month-preflight__ask { color: var(--text-secondary); }
.month-recheck { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.4rem 0 0; }
.month-recheck__why, .month-summary__status { font-size: 13.5px; color: var(--text-secondary); }
.month-preflight .btn, .month-recheck .btn, .month-summary .btn, .month-send__csv .btn { white-space: normal; text-align: left; max-width: 100%; }
.month-report__next { display: block; font-weight: 600; }
/* A1 (R4-06): the decision bar under the headline: readiness line + the same Approve button. */
.month-decide { display: grid; gap: 0.4rem; justify-items: start; padding: 0.75rem 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--stock); overflow-wrap: anywhere; }
.month-decide__line { margin: 0; font-weight: 600; font-size: 15px; }
.month-decide .btn, .month-send > .btn { white-space: normal; text-align: center; max-width: 100%; }
/* X3: the Muse panel above the month cards: the decision bar's shape, marked as the Muse lane. */
.month-muse { border-left: 4px solid var(--trust); margin-top: 0.75rem; }
.month-muse__note { margin: 0; font-size: 14px; color: var(--text-secondary); }
.month-banner__worth { color: var(--text-secondary); }
.month-banner--details { border-left-width: 1px; }
.month-banner--details > .month-banner__note:first-child { margin-top: 0; }
.month-summary { display: grid; gap: 0.5rem; }
.month-summary__text { margin: 0; padding: 0.75rem 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--stock); font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
.month-summary__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; }
/* P8: the email-the-summary block reuses .month-summary; a refusal reads as one, the status line never goes empty-wide. */
.month-mail .month-summary__status { margin: 0; }
.month-mail .month-summary__status[data-kind='error'] { color: var(--ink-reject); font-weight: 600; overflow-wrap: anywhere; }
.month-mail .month-summary__status:empty { display: none; }
/* A19 Prepare this month */
.month-prepare { display: grid; gap: 0.5rem; padding: 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--stock); }
.month-prepare__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin: 0; }
.month-prepare__cost { flex: 1 1 16rem; font-size: 13px; line-height: 1.45; color: var(--text-secondary); } /* W1A: the cost, beside the one-click Prepare */
.month-prepare__status { display: grid; gap: 0.35rem; font-size: 14px; overflow-wrap: anywhere; }
.month-prepare__status:empty { display: none; }
.month-prepare__msg { margin: 0; font-size: 14px; }
/* F1: once the month has content, Prepare folds away under the posts. */
.month-prepare--folded { display: block; }
.month-prepare--folded > * + * { margin-top: 0.5rem; }
.month-prepare__summary { cursor: pointer; font-weight: 600; font-size: 15px; }
.month-prepare__msg[data-kind='success'] { font-weight: 600; }
/* B2: the calm card under Prepare's rows while it runs. No motion at all, so reduced motion needs nothing. */
.month-wait { display: grid; gap: 0.35rem; padding: 0.75rem 1rem; border: 1px solid var(--rule); border-radius: var(--radius); }
.month-wait__h { margin: 0; font-size: 14px; font-weight: 600; }
.month-wait__facts { margin: 0; padding-left: 1.1rem; display: grid; gap: 0.2rem; font-size: 14px; overflow-wrap: anywhere; }
.month-wait__line { margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-secondary); }
.month-prepare__msg[data-kind='error'], .month-prepare li[data-status='failed'] { color: var(--ink-reject); }
@media (max-width: 600px) {
  .month-post { grid-template-columns: 64px 1fr; }
  .month-post__img { width: 64px; height: 64px; }
}

/* B2 Delivery log (public/delivery-log.js) */
/* QA-T13 (R4-10): `overflow-wrap: anywhere` here made every cell's minimum
   width one letter, so the auto table layout squeezed 12 columns into the box
   and broke words mid-letter ("Wha t a Heat Pum p", "Sche duled", "not publ
   ic"). Normal wrapping now (a word breaks only when it alone is wider than
   its cell), with real minimums on the text columns; the table scrolls
   sideways inside .table-scroll (edge shadow cue) instead of shredding words.
   Short values never wrap; a long live URL is cut with an ellipsis (full
   address in the link's title). */
.dlog-table td { vertical-align: top; overflow-wrap: break-word; }
.dlog-table .dlog-c-month,
.dlog-table .dlog-c-status,
.dlog-status { white-space: nowrap; }
/* W6B: five columns by default (public/delivery-log.js); "Where it lives" and "Scheduled" now wrap at a real
   minimum, so the hub link and the live address stay on screen at 1280 instead of past the right edge. */
.dlog-table .dlog-c-link { min-width: 9.5rem; }
.dlog-table .dlog-c-when { min-width: 10rem; font-size: 14px; }
.dlog-table .dlog-c-title { min-width: 13rem; }
.dlog-table .dlog-c-type { min-width: 7rem; }
.dlog-table .dlog-c-keyword { min-width: 9rem; }
.dlog-table .dlog-c-h2 { min-width: 14rem; }
.dlog-table .dlog-c-url a {
  display: inline-block;
  max-width: 9rem; /* W7: with the address under Where it lives the five default columns still fit 774 px at 1280 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}
.dlog-h2 { margin: 0; padding-left: 1.1rem; font-size: 13px; }
.dlog-social { white-space: pre-wrap; max-width: 32rem; font-size: 13px; }
.dlog-status { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; background: var(--rule-soft); color: var(--text-secondary); }
.dlog-status[data-status="live"] { background: var(--growth-bg); color: var(--growth); }
.dlog-status[data-status="scheduled"] { background: var(--trust-bg); color: var(--trust); }
.dlog-status[data-status="failed"] { background: var(--alert-light); color: var(--ink-reject); }
#dlog-status[data-kind="error"] { color: var(--ink-reject); }

/* ==========================================================================
   QA-T4 (2026-10-16): the 44px touch floor (QA D7 + D12, WebKit phone crawl).
   The round-2 WebKit crawl at 390px found 184 small-target clusters; they
   collapse into the families below. One rule per family, all inside ONE
   phone/touch block, so a mouse-and-keyboard desktop keeps today's density
   (a touch laptop or iPad at desktop width gets the floor too, on purpose).
   Links inside running text (.month-proof li a, .hint a, small a) are left
   alone: WCAG 2.5.8 exempts inline links and a 44px line box would break
   the sentence. Pinned by tests/phone-layout-floor.test.js.
   ========================================================================== */
@media (pointer: coarse), (max-width: 600px) {
  /* Fields. WebKit draws a native <select> as a fixed ~24px menulist and
     ignores padding and min-height (measured: 24px either way), so selects
     drop the native look for the same padded field plus a drawn chevron. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]):not([type="file"]) {
    min-height: 44px;
  }
  input[type="color"] { min-width: 44px; }
  select:not([multiple]):not([size]) {
    min-height: 44px;
    -webkit-appearance: none;
    appearance: none;
    /* !important on the chevron and its gutter only: with the native arrow
       gone, an id rule or inline style that sets `background` / `padding`
       (#longform-regen-section, #setup-tenant-select) would otherwise erase
       the only sign this is a dropdown or run the text under it. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%238a847e' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: right 0.9rem center !important;
    background-size: 12px 8px !important;
    padding-right: 2.4rem !important;
    max-width: 100%;
    /* WebKit lays a long selected option (the QA long-name client) out at
       full width inside the select and lets it count toward the page's
       scroll width (Quotes/Social/Appointments 916px, Setup 807px at 390)
       even though the select box itself fits. Clip it to the box. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .regen-row__field { flex: 1 1 100%; }
  #longform-regen-section { flex: 1 1 12rem; }

  /* Checkboxes: a 44px hit box drawn as a 20px box. The -11px margin keeps
     the layout footprint at 22px, so rows do not grow. */
  input[type="checkbox"],
  .login-card .setup__field--checkbox input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 44px;
    height: 44px;
    min-width: 44px; /* the Settings toggles carry an inline width:auto */
    min-height: 44px;
    margin: -11px;
    flex: none;
    display: inline-grid;
    place-items: center;
    vertical-align: middle;
    background: none;
    border: 0;
    cursor: pointer;
  }
  input[type="checkbox"]::before,
  input[type="checkbox"]::after {
    content: "";
    grid-area: 1 / 1;
  }
  input[type="checkbox"]::before {
    width: 20px;
    height: 20px;
    box-sizing: border-box;
    border: 2px solid var(--text-muted);
    border-radius: 5px;
    background: var(--stock);
    transition: background-color 150ms ease, border-color 150ms ease;
  }
  input[type="checkbox"]::after {
    width: 6px;
    height: 11px;
    border: solid var(--on-action);
    border-width: 0 2.5px 2.5px 0;
    transform: translateY(-1px) rotate(45deg);
    opacity: 0;
  }
  input[type="checkbox"]:checked::before { background: var(--action); border-color: var(--action); }
  input[type="checkbox"]:checked::after { opacity: 1; }
  input[type="checkbox"]:disabled { opacity: 0.5; cursor: not-allowed; }
  input[type="checkbox"]:focus-visible { outline: 2px solid var(--action); outline-offset: -7px; }

  /* Compact buttons: the box grows to 44px, the look does not change
     (a native button centres its content). */
  #whats-new-trigger,
  #theme-toggle,
  #jason-setup-tab,
  .notice__dismiss,
  .db-card__name,
  .db-stage,
  .settings-jumpnav__link,
  .admin-jumpnav__link,
  .field-toggle,
  .assistant-bar__chip,
  .assistant-bar__reset,            /* D3: the assistant bar on the landing (25-40px tall before) */
  .assistant-bar__capabilities-link,
  .assistant-bar__clear-memory,
  .assistant-bar__attach,
  .assistant-bar__mic,
  .cc-strip__name,
  .cc-strip__go,
  .progress-ring-card__link,
  .btn--sm {
    min-height: 44px;
    min-width: 44px;
  }
  /* Settings / Admin jump-navs are sticky. Wrapped at phone width they were
     already ~20 rows of pills pinned over the page; with 44px pills they
     would outgrow the screen. One scrollable row instead. */
  .settings-jumpnav {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* The scrollbar is hidden, so fade both ends to show the row continues;
       app.js (revealJumpLink) keeps the active pill scrolled into view. */
    -webkit-mask-image: linear-gradient(to right, transparent 0, black 14px, black calc(100% - 28px), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, black 14px, black calc(100% - 28px), transparent 100%);
  }
  .settings-jumpnav__link { flex: none; }
  /* W6B (Apple-lens rehearsal 4): Admin has four links, not Settings' many.
     As a scrolling row at 390px "Recent content" was cut in half under the
     fade with nothing saying the row moved. Four equal cells instead, all in
     view, a two-word label on two lines inside its 44px button. */
  .admin-jumpnav {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.3rem;
    padding: 0.4rem;
  }
  .admin-jumpnav__link {
    white-space: normal;
    line-height: 1.15;
    padding: 0.25rem 0.3rem;
    border-radius: var(--radius-lg);
    text-align: center;
  }
  /* Setup's client picker shares an inline flex row with its label; on a
     phone the select was squeezed to 2px wide. Let the row wrap. */
  .setup-picker { flex-wrap: wrap; }
  #setup-tenant-select { min-width: min(100%, 12rem); }
  /* Same shape: Admin's library search + client filter row (crawl: the
     select squeezed to 21px). Inline styles set display:flex only. */
  #admin-sec-fleet-library .admin-card__head > div { flex-wrap: wrap; }
  #master-lib-search, #master-lib-client { min-width: min(100%, 12rem); }
  /* The password eye sits inside its field: give it the field's right edge. */
  .field-toggle { right: 0; }
  .field-toggle-wrap input { padding-right: 3rem; }

  /* Standalone links: inline-flex so the 44px box centres the text. */
  #view-gallery .cgal-card__link,
  .cc-strip__more,
  .module-profile-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    min-width: 44px;
  }
  /* Breadcrumb crumbs keep their ellipsis (inline-flex would defeat it). */
  .breadcrumb-bar a {
    display: inline-block;
    min-height: 44px;
    min-width: 44px;
    line-height: 44px;
  }
  summary {
    min-height: 44px;
    min-width: 44px;
    padding-block: 0.7rem;
    box-sizing: border-box;
  }
}

/* B3 Client gallery (public/gallery.js, A18). Own cgal- prefix, scoped to #view-gallery: the
   Client Profile / Images asset grid already owns .gallery-grid / .gallery-card (above). Tokens only. */
#view-gallery .cgal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr)); gap: 1rem; max-width: 80rem; }
#view-gallery .cgal-card { display: grid; gap: 0.6rem; align-content: start; min-width: 0; padding: 1rem; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--stock); }
#view-gallery .cgal-card__head { display: grid; grid-template-columns: 56px 1fr; gap: 0.75rem; align-items: center; }
#view-gallery .cgal-card__logo { width: 56px; height: 56px; object-fit: contain; border-radius: var(--radius); background: var(--stock-warm); }
#view-gallery .cgal-card__logo--initials { display: grid; place-items: center; font: 700 18px / 1 var(--ui); color: var(--text-secondary); border: 1px solid var(--rule-soft); }
#view-gallery .cgal-card__id { min-width: 0; }
#view-gallery .cgal-card__name { margin: 0; font-size: 1rem; overflow-wrap: anywhere; }
#view-gallery .cgal-card__meta { margin: 0.15rem 0 0; font-size: 13.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
#view-gallery .cgal-card__cta, #view-gallery .cgal-card__links, #view-gallery .cgal-card__foot { margin: 0; font-size: 14px; overflow-wrap: anywhere; }
#view-gallery .cgal-card__links { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; }
#view-gallery .cgal-card__kind, #view-gallery .cgal-chip__label { font: 600 12px / 1.4 var(--ui); color: var(--text-muted); } /* U2: sentence-case labels, UI face */
#view-gallery .cgal-chips { margin: 0; padding: 0.6rem 0 0; list-style: none; display: grid; gap: 0.45rem; border-top: 1px solid var(--rule-soft); }
#view-gallery .cgal-chip { display: grid; gap: 0.2rem; justify-items: start; padding: 0.4rem 0.6rem; border-radius: var(--radius); border-left: 3px solid var(--rule); background: var(--stock-warm); font-size: 13.5px; }
#view-gallery .cgal-chip[data-kind="check"][data-state="pass"] { border-left-color: var(--growth); }
#view-gallery .cgal-chip[data-kind="check"][data-state="warn"], #view-gallery .cgal-chip[data-kind="check"][data-state="stale"] { border-left-color: var(--trust); }
#view-gallery .cgal-chip[data-kind="check"][data-state="fail"] { border-left-color: var(--ink-reject); }
#view-gallery .cgal-chip__list { display: flex; flex-wrap: wrap; gap: 0.2rem 0.75rem; }
#view-gallery .cgal-switch[data-on="true"] b { color: var(--growth); }
#view-gallery .cgal-switch[data-on="false"] b { color: var(--text-muted); }
#view-gallery #gallery-status[data-kind="error"] { color: var(--ink-reject); }

/* QA-T9 load-error state (public/load-error.js renderLoadError): shown in
   place of a list or view that failed to load, with a Try again button.
   Used by the Clients list, Client gallery, Delivery log and Month.
   #client-list is a .tickets list: its `#client-list li` flex row and
   `.tickets button` reset (full-width, no border) are undone here so the
   box stacks and the button still reads as a button. Tokens only. */
.load-error, #client-list > li.load-error { display: grid; gap: 0.35rem; justify-items: start; align-items: start; margin: 0.25rem 0 0; padding: 0.9rem 1rem; list-style: none; border: 1px solid var(--rule); border-left: 3px solid var(--ink-reject); border-radius: var(--radius); background: var(--alert-light); color: var(--text); overflow-wrap: anywhere; }
.load-error__title { margin: 0; font-size: 14.5px; font-weight: 600; line-height: 1.35; color: var(--text); }
.load-error__msg { margin: 0; font-size: 14px; line-height: 1.45; color: var(--text-secondary); }
.load-error .load-error__retry { width: auto; min-height: 44px; margin-top: 0.35rem; padding: 0.45rem 1.15rem; display: inline-flex; align-items: center; justify-content: center; border: 1.5px solid var(--rule); border-radius: var(--radius-pill); background: var(--stock); font-size: 14px; font-weight: 600; }
.load-error .load-error__retry:disabled { background: var(--rule-soft); color: var(--text-muted); cursor: progress; transform: none; }
.load-error__title:focus:not(:focus-visible) { outline: none; }
/* QA-T9 skeptic pass: "still loading" line, the failed-client-list notice
   (app level), the inline notice under a screen's client picker, and the Try
   again in Content Engine's blocked line. */
.load-slow { margin: 0.5rem 0; padding: 0.5rem 0.75rem; list-style: none; font-size: 13.5px; line-height: 1.4; color: var(--text-secondary); }
.load-error--inline { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin-top: 0.5rem; padding: 0.5rem 0.75rem; }
.load-error-notice { flex-wrap: wrap; gap: 0.5rem 1rem; background: var(--alert-light); color: var(--text); border-left: 3px solid var(--ink-reject); font-size: 14px; }
.load-error-notice .load-error__retry, .load-error--inline .load-error__retry, .setup__blocked .load-error__retry { width: auto; min-height: 44px; padding: 0.35rem 1rem; font-size: 14px; }
.load-error--inline .load-error__retry { margin-top: 0; }
.setup__blocked .load-error__retry { margin-left: 0.5rem; }

/* QA-T14 (round 3): a failed or unanswered WordPress send reads as one on the Month card (R3-4); the Operators search + pager (R3-5). */
.month-card__sent[data-delivery="failed"], .month-card__sent[data-delivery="unknown"], .month-card__sent[data-delivery="by-hand-open"] { color: var(--alert); font-weight: 600; } /* proof fix 3: a by-hand article with work left */
.op-find { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin: 1rem 0 0.5rem; }
.op-find input[type="search"] { flex: 1 1 16rem; max-width: 28rem; min-height: 44px; }
.op-find #op-count { flex-basis: 100%; margin: 0; }
.op-more { margin: 0.75rem 0; }
/* QA-T14 fix: the Admin dashboard says which totals are unknown; an unreadable uptime is a hollow dot. */
.admin-degraded { margin: 0.5rem 0; padding: 0.5rem 0.75rem; background: var(--alert-light); border-left: 3px solid var(--ink-reject); color: var(--text); font-size: 14px; }
.status-dot--unknown { background: transparent; box-shadow: inset 0 0 0 1.5px var(--text-muted); }

/* R4-07 (A2): the landing's "This month for <client>" button. Wraps a long
   client name (never overflows), at least 44px tall, full width on a phone. */
.cc-month-card { margin: 0 0 0.85rem; }
.cc-month-card__btn { max-width: 100%; min-height: 48px; height: auto; white-space: normal; text-align: left; justify-content: flex-start; line-height: 1.3; }
.cc-month-card__name { min-width: 0; overflow-wrap: anywhere; }
.cc-month-card__icon { flex: none; }
@media (max-width: 600px) { .cc-month-card__btn { width: 100%; } }

/* P20: "Waiting on you" (public/approvals-inbox.js). Rows ruled with hairlines on the sheet, no cards: the kind and
   the wait in the slug face, the client, one plain line, and the one button that opens where the decision is made.
   The kind's words always sit beside its tone. Stacks on a phone, button full width. */
.waiting-summary { margin: 0 0 0.75rem; font-size: 15px; color: var(--text-secondary); }
.waiting-summary:empty { display: none; }
.waiting-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
.waiting-list:empty { border-top: 0; } /* the empty state draws its own box: no second rule above it */
.waiting-row { display: grid; grid-template-columns: 10.5rem minmax(0, 1fr) auto; gap: 0.35rem 1.25rem; align-items: start; padding: 0.9rem 0; border-bottom: 1px solid var(--rule); }
.waiting-row__meta { margin: 0.15rem 0 0; display: flex; flex-direction: column; gap: 0.2rem; font-size: 13px; } /* W4C: sentence case */
.waiting-row__kind { display: inline-flex; align-items: center; gap: 0.45rem; font-weight: 600; color: var(--text); }
.waiting-row__kind::before { content: ""; flex: none; width: 8px; height: 8px; background: var(--text-muted); }
.waiting-row__kind--alert::before { background: var(--alert); }
.waiting-row__kind--growth::before { background: var(--growth); }
.waiting-row__age { color: var(--slug); font-variant-numeric: tabular-nums; padding-left: calc(8px + 0.45rem); }
.waiting-row__body { min-width: 0; }
.waiting-row__client { margin: 0; font-weight: 700; color: var(--text); overflow-wrap: anywhere; }
.waiting-row__line { margin: 0.2rem 0 0; color: var(--text-secondary); line-height: 1.5; max-width: 68ch; overflow-wrap: anywhere; }
.waiting-row__open { align-self: center; white-space: nowrap; min-height: 44px; }
.waiting-notes { list-style: none; margin: 0.75rem 0 0; padding: 0; }
.waiting-coming { margin-top: 2rem; } /* W4C (defect 5): the next 30 days, under what is waiting */
.waiting-coming__h { margin: 0 0 0.25rem; font-size: 1.05rem; }
.waiting-coming__summary { margin: 0 0 0.75rem; }
.waiting-notes:empty { display: none; }
#view-approvals-inbox .empty__headline:empty, #view-approvals-inbox .empty__body:empty { display: none; }
@media (max-width: 700px) {
  .waiting-row { grid-template-columns: minmax(0, 1fr); gap: 0.4rem; }
  .waiting-row__meta { flex-direction: row; flex-wrap: wrap; justify-content: space-between; }
  .waiting-row__age { padding-left: 0; }
  .waiting-row__open { width: 100%; white-space: normal; }
}

/* W4C (Monday plan defect 7): Review's line for a hub-only client, in place of the WordPress send. */
.ship-hub-only { margin: 0.5rem 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
.ship-hub-only .btn { min-height: 44px; }

/* Night 2 gaps (3): the Month screen between a reload and the month's first paint (public/month.js
   renderMonthPlaceholderHtml). The sheet before it is inked: the four process patches outlined (pending), and
   hairline cells with crop marks at the sizes the banner, the Muse panel and the article pair paint at, so the first
   paint fills the cells rather than pushing the page down. Static: nothing here moves, so reduced motion is the same. */
.month-root[aria-busy='true']:has(> .month-pulling) { opacity: 1; } /* the busy dim would put the line under 4.5:1 */
.month-pulling { display: grid; gap: 0.75rem; }
.month-pulling__line { margin: 0; font-family: var(--ui); font-size: 15px; color: var(--text); }
.month-pulling__sheet { display: grid; gap: 1.25rem; }
.month-pulling__bar { display: flex; gap: 4px; }
.month-pulling__bar i { display: block; width: 28px; height: 10px; border: 1px solid var(--ink-c); }
.month-pulling__bar i:nth-child(2) { border-color: var(--ink-m); }
.month-pulling__bar i:nth-child(3) { border-color: var(--ink-y); }
.month-pulling__bar i:nth-child(4) { border-color: var(--ink-k); }
.month-pulling__pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.25rem; }
.month-pulling__cell {
  display: block;
  border: 0.5px solid var(--rule);
  background-color: var(--stock);
  /* Crop marks: the .imposed primitive (four corner Ls). */
  background-image:
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule)),
    linear-gradient(var(--rule), var(--rule)), linear-gradient(var(--rule), var(--rule));
  background-repeat: no-repeat;
  background-size: var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop),
    var(--crop) 1px, 1px var(--crop), var(--crop) 1px, 1px var(--crop);
  background-position: left top, left top, right top, right top, left bottom, left bottom, right bottom, right bottom;
}
/* Heights: measured off the painted Muse-led November month at 1280 px (night 2 gaps render: banner 85 px, Muse panel
   135 px); they only reserve space, so the first paint fills them instead of moving the page. */
.month-pulling__cell--banner { height: 5.5rem; }
.month-pulling__cell--panel { height: 8.5rem; }
.month-pulling__cell--article { height: 10rem; }
@media (max-width: 600px) { /* the same month at 390 px: banner 57 px, Muse panel 278 px */
  .month-pulling__cell--banner { height: 3.5rem; }
  .month-pulling__cell--panel { height: 17rem; }
}
/* end month-pulling */

/* Night 2 gaps (5): the row a new client's Save just landed on. A mono state word, as every status on the sheet. */
#client-list li button[data-id][data-just-saved]::after {
  content: 'Saved';
  grid-column: 2;
  justify-self: start;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--growth-hover); /* contrast on stock is 5.56:1; the plain --growth token is 4.35:1, under 4.5:1 for 11px text */
}
