/* ==========================================================================
   sect-ui.css - the one shared stylesheet.
   Built by docs/32 spec-the-visual-system.md, session A.

   BEFORE THIS FILE there was no shared CSS in the app at all. Every page in
   public/ carried its own copy of :root and its own component rules, and they
   had drifted to 5 container widths, 11 button definitions, ~20 neutrals,
   12 shadows and 8 declarations of @keyframes spin.

   LOAD IT FIRST, as a real <link>, before every sect-*.js:

     <link rel="stylesheet" href="/sect-ui.css" />
     <script src="/sect-theme.js"></script>
     <script src="/sect-api.js"></script>

   A link tag is render-blocking, so the rules exist before first paint. That
   is the same guarantee sect-nav.js's injected <style> used to provide,
   obtained the ordinary way. A page that loads this AFTER a sect-*.js will
   flash, so order is not cosmetic.

   A page's own <style> block comes later in the document, so a deliberate
   page-specific override still wins. Page-specific means LAYOUT. A page that
   declares :root, a color literal, or a component this file already provides
   is half-migrated, which is worse than untouched - see spec 32 section 10.1.
   ========================================================================== */

/* ==========================================================================
   0. TYPEFACE
   Inter, self-hosted in public/fonts/ (spec 32 section 5.4). system-ui is the
   loudest available signal that nobody chose anything, and at the display sizes
   this file introduces the difference stops being subtle.

   SELF-HOSTED, not linked from Google Fonts: a render-blocking third-party
   request on every page load is at odds with "Super Easy", it is one fewer live
   dependency, and the app already paints from cache specifically to avoid
   waiting on the network.

   TWO FILES, NOT EIGHT. Google serves Inter as a VARIABLE font - the four
   weights this system uses (400/500/600/700) all resolve to one file per
   subset, carrying fvar/gvar/avar/STAT, so `font-weight: 100 900` instances it
   continuously. Requesting the four weights separately downloads the identical
   file four times: 533KB for what is actually 133KB. Verified by probing the
   woff2 table directory, not assumed from the URLs matching.

   THE URLS ARE RELATIVE TO THIS STYLESHEET, and that is load-bearing. The five
   standalone docs in docs/ link `../public/sect-ui.css` and are opened as files
   rather than served, so a root-absolute `/fonts/...` would 404 for every one
   of them while this form resolves in both cases.

   THE unicode-range SPLIT IS THE POINT of shipping two files: a page with no
   accented characters never fetches latin-ext at all. It is NOT optional
   padding - this app renders coach-typed client names, and a name like Kovač
   is Latin Extended-A. Dropping latin-ext would render every letter of it in
   Inter except the c-caron, which would come from the fallback font, mid-word.

   Some UI symbols sit outside both subsets and render from the fallback stack:
   the arrows (U+2190/2192), the tick (U+2713) and the chevrons (U+25B4/25BC).
   That is normal for a subsetted webface and invisible on glyphs of that kind.

   font-display: swap, with the old system stack still in --font behind Inter,
   so a font failure degrades to exactly today's rendering rather than to
   invisible text.

   License: SIL OFL 1.1. It requires the license travel with the font, so
   public/fonts/OFL.txt ships alongside and must not be tidied away.
   ========================================================================== */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/Inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/Inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   1. TOKENS
   Named for ROLE, never for color and never for a page. This is the whole
   reason the --amber-is-actually-blue defect cannot come back: a variable
   named for a color has to be re-pointed every time the color changes; a
   variable named for a role never does.
   ========================================================================== */
:root {
  color-scheme: light;

  /* ---- Type: 7 steps ---- */
  --t-display: 36px;
  --t-h1:      27px;
  --t-h2:      20px;
  --t-lg:      17px;
  --t-body:    16px;
  --t-sm:      14px;
  --t-xs:      13px;
  --t-micro:   11px;

  --lh-tight: 1.15;
  --lh-snug:  1.35;
  --lh-body:  1.65;

  --track-display: -0.025em;
  --track-caps:     0.08em;

  /* ---- Space: 4pt. There is no eighth value. ---- */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  /* ---- Radius ---- */
  --r-sm: 10px; --r-md: 14px; --r-lg: 20px; --r-pill: 999px;

  /* ---- Elevation: 4 steps, replacing 12 ad-hoc values ---- */
  --shadow-xs: 0 1px 2px rgba(16,24,40,.05);
  --shadow-sm: 0 2px 6px rgba(16,24,40,.08), 0 1px 2px rgba(16,24,40,.05);
  --shadow-md: 0 10px 28px rgba(16,24,40,.10), 0 2px 6px rgba(16,24,40,.05);
  --shadow-lg: 0 16px 40px rgba(16,24,40,.12), 0 4px 10px rgba(16,24,40,.05);

  /* ---- Motion ---- */
  --ease: cubic-bezier(.32,.72,0,1);
  --dur-fast: 160ms;
  --dur:      240ms;
  --dur-slow: 460ms;

  /* ---- Surface and ink ----
     --canvas carries a WHISPER of the accent, derived rather than hand-picked.

     Spec 32 section 5.3.1 fixed the page background outright, because the old
     behavior tinted it per preset from five literals and made the five themes
     read as five different apps. That reasoning holds for the literals. It went
     one step too far in flattening the page completely: Linda's read on seeing
     all six side by side was that the themes stopped feeling like hers.

     So the tint comes back as a derivation, not a table. One number controls
     the whole app, custom:#rrggbb gets one for free, and no JS writes it -
     sect-theme.js sets --accent on the same element this block targets, so
     --canvas recomputes on a theme change by itself.

     --tint is the dial. 0% is the flat page; above about 8% the page starts
     competing with the accent instead of supporting it. */
  --tint:          4%;
  --canvas:        color-mix(in srgb, var(--accent) var(--tint), #f7f7f5);
  --surface:       #ffffff;
  --surface-sunk:  #f3f4f6;
  --surface-raised:#ffffff;

  /* THREE INKS, AND SINCE THE G7 AUDIT THEY ARE NOT THREE SHADES OF ONE
     THING (spec 32 section 10.7.13, answered in session D3).

     --text and --text-muted are for TEXT and both clear 4.5:1 on every surface
     this app paints, all five canvases included: 15.59:1 and 5.38:1 at worst.
     Any string a coach reads takes one of them.

     --text-subtle is NOT a text ink. What is left on it after the audit is
     separator glyphs, chevrons, close crosses and icon strokes - non-text
     content, which WCAG 1.4.11 asks to clear 3:1 rather than 4.5:1, and
     decorative generated content, which it exempts. #858e9e measured 2.89:1 on
     the worst canvas and so did not even clear the threshold that DOES apply
     to it; #7b8494 is that value darkened ten points on every channel and
     measures 3.30:1 there.

     THE ALTERNATIVE WAS REJECTED FOR A REASON WORTH KEEPING. Darkening this to
     #666d7a would have cleared 4.5:1 and left it 5.21 against --text-muted's
     6.14 - the indistinguishable pair section 4.4 collapsed four tokens to
     three to eliminate, traded in for the contrast failure rather than fixing
     it. Promoting the copy and keeping this one clearly lighter is what makes
     the ramp mean something.

     SO: NEVER PUT A SENTENCE ON --text-subtle. Working/migration-check.js
     holds the allowlist of what may use it and fails the build on a new one. */
  --text:        #14181f;
  --text-muted:  #596273;
  --text-subtle: #7b8494;

  --border:        #e6e5e1;
  --border-strong: #d3d2cd;

  /* ---- Accent: the ONLY themed group. sect-theme.js writes four of these. ----
     These are the values a page paints with BEFORE the theme has been applied:
     first login with no cached theme, or a logged-out page where /api/theme
     answers nothing. So they have to be exactly what sect-theme.js would write
     for ocean, or the unthemed paint and the ocean paint differ by a hair for
     no reason anybody could name later.
       --accent       = PRESETS.ocean.accent                = #1d6fb8
       --accent-soft  = PRESETS.ocean.accentLight           = #e6f1fb
       --accent-hover = hexMix('#1d6fb8', '#000000', 0.86)  = #195f9e
       --accent-ink   = hexMix('#1d6fb8', '#000000', 0.78)  = #12507f
     Spec 32 section 4.2 wrote #185d9c and #e9f1fa here, which were the demo's
     hand-picked values from before either was computed. Corrected in session C,
     where a logged-out /prep painted the defaults and a themed one painted
     ocean, and the two were measurably not the same blue. Ocean is also what
     applyTheme falls back to for an unknown theme value, so these four ARE
     ocean or they are a sixth palette nobody chose. */
  --accent:       #1d6fb8;
  --accent-hover: #195f9e;
  --accent-soft:  #e6f1fb;
  --accent-ink:   #12507f;
  --on-accent:    #ffffff;

  /* ---- Semantic state: fixed, never themed. ----
     Color that carries no meaning is noise competing with color that does,
     which is why the six undocumented badge hues were retired. */
  --success: #0f6e56;  --success-soft: #e3f4ee;
  --warn:    #8a5a00;  --warn-soft:    #fdf3dd;
  --danger:  #a8331a;  --danger-soft:  #fbeee9;

  /* Derived from --accent rather than frozen at ocean's blue, so a coach on
     Rose does not get a blue focus ring. sect-theme.js overrides --accent as an
     inline style on the same element this block targets, so this recomputes on
     a theme change with no fifth setProperty call and no extra token. */
  --focus: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);

  /* ---- Type stack ----
     Inter is self-hosted and declared in section 0. The old system stack stays
     behind it: if the font files ever fail to load, this degrades to exactly
     today's rendering rather than to something worse. */
  --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Page-level background treatment. Transparent in the Calm house style. */
  --hero-bg: transparent;
}

/* --------------------------------------------------------------------------
   The Bold surface treatment. NOT APPLIED - decided against as the house
   style (spec 32 section 2), retained rather than deleted so switching it on
   later is one attribute rather than a rediscovery. The open question about
   whether the outward-facing opt-in and sales pages want it is spec 32
   section 12.2, and it belongs with spec 28.

   [data-style="bold"] {
     --t-display: 46px;
     --t-h1: 32px;
     --hero-bg: linear-gradient(155deg, var(--accent-soft) 0%, transparent 62%);
   }
   -------------------------------------------------------------------------- */

/* ==========================================================================
   2. RESET AND BASE
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

/* THE `hidden` ATTRIBUTE HAS TO BEAT EVERY COMPONENT IN THIS FILE, and without
   this line it does not (found live on offer-builder.html, 2026-08-10).
   The browser's own [hidden] rule is `display: none` at attribute specificity,
   which TIES with a one-class component rule and therefore loses on source
   order to everything declared in section 4 below. `.sect-stack` sets
   `display: flex`, so a `<div class="sect-card sect-stack" hidden>` renders as
   though the attribute were not there - and it did, on a card that is supposed
   to be ABSENT rather than empty for a coach with nothing to notice.

   !important is the only thing that fixes it in one place: moving the rule
   below section 4 would fight section 9's "focus must stay LAST" instruction,
   and per-component `[hidden]` variants would need writing again for every new
   component. This is the same use normalize.css makes of it, and it joins the
   only other !important block in this file (prefers-reduced-motion, section 6).

   This is spec 32 session B's lesson arriving a fourth time: A SHARED
   STYLESHEET'S BUGS DO NOT APPEAR ON THE PAGE THAT INTRODUCES THEM. `.sect-stack`
   shipped in session B and sat on pages that never once hid one. */
[hidden] { display: none !important; }

html, body { margin: 0; padding: 0; }

body {
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4 {
  margin: 0;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-display);
  font-weight: 700;
}
h1 { font-size: var(--t-h1); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-lg); line-height: var(--lh-snug); }
h4 { font-size: var(--t-body); line-height: var(--lh-snug); }

p { margin: 0; }

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }

code {
  font-family: var(--font-mono);
  font-size: .88em;
  background: var(--surface-sunk);
  padding: .12em .38em;
  border-radius: 4px;
}

hr { border: 0; border-top: 1px solid var(--border); margin: var(--s6) 0; }

/* Focus lives at the BOTTOM of this file, in section 9. It has to: the ring is
   drawn with box-shadow, every rule here is one class deep, and a component
   that sets its own box-shadow later in the file would otherwise win the tie
   and swallow the ring. See the note there. */

/* ==========================================================================
   3. LAYOUT
   ========================================================================== */
.sect-wrap      { max-width: 860px;  margin: 0 auto; padding: var(--s7) var(--s5) var(--s8); }
.sect-wrap--wide{ max-width: 1080px; }
.sect-wrap--narrow { max-width: 680px; }
.sect-hero-band { background: var(--hero-bg); }

.sect-pagehead        { margin-bottom: var(--s6); }
.sect-pagehead h1     { font-size: var(--t-display); margin-bottom: var(--s3); }
.sect-pagehead .lede  { font-size: var(--t-lg); color: var(--text-muted); max-width: 58ch; line-height: 1.55; }
/* GUIDANCE BREATHES. A page's opening guidance runs to two or three RENDERED
   LINES before it breaks, never a block (Linda, 2026-08-06) - the same reason
   PARAGRAPH_RHYTHM_RULE exists for generated prose, applied to the copy we
   write ourselves: a coach reads this on a phone between calls, and a block
   is skipped rather than read slowly.

   Both shapes get the gap, because pages already use both: two sibling
   <p class="lede"> elements, or one .lede wrapping its own paragraphs. A page
   adding its own margin here is half-migrated - the gap belongs to the
   component, at one size, in one place. */
.sect-pagehead .lede + .lede,
.sect-pagehead .lede p + p { margin-top: var(--s3); }
/* AND THE SAME FOR A BARE <p>, because guidance is not always classed .lede
   (Linda, 2026-08-07). Two pages wrote four sibling paragraphs of opening
   guidance and every gap computed to ZERO, so it rendered as one block with
   line breaks in it. Purely additive: no page had multiple paragraphs in a
   pagehead before this, so nothing already shipped moves.

   THE GAP HAS TO BE MEASURED, NOT READ OFF A TEXT DUMP. Extracted page text
   shows paragraph boundaries whether or not any space renders, so it looked
   right and the page did not - twice, on two different pages, before anyone
   checked getBoundingClientRect. */
.sect-pagehead p + p { margin-top: var(--s3); }
/* Unfilled accent eyebrow above the title. Not a chip: a chip is a filled
   micro-label attached to a thing, this labels the page itself. */
.sect-pagehead .eyebrow {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--t-micro); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--accent); margin-bottom: var(--s3);
}

.sect-sechead        { display: flex; align-items: baseline; gap: var(--s3); margin: var(--s7) 0 var(--s4); }
.sect-sechead h2     { font-size: var(--t-h1); }
.sect-sechead .spacer{ flex: 1; }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sect-sechead .meta  { font-size: var(--t-xs); color: var(--text-muted); }

/* The back link that sits above a page title. Layout, not a tenth component:
   prep.html, theme.html and all three settings sub-pages carry one, and
   theme.html's was an inline style with two onmouseover handlers. */
.sect-backlink {
  display: inline-block;
  font-size: var(--t-xs); color: var(--text-muted); text-decoration: none;
  margin-bottom: var(--s4);
  transition: color var(--dur-fast) var(--ease);
}
.sect-backlink:hover { color: var(--text); }

/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sect-groupline {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-muted);
  margin: var(--s6) 0 var(--s3);
}

/* ==========================================================================
   4. COMPONENTS
   Nine. A page that needs a tenth is a conversation, not a local <style>.

   SEVEN OF THE NINE ARE HERE. The skeleton and the status rail moved to
   section 5.6 in session D, because both are emitted by a RENDERER
   (sect-reveal.js, sect-cards.js) rather than written into a page, and section
   5 is where the renderer-owned rules live. The count and the rule are
   unchanged - four pages say "not a tenth component" and all four are still
   right. What changed is where two of the nine are declared.
   ========================================================================== */

/* ---- 4.1 Button. Replaces 11 definitions, including a .btn-primary that
        was accent on eight pages and solid black on notes.html, and a
        .btn-small that sect-cards.js emitted and no file ever defined. ---- */
.sect-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font-size: var(--t-sm); font-weight: 600; line-height: 1;
  padding: 11px var(--s4);
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  transition: transform 90ms var(--ease),
              background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.sect-btn:hover   { background: var(--surface-sunk); color: var(--text); }
.sect-btn:active  { transform: scale(.975); }
.sect-btn:disabled, .sect-btn[aria-disabled="true"] { opacity: .55; cursor: default; transform: none; }

.sect-btn--primary        { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.sect-btn--primary:hover  { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent); }
.sect-btn--ghost          { border-color: transparent; background: transparent; color: var(--text-muted); }
.sect-btn--ghost:hover    { background: var(--surface-sunk); color: var(--text); }
.sect-btn--danger         { border-color: var(--danger); color: var(--danger); background: var(--surface); }
.sect-btn--danger:hover   { background: var(--danger-soft); }
/* The borderless text action. NOT a tenth component - a button variant, the
   same standing as --ghost, and the same call session A made about
   .sect-backlink. It arrives here because credibility-package, promo-emails and
   website-copy each carried a byte-identical `.btn-link` of their own, which is
   the honest three that .expiryPillClass() was extracted at. --ghost is not a
   substitute: this one is accent-colored and underlines, because it sits in a
   run of text ("+ Add a testimonial", "Save this for next time") rather than in
   a row of controls. */
.sect-btn--link           { border-color: transparent; background: transparent; color: var(--accent); padding: var(--s1) 0; }
.sect-btn--link:hover     { background: transparent; color: var(--accent-hover); text-decoration: underline; }
.sect-btn--sm             { font-size: var(--t-xs); padding: 7px 11px; }
.sect-btn--lg             { font-size: var(--t-body); padding: 14px var(--s5); border-radius: var(--r-md); }

/* THE BUSY STATE, and it is a state rather than a variant - is-, not --.
   Spec 32 section 7.2 deletes the full-page dim, so the control she pressed is
   what has to say it is working. It KEEPS ITS LABEL: swapping the text for
   "Working…" would be seven new strings across seven pages for something a
   spinner says without words, and section 3 keeps copy out of this spec.
   The ::before is what the gap in .sect-btn was already reserving space for. */
.sect-btn.is-busy::before {
  content: ""; width: 14px; height: 14px; flex: 0 0 auto;
  border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  opacity: .75;
  animation: sect-spin .7s linear infinite;
}

/* ---- 4.2 Card. Replaces 9 paddings and 5 shadows. ---- */
.sect-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--s5);
}
/* Stacking is the CONTAINER's job, never the card's. This was
   `.sect-card + .sect-card { margin-top: var(--s4) }` until session B, which
   quietly staggered every card that sits in a grid or a flex row: settings.html's
   four hub cards and theme.html's five preset swatches were each pushed 16px
   below their own first sibling, because a margin on a grid item moves the item
   inside its own cell. Found by putting a five-up card grid on progress.html and
   watching it stair-step.
   The general shape, worth carrying: an adjacent-sibling margin is a layout
   opinion the component has no way to be right about. */
.sect-stack { display: flex; flex-direction: column; gap: var(--s4); }
.sect-card--interactive { transition: transform 200ms var(--ease), box-shadow 200ms var(--ease), border-color 200ms var(--ease); }
.sect-card--interactive:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--border-strong); }

/* ---- 4.3 Field ---- */
.sect-field { margin-bottom: var(--s4); }
.sect-field:last-child { margin-bottom: 0; }
.sect-field > label { display: block; font-size: var(--t-sm); font-weight: 600; margin-bottom: 6px; }
.sect-field .hint   { font-size: var(--t-xs); color: var(--text-muted); margin-bottom: var(--s2); line-height: 1.5; }
.sect-field input[type="text"],
.sect-field input[type="email"],
.sect-field input[type="password"],
.sect-field input[type="date"],
.sect-field input[type="number"],
.sect-field select,
.sect-field textarea {
  width: 100%;
  font-size: var(--t-body);
  padding: 11px 13px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  line-height: 1.55;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.sect-field textarea { resize: vertical; min-height: 92px; }
.sect-field input:focus, .sect-field select:focus, .sect-field textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: var(--focus);
}

/* ---- 4.4 Chip. Collapses three treatments of one micro-label role:
        sect-cards' filled pill, sect-prep's bare text, sect-setup's 11px. ---- */
.sect-chip {
  display: inline-block;
  font-size: var(--t-micro); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--accent-ink); background: var(--accent-soft);
  padding: 4px 10px; border-radius: var(--r-pill);
}

/* ---- 4.5 Pill. Replaces six badge colors that carried no semantics and
        were assigned by section rather than by meaning. ---- */
.sect-pill {
  display: inline-block;
  font-size: var(--t-micro); font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase;
  padding: 3px 9px; border-radius: var(--r-pill);
  color: var(--accent-ink); background: var(--accent-soft);
}
/* The OFF state. Added in session C for settings/integrations.html's "Not
   connected", which was reading as good news in accent blue. A pill only earns
   a color when it is reporting state (section 4.5), and "this is not switched
   on" is a state whose honest color is none. */
/* --text-muted, not --text-subtle: "Not connected" is a word she reads. The
   pill's point is that it carries no SEMANTIC color, which a neutral ink keeps
   as well as a lighter one did. G7 audit, spec 32 section 10.7.13. */
.sect-pill--quiet  { color: var(--text-muted); background: var(--surface-sunk); }
.sect-pill--ok     { color: var(--success); background: var(--success-soft); }
.sect-pill--warn   { color: var(--warn);    background: var(--warn-soft); }
.sect-pill--danger { color: var(--danger);  background: var(--danger-soft); }

/* ---- 4.6 Panel ---- */
.sect-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-sm); overflow: hidden;
}
.sect-panel-top { display: flex; align-items: center; gap: var(--s3); padding: var(--s4) var(--s5); border-bottom: 1px solid var(--border); }
.sect-panel-top h3 { font-size: var(--t-lg); font-weight: 600; }
.sect-panel-top .spacer { flex: 1; }
.sect-row { display: flex; align-items: center; gap: var(--s3); padding: var(--s3) var(--s5); border-bottom: 1px solid var(--border); transition: background var(--dur-fast) var(--ease); }
.sect-row:last-child { border-bottom: 0; }
.sect-row:hover { background: var(--surface-sunk); }
.sect-panel-foot { padding: var(--s3) var(--s5); font-size: var(--t-xs); }

/* ---- 4.7 Prose. THE important one.
        Generated output is typeset prose, never a form field. A textarea says
        "here is a draft you have to fix"; set type says "this was written for
        you". Editing is an explicit affordance, not the default state.
        Output is LARGER than input (17px against 16px); today it is inverted,
        her writing at 15px and the model's at 14px. Measure is capped at
        64ch; today it runs to about 120. ---- */
/* font-size here is NOT decoration and it is not inherited by anything that
   matters - every child below sets its own. It exists so `64ch` resolves
   against the size the text is ACTUALLY set at. The ch unit is the advance of
   "0" in the element's own font, so with the container left at the 16px body
   size the cap computed 646px while the 17px paragraphs inside it wanted 686px:
   a measure of about 60 characters where the rule says 64. Still inside the
   readable band, which is why nothing looked wrong - and a token that means
   something other than what it says is the defect this whole spec exists to
   stop. Measured, not assumed. */
.sect-prose { max-width: 64ch; font-size: var(--t-lg); }
.sect-prose p  { font-size: var(--t-lg); line-height: 1.72; color: var(--text); }
.sect-prose p + p { margin-top: var(--s4); }
.sect-prose ul, .sect-prose ol { margin: var(--s3) 0 0; padding-left: 1.15em; }
.sect-prose li { font-size: var(--t-lg); line-height: 1.62; margin-bottom: 8px; }
.sect-prose li::marker { color: var(--accent); }
.sect-prose em { color: var(--text-muted); }

/* Visual weight matches content weight. Today a one-line "Button label" and a
   six-paragraph "Program section" get the identical chip, border and 140px box. */
.sect-prose--headline p { font-size: clamp(24px, 3.4vw, 31px); font-weight: 700; line-height: 1.22; letter-spacing: var(--track-display); max-width: 22ch; }
.sect-prose--sub p      { font-size: 19px; color: var(--text-muted); line-height: 1.5; max-width: 42ch; }
.sect-prose--quote p    { font-size: 19px; font-style: italic; line-height: 1.55; }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sect-prose--quote .attrib { margin-top: var(--s3); font-size: var(--t-sm); color: var(--text-muted); font-style: normal; }

.sect-prose[contenteditable="true"] {
  outline: none; max-width: none;
  background: var(--surface-sunk);
  border-radius: var(--r-sm); padding: var(--s3);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* ---- 4.8 Skeleton and 4.9 Status rail BOTH LIVE IN SECTION 5.6.

        Session 0 wrote both here as a forecast, under .sect-sk* and
        .sect-rail-*. Session D built them for real, and sect-reveal.js and
        sect-cards.js emit .sect-skel__bar and .sect-rail__text - so nothing in
        the app ever matched what stood here. Deleted in session D3 rather than
        recoloured for G7, because three of this file's --text-subtle uses were
        in rules that never render, and answering a contrast gate by editing
        dead CSS is answering it wrongly.

        The rail's duplicate was worse than unused: it declared .sect-rail
        itself, earlier in the file than the live one, so the real component was
        taking whatever section 5.6 did not override from a block nobody would
        think to open. A forecast that outlives the thing it forecast is not a
        comment, it is a second definition. ---- */

/* ---- 4.10 Confirm box. THE TENTH COMPONENT, and it was a conversation
        (Linda, 2026-08-07) rather than a decision taken by writing it.

        Asking before a regenerate destroys something she typed. Nine pages
        want it: progress-recap built it first, results-model and the voice
        profile preview followed, and the six remaining generation tools all
        replace edited output silently today and get it as their copy review
        comes up. It moved here at the third page rather than the ninth,
        because the alternative was six more copies of one rule.

        THE BUTTON ROW IS PART OF THE COMPONENT, not borrowed from the page.
        The three pages that had it each nested a different local class
        (.input-actions, .actions, .preview-actions) which happened to agree on
        flex and gap. Depending on that agreement is how the box ends up laid
        out differently on the fourth page, so it declares its own row. ---- */
.sect-confirm {
  display: none;
  margin-top: 10px;
  padding: 10px var(--s3);
  background: var(--accent-soft);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
  color: var(--text);
  line-height: 1.5;
}
.sect-confirm.is-open { display: block; }
.sect-confirm__actions {
  display: flex; align-items: center; gap: 10px;
  margin-top: var(--s2); flex-wrap: wrap;
}

/* ---- 4.11 Draft note. The restore line sect-draft.js writes: when the work
        was last saved, and the choice between keeping it and starting over.

        ELEVEN HAND-MAINTAINED DECLARATIONS IN THREE SHAPES before this
        (2026-08-07). Seven pages shared one .draft-note rule, three shared a
        different one, promo-emails wore .voice-note - another component's class
        - and notes.html carries two elements. Nothing had gone visibly wrong
        yet, which is the point: eleven copies agree right up until one of them
        is edited.

        THE ALIGNMENT IS DERIVED, NOT NUDGED. The note sits after the card it
        describes, so it was landing on the card's OUTER edge - measured on
        social-content, the note at 234 against the fields inside the card at
        259. It was aligned to a border, the one thing on the page that is not
        content. The adjacent-sibling rule below indents it by the card's own
        padding token plus its border, so it tracks .sect-card rather than
        restating a number that would silently stop matching.

        The negative top margin belongs to that same rule and not to the base:
        it exists to pull the note up under the card it follows, which is
        meaningless for the pages that place it elsewhere. ---- */
.sect-draft-note {
  display: none;                 /* sect-draft.js sets an inline display */
  font-size: var(--t-sm);
  color: var(--text-muted);
  line-height: 1.5;
  margin-bottom: var(--s3);
}
.sect-card + .sect-draft-note {
  padding-left: calc(var(--s5) + 1px);
  margin-top: calc(var(--s3) * -1);
  margin-bottom: var(--s5);
}

/* ---- 4.12 The no-client picker. Spec 35 gave /progress and /prep dashboard
        cards, and a card is only honest if its destination works when you get
        there with nothing - both used to answer "No client selected" or hand a
        null id to a renderer.

        SHARED AT THE SECOND PAGE, not copied to a third. That is 4.10's rule
        applied earlier: the confirm box moved once nine pages wanted it, and
        the lesson recorded there was that it should have moved sooner. Two
        pages need this today; progress-recap.html and renewal-composer.html
        each carry their OWN older picker, so this is also where those two
        collapse into one if anyone opens them.

        The row builds on .sect-row, which already brings padding, the hover and
        a focus ring. What is added is only what a LINK row needs plus the two
        labels. ---- */
.sect-picker-head { font-size: var(--t-display); margin-bottom: var(--s2); }
.sect-picker-blurb { font-size: var(--t-lg); color: var(--text-muted); margin-bottom: var(--s5); }
.sect-picker-row {
  align-items: baseline;
  flex-wrap: wrap;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
.sect-picker-name { font-size: var(--t-body); font-weight: 600; }
.sect-picker-program { font-size: var(--t-xs); color: var(--text-muted); }
.sect-picker-empty { padding: var(--s6); text-align: center; color: var(--text-muted); font-size: var(--t-body); }

/* ---- The one spinner. Today @keyframes spin is declared 8 times at two
        different durations. ---- */
.sect-spinner {
  width: 16px; height: 16px; flex: 0 0 auto;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  animation: sect-spin .7s linear infinite;
}

/* ==========================================================================
   5. SHARED RENDERERS
   Several files draw markup no single page owns: sect-prep.js (the session prep
   brief, on /prep and inside notes.html's cockpit drawer), sect-setup.js (the
   first run path strip, on /results-model and /settings/voice-profile),
   sect-cards.js (the generated-output card grid, on six tool pages) and
   sect-offer.js (the offer editor, on /promo-emails and /offer-builder).

   Their CSS used to be injected from inside each file, for the same reason
   sect-nav.js injected the nav's: there was no shared stylesheet to put it in.
   Now there is, so it lives here (spec 32 section 5.2).

   THESE ARE NOT A TENTH COMPONENT. Everything below is layout and composition
   over section 4's set - a card is .sect-card, a micro-label is .sect-chip, a
   button is .sect-btn. A renderer that reaches for a color literal or invents
   a second card is doing the thing this file exists to stop.

   NO RENDERER INJECTS ITS OWN CSS ANY MORE. Each kept a legacy copy while its
   consumers were unmigrated, because a shared renderer cannot be migrated ahead
   of them (spec 32 section 10.5.2); all four copies died in session E once every
   page linked this file. sect-offer.js (5.7) never needed one - it arrived after
   the migration with both of its consumers already here, which is the condition
   session D set for putting a renderer's rules in this file rather than in it.
   ========================================================================== */

/* ---- 5.1 The session prep brief (sect-prep.js) ----
        Every class is namespaced `sp-` because this renderer mounts inside
        notes.html, which has a large stylesheet of its own and where a generic
        `.block` would collide. */
/* The brief is a column of cards, so the GAP lives on the container. Section
   4.2 records why a card may not carry its own margin; this is that rule used
   as intended. The two async slots (.sp-read, .sp-icebreaker) are containers
   that are empty until their fetch lands, and an empty flex child still earns
   a gap - hence the :empty rule, which is what stops the brief showing a hole
   where the AI read is going to be. */
.sp-root { color: var(--text); display: flex; flex-direction: column; gap: var(--s4); }
.sp-root > :empty { display: none; }
.sp-read { display: flex; flex-direction: column; gap: var(--s4); }
.sp-header { margin-bottom: 0; }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sp-skeleton { color: var(--text-muted); font-size: var(--t-sm); padding: var(--s6) 0; text-align: center; }
.sp-empty {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-sm);
  padding: var(--s6); text-align: center; color: var(--text-muted);
}

.sp-meta { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3); margin-top: var(--s3); font-size: var(--t-xs); color: var(--text-muted); align-items: center; }
.sp-meta .sp-chunk { white-space: nowrap; }

/* The block head: chip on the left, provenance on the right. Splitting the
   date out of the label is what lets the label become a real .sect-chip - a
   filled uppercase pill cannot also carry " · Session 4" in regular weight,
   which is why .sp-label used to be bare text with a lighter span inside it. */
.sp-head { display: flex; align-items: baseline; gap: var(--s3); margin-bottom: var(--s3); flex-wrap: wrap; }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sp-head .sp-date { font-size: var(--t-xs); color: var(--text-muted); }

/* The strongest assembled block: the "For next session" prompts this coach
   wrote at the end of last session, surfaced at the moment they were written
   for. Accented on purpose, and the only accented block in the brief. */
.sp-highlight { background: var(--accent-soft); border-color: transparent; box-shadow: none; border-left: 3px solid var(--accent); }

.sp-sub + .sp-sub { margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--border); }
.sp-item { display: flex; gap: var(--s2); align-items: baseline; margin-bottom: 7px; font-size: var(--t-body); line-height: 1.55; }
.sp-item:last-child { margin-bottom: 0; }
.sp-dot { color: var(--accent); flex-shrink: 0; }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.sp-when { font-size: var(--t-xs); color: var(--text-muted); white-space: nowrap; margin-left: auto; }

.sp-noticing { list-style: none; margin: 0; padding: 0; }
.sp-noticing li { position: relative; padding-left: 18px; margin-bottom: var(--s2); font-size: var(--t-lg); line-height: 1.6; }
.sp-noticing li:last-child { margin-bottom: 0; }
.sp-noticing li::before { content: ''; position: absolute; left: 0; top: 11px; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }

/* The opening question is the one line in the brief she may read aloud, so it
   is set larger than the prose around it rather than smaller. */
.sp-question { font-size: var(--t-h2); line-height: 1.45; font-weight: 500; max-width: 46ch; }
.sp-quiet-btn { margin-top: var(--s3); }

.sp-actions { margin-top: var(--s6); display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; }
.sp-link { font-size: var(--t-sm); color: var(--text-muted); text-decoration: none; }
.sp-link:hover { color: var(--accent); }

/* ---- 5.2 The first run path strip (sect-setup.js) ----
        A slim bar at the top of a step's own page. It is deliberately NOT a
        .sect-card: a card is a thing on the page, this is a thing about the
        page, and giving it card elevation would put it in competition with
        the content it is introducing. */
.sect-setup-strip {
  display: none; align-items: center; gap: var(--s4); flex-wrap: wrap;
  border: 1px solid var(--accent-soft); border-left: 4px solid var(--accent);
  background: var(--surface); border-radius: var(--r-md);
  padding: var(--s3) var(--s4); margin-bottom: var(--s5);
}
.sect-setup-strip.visible { display: flex; }
.sect-setup-strip-main { flex: 1; min-width: 220px; }
.sect-setup-strip-step { margin-bottom: 5px; }
.sect-setup-strip-title { font-size: var(--t-lg); font-weight: 700; line-height: var(--lh-snug); }
.sect-setup-strip-note { font-size: var(--t-sm); line-height: 1.5; color: var(--text-muted); margin-top: var(--s1); }
.sect-setup-strip-cta { flex-shrink: 0; }

/* ---- 5.3 The generated-output card grid (sect-cards.js) ----
        SIX CONSUMERS, ALL MIGRATED IN SESSION D, WHICH IS WHY THERE IS NO
        LEGACY COPY HERE and there are four shims elsewhere in the app. Session
        C deliberately left this file's CSS where it was, because moving it then
        would have put the real rules on zero pages and a shim on six (spec 32
        section 10.7.3). Session D migrates credibility-package,
        onboarding-experience, progress-recap, promo-emails, renewal-composer
        and website-copy - every page that calls SectCards.create() - so on the
        day these rules move there is nothing left to bridge. social-content,
        notes and settings/voice-profile only MENTION the file in comments; none
        of them loads it.

        THE SECTION 6 COLLAPSE LANDED WITH THE MOVE. The renderer emits
        `sect-card sect-gencard` rather than a second card definition, its chip
        is .sect-chip, its buttons are .sect-btn, and its spinner and its pulse
        are the shared ones. What is left below is only what is genuinely
        specific to a card holding generated output. */
.sect-cards-grid { display: flex; flex-direction: column; gap: var(--s4); }

/* The chip labels the card from inside it, so the space below it is this
   card's own business rather than the container's. Section 4.2's rule is about
   a card's OUTER margin deciding its neighbours' spacing; this decides nothing
   outside its own box. */
.sect-gencard > .sect-chip { margin-bottom: 10px; }

.sect-gencard input[type="text"],
.sect-gencard textarea {
  width: 100%;
  font-size: var(--t-sm);
  padding: 9px var(--s3);
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-sunk);
  color: var(--text);
  font-family: inherit;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.sect-gencard input[type="text"]:focus,
.sect-gencard textarea:focus { outline: none; border-color: var(--accent); background: var(--surface); box-shadow: var(--focus); }
.sect-gencard input[type="text"] { font-weight: 600; }
.sect-gencard textarea { resize: vertical; line-height: 1.6; min-height: 140px; }

.sect-card-label { display: block; font-size: var(--t-xs); font-weight: 600; color: var(--text-muted); margin: var(--s2) 0 var(--s1); }
.sect-card-actions { display: flex; align-items: center; gap: var(--s2); margin-top: 10px; flex-wrap: wrap; }
.sect-card-status { font-size: var(--t-xs); color: var(--success); }
.sect-card-error {
  font-size: var(--t-xs); color: var(--danger);
  padding: var(--s2) var(--s3); background: var(--danger-soft);
  border-radius: var(--r-sm); margin-top: 10px; display: none;
}
.sect-card-statusbox {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: var(--s5) 0; font-size: var(--t-sm); color: var(--text-muted);
}
.sect-gencard--error .sect-card-statusbox { color: var(--danger); }
.sect-gencard--error .sect-card-statusbox .sect-btn { flex-shrink: 0; }
.sect-cards-savehint { font-size: var(--t-xs); color: var(--text-muted); line-height: 1.5; }

/* ---- 5.4 The saved-testimonial picker (sect-testimonials.js) ----
        Three consumers, all session D pages, so this moves on the same terms
        as 5.3 and likewise needs no legacy copy. */
.sect-tst-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.sect-tst-item {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 9px var(--s3); border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.sect-tst-item:hover { border-color: var(--accent); }
.sect-tst-item.is-picked { border-color: var(--accent); background: var(--accent-soft); }
/* The cap is enforced live rather than at generate time, so an unticked row
   beyond the allowance is shown as unavailable rather than removed - she can
   see what she would have to untick to reach it. */
.sect-tst-item.is-blocked { opacity: 0.5; cursor: default; }
.sect-tst-item.is-blocked:hover { border-color: var(--border); }
.sect-tst-item input[type="checkbox"] { margin: 2px 0 0; flex-shrink: 0; cursor: inherit; accent-color: var(--accent); }
.sect-tst-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sect-tst-name { font-size: var(--t-xs); font-weight: 600; color: var(--text); }
.sect-tst-name.is-unnamed { font-weight: 500; color: var(--text-muted); font-style: italic; }
/* overflow-wrap because a testimonial can arrive as one unbroken URL or a
   pasted string with no spaces, and the row must not push the card wider. */
.sect-tst-snippet { font-size: var(--t-xs); color: var(--text-muted); line-height: 1.5; overflow-wrap: anywhere; }
.sect-tst-note { font-size: var(--t-xs); color: var(--text-muted); line-height: 1.6; margin-bottom: 10px; }
.sect-tst-cap  { font-size: var(--t-xs); color: var(--text-muted); margin-bottom: 10px; }

/* ---- 5.5 The offer-logistics scaffold button (sect-logistics.js) ----
        Two consumers, both session D pages. Composed onto .sect-btn--sm, so
        what is left is the one thing that makes it itself: an ACCENT OUTLINE
        rather than a neutral one.

        That is deliberate and Linda decided it (2026-07-29: "it's not clear
        that start me off is a link"). It sat under a textarea with no border
        and no background and nothing marked it clickable. Outlined it reads as
        a button while staying quieter than the primary action beside it, which
        is right: this is a helper, not the thing the coach came to press.

        Both consumers render it as static markup and neither toggles its
        display, so section 10.7.9's inline-flex trap does not apply here. */
.sect-scaffold-btn { margin-top: var(--s2); color: var(--accent); border-color: var(--accent); }
.sect-scaffold-btn:hover { background: var(--accent-soft); color: var(--accent); }

/* ---- 5.6 Progressive reveal: the status rail and the skeletons ----
        (sect-reveal.js draws the rail; sect-cards.js draws the skeletons)

        THIS REPLACES THE DIM-AND-OVERLAY ON EVERY GENERATING PAGE (spec 32
        section 7.2). What it replaces was a screen that went gray and
        unreadable behind a floating box, which told the coach one thing:
        wait. What is here tells her what is coming, how much of it has
        arrived, and when something did not.

        THE PAGE NEVER DIMS NOW. There is no .dimmed and no overlay: the form
        stays legible and re-readable while she waits, the submit button
        carries its own busy state, and the output region carries the rail and
        the skeletons. A coach re-reading what she typed while the model works
        is doing something reasonable, and the old treatment stopped her.

        Seven consumers, all migrated, so this needs no legacy copy - the same
        terms as 5.3 to 5.5. notes.html is section 7.6's and lands with its
        migration in session E, because a shared renderer cannot be given to a
        page that does not link this file and the answer to a page needing a
        shim is to migrate it. */

/* The rail sits above the output and STAYS THERE while she scrolls - a
   progress indicator that scrolls away is one she has to hunt for at the
   moment she wants it. 68px clears the 60px nav plus a hair, so the two never
   touch. */
.sect-rail {
  position: sticky; top: 68px; z-index: 50;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  animation: sect-rail-in var(--dur) var(--ease);
}

/* The ring is the spinner at rail scale. It is a separate element from
   .sect-spinner because it swaps for a tick on completion and a plain dot on
   a partial, and a component that changes what it IS wants its own class. */
.sect-rail__ring {
  flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--accent-soft); border-top-color: var(--accent);
  animation: sect-spin .7s linear infinite;
}
.sect-rail__body  { flex: 1; min-width: 0; }
.sect-rail__text  { font-size: var(--t-sm); font-weight: 600; color: var(--text); }
/* --text-muted, never --text-subtle: this is a sentence she is meant to read.
   The G7 audit (section 10.7.13) settled that distinction. */
.sect-rail__sub   { font-size: var(--t-xs); color: var(--text-muted); margin-top: 2px; }
.sect-rail__count {
  flex-shrink: 0; font-size: var(--t-xs); font-weight: 700;
  color: var(--text-muted); font-variant-numeric: tabular-nums;
}

/* Completion. The ring stops being a ring and becomes a tick, because a
   spinner that has stopped spinning still reads as "working" for a second or
   two and this state lasts about that long before it dismisses itself. */
.sect-rail.is-done { border-color: var(--success); background: var(--success-soft); }
.sect-rail.is-done .sect-rail__text,
.sect-rail.is-done .sect-rail__count { color: var(--success); }
.sect-rail.is-done .sect-rail__sub   { color: var(--success); opacity: .85; }
.sect-rail.is-done .sect-rail__ring,
.sect-rail.is-partial .sect-rail__ring { animation: none; border: 0; }
.sect-rail.is-done .sect-rail__ring::before,
.sect-rail.is-partial .sect-rail__ring::before {
  content: ""; display: block; width: 18px; height: 18px;
  background: currentColor; color: var(--success);
  -webkit-mask: var(--sect-tick) center / 18px 18px no-repeat;
  mask: var(--sect-tick) center / 18px 18px no-repeat;
}

/* A PARTIAL DOES NOT DISMISS ITSELF, and that asymmetry is the point (section
   7.3). A completed generation can announce itself and get out of the way; a
   partial one is naming a failure the coach has to decide about, so it stays
   until the retry that clears it. It is --warn rather than --danger because
   nothing is broken: most of the page arrived and one part did not. */
.sect-rail.is-partial { border-color: var(--warn); background: var(--warn-soft); }
.sect-rail.is-partial .sect-rail__text,
.sect-rail.is-partial .sect-rail__count { color: var(--warn); }
.sect-rail.is-partial .sect-rail__sub   { color: var(--warn); opacity: .85; }
.sect-rail.is-partial .sect-rail__ring::before {
  color: var(--warn);
  -webkit-mask: var(--sect-alert) center / 18px 18px no-repeat;
  mask: var(--sect-alert) center / 18px 18px no-repeat;
}

/* Inline SVG as data URIs, so the two rail icons need no network and no file.
   Declared here rather than in section 1 because nothing else uses them. */
.sect-rail {
  --sect-tick:  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 12 5.5 5.5L20 7'/%3E%3C/svg%3E");
  --sect-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 8v5'/%3E%3Cpath d='M12 17h.01'/%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3C/svg%3E");
}

/* ---- The skeletons ----
   CONTENT-SHAPED, not a generic spinner, and that is the whole idea: a card
   waiting for a headline shows one short bar, a card waiting for three
   hundred words shows a paragraph. She can tell what is coming before it
   arrives, and the page does not jump when it does.

   The shimmer is one element sweeping across each bar rather than an
   animation on the bar itself, so the bars keep a flat fill and only the
   highlight moves. Section 6's reduced-motion guard freezes it, which is
   correct: a static skeleton still communicates shape. */
.sect-skel { display: flex; flex-direction: column; gap: 10px; padding: var(--s2) 0; }
.sect-skel__bar {
  position: relative; overflow: hidden;
  height: 12px; border-radius: var(--r-pill);
  background: var(--surface-sunk);
}
.sect-skel__bar::after {
  content: ""; position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent);
  animation: sect-shimmer 1.25s var(--ease) infinite;
}
/* A headline bar is taller and shorter than a body line, because that is what
   a headline looks like once it lands. */
.sect-skel__bar--head { height: 20px; }

/* ---- The card entrance ----
   Only cards that JUST became done animate (sect-cards.js diffs the previous
   render's states), so a wave landing does not re-animate the wave that
   landed before it. The stagger is set per card as an inline
   animation-delay. */
.sect-gencard--loading .sect-card-statusbox { padding: 0; }

/* ---- 5.7 The offer editor (sect-offer.js) ----
        Two consumers from day one - promo-emails.html, where this form was
        born, and offer-builder.html's standalone offer - so it goes straight
        in here rather than being injected. Session D's condition was that
        every consumer be migrated first, and both of these already are.

        These rules are promo-emails.html's own, renamed and moved rather than
        rewritten: the form a coach has been using is the form she keeps. What
        changed is that the class names are namespaced, because the promo page
        still styles .field-row and .section-title for the GENERATE card that
        sits below the editor and stays page-local.

        NOTHING HERE SETS display: none FOR A HIDEABLE PIECE. The error line,
        the saved note and the logistics blanks warning all use the `hidden`
        attribute, which section 2 now enforces with !important - a rule of our
        own would tie with it on specificity and win on source order, which is
        the .sect-stack defect from spec 37 session B. */
.sect-offer { display: none; }
.sect-offer.is-open { display: block; }
/* .sect-field is on the CARD, so its own bottom margin is cleared - the card
   already carries one. */
.sect-offer.sect-field { margin-bottom: var(--s5); }
.sect-offer textarea { min-height: 70px; }

.sect-offer-title { font-size: var(--t-lg); font-weight: 700; margin-bottom: var(--s3); }
.sect-offer-row { margin-bottom: 14px; }
.sect-offer-row label { display: block; font-size: var(--t-sm); font-weight: 600; margin-bottom: 6px; }
.sect-offer-help { font-size: var(--t-xs); color: var(--text-muted); line-height: 1.5; margin-top: 5px; }
.sect-offer-help a { text-decoration: none; }
.sect-offer-help a:hover { text-decoration: underline; }
/* A nudge, not an error: unfilled scaffold blanks are worth flagging because
   the generation prompts drop bracketed placeholders, but they never block a
   save. Accent-toned rather than danger-toned for exactly that reason. */
.sect-offer-warn { font-size: var(--t-xs); color: var(--text); line-height: 1.5; margin-top: 6px; padding: 7px 10px; background: var(--accent-soft); border-radius: var(--r-sm); }
.sect-offer-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 560px) { .sect-offer-grid2 { grid-template-columns: 1fr; } }

/* The radio labels sit INSIDE a .sect-offer-row, so they need the block label
   rule above turned back off. */
.sect-offer-radio { display: flex; align-items: center; gap: var(--s2); margin-bottom: 7px; }
.sect-offer-radio input[type="radio"] { margin: 0; flex-shrink: 0; width: auto; accent-color: var(--accent); }
.sect-offer-radio label { display: inline; font-size: var(--t-sm); font-weight: 400; color: var(--text); margin: 0; cursor: pointer; }

.sect-offer-pairlabel { font-size: var(--t-sm); font-weight: 600; margin: 18px 0 var(--s2); }
.sect-offer-pair { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s2); }
.sect-offer-pair input { flex: 1; min-width: 0; }
/* The literal words, in front of the second box: the framing is taught by the
   row rather than by help text under it. */
.sect-offer-sothat { font-size: var(--t-sm); color: var(--text-muted); flex-shrink: 0; white-space: nowrap; }
.sect-offer-rowremove { flex-shrink: 0; background: none; border: none; color: var(--text-muted); font-size: 18px; line-height: 1; cursor: pointer; padding: 2px 6px; }
.sect-offer-rowremove:hover { color: var(--danger); }

.sect-offer-actions { display: flex; align-items: center; gap: 10px; margin-top: var(--s5); }
.sect-offer-saved { font-size: var(--t-sm); color: var(--success); }
.sect-offer-error { font-size: var(--t-sm); color: var(--danger); padding: var(--s2) var(--s3); background: var(--danger-soft); border-radius: var(--r-sm); margin-top: 10px; }

/* ==========================================================================
   6. MOTION
   ========================================================================== */
@keyframes sect-spin     { to { transform: rotate(360deg); } }
@keyframes sect-shimmer  { to { transform: translateX(100%); } }
@keyframes sect-rail-in  { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes sect-enter    { to { opacity: 1; transform: none; } }
@keyframes sect-pulse    { 0%,100% { transform: scale(1); } 50% { transform: scale(1.07); } }

.sect-enter    { opacity: 0; transform: translateY(10px); }
.sect-enter.is-in { animation: sect-enter var(--dur-slow) var(--ease) forwards; }
.sect-pulse    { animation: sect-pulse .34s ease-in-out 2; }

/* One guard, globally. Today two files guard one save-pulse and notes.html
   has none at all, so its infinite 2s live dot and both drawer slides run
   regardless of the system setting. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   7. NAV
   Moved here out of sect-nav.js, which keeps NAV_LINKS, the markup builder,
   renderUser, render and logout, and loses only injectCss().

   The var(--x, fallback) pattern the old NAV_CSS used everywhere existed
   because not every page declared the full palette. One stylesheet on every
   page makes the fallbacks dead weight, so they are gone.
   ========================================================================== */
.nav {
  position: sticky; top: 0; z-index: 200;
  display: flex; align-items: center; gap: var(--s5);
  height: 60px; padding: 0 var(--s5);
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
}
.nav-logo { display: flex; align-items: center; gap: var(--s2); font-weight: 700; font-size: var(--t-sm); letter-spacing: -.01em; }
.nav-links { display: flex; align-items: center; gap: var(--s1); margin-left: var(--s4); }
.nav-links a {
  color: var(--text-muted); text-decoration: none;
  font-size: var(--t-sm); font-weight: 500;
  padding: 7px var(--s3); border-radius: var(--r-sm);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  /* NO MID-LABEL WRAPPING (2026-08-09). These are flex items and inherited the
     default flex-shrink: 1, so on a phone they were squeezed narrower than
     their own text and "My Clients" broke onto two lines - measured at 60px
     tall against "Dashboard" at 37px, which is what made the whole bar look
     broken. A nav label is one word-group; it either fits or the bar scrolls. */
  white-space: nowrap; flex: 0 0 auto;
}
.nav-links a:hover { background: var(--surface-sunk); color: var(--text); }
.nav .spacer { flex: 1; }
/* The theme dot. It shows the live accent, which is why it stayed a dot rather
   than becoming the demo's outline icon button: the color IS the affordance.
   Was six inline style declarations plus two inline mouse handlers in
   sect-nav.js's markup string. */
.nav-dot {
  width: 15px; height: 15px; flex: 0 0 auto; margin-left: var(--s2);
  border-radius: 50%; border: 2px solid var(--border);
  background: var(--accent); cursor: pointer;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.nav-dot:hover { transform: scale(1.25); border-color: var(--border-strong); }
/* --text-muted: a sentence she reads. G7 audit, spec 32 section 10.7.13. */
.nav-user { font-size: var(--t-xs); color: var(--text-muted); }
.btn-logout {
  font-size: var(--t-xs); padding: 7px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--border-strong); background: transparent; color: var(--text-muted);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  /* Same squeeze as the links above: "Log out" was breaking into two lines and
     stretching the button to 59px inside a 60px bar. */
  white-space: nowrap; flex: 0 0 auto;
}
.btn-logout:hover { background: var(--surface-sunk); color: var(--text); }

/* ==========================================================================
   8. BREAKPOINTS
   Three, defined once. Today there are three width queries in the entire
   application, at two breakpoints, and the nav has no mobile handling at all
   despite the stated ideal user being a coach on a phone between calls.
   ========================================================================== */
@media (max-width: 1024px) {
  .sect-wrap--wide { max-width: 100%; }
}

@media (max-width: 760px) {
  .sect-wrap { padding: var(--s5) var(--s4) var(--s7); }
  .sect-sechead { flex-wrap: wrap; }

  /* The LINKS survive and the chrome around them goes. The wordmark tells a
     coach nothing she does not know - she is in the app - and the email
     address is display only, never a gate. Hiding the links instead would
     leave a phone with no navigation at all and no menu to replace it, and
     the stated ideal user is a coach on a phone between calls. */
  .nav { gap: var(--s2); padding: 0 var(--s3); }
  .nav-logo  { display: none; }
  .nav-user  { display: none; }
  .nav-links { margin-left: 0; }
}

@media (max-width: 480px) {
  :root { --t-display: 30px; --t-h1: 24px; }
  .sect-card, .sect-panel-top, .sect-row, .sect-panel-foot { padding-left: var(--s4); padding-right: var(--s4); }
  .nav-links a { padding: 7px var(--s2); }
  .nav-dot { margin-left: var(--s1); }
  .btn-logout { padding: 7px var(--s2); }
}

/* ==========================================================================
   9. FOCUS - LAST ON PURPOSE
   Before this file the app removed the native outline on inputs and never
   replaced it on buttons or links, and :focus-visible appeared zero times in
   26 files. One treatment, here.

   IT HAS TO BE LAST, and that is not tidiness. The ring is a box-shadow and
   every selector in this file is one class deep, so `.sect-card` and
   `.sect-panel` - which set their own elevation - tie with `:focus-visible` on
   specificity and win by source order. Written up in section 2 the ring drew
   correctly on a bare link and was silently swallowed by every card and panel
   in the app, including the four settings hub cards and the five theme preset
   swatches, all of which are keyboard-reachable. Found by tabbing the dashboard
   and reading the computed box-shadow of document.activeElement, which is the
   only way to tell "no ring" from "ring behind something".

   Each shadowed component therefore COMPOSES the ring with its own elevation
   rather than replacing it, and restores its own radius, since the generic rule
   below sets --r-sm for the benefit of elements that have no radius at all.
   ========================================================================== */
:focus-visible {
  outline: none;
  box-shadow: var(--focus);
  border-radius: var(--r-sm);
}
.sect-card:focus-visible  { border-radius: var(--r-lg); box-shadow: var(--focus), var(--shadow-sm); }
.sect-card--interactive:focus-visible { box-shadow: var(--focus), var(--shadow-md); }
.sect-panel:focus-visible { border-radius: var(--r-lg); box-shadow: var(--focus), var(--shadow-sm); }
.sect-btn--lg:focus-visible { border-radius: var(--r-md); }
.sect-chip:focus-visible, .sect-pill:focus-visible { border-radius: var(--r-pill); }
/* A row lives inside .sect-panel, which sets overflow: hidden so the rows clip
   to the panel's rounded corners. An OUTER ring on the first or last row is
   drawn into exactly that clipped area and simply disappears - focused,
   reachable, invisible, which is the state section 8 exists to prevent. So the
   row's ring is drawn INSIDE its own box. Found by making pick.html's client
   rows keyboard-reachable, which is the first time a row in a panel could take
   focus at all. */
.sect-row:focus-visible { border-radius: 0; box-shadow: inset 0 0 0 2px var(--accent); }
