/*
 * Stetworth /app — base visual system.
 *
 * This stylesheet establishes the typography, layout primitives, and
 * shared component vocabulary for the /app surface.  It originates
 * from the mockup's stylesheet (web/mockup/css/style.css) and keeps
 * that shared vocabulary whole; the per-surface sections (composer,
 * sign-in, task detail, support chat, gallery states, working set,
 * suggestions, the signed-in page family) were cut out to sibling
 * sheets under web/app/css/, each linked by the shell BEFORE this
 * file (docs/APP.md#the-split-stylesheets).  Linking this file last
 * is load-bearing: a handful of rules deliberately stayed behind
 * beside the shared groups they must beat on source order at equal
 * specificity, and the responsive tail at the end of this file keeps
 * overriding the split sheets' base rules the same way.
 *
 * Visual rules ("Oxblood Library" palette — scoped to this surface):
 *   - Warm editorial palette: a lightened parchment page background
 *     (#F6EFDF) + Ink (#14110F) cover ~85% of any screen; Stone
 *     (#69615A) does the structural gray work — darkened from #8E847A,
 *     which was 3.20:1 and below WCAG AA for text. Text boxes (composer /
 *     modal inputs, preview panes) fill with the lighter #FBF8F0 card
 *     cream and read as recessed wells via a stronger border + inset
 *     shadow rather than a darker fill (Parchment #F0E9D9 remains
 *     declared for palette-compliance tooling but is no longer used
 *     for input wells).
 *     Oxblood (#6B1F1F) is the brand / primary-CTA accent — it colors
 *     the Stetworth wordmark (the hero <h1>, uppercased and tracked
 *     by the .wordmark rule over the h1 rule's EB Garamond) and the
 *     primary CTA, with a darker #571919 (--c-oxblood-deep)
 *     press/hover shade for that CTA — and Burnished (#9C7943) the
 *     premium / in-progress accent, with #795E34
 *     (--c-burnished-text) for gold TEXT, which answers to the
 *     4.5:1 floor rather than 3:1.  Together they stay under ~5%
 *     (punctuation, not paragraphs).
 *   - Two earthy status accents harmonized with the palette: moss green
 *     (#4E6A47) for "complete", brick red (#9C3B2E) for "failed".
 *   - EB Garamond (regular) for serif accents.
 *   - Instrument Sans for all body, controls, and UI text.
 *   - Centered single-column layout with a 1280px max width.
 *   - Generous spacing; hairline rules; minimal shadow.
 *   - A tokenized design layer appended inside :root: the ten-step
 *     --fs-* type scale, --font-mono, --track-tag + --radius-pill,
 *     the --sh-* elevation set with --focus-ring, and the --z-*
 *     ladder.  The mono stack, pill radii and z-index resolve
 *     through tokens (tests/test_app_design_tokens.py).
 *   - One .tag micro-label base (grouped with the chips it styles):
 *     micro size, 600 weight, uppercase, --track-tag, --radius-pill.
 *   - Edge law, two devices: TOP head rules are letterhead/brand —
 *     oxblood belongs to the top device; LEFT edges are status at a
 *     single 3px width — burnished waiting, moss done, brick failed,
 *     ink-faint annotation (tests/test_app_design_tokens.py).
 *   - Elevation/focus law: every shadow composes the --sh-* set;
 *     one --focus-ring; text inputs share one focus treatment —
 *     burnished border + --sh-well + --focus-ring, each site keeping
 *     its own pseudo-class (tests/test_app_design_tokens.py).
 *   - Button law: .btn-secondary and .btn-tertiary are aliases of the
 *     .btn base — resting values are the base's own, one grouped rule
 *     carries their shared hover fill; .btn-link is the
 *     underlined-text-button utility.
 *
 * Class contract (consumed verbatim by units 3-5):
 *   .page  .page-hero  .wordmark  .page-section
 *   .card  .card-header  .card-body  .card-meta
 *   .pill  .pill-queued  .pill-in-progress  .pill-complete  .pill-failed
 *   .panel  .panel-header  .panel-title  .panel-divider
 *   .btn  .btn-primary  .btn-secondary  .btn-ghost  .btn-icon
 *   .btn-danger  .btn-small  .btn-tertiary
 *   .kbd  .divider  .fade-in
 *   .login  .login-lede  .login-email  .page-nav-session
 *   (.login-form, .login-awaiting, .login-awaiting-title,
 *   .hero-tagline, and .sign-out-btn are JS/test hooks with no rules
 *   here, like .pref-add-btn / .pref-save-btn — .hero-tagline is
 *   styled entirely by the existing .page-hero p rule.)
 */

:root {
    /* Typography */
    --font-body:   'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-accent: 'EB Garamond', Georgia, 'Times New Roman', serif;

    /* Oxblood Library palette — warm parchment/ink surfaces with stone grays */
    --c-paper:       #fbf8f0;  /* card / control surface (lighter than the page) */
    --c-paper-bg:    #f6efdf;  /* page / body background — Parchment lightened a touch */
    --c-paper-warm:  #f0e9d9;  /* Parchment — reserved; unused since input wells moved to --c-paper */
    --c-paper-soft:  #e7decb;  /* soft fill: pills, panel + modal headers, counts */
    --c-rule:        #ddd3c0;  /* hairline borders / dividers (light stone) */
    --c-rule-strong: #c7bba4;  /* stronger borders (mid stone) */
    --c-ink-faint:   #a89e92;  /* Decoration + genuinely inactive controls.
                                  1.97:1 on --c-paper-soft, so it is not a
                                  text color for anything a user must read —
                                  but WCAG 1.4.3's "inactive user interface
                                  component" exception does cover a disabled
                                  control, and two rules below rely on that.
                                  Not test-enforced: nothing checks usage.
                                  See web/site/css/style.css "Contrast tiers". */
    --c-ink-muted:   #665e58;  /* Stone — secondary text, metadata, timestamps.
                                  Darkened from #8e847a (3.20:1) to clear
                                  4.5:1 on paper, paper-bg and paper-soft. */
    --c-ink-soft:    #3a322c;  /* body text */
    --c-ink:         #14110f;  /* Ink — default text / headlines */
    --c-ink-pure:    #14110f;  /* Ink (warmer than pure black) */

    /* Brand + premium accents (oxblood + burnished — used sparingly) */
    --c-oxblood:      #6b1f1f;  /* signature: brand mark + primary CTA */
    --c-oxblood-deep: #571919;  /* primary-CTA hover */
    --c-burnished:      #957340;  /* premium / in-progress accent + hover/focus.
                                     Decorative tier: clears the 3.25:1 UI
                                     floor, not 4.75:1 */
    --c-burnished-text: #755b32;  /* the same gold as TEXT (>= 4.75:1) — small
                                     status labels, chip states, step markers,
                                     and any gold GROUND under light text
                                     (.approval-badge), since contrast is
                                     symmetric */

    /* Earthy status accents harmonized with the palette */
    --c-status-complete: #4b6645;  /* moss green */
    --c-status-failed:   #9c3b2e;  /* brick red */

    /* Spacing scale */
    --s-1: 0.25rem;
    --s-2: 0.5rem;
    --s-3: 0.75rem;
    --s-4: 1rem;
    --s-5: 1.5rem;
    --s-6: 2rem;
    --s-7: 3rem;
    --s-8: 4rem;
    --s-9: 6rem;

    /* Radii */
    --radius-sm: 4px;
    --radius:    8px;
    --radius-lg: 14px;

    /* Motion */
    --t-fast: 0.12s ease;
    --t-base: 0.2s ease;
    --t-slow: 0.35s ease;

    /* Layout */
    --page-max: 1280px;

    /* Raised-sheet additions — app-only; not part of the twelve-token
       site twin.  Shadow tints reuse the ink channels (20, 17, 15) the
       sheet already sanctions; --sh-slip is the resting occlusion under
       every suggestion slip (the interactive card's hover casts a
       stronger lift above it). */
    --sh-sheet: 0 1px 2px rgba(20, 17, 15, 0.08), 0 18px 44px -28px rgba(20, 17, 15, 0.4);
    --sh-slip: 0 1px 3px rgba(20, 17, 15, 0.08);
    --measure-turn: 60ch;

    /* Design-token layer — app-only; not part of the twelve-token site
       twin.  The type scale, the micro-label tracking + pill radius,
       the mono stack, elevation companions to the two raised-sheet
       shadows above, the shared focus ring, and the fixed-chrome
       z ladder.  Shadow and ring tints reuse only the sanctioned ink
       (20, 17, 15) and burnished (149, 115, 64) channels. */
    --fs-micro:   0.7rem;   /* uppercase tags, eyebrows, pill labels        */
    --fs-meta:    0.78rem;  /* metadata, counts, kbd, chip sublabels        */
    --fs-ui:      0.85rem;  /* nav links, small controls, secondary UI text */
    --fs-body:    0.92rem;  /* card/list body prose                         */
    --fs-base:    1rem;     /* base body                                    */
    --fs-lede:    1.05rem;  /* ledes + the assistant's serif correspondence */
    --fs-title:   1.15rem;  /* card/panel titles                            */
    --fs-section: 1.4rem;   /* section titles                               */
    --fs-display: 1.75rem;  /* h2-tier display                              */
    --fs-hero:    2.25rem;  /* h1 / detail title                            */
    --track-tag: 0.08em;
    --radius-pill: 999px;
    --font-mono: 'SF Mono', Menlo, Consolas, monospace;
    --sh-overlay: 0 18px 60px rgba(20, 17, 15, 0.24);
    --sh-well: inset 0 1px 3px rgba(20, 17, 15, 0.08);
    --sh-lift: 0 6px 20px rgba(20, 17, 15, 0.10);
    --sh-float: 0 10px 30px rgba(20, 17, 15, 0.28);
    --focus-ring: 0 0 0 3px rgba(149, 115, 64, 0.35);
    --z-nav: 40;
    --z-launcher: 90;
    --z-panel: 95;
    --z-modal: 100;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The SPA's optional chrome is painted into stable mounts: an empty
   element rendered once carrying `hidden`, which its paint method fills
   and un-hides rather than re-rendering the container around it (that
   container is often the composer form, and a wholesale repaint would
   take the user's draft with it).
 *
 * `[hidden]` is a USER-AGENT rule, so any author-origin `display`
 * outranks it however weak the selector — and these mounts are gapped
 * columns and rows, so nearly all of them set one.  The result is an
 * empty mount that paints its padding, border and fill while "hidden";
 * for a bordered, filled one that reads as an accented bar sitting
 * across the composer for no reason.
 *
 * This one declaration makes that unrepresentable, which is why the
 * seven hand-written `.<class>[hidden]` opt-outs it replaced are gone:
 * they were the same fix applied one class at a time, and the mount
 * that forgets one is always the next one somebody adds.
 *
 * `!important` is load-bearing, not decoration.  Bare `[hidden]` is
 * (0,1,0) — a tie with `.suggestion-deck`, lost on source order — so
 * without it this rule would fix nothing.
 *
 * `TestHiddenMountsCollapse` in tests/test_app_static.py holds the line,
 * and the check that matters is a real one: it boots this SPA in
 * Chromium against these served bytes, forces every stable mount
 * `hidden`, and fails if any `[hidden]` element ends up with a
 * displaying `display` or a non-zero box.  No text sweep can do that —
 * an earlier CSS-parsing version was green against a bypass nested
 * inside this very rule's braces.  Two lints cover what the browser
 * cannot (it computes one viewport in `screen` media, and CI has no
 * browser at all): the line below must appear **verbatim** and at the
 * top level of this file — do not reformat it, do not comment it out,
 * and do not move it inside an `@media` or `@supports` block, both of
 * which leave these bytes looking present while the reset stops
 * applying — and no `!important` in ANY stylesheet under web/app/css/,
 * at-rule bodies included, may set a displaying `display` (or `all`).
 * That second lint reads the whole directory, not this file: a rule in
 * a `components.css` next door outranks this reset exactly as well as
 * one written here.
 *
 * The top-level requirement also rules out an `@layer` wrapper, and
 * that part is stricter than the cascade needs — layer order inverts
 * for `!important`, so a layered copy of this rule is if anything
 * stronger.  It stays rejected because one uniform condition is easier
 * to trust than one with an exception in it; if this sheet ever adopts
 * layers deliberately, update the lint on purpose.
 *
 * One rule stays text-only because a browser cannot check it:
 * `hidden="until-found"` cannot be used on this surface — it needs
 * `content-visibility: hidden`, which this rule overrides.
 *
 * See docs/APP.md#stable-mounts-and-the-hidden-rule. */
[hidden] { display: none !important; }

html, body {
    background-color: var(--c-paper-bg);
}

body {
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.55;
    color: var(--c-ink);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    min-height: 100vh;
}

#app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

/* No paper grain on this surface, deliberately: the site's .grain tile
   is a data-URI image, and the /app styles are pinned asset-free — no
   CSS image reference of any form in any sheet under web/app/css/,
   comments included (see tests/test_app_integrations_frontend.py,
   which sweeps every sheet in the directory, not a slice).  The
   parchment ground carries the stock on its own. */

h1, h2, h3, h4 {
    font-family: var(--font-accent);
    font-weight: 400;
    color: var(--c-ink);
    letter-spacing: -0.005em;
}

h1 { font-size: var(--fs-hero);    line-height: 1.1; }
h2 { font-size: var(--fs-display); line-height: 1.2; }
h3 { font-size: var(--fs-title);   line-height: 1.3; }
h4 { font-size: var(--fs-base);    line-height: 1.4; }

p { color: var(--c-ink-soft); }

a {
    color: var(--c-ink);
    text-decoration: none;
    border-bottom: 1px solid var(--c-rule);
    transition: border-color var(--t-base);
}

a:hover { border-bottom-color: var(--c-ink); }

/* Skip-to-content link (web/app/index.html, first child of <body>).
   Parked off-screen rather than `hidden` — a `hidden` element cannot
   receive focus, and the reveal below is the whole point: keyboard
   focus makes it the first thing on every page.  app.js intercepts
   the click (the fragment would otherwise ride the hash router). */
.skip-link {
    position: absolute;
    top: -100vh;
    left: var(--s-4);
    z-index: 110; /* the top rung of the ladder — sticky header and its
                     menu panel (40), support launcher (90), support
                     panel (95), modal overlay (100): it is only ever
                     visible while focused */
    padding: var(--s-2) var(--s-3);
    background: var(--c-paper);
    color: var(--c-ink);
    border: 1px solid var(--c-rule-strong);
}
.skip-link:focus { top: var(--s-4); }

/* The shell's <noscript> note (web/app/index.html, immediately after
   the SPA root).  Inert whenever scripting is on — the browser never
   renders noscript content then, which is why the note ships without
   `hidden` — and the whole page when it is off, so it reads as a
   centered line of body text.  Tokens only. */
.noscript-note {
    text-align: center;
    margin-top: var(--s-7);
    color: var(--c-ink);
    font-family: var(--font-body);
}

/* ============================================================
 * Layout primitives
 * ============================================================ */

.page {
    flex: 1;
    width: 100%;
    max-width: var(--page-max);
    margin: 0 auto;
    padding: var(--s-7) var(--s-5) var(--s-9);
}

.page-hero {
    text-align: center;
    margin: 0 auto var(--s-5);
    max-width: 720px;
    padding-top: var(--s-3);
}

.page-hero h1 {
    margin-bottom: var(--s-3);
}

.page-hero p {
    color: var(--c-ink-muted);
    font-size: var(--fs-lede);
    max-width: 560px;
    margin: 0 auto;
}

/* The Stetworth brand wordmark — the signed-out heroes' <h1> and the
   signed-in header's <span> (wrapped in the .app-header-home anchor).
   Tracked uppercase in the oxblood brand accent, and bold by founder
   decision — the wordmark re-cut that superseded the mockup-era
   400-only contract: the 700 declaration is backed by the 0,700 face
   the shell's Google Fonts URL loads, because declaration and load
   travel together or the browser synthesizes a smeared faux bold.
   EB Garamond is inherited from the base h1 rule on the heroes, and
   declared by the header's own compound rule on the span (a span
   inherits the body stack instead).  The negative right margin
   cancels the last glyph's letter-spacing so the centered mark stays
   optically centered.  The rule body below is locked, and mirrored
   byte-for-byte by web/mockup/css/style.css's copy (parity is
   test-enforced); the header's compound in the nav region is the
   mark's only other treatment. */
.wordmark {
    text-transform: uppercase;
    letter-spacing: 0.16em;
    margin-right: -0.16em;
    color: var(--c-oxblood);
    font-size: 1.75rem;
    font-weight: 700;
}

/* The signed-in home family's page heading — "Home", "New task",
   "Your tasks": one name per page, equal to the nav label and the
   route title.  A page heading in the print register, not a
   resurrected hero: h2-tier on the scale, EB Garamond and its 400
   weight inherited from the base h1 rule (no new fonts or weights),
   left-set with the page body rather than centered like the retired
   hero.  "The page body" is the 880px content measure every
   home-family section centers itself on (.page-composer-only
   .composer, .page-tasks-only .task-gallery, .first-run,
   .inflight-strip — and the detail family's own .detail container,
   whose .detail-title demonstrates the intended alignment), so the
   heading takes the same measure: without it the h1 sat at the
   page's 24px gutter while everything under it started at the
   column, ~176px to the right at 1280.  The detail family keeps its
   own .detail-title. */
.page-title {
    margin: 0 auto var(--s-5);
    max-width: 880px;
    width: 100%;
    font-size: var(--fs-display);
    line-height: 1.2;
}

.page-section {
    margin: var(--s-7) auto;
    max-width: var(--page-max);
}

.divider {
    border: 0;
    border-top: 1px solid var(--c-rule);
    margin: var(--s-5) 0;
}

/* ============================================================
 * Buttons
 * ============================================================ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    padding: 0.6rem 1.1rem;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-weight: 500;
    color: var(--c-ink);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius);
    cursor: pointer;
    text-decoration: none;
    transition: background var(--t-base), color var(--t-base),
                border-color var(--t-base), transform var(--t-fast);
}

.btn:hover { border-color: var(--c-ink); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:focus-visible {
    /* A solid outline atop the soft ring (--focus-ring) — the
       35%-alpha wash alone sits under the WCAG focus-visible contrast
       floor on parchment (docs/APP.md); the ring stays for warmth,
       the outline carries the obligation. */
    outline: 2px solid var(--c-burnished);
    outline-offset: 2px;
    box-shadow: var(--focus-ring);
}

.btn-primary {
    color: var(--c-paper);
    background: var(--c-oxblood);
    border-color: var(--c-oxblood);
}
.btn-primary:hover { background: var(--c-oxblood-deep); border-color: var(--c-oxblood-deep); }

/* .btn-secondary and .btn-tertiary are aliases of the .btn base: their
 * resting blocks restated the base's own values byte-for-byte and were
 * deleted — the classes stay, because the per-tool renderer modules
 * (web/app/renderers/) and 17 pinned app.js markup sites still emit
 * them, written against the retired surface's button vocabulary, and
 * markup removal rides later slices that reopen those method bodies.
 * Only the hover fill differs from the base's border-only hover, so
 * one grouped rule carries it for both aliases. */
.btn-secondary:hover, .btn-tertiary:hover {
    background: var(--c-paper-soft);
    border-color: var(--c-ink);
}

.btn-ghost {
    color: var(--c-ink-soft);
    background: transparent;
    border-color: transparent;
}
.btn-ghost:hover { background: var(--c-paper-soft); color: var(--c-ink); }

.btn-danger {
    color: var(--c-paper);
    background: var(--c-status-failed);
    border-color: var(--c-status-failed);
}
.btn-danger:hover { opacity: 0.9; }

/* The submitted-build bubble's Cancel control
 * (docs/ASSISTANT.md#the-submitted-build-bubble).  Quieter than the
 * composer's Submit, like the draft controls: this one undoes, and
 * nothing on that surface should make undoing look like the primary
 * act.  It lives HERE, beside the `.btn` family it overrides, rather
 * than with its bubble's other rules in web/app/css/assistant.css:
 * both rules tie at (0,1,0) on `background`, assistant.css links
 * before this sheet, and a cross-sheet tie pair stays in one file —
 * the `.btn-small` / `.sign-out-btn` stays rule
 * (docs/APP.md#the-stylesheet-split). */
.assistant-submit-cancel {
    background: var(--c-paper-soft);
    border-color: var(--c-rule-strong);
    color: var(--c-ink);
}

.btn-small {
    padding: 0.35rem 0.7rem;
    font-size: var(--fs-ui);
    border-radius: var(--radius-sm);
}

.btn-icon {
    width: 36px;
    height: 36px;
    padding: 0;
    border-radius: var(--radius);
    color: var(--c-ink-muted);
    background: transparent;
    border: 1px solid transparent;
}
.btn-icon:hover { background: var(--c-paper-soft); color: var(--c-ink); border-color: var(--c-rule); }

.btn svg, .btn-icon svg {
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
}

/* The underlined-text-button family.  One .btn-link base carries the
   shared reset and quiet-link treatment; the adopted classes keep only
   their deltas at their own rules later in the sheet, winning (0,1,0)
   ties by source order, while the grouped :hover / :focus-visible
   below win interaction states at (0,2,0).  .btn-link itself is the
   name new markup uses from the next spec on — today it matches
   nothing. */
.btn-link, .destination-action, .card-context-clear,
.context-hint-action, .context-hint-dismiss, .readiness-route-link {
    padding: 0;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: inherit;
    font-weight: inherit;
    color: var(--c-ink-muted);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.btn-link:hover, .destination-action:hover, .card-context-clear:hover,
.context-hint-action:hover, .context-hint-dismiss:hover, .readiness-route-link:hover {
    color: var(--c-ink);
}

.btn-link:focus-visible, .destination-action:focus-visible,
.card-context-clear:focus-visible, .context-hint-action:focus-visible,
.context-hint-dismiss:focus-visible, .readiness-route-link:focus-visible {
    outline: 2px solid var(--c-burnished);
    outline-offset: 2px;
}

/* Inline keyboard-shortcut hint, e.g. <kbd>⌘K</kbd> */
.kbd {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    line-height: 1;
    color: var(--c-ink-muted);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-bottom-width: 2px;
    border-radius: var(--radius-sm);
    padding: 0.2rem 0.45rem;
    vertical-align: middle;
}

/* ============================================================
 * Status pills
 * ============================================================ */

/* The micro-label device.  One .tag base carries the type treatment
   every chip shares; the second grouped rule adds the boxed pill
   silhouette.  Members whose deltas stayed in this sheet keep them at
   their own rules later in the sheet, winning (0,1,0) ties by source
   order.  Three members keep their deltas in their surface sheets
   instead — .inflight-item-status in assistant.css, .attach-chip-state
   in composer.css, .detail-section-count in detail.css, all linked
   before style.css — so those deltas must stay property-disjoint from
   these group rules: no source-order tie-break exists across files
   (docs/APP.md#the-split-stylesheets). */
.tag, .pill, .review-row-tag, .attach-chip-state,
.approval-confirmation-badge, .working-set-badge,
.integration-snapshot-state, .approval-badge, .correction-badge,
.inflight-item-status, .assistant-draft-item-meta, .destination-fact,
.detail-section-count {
    font-family: var(--font-body);
    font-size: var(--fs-micro);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--track-tag);
}

.tag, .pill, .section-count, .review-row-tag,
.approval-confirmation-badge, .working-set-badge,
.integration-snapshot-state, .approval-badge, .correction-badge,
.sc-integration {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.18rem 0.55rem;
    color: var(--c-ink-muted);
    background: var(--c-paper-soft);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius-pill);
}

.pill {
    flex-shrink: 0;
}

.pill::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}

.pill-queued {
    color: var(--c-ink-muted);
    background: var(--c-paper-soft);
    border-color: var(--c-rule);
}

.pill-in-progress {
    color: var(--c-burnished-text);
    background: var(--c-paper);
    border-color: var(--c-rule-strong);
}
.pill-in-progress::before {
    animation: pill-pulse 1.6s ease-in-out infinite;
}

.pill-complete {
    color: var(--c-status-complete);
    background: var(--c-paper);
    border-color: var(--c-rule-strong);
}

.pill-failed {
    color: var(--c-status-failed);
    background: var(--c-paper);
    border-color: var(--c-rule-strong);
}

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

/* ============================================================
 * Cards
 * ============================================================ */

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-5);
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius);
    color: inherit;
    transition: border-color var(--t-base), transform var(--t-fast),
                box-shadow var(--t-slow);
}

.card-hover-elevate:hover {
    border-color: var(--c-ink-faint);
    transform: translateY(-2px);
    box-shadow: var(--sh-lift);
}

.card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s-3);
    margin-bottom: var(--s-1);
}

.card-header h3, .card-header h4 {
    margin: 0;
    line-height: 1.2;
}

.card-body {
    color: var(--c-ink-soft);
    font-size: var(--fs-body);
    line-height: 1.55;
}

.card-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-3);
    font-size: var(--fs-meta);
    color: var(--c-ink-muted);
    padding-top: var(--s-3);
    border-top: 1px solid var(--c-rule);
    margin-top: auto;
}

/* ============================================================
 * Panels (heavier-weight container; section-style)
 * ============================================================ */

.panel {
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-4) var(--s-5);
    background: var(--c-paper-soft);
    border-bottom: 1px solid var(--c-rule);
}

.panel-title {
    font-family: var(--font-accent);
    font-weight: 400;
    font-size: var(--fs-title);
    margin: 0;
    color: var(--c-ink);
}

.panel-divider {
    border: 0;
    border-top: 1px solid var(--c-rule);
    margin: 0;
}

/* Stayed behind when the sign-in section was cut to signin.css: this
   is session chrome (a header-cluster row), not a sign-in screen, and
   tests/test_app_static.py pins it here.

   "Signed in as …" note beside the sign-out button — a row of the
   Account menu's panel, visible at every width once the menu is
   toggled (it used to ride the header row itself, hidden as chrome
   noise at tablet widths; the menu is what retired that hide). */
.page-nav-session {
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
}

/* ============================================================
 * Task gallery (Unit 3)
 *
 * The gallery is the approval-fed "Needs your input" section
 * followed by the four status sections (Active, Queued,
 * Completed, Failed) stacked under the composer.  Each section
 * is its own block with a header (label + count) and a
 * responsive grid of cards.
 * ============================================================ */

.task-gallery {
    display: flex;
    flex-direction: column;
    gap: var(--s-7);
    margin: 0 auto;
    width: 100%;
}

.status-section {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

.status-section-header {
    display: flex;
    align-items: baseline;
    justify-content: flex-start;
    gap: var(--s-3);
    padding-bottom: var(--s-3);
    border-bottom: 1px solid var(--c-rule);
}

.status-section-title {
    font-family: var(--font-accent);
    font-weight: 400;
    font-size: var(--fs-section);
    color: var(--c-ink);
    margin: 0;
    letter-spacing: -0.005em;
}

.section-count {
    font-family: var(--font-body);
    font-size: var(--fs-meta);
    font-weight: 500;
    min-width: 1.5rem;
    height: 1.5rem;
    justify-content: center;
    padding: 0 0.45rem;
    font-variant-numeric: tabular-nums;
}

.task-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--s-4);
}

/* Cards inside the gallery use the .card base from above plus a
 * little extra structure for the title + meta-row layout. */

/* The grid item is the shell, not the card, because the card is an
 * anchor and a nested anchor is not parseable HTML — the optional
 * "View pull request" link has to be its sibling.  The shell adds no
 * chrome of its own, and the card keeps stretching to the tallest in
 * its row exactly as it did when it was the grid item itself. */
.task-card-shell {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-width: 0;
}

.task-card-shell > .task-card { flex: 1 1 auto; }

/* Same treatment as .budget-banner-pr-link: the global `a` rule
 * already supplies the hairline underline, so this only recolors it
 * and keeps it from stretching the full column width. */
.task-card-pr-link {
    align-self: flex-start;
    font-size: var(--fs-ui);
    color: var(--c-oxblood);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.task-card-pr-link:hover { border-bottom-color: var(--c-oxblood); }

/* ----------------------------------------------------------------
 * Cancel control (gallery card + task detail header)
 *
 * One wrapper class for both surfaces, because one renderer emits
 * both (`_renderCancelControl` in web/app/js/app.js).  No new
 * foundations: the button is
 * the existing `.btn.btn-secondary.btn-small`, and the inline error
 * is the failure banner's own `.modal-error`.  The layout rule lives
 * here in the stay-put gallery section (this sheet loads last, and
 * the class is shared across both surfaces); the detail header's
 * always-present mount rule (`.detail-cancel:empty`) lives with the
 * other detail mounts in web/app/css/detail.css.
 * ---------------------------------------------------------------- */

.task-cancel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-2);
}

.task-card {
    /* Override the anchor underline inherited from the base 'a' rule
     * — task cards present as solid surfaces, not links. */
    border-bottom: 1px solid var(--c-rule);
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    min-height: 168px;
}

.task-card:hover { border-bottom-color: var(--c-ink-faint); }

.task-card-title {
    font-family: var(--font-accent);
    font-weight: 400;
    font-size: var(--fs-title);
    line-height: 1.25;
    color: var(--c-ink);
    margin: 0;
    /* Cap to 2 visual lines so titles of varying length keep a
     * consistent card height without truncating mid-word. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.task-card-description {
    color: var(--c-ink-muted);
    font-size: var(--fs-body);
    line-height: 1.5;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.task-card-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
}

.task-card-time {
    color: var(--c-ink-muted);
}

.task-card-progress {
    color: var(--c-ink-soft);
    font-variant-numeric: tabular-nums;
}

/* ============================================================
 * Detail-page vocabulary that stays with this sheet
 *
 * The task detail page's own sections were cut to detail.css; the
 * sub-sections below (Timeline through the Modal) stayed here
 * because they are shared vocabulary — the review panels and their
 * disclosures dress the preferences and account pages as much as the
 * detail page, and the header cluster inside the Preferences span is
 * pinned to this sheet by tests/test_app_static.py.  See
 * docs/APP.md#the-split-stylesheets for the full stays list.
 * ============================================================ */

/* ----------------------------------------------------------------
 * Timeline
 * ---------------------------------------------------------------- */

.timeline {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
    position: relative;
}

.timeline::before {
    content: '';
    position: absolute;
    left: 17px;
    top: 6px;
    bottom: 6px;
    width: 1px;
    background: var(--c-rule);
}

.timeline-step {
    position: relative;
    display: grid;
    grid-template-columns: 36px 1fr;
    gap: var(--s-4);
    align-items: flex-start;
}

.timeline-step-marker {
    position: relative;
    z-index: 1;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-body);
    font-weight: 500;
    color: var(--c-ink-muted);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: 50%;
    font-variant-numeric: tabular-nums;
}

.timeline-step-marker-complete {
    color: var(--c-status-complete);
    border-color: var(--c-status-complete);
}

.timeline-step-marker-failed {
    color: var(--c-status-failed);
    border-color: var(--c-status-failed);
}

.timeline-step-marker-active {
    color: var(--c-burnished-text);
    border-color: var(--c-burnished-text);
}

.timeline-step-marker-in_progress {
    color: var(--c-burnished-text);
    border-color: var(--c-burnished-text);
}

/* --c-ink-muted, not faint: this colors the step numeral, which is
   informational text in a progress display rather than an inactive UI
   component, so WCAG 1.4.3's exception does not apply.  The unfilled ring
   carries "pending" on its own. */
.timeline-step-marker-pending {
    color: var(--c-ink-muted);
}

.timeline-step-pulse {
    position: absolute;
    right: -2px;
    top: -2px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    /* Matches the marker ring it overlaps (right/top -2px straddles the
       stroke); leaving it on --c-burnished put two golds on one edge. */
    background: var(--c-burnished-text);
    animation: pill-pulse 1.6s ease-in-out infinite;
}

.timeline-step-body {
    padding-top: 6px;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.timeline-step-summary {
    margin: 0;
    color: var(--c-ink-soft);
    line-height: 1.5;
}

.timeline-step-failed .timeline-step-summary {
    color: var(--c-ink-soft);
}

.timeline-correction {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--c-paper-soft);
    border-left: 3px solid var(--c-ink-faint);
    border-radius: var(--radius-sm);
}

.correction-badge {
    background: var(--c-paper);
    border-color: var(--c-rule-strong);
    align-self: flex-start;
    margin-top: 2px;
}

.timeline-correction-body {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
}

.timeline-correction-headline {
    margin: 0;
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink);
}

.timeline-correction-text {
    margin: 0;
    font-size: var(--fs-body);
    color: var(--c-ink-soft);
    line-height: 1.5;
}

/* ----------------------------------------------------------------
 * Review panels (best-practices, assumptions, improvements,
 * deliverables — share .review-panel + .review-row).
 * ---------------------------------------------------------------- */

.review-panel {
    display: flex;
    flex-direction: column;
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius);
    overflow: hidden;
}

.review-row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--s-4);
    align-items: flex-start;
    padding: var(--s-4) var(--s-5);
    border-bottom: 1px solid var(--c-rule);
}

.review-row:last-child {
    border-bottom: 0;
}

.review-row-body {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-width: 0;
}

.review-row-title {
    font-family: var(--font-accent);
    font-weight: 400;
    font-size: var(--fs-lede);
    line-height: 1.3;
    color: var(--c-ink);
    margin: 0;
}

.review-row-rationale {
    margin: 0;
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink-muted);
}

.review-row-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: var(--s-2);
    flex-shrink: 0;
}

/* ----------------------------------------------------------------
 * Panel disclosures (native <details>)
 *
 * The criteria panel folds its low-signal rows — the ones the checks
 * confirmed — behind a native disclosure while still writing every
 * row into the DOM (the SPA binds handlers per element; a row left
 * out of the write would have dead controls): .review-disclosure
 * inside a .review-panel, whose <summary> reads as one more panel
 * row — same horizontal padding and hairline rule as .review-row, a
 * soft fill so the fold line is legible at a glance — with
 * .review-disclosure-rows holding the folded rows, which keep their
 * own row borders.  (The best-practices panel used to carry a second
 * placement, .review-disclosure-section, wrapping the whole panel;
 * that section now folds at section level instead —
 * .detail-section-disclosure, in the task-detail block above — so
 * the inner placement retired with its only emitter.)
 * The native marker triangle is kept (no vendor marker overrides, no
 * chevron element): it is the affordance every browser already ships
 * for <details>, and this surface has no chevron vocabulary of its
 * own.  Focus follows the shared .btn focus-visible ring.
 * ---------------------------------------------------------------- */

/* The one-line stand-in for the folded criteria rows, rendered as the
   panel's first row whenever the task has criteria at all. */
.review-panel-summary {
    margin: 0;
    padding: var(--s-4) var(--s-5);
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink-soft);
    border-bottom: 1px solid var(--c-rule);
}

/* The best-practices summary carries the panel summary's type
   treatment, but sits in the section disclosure's open body between
   the lede and the panel — outside any .review-panel.  So it drops
   the panel's horizontal inset and its dividing rule: there is no
   panel body beneath it to divide. */
.review-panel-summary.review-section-summary {
    padding-left: 0;
    padding-right: 0;
    border-bottom: none;
}

details.review-disclosure {
    min-width: 0;
}

summary.review-disclosure-summary {
    padding: var(--s-3) var(--s-5);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink-muted);
    background: var(--c-paper-soft);
    cursor: pointer;
}

summary.review-disclosure-summary:hover {
    color: var(--c-ink);
}

summary.review-disclosure-summary::marker {
    color: var(--c-ink-faint);
}

/* Focus ring drawn INSIDE the summary's own box.
 *
 * The .btn pattern (`outline: none` + an outward `box-shadow` ring) is
 * wrong here: this <summary> is a full-bleed block child of
 * .review-panel, which sets `overflow: hidden` for its border-radius,
 * and that clips descendant painting to the padding box.  An outward
 * ring is cut off left and right entirely, and on the bottom whenever
 * the closed <details> is the panel's last child — the common case.
 * The .btn uses survive only because .review-row insets them.
 *
 * `outline` + a negative `outline-offset` paints within the element, so
 * no ancestor clip can remove it, and it keeps the UA's own focus
 * affordance rather than replacing it with nothing.  This is the only
 * new interactive control in the criteria panel, and on a task whose
 * gate never ran it is the control gating every row.  Its 0.55 alpha
 * is a deliberate tier of its own, distinct from --focus-ring's 0.35
 * wash — an outline, not a box-shadow, so it sits outside the
 * elevation tokens by construction. */
summary.review-disclosure-summary:focus-visible {
    outline: 3px solid rgba(149, 115, 64, 0.55);
    outline-offset: -3px;
}

/* Open: the summary keeps its own rule against the rows it revealed. */
details.review-disclosure[open] > summary.review-disclosure-summary {
    border-bottom: 1px solid var(--c-rule);
}

.review-disclosure-rows {
    display: flex;
    flex-direction: column;
}

/* ----------------------------------------------------------------
 * Preferences & criteria
 *
 * The detail page's "Criteria enforced on this task" section and the
 * standalone Preferences page reuse .review-panel / .review-row.
 * The additions below are the source chips, the "Saved" indicator,
 * the section header row (title + add button — the Preferences
 * page's shape), and the simple nav
 * links to the Preferences page.  All colors come from the shared
 * palette tokens (--c-ink-*, --c-paper-*, and the two status accents),
 * so the palette guard in test_mockup_styles.py stays green.
 * ---------------------------------------------------------------- */

/* Section header row: title on the left, an action on the right
   ("+ Add preference" here; "Download all" on the deliverables
   panel). */
.detail-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-4);
    flex-wrap: wrap;
}

/* Source chip shown above a row title ("From this task" / "Saved · from …"). */
.review-row-tag {
    align-self: flex-start;
}

/* Saved preferences read as the established/default state … */
.review-row-tag-saved {
    color: var(--c-ink-soft);
    background: var(--c-paper-soft);
    border-color: var(--c-rule-strong);
}

/* … task criteria read as provisional (dashed border) … */
.review-row-tag-criterion {
    color: var(--c-ink-muted);
    background: var(--c-paper);
    border-style: dashed;
}

/* … general best practices stay neutral. */
.review-row-tag-practice {
    color: var(--c-ink-muted);
    background: var(--c-paper);
}

/* "Saved ✓" confirmation that replaces the save button once promoted. */
.review-row-saved {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-status-complete);
    white-space: nowrap;
}

/* The one header — wordmark left, plain text-style nav links right —
   sticky on wide viewports.  At rest it is deliberately NOT a bar:
   its ground is the page's own parchment (--c-paper-bg, opaque so
   scrolled content disappears cleanly behind it) with no rule and no
   frost, so the links simply sit on the page — the founder call kept
   from the nav this header replaced.  What the founder revised is the
   scrolled edge: once the page is scrolled, a 1px --c-rule hairline
   terminates the header (the .app-scrolled root flag, toggled by one
   passive scroll listener), so sliding content meets a rule instead
   of an invisible boundary.  The border is transparent at rest rather
   than absent so the toggle never shifts geometry.  The negative
   margins cancel .page's own padding so the sticky region spans the
   page box edge-to-edge and sits flush with the viewport top.  Below
   720px it goes static again (see the responsive block). */
.app-header {
    max-width: none;
    margin: calc(-1 * var(--s-7)) calc(-1 * var(--s-5)) var(--s-5);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
    position: sticky;
    top: 0;
    z-index: var(--z-nav);
    padding: var(--s-3) var(--s-5);
    background: var(--c-paper-bg);
    border-bottom: 1px solid transparent;
}

.app-scrolled .app-header {
    border-bottom-color: var(--c-rule);
}

/* The header's brand mark rides the nav row at the settled 1.2rem —
   the size the retired conversation-settle rule already shipped and
   the founder accepted.  The compound selector (0,2,0) beats the
   locked top-level .wordmark rule (0,1,0) without touching it — this
   is the mark's only new sizing, at every width.  The accent face is
   declared here because the mark is a <span> now: the heroes' <h1>
   inherited EB Garamond from the heading rule, a span inherits the
   body stack, and the header stays on the loaded faces — since the
   founder's wordmark re-cut those are Garamond 400 and the 700 the
   locked .wordmark rule's true bold rides on (the 400-only "no new
   fonts" decision this comment used to record is superseded). */
.app-header .wordmark {
    font-family: var(--font-accent);
    font-size: 1.2rem;
}

/* The mark's anchor wrapper — the header wordmark is a home link now
   (the founder's logo-to-home decision; on the home page the click
   is a deliberately inert self-link in every spelling, because the
   dispatcher's fall-through canonicalizes the boot's empty hash and
   unknown addresses to #/ before painting — with the hash already
   #/, the click fires no hashchange).  The app-side analogue of the
   site's .site-nav-home wrapper (web/site/css/style.css): the brand
   mark must not carry the body-link rule, so the base `a` rule's
   border-bottom hairline is removed here — that rule already sets
   text-decoration: none.  Navigation is native anchor semantics; no
   JS handler exists.  Its :focus-visible ring is the header row's
   own, comma-joined into the grouped rule below the nav styles; its
   phone-width 44px touch floor rides the row's floored group in the
   720px block. */
.app-header-home {
    border-bottom: none;
}

/* The header's inner nav: a plain baseline flex row.  The sticky
   treatment, the edge-to-edge margins and the parchment ground all
   belong to .app-header above; this row only lays out the links. */
.page-nav {
    display: flex;
    align-items: baseline;
    gap: var(--s-3);
    flex-wrap: wrap;
}

.page-nav-link {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink-muted);
    border-bottom: 1px solid transparent;
}

.page-nav-link:hover {
    color: var(--c-ink);
    border-bottom-color: var(--c-ink);
}

/* Current-page marker — live since the one-header restructure: the
   header renders the page under the reader as a non-link
   <span class="page-nav-link page-nav-current" aria-current="page">,
   which this ruleset styles (it pre-landed dead so activation could
   be a markup-only change, and that is exactly how it activated).
   Ink text, oxblood underline: the accent used as punctuation — a
   small "you are here" mark — not decoration.  Specificity: the
   marker ties .page-nav-link:hover at (0,2,0), so source order
   after it is what makes the marker win at rest, while the grouped
   :hover variant at (0,3,0) wins on its own. */
.page-nav-link[aria-current="page"],
.page-nav-link[aria-current="page"]:hover {
    color: var(--c-ink);
    border-bottom-color: var(--c-oxblood);
}

/* Keyboard focus on the header row's text controls — links and a
   bare button, so no rule gave them a visible ring before; the hover
   underline never fired for a keyboard reader.  The brand mark's
   home anchor is grouped in rather than given a standalone copy: one
   rule, so a retuned ring can never fork into two treatments across
   the same row. */
.app-header-home:focus-visible,
.page-nav-link:focus-visible,
.page-nav-balance:focus-visible,
.sign-out-btn:focus-visible {
    outline: 2px solid var(--c-burnished);
    outline-offset: 2px;
}

/* Credits-balance indicator — a compact "N.N credits" chip riding the
 * session controls in every page nav, linking to the account page.  It
 * reads its numbers from data the SPA already fetches (see
 * docs/CREDITS.md#user-facing-api); a negative (overdrawn) balance
 * switches to the sanctioned failed-status red so it reads at a glance.
 * The amount + org spans have stable hooks so `_updateBalanceIndicator`
 * can refresh their text in place. */
/* The mount `_renderSessionControls` emits.  `display: contents` keeps the
   chip a direct flex child of the nav, so wrapping it changed no layout;
   an empty mount renders nothing at all, which is the point — the chip is
   absent until there is a balance to show, rather than present at zero
   size (see _updateBalanceIndicator). */
.page-nav-balance-mount {
    display: contents;
}

.page-nav-balance {
    display: inline-flex;
    align-items: baseline;
    gap: var(--s-2);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink-soft);
    border-bottom: 1px solid transparent;
}

.page-nav-balance:hover {
    color: var(--c-ink);
    border-bottom-color: var(--c-ink);
}

/* Tabular figures so the balance doesn't jitter as digits change. */
.page-nav-balance-amount {
    font-variant-numeric: tabular-nums;
}

/* Shared-organization name, subordinate to the amount by colour alone —
   it inherits the chip's 0.85rem, so the whole row reads at one text
   size.  Collapses when empty (a personal org, or before the first
   credits read) so the chip carries no stray gap. */
.page-nav-balance-org {
    color: var(--c-ink-muted);
}

.page-nav-balance-amount:empty,
.page-nav-balance-org:empty {
    display: none;
}

/* Overdrawn balance: the amount (which inherits the chip's colour) goes
   the sanctioned failed-status red, and its hover underline follows. */
.page-nav-balance-negative {
    color: var(--c-status-failed);
}

.page-nav-balance-negative:hover {
    color: var(--c-status-failed);
    border-bottom-color: var(--c-status-failed);
}

/* Sign out as a text-metrics peer of the nav links: the row's shared
   0.85rem, with the padding trimmed so the button's box rides the text
   line instead of inflating the row.  .btn-small sets both properties
   at the same (0,1,0) specificity, so source order alone decides the
   tie — this rule must stay after .btn-small in the sheet. */
.sign-out-btn {
    font-size: var(--fs-ui);
    padding: 0.1rem 0.45rem;
}

/* The Account menu — the session cluster (account entry, balance chip,
   signed-in note, sign-out control) folded behind one "Account" word at
   the row's end.  A native <details> disclosure patterned on the site's
   industry selector: the summary toggles it with no script, every route
   repaint rebuilds it collapsed, and the panel drops absolutely against
   the relative shell so the toggle never moves the row.  No animation
   on the toggle in either direction — nothing here to reconcile with
   the reduced-motion block. */
.account-menu {
    position: relative;
}

/* The summary is a text peer of the nav links (it carries the
   .page-nav-link class in markup); list-style: none removes the native
   disclosure triangle, and the vendor pseudo-element line beside it is
   the site industry selector's own second half of that removal —
   WebKit builds through Safari 18.3 ignore list-style on summary and
   draw the legacy marker anyway, which would put the native triangle
   beside the caret below. */
.account-menu > summary {
    list-style: none;
    cursor: pointer;
}

.account-menu > summary::-webkit-details-marker {
    display: none;
}

/* NOT --c-ink-faint.  With the native triangle removed above, this
   caret is the only thing marking the summary as a menu — an
   informational UI glyph under WCAG 1.4.11, per the site selector's
   own recorded rationale — so it takes the muted ink, which meets the
   3:1 floor where the faint tier's 2.30:1 did not.  The gap before
   the glyph is a margin, not a space inside the content string: the
   phone block makes the summary inline-flex, which turns this ::after
   into its own flex item and trims leading white-space, so a
   content-string space silently vanished exactly where the summary is
   a tap target. */
.account-menu > summary::after {
    content: "\25BE";
    margin-left: 0.35em;
    font-size: 0.7em;
    color: var(--c-ink-muted);
}

.account-menu > summary:focus-visible {
    outline: 2px solid var(--c-burnished);
    outline-offset: 2px;
}

/* The dropped panel: a raised slip on the parchment, under the 3px
   oxblood head rule — the letterhead device the raised sheet
   established, extended to the menu rather than genericized.  Rows
   stack on a grid: the account entry, then the session controls
   (balance chip, signed-in note, sign out) in served order.  Width is
   content-sized (max-content over the 16rem floor) rather than pinned:
   inside the ~60px relative shell an absolute box shrink-wraps to
   min-content, which broke the balance chip onto two lines and split
   the email off its "Signed in as" label; the viewport cap keeps a
   long email from carrying the slip past the page edge. */
.account-menu-panel {
    position: absolute;
    top: calc(100% + 0.6rem);
    right: 0;
    z-index: var(--z-nav);
    width: max-content;
    min-width: 16rem;
    max-width: calc(100vw - 2 * var(--s-4));
    padding: var(--s-3);
    display: grid;
    gap: var(--s-2);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-top: 3px solid var(--c-oxblood);
    border-radius: var(--radius);
    box-shadow: var(--sh-slip);
}

/* Sign out is a grid item here, so the .btn inline-flex blockifies and
   would stretch to the full column with its label centred under the
   panel's left-aligned rows; start-aligned it reads as the control it
   is, and its hover ghost fill hugs the label instead of banding the
   panel. */
.account-menu-panel > .sign-out-btn {
    justify-self: start;
}

/* ----------------------------------------------------------------
 * Account page (route #/account) — the fuller view behind the
 * balance indicator.  It reuses the detail page skeleton
 * (.page / .detail-header / .detail-section) and the shared
 * .review-panel / .review-row for the usage-history ledger, so the
 * page is stylistically indistinguishable from #/preferences.  The
 * additions below are only the organization/balance header block,
 * the shared-org member list, and the per-row credit amount + task
 * link.  All colours are shared palette tokens.
 * ---------------------------------------------------------------- */

/* Organization identity + balance: name/type on the left, the
   current balance on the right, wrapping on narrow viewports. */
.account-org {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--s-4);
    padding: var(--s-4) var(--s-5);
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius);
}

.account-org-identity {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
}

.account-org-name {
    font-family: var(--font-accent);
    font-size: var(--fs-title);
    color: var(--c-ink);
    word-break: break-word;
}

.account-org-type {
    font-size: var(--fs-meta);
    color: var(--c-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.account-balance {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--s-1);
    flex-shrink: 0;
}

.account-balance-label {
    font-size: var(--fs-meta);
    color: var(--c-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.account-balance-amount {
    font-family: var(--font-accent);
    font-size: var(--fs-section);
    color: var(--c-ink);
    font-variant-numeric: tabular-nums;
}

/* Overdrawn balance takes the sanctioned failed-status red, matching
   the balance indicator's negative treatment. */
.account-balance-negative .account-balance-amount {
    color: var(--c-status-failed);
}

/* Shared-organization member list (personal orgs render none). */
.account-members {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    margin-top: var(--s-4);
}

.account-subhead {
    margin: 0;
    font-size: var(--fs-ui);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-ink-muted);
}

.account-member-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
}

.account-member {
    font-size: var(--fs-ui);
    color: var(--c-ink-soft);
    background: var(--c-paper-soft);
    border-radius: var(--radius-sm);
    padding: var(--s-1) var(--s-3);
    word-break: break-word;
}

/* Usage-history ledger rows reuse .review-panel + .review-row; the
   credit amount sits in the row's right-hand (auto) column. */
.account-ledger-amount {
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    white-space: nowrap;
    align-self: center;
}

/* A consumed (build) amount is a negative delta — the failed-status
   red; a granted (credit) amount is the complete-status green. */
.account-ledger-amount-debit {
    color: var(--c-status-failed);
}

.account-ledger-amount-credit {
    color: var(--c-status-complete);
}

/* The build row's title links to the task detail. */
.account-ledger-link {
    color: var(--c-oxblood);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}

.account-ledger-link:hover {
    border-bottom-color: var(--c-oxblood);
}

/* Modal form fields used by the add / edit preference dialog. */
.modal-field {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.modal-label {
    font-size: var(--fs-ui);
    font-weight: 600;
    color: var(--c-ink-soft);
}

.modal-input {
    width: 100%;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-sm);
    padding: var(--s-3) var(--s-4);
    box-shadow: var(--sh-well);
}

.modal-input:focus {
    outline: none;
    border-color: var(--c-burnished);
    box-shadow: var(--sh-well), var(--focus-ring);
}

/* Compound selector (0,2,0) so this shorter height beats the base
   `.modal-textarea` rule (0,1,0).  The base rule is declared later in
   this file, so a single-class `.modal-textarea-short` would lose on
   source order and the modifier would be inert. */
.modal-textarea.modal-textarea-short {
    min-height: 96px;
}

/* Inline validation message inside a modal field (e.g. duplicate
   preference title).  Uses the sanctioned failed-status red. */
.modal-error {
    margin: var(--s-2) 0 0;
    font-size: var(--fs-ui);
    color: var(--c-status-failed);
}

/* ----------------------------------------------------------------
 * Compliance marker (✓ / ✗ / —).  The three-column layout is scoped
 * to the best-practices panel (every row there carries a marker) and
 * to the per-row .review-row-with-status modifier — a criterion row
 * whose payload carries an outcome — so assumptions / improvements /
 * deliverables and marker-less criterion rows (which share
 * .review-row) keep their original two-column layout.  Shares the
 * visual language of the timeline step markers.
 * ---------------------------------------------------------------- */

.review-panel-best-practices .review-row,
.review-row-with-status {
    grid-template-columns: auto 1fr auto;
}

.review-row-status {
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-top: 2px;
    font-size: var(--fs-body);
    font-weight: 500;
    line-height: 1;
    color: var(--c-ink-muted);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: 50%;
    flex-shrink: 0;
}

.review-row-status-complied {
    color: var(--c-status-complete);
    border-color: var(--c-status-complete);
}

.review-row-status-not_complied {
    color: var(--c-status-failed);
    border-color: var(--c-status-failed);
}

/* "Not verified" (ran, proved nothing) and "Not checked" (never ran)
 * share the neutral dash treatment; the distinct classes keep them
 * separately addressable, and the accessible label / tooltip carries
 * the distinction. */
.review-row-status-unverified,
.review-row-status-not_checked {
    color: var(--c-ink-muted);
    border-color: var(--c-rule-strong);
}

/* The publish-approval rules below stayed behind when their section
   was cut to detail.css: each must beat a rule earlier in THIS sheet
   on source order at equal specificity (the badge trio against the
   shared pill groups, the card modifier against the base card rules,
   the reject-reason height against the short-textarea modifier), so
   moving them to a sheet linked before this one would flip the tie.

   "Confirmation required" — the per-group cue that this group's author
   marked it as needing a human before it fires.  Deliberately quiet: it
   sits inside the group name rather than shouting alongside the Approve
   button, because the gate itself is already the loud part.  Its job is
   to differentiate groups from one another, not to re-alarm the user. */
.approval-confirmation-badge {
    background: none;
    border-color: var(--c-oxblood);
    color: var(--c-oxblood);
    white-space: nowrap;
    vertical-align: middle;
    margin-left: var(--s-2);
}
/* The reject-reason field composes .modal-field / .modal-label /
 * .modal-textarea; only the reduced height is banner-specific. */
.approval-reject-field .approval-reject-reason {
    min-height: 72px;
}

/* Gallery-card "Needs your approval" treatment: a burnished badge
 * strip on the card (rendered between header and description) plus
 * an accent border via the card modifier. */
.approval-badge {
    color: var(--c-paper);
    /* --c-burnished is the 3.25:1 decorative tier; as a GROUND under 11.2px
       paper text it left the label at 4.12:1, under the 4.75 text floor.
       Contrast is symmetric, so --c-burnished-text clears that floor from
       either side — 6.00:1 here. */
    background: var(--c-burnished-text);
    border-color: var(--c-burnished-text);
    align-self: flex-start;
}

/* "Ready to publish" — the same badge shape in the completed accent,
 * matching the detail banner's variant. */
.approval-badge-unpublished {
    background: var(--c-status-complete);
    border-color: var(--c-status-complete);
}

.task-card-needs-approval {
    border: 1px solid var(--c-burnished-text);
    border-left-width: 3px;
}

/* Keep the accent on hover — the base .task-card:hover rule only
 * retints the bottom edge, which would break the accent frame. */
.task-card-needs-approval:hover {
    border-bottom-color: var(--c-burnished-text);
}

/* ----------------------------------------------------------------
 * Per-tool renderer output inside the approval preview
 *
 * `_renderApprovalPreviewGroups` delegates each staged call to the
 * renderer registry (web/app/renderers/), which returns markup
 * written against the console's class vocabulary —
 * `.publish-preview-call`, `.call-tool-name`, `.slack-bubble`,
 * `.email-card`, and so on.  Those modules are shared, surface-
 * agnostic code: they were moved into /app when /console was retired
 * and their class names deliberately were NOT rewritten, because
 * renaming markup in five modules to match one surface is how a
 * "port" turns into a fork.
 *
 * So the class names are the console's and the *styling* is entirely
 * this surface's: parchment cards, stone hairlines, the oxblood /
 * burnished / moss accent set.  None of the console's brand-colored
 * treatments (Slack aubergine, Linear indigo, QuickBooks green) is
 * carried over — vendor brand colors inside an Oxblood Library page
 * read as foreign chrome, and the tool is already named on the card.
 *
 * Every rule is scoped under `.approval-preview-call` so this
 * vocabulary cannot leak into the rest of the surface.
 * ---------------------------------------------------------------- */

.approval-preview-call .publish-preview-call {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-width: 0;
}

.approval-preview-call .call-tool-name code {
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    color: var(--c-ink-muted);
    word-break: break-word;
}

.approval-preview-call .call-description {
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink-soft);
}

/* Collapsed disclosures — the raw argument dict and the declared
 * preview-source file.  Closed by default (the renderer's own idiom
 * is the headline; the dict is the audit trail underneath it), and
 * the summary reads as a quiet control rather than a heading. */
.approval-preview-call .call-args > summary,
.approval-preview-call .preview-source > summary {
    cursor: pointer;
    font-size: var(--fs-meta);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--c-ink-muted);
}

.approval-preview-call .call-args > summary:hover,
.approval-preview-call .preview-source > summary:hover {
    color: var(--c-ink);
}

.approval-preview-call .preview-source code {
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    text-transform: none;
    letter-spacing: 0;
}

/* The raw args dump and any inlined preview text share the recessed
 * monospace well the deliverable previews use, capped so a long
 * argument bag cannot push the decision controls off screen. */
.approval-preview-call .call-args pre,
.approval-preview-call .preview-text {
    margin: var(--s-2) 0 0;
    padding: var(--s-3) var(--s-4);
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    line-height: 1.5;
    color: var(--c-ink-soft);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--sh-well);
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 30vh;
    overflow-y: auto;
}

/* Shared card frame for every per-tool body.  The accent colour on
 * the leading edge is the only per-tool differentiation. */
.approval-preview-call .slack-bubble,
.approval-preview-call .email-card,
.approval-preview-call .linear-issue-card,
.approval-preview-call .linear-comment-card,
.approval-preview-call .quickbooks-card {
    padding: var(--s-3) var(--s-4);
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-left-width: 3px;
    border-radius: var(--radius-sm);
}

/* Slack — message bubble */
.approval-preview-call .slack-bubble { border-left-color: var(--c-burnished); }

.approval-preview-call .slack-channel {
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--c-ink-muted);
    margin-bottom: var(--s-1);
}

.approval-preview-call .slack-text {
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--c-ink);
    white-space: pre-wrap;
    word-break: break-word;
}

.approval-preview-call .slack-thread,
.approval-preview-call .slack-action {
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
}

.approval-preview-call .slack-thread { margin-top: var(--s-1); }

/* Email — labeled header fields over the body */
.approval-preview-call .email-card { border-left-color: var(--c-oxblood); }

.approval-preview-call .email-field {
    display: flex;
    gap: var(--s-2);
    font-size: var(--fs-ui);
    padding: 0.15rem 0;
    border-bottom: 1px solid var(--c-rule);
}

.approval-preview-call .email-field:last-of-type { border-bottom: none; }

.approval-preview-call .email-label {
    min-width: 4.5em;
    font-weight: 600;
    color: var(--c-ink-muted);
}

.approval-preview-call .email-value {
    color: var(--c-ink);
    word-break: break-word;
}

.approval-preview-call .email-subject { font-weight: 600; }

.approval-preview-call .email-body { padding-top: var(--s-2); }

.approval-preview-call .email-body-pre {
    margin: 0;
    padding: 0;
    background: transparent;
    border: none;
    box-shadow: none;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--c-ink);
    white-space: pre-wrap;
    word-break: break-word;
}

/* Linear — issue / comment card */
.approval-preview-call .linear-issue-card,
.approval-preview-call .linear-comment-card {
    border-left-color: var(--c-ink-muted);
}

.approval-preview-call .linear-issue-header,
.approval-preview-call .linear-comment-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
    margin-bottom: var(--s-1);
}

.approval-preview-call .linear-issue-title {
    font-weight: 600;
    color: var(--c-ink);
}

.approval-preview-call .linear-priority {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-ink-soft);
    background: var(--c-paper-soft);
    border-radius: var(--radius-pill);
}

/* Urgent / high borrow the failed + burnished accents; normal and low
 * stay in the stone range so only the top two read as loud. */
.approval-preview-call .linear-priority-1 {
    color: var(--c-paper);
    background: var(--c-status-failed);
}
.approval-preview-call .linear-priority-2 {
    color: var(--c-paper);
    background: var(--c-burnished);
}

.approval-preview-call .linear-labels {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-1);
    margin-bottom: var(--s-1);
}

.approval-preview-call .linear-label {
    padding: 0.1rem 0.5rem;
    font-size: var(--fs-micro);
    color: var(--c-ink-soft);
    background: var(--c-paper-soft);
    border-radius: var(--radius-pill);
}

.approval-preview-call .linear-meta {
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
    margin-bottom: var(--s-1);
}

.approval-preview-call .linear-description pre,
.approval-preview-call .linear-comment-body pre {
    margin: var(--s-1) 0 0;
    padding: 0;
    background: transparent;
    border: none;
    box-shadow: none;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--c-ink);
    white-space: pre-wrap;
    word-break: break-word;
}

/* QuickBooks — ledger card */
.approval-preview-call .quickbooks-card {
    border-left-color: var(--c-status-complete);
}

.approval-preview-call .quickbooks-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2);
    margin-bottom: var(--s-2);
}

.approval-preview-call .quickbooks-kind-badge {
    padding: 0.1rem 0.5rem;
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-paper);
    background: var(--c-status-complete);
    border-radius: var(--radius-pill);
}

.approval-preview-call .quickbooks-meta-item {
    font-size: var(--fs-ui);
    color: var(--c-ink-soft);
}

/* Money tables scroll inside the card rather than widening the
 * detail column on a narrow viewport. */
.approval-preview-call .quickbooks-line-items {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-ui);
    display: block;
    overflow-x: auto;
}

.approval-preview-call .quickbooks-line-items th,
.approval-preview-call .quickbooks-line-items td {
    padding: var(--s-1) var(--s-2);
    text-align: left;
    border-bottom: 1px solid var(--c-rule);
}

.approval-preview-call .quickbooks-line-items th {
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-ink-muted);
}

.approval-preview-call .quickbooks-line-items th.qb-amount,
.approval-preview-call .quickbooks-line-items td.qb-amount {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.approval-preview-call .quickbooks-total {
    margin-top: var(--s-2);
    text-align: right;
    font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
    color: var(--c-ink);
}

/* Fallback key/value list both the Linear and QuickBooks renderers
 * fall back to for shapes they do not recognize. */
.approval-preview-call .linear-arg-list,
.approval-preview-call .quickbooks-arg-list {
    margin: var(--s-2) 0 0;
    padding-left: var(--s-5);
    font-size: var(--fs-ui);
    color: var(--c-ink-soft);
}

.approval-preview-call .linear-arg-key,
.approval-preview-call .quickbooks-arg-key {
    font-weight: 600;
    color: var(--c-ink-muted);
}

/* Git — the prepared branch awaiting its push.  Reads as a labelled
 * destination block (repository / base branch / branch) over the
 * diffstat, because those are the questions in the order a reader asks
 * them: where does this land, and what is in it. */
.approval-preview-call .git-destination {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    padding: var(--s-3) var(--s-4);
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-left: 3px solid var(--c-burnished);
    border-radius: var(--radius-sm);
}

.approval-preview-call .git-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2);
    font-size: var(--fs-ui);
}

.approval-preview-call .git-label {
    min-width: 8rem;
    font-size: var(--fs-micro);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-ink-muted);
}

/* A build with no remote is a normal outcome, not a fault — muted
 * rather than alarming. */
.approval-preview-call .git-repo-none {
    font-size: var(--fs-ui);
    font-style: italic;
    color: var(--c-ink-soft);
}

.approval-preview-call .git-changes {
    margin-top: var(--s-3);
}

.approval-preview-call .git-scale {
    font-size: var(--fs-ui);
    font-weight: 600;
    color: var(--c-ink-soft);
}

.approval-preview-call .git-diffstat {
    margin: var(--s-2) 0 0;
    padding: var(--s-3) var(--s-4);
    max-height: 30vh;
    overflow: auto;
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    line-height: 1.5;
    color: var(--c-ink-soft);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-sm);
    white-space: pre;
}

/* An unreadable diff is stated, not styled away — the reader has to be
 * able to tell "could not read it" from "nothing in it". */
.approval-preview-call .git-changes-unavailable .git-scale {
    color: var(--c-status-failed);
}

/* ----------------------------------------------------------------
 * Inline edit forms (renderer `editForm` output)
 *
 * `_openInlineEditForm` swaps a group's block for the renderer's own
 * form, so these rules sit inside `.approval-preview-group` rather
 * than inside a call.  Fields reuse the modal-input treatment (the
 * recessed cream well) so an edit here looks like every other place
 * this surface takes typed input.
 * ---------------------------------------------------------------- */

.approval-preview-group .renderer-edit-form {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.approval-preview-group .renderer-edit-field {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
}

.approval-preview-group .renderer-edit-field > span {
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--c-ink-muted);
}

.approval-preview-group .renderer-edit-field input,
.approval-preview-group .renderer-edit-field textarea {
    width: 100%;
    padding: var(--s-2) var(--s-3);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--sh-well);
}

.approval-preview-group .renderer-edit-field input:focus,
.approval-preview-group .renderer-edit-field textarea:focus {
    outline: none;
    border-color: var(--c-burnished);
    box-shadow: var(--sh-well), var(--focus-ring);
}

.approval-preview-group .renderer-edit-field textarea {
    min-height: 96px;
    resize: vertical;
}

.approval-preview-group .renderer-edit-actions {
    display: flex;
    align-items: center;
    gap: var(--s-2);
}

/* Stayed behind when the budget-gate section was cut to detail.css:
   the margin reset must beat `.modal-error`'s own top margin (declared
   earlier in this sheet) on source order at equal specificity.

   The inline decision error composes `.modal-error` for its colour and
 * size; the margin reset is panel-specific — `.budget-banner` is a
 * gapped flex column, so `.modal-error`'s own top margin would stack on
 * top of the gap and orphan the message from the controls it explains. */
.budget-banner-error {
    margin-top: 0;
}

/* ----------------------------------------------------------------
 * Modal (add / edit preference dialog)
 * ---------------------------------------------------------------- */

.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    background: rgba(20, 17, 15, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s-4);
    animation: fade-in var(--t-base) ease-out both;
}

.modal {
    width: 100%;
    max-width: 640px;
    max-height: calc(100vh - 4rem);
    display: flex;
    flex-direction: column;
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--sh-overlay);
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-4) var(--s-5);
    background: var(--c-paper-soft);
    border-bottom: 1px solid var(--c-rule);
}

.modal-title {
    font-family: var(--font-accent);
    font-weight: 400;
    font-size: var(--fs-title);
    color: var(--c-ink);
    margin: 0;
}

.modal-body {
    padding: var(--s-5);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s-2);
    padding: var(--s-3) var(--s-5);
    border-top: 1px solid var(--c-rule);
    background: var(--c-paper);
}

.modal-lede {
    margin: 0;
    color: var(--c-ink-muted);
    font-size: var(--fs-body);
    line-height: 1.5;
}

.modal-textarea {
    width: 100%;
    min-height: 180px;
    resize: vertical;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: 1.5;
    color: var(--c-ink);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-sm);
    padding: var(--s-3) var(--s-4);
    box-shadow: var(--sh-well);
}

.modal-textarea:focus {
    outline: none;
    border-color: var(--c-burnished);
    box-shadow: var(--sh-well), var(--focus-ring);
}

/* ============================================================
 * Animations & utilities
 * ============================================================ */

.fade-in {
    animation: fade-in var(--t-slow) ease-out both;
}

@keyframes fade-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}

.muted { color: var(--c-ink-muted); }
.center-text { text-align: center; }
.tabular-nums { font-variant-numeric: tabular-nums; }

/* ============================================================
 * Server-backed task-detail states (/app-only)
 *
 * The detail view fetches its payload from
 * `GET /api/app/tasks/{id}` and streams live progress over SSE
 * (web/app/js/app.js `_renderTaskDetail` / `_startDetailLive`), so
 * it composes the gallery-state treatments above for its own
 * loading and retryable-error panels — `.detail-state` marks that
 * reuse inside the `.detail` column (and gives the panel breathing
 * room the gallery gets from its section gap).  The /app-only
 * additions below stay inside the established vocabulary:
 *   - .detail-progress-chip — the in-place-updated progress text in
 *     the detail status row ("Step 4 of 9"), echoing the card chip.
 *   - .detail-output-actions — the row holding the single real
 *     "Download output" navigation (`.detail-download-btn` is a
 *     JS/test hook on a plain .btn anchor).
 *   - .failure-banner-error — hook for the banner's inline resume
 *     error; the visual comes from composing `.modal-error`.
 *   - .detail-refreshing / .detail-refreshing-note — the detail
 *     header's stale-while-revalidate slot: an always-present mount
 *     (`[data-detail-refreshing]`, `:empty`-collapsed like the
 *     approval mount) holding the quiet note a full-cache-hit entry
 *     paints while its re-read is in flight.  The note copies the
 *     gallery's `.gallery-refreshing-note` shape — same token
 *     vocabulary, same painted-on-demand lifecycle, never shipped
 *     `hidden`.
 * ============================================================ */

.detail-state {
    margin-top: var(--s-4);
}

.detail-progress-chip {
    color: var(--c-ink-soft);
}

.detail-output-actions {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

/* The `.detail-header` is a gapped column, so the note's mount must
 * take no space while it holds nothing. */
.detail-refreshing:empty {
    display: none;
}

.detail-refreshing-note {
    font-size: var(--fs-ui);
    font-style: italic;
    color: var(--c-ink-muted);
    margin: 0;
}

/* ============================================================
 * Integrations (/app-only)
 *
 * The `#/integrations` provider cards, the composer's
 * per-submission toggle chips, and the task-detail
 * available-vs-excluded snapshot rows
 * (web/app/js/app.js `_renderIntegrationsPage` /
 * `_paintComposerIntegrations` / `_renderIntegrationsSnapshot`).
 * Everything composes the established vocabulary — `.card`,
 * `.pill-*` for the textual status pills, the `.btn` family for
 * every action — and the existing custom-property palette; the
 * provider marks are inline SVGs stroked with `currentColor`, so
 * `.integration-mark`'s oxblood is the single source of their
 * color (no fetched logo assets, no icon fonts).  Chips are real
 * `<button>`s carrying `aria-pressed` plus a textual state word,
 * with the `.btn` focus ring so keyboard position stays visible.
 * ============================================================ */

.integrations-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--s-4);
    margin-top: var(--s-4);
}

.integration-card {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.integration-card-header {
    align-items: center;
    gap: var(--s-3);
    margin-bottom: 0;
}

.integration-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.4rem;
    height: 2.4rem;
    flex-shrink: 0;
    color: var(--c-oxblood);
    background: var(--c-paper-soft);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius);
}

.integration-mark svg {
    width: 1.3rem;
    height: 1.3rem;
    display: block;
}

.integration-name {
    flex: 1;
    font-family: var(--font-accent);
    font-size: var(--fs-title);
    font-weight: 400;
    color: var(--c-ink);
}

.integration-enables {
    color: var(--c-ink-soft);
    font-size: var(--fs-body);
}

.integration-state-line {
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
}

.integration-state-line strong {
    color: var(--c-ink-soft);
    font-weight: 600;
}

.integration-state-attention {
    color: var(--c-status-failed);
}

/* The GitHub card's permanent "Manage repository access" deep link. An
 * inline link rather than a button: it leaves the app for github.com's
 * own settings, and the underline is what says so. */
.integration-manage-link {
    color: var(--c-oxblood);
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.integration-manage-link:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: 2px;
}

/* A provider this server cannot link at all (the wire entry's
 * `available: false`) — not a user state but a deployment one, so the
 * card reads as inert rather than failed: the mark loses its accent,
 * the copy stays legible, and the state is carried in words by the
 * Unavailable pill and the reason line beneath it. There is no
 * `pointer-events: none` here because there is nothing to suppress —
 * the renderer emits no Connect control on these cards at all. */
.integration-card-unavailable .integration-mark {
    color: var(--c-ink-muted);
    background: var(--c-paper-soft);
}

.integration-card-unavailable .integration-name,
.integration-card-unavailable .integration-enables {
    color: var(--c-ink-muted);
}

.integration-state-unavailable {
    color: var(--c-ink-soft);
}

.integration-actions {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-top: auto;
    padding-top: var(--s-2);
}

/* One-shot page feedback (OAuth callback outcome, Linear connect,
 * inline connect/disconnect failures).  Text-first: the tone tints
 * the border and text, but the message itself carries the outcome. */
.integrations-notice {
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius);
    background: var(--c-paper);
    font-size: var(--fs-body);
    margin-bottom: var(--s-4);
}

.integrations-notice-ok {
    color: var(--c-status-complete);
    border-color: var(--c-status-complete);
}

.integrations-notice-error {
    color: var(--c-status-failed);
    border-color: var(--c-status-failed);
}

/* Composer chips row: connected providers, on by default. */
.composer-integrations {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
    margin-top: var(--s-3);
}

.composer-integrations-label {
    font-size: var(--fs-meta);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--c-ink-muted);
}

.composer-chip-row {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
}

.composer-integrations-link {
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink-muted);
    text-decoration: underline;
    text-underline-offset: 2px;
    margin-left: auto;
}

.composer-integrations-link:hover {
    color: var(--c-ink);
}

/* A chip is a real toggle button: pill-shaped, `aria-pressed`
 * carrying the on/off state, and the state word ("included" /
 * "excluded") rendered beside the provider name so the state never
 * rides on color alone. */
.integration-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-family: var(--font-body);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink-soft);
    background: var(--c-paper);
    border: 1px solid var(--c-rule-strong);
    border-radius: var(--radius-pill);
    padding: 0.28rem 0.8rem;
    cursor: pointer;
    transition: background var(--t-base), color var(--t-base),
                border-color var(--t-base);
}

.integration-chip:hover {
    border-color: var(--c-ink);
}

.integration-chip:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.integration-chip-state {
    font-size: var(--fs-micro);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--c-status-complete);
}

.integration-chip-excluded {
    color: var(--c-ink-muted);
    background: transparent;
    border-style: dashed;
    border-color: var(--c-rule-strong);
}

.integration-chip-excluded .integration-chip-state {
    color: var(--c-ink-muted);
}

/* ---------------------------------------------------------------
 * The composer's destination line — which repository a task is
 * PLANNED for (docs/APP.md#the-repository-preflight).
 *
 * Shares the `.composer-integrations` band, so the line sits in the
 * same rhythm as the integration chips and
 * disappears just as completely when the server offers no repositories
 * (preflight off, no GitHub connection, no index yet).
 *
 * `.destination-note` takes the whole row on purpose: it carries the
 * one sentence that keeps "planned target" from reading as "will push
 * here", and a sentence that matters is not a sentence to tuck into a
 * gap between two links.
 * --------------------------------------------------------------- */
.destination-name {
    font-family: var(--font-mono);
    font-size: var(--fs-ui);
    font-weight: 500;
    color: var(--c-ink);
}

.destination-fact {
    color: var(--c-ink-faint);
    margin-left: 0.45rem;
}

.destination-note {
    flex-basis: 100%;
    font-size: var(--fs-meta);
    color: var(--c-ink-muted);
}

/* change / remove / choose — real buttons (they change local state, they
   do not navigate), styled as the quiet links they read as.  Their own
   class rather than `.composer-integrations-link`, which is an anchor
   rule carrying `margin-left: auto` for the manage link at the end of
   the chip row.  The reset, underline, and interaction states live on
   the grouped .btn-link base (Buttons); only the type deltas stay
   here. */
.destination-action {
    font-family: var(--font-body);
    font-size: var(--fs-ui);
    font-weight: 500;
}

.destination-picker,
.branch-picker {
    flex-basis: 100%;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

/* The branch picker is the repository picker one level down, so it is
   the same list rule above rather than a second one that could drift.
   `.branch-picker` exists as its own class for the row below it and for
   anything that needs to find the branch list specifically. */
.branch-picker {
    margin-top: var(--s-1);
}

/* The filter box, rendered only once the list is long enough to need
   one (`BRANCH_FILTER_MIN_COUNT`). Takes its own row like the picker it
   sits above, and borrows the working-set description input's shape so
   the two text fields on this surface read as one control. */
.branch-filter {
    flex-basis: 100%;
    font-family: var(--font-body);
    font-size: var(--fs-ui);
    color: var(--c-ink);
    background: var(--c-paper);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius-sm);
    padding: 0.3rem var(--s-2);
}

.branch-filter:focus {
    outline: none;
    border-color: var(--c-burnished);
    box-shadow: var(--sh-well), var(--focus-ring);
}

/* The chip region, which exists so a keystroke in the filter box can
   repaint the chips without replacing the box itself (replacing a
   focused input ends an IME composition mid-word). It is a flex item on
   the destination line like the picker it wraps, so it takes the same
   full row — without this the list would try to sit beside the filter
   box rather than under it. */
.branch-list-region {
    flex-basis: 100%;
}

/* Loading, empty and truncated all say their piece on the destination
   sentence's own row rule (`.destination-note` carries the muted, full
   width treatment); the extra class only tightens the spacing under the
   chips it belongs to. */
.branch-note {
    margin: 0;
}

/* The detail view's counterpart: the same fact, past tense, in the
   ordinary detail-section rhythm. */
.detail-section-target-repository .destination-name {
    margin: 0;
}

/* Stayed behind when the working set was cut to working_set.css: the
   state tags below must beat the shared pill groups earlier in this
   sheet on source order at equal specificity.

   Task-detail snapshot rows: the persisted available-vs-excluded
 * record, one row per snapshot entry, led by the textual state tag
 * (composing the `.review-row-tag` look). */
.integration-snapshot-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    border: 1px solid var(--c-rule);
    border-radius: var(--radius-lg);
    background: var(--c-paper);
    padding: var(--s-4);
}

.integration-snapshot-row {
    display: flex;
    align-items: baseline;
    gap: var(--s-3);
}

.integration-snapshot-state {
    min-width: 5.2rem;
    text-align: center;
    flex-shrink: 0;
}

.integration-snapshot-state-available {
    color: var(--c-status-complete);
    border-color: var(--c-status-complete);
    background: var(--c-paper);
}

.integration-snapshot-state-excluded {
    color: var(--c-ink-muted);
    border-style: dashed;
}

.integration-snapshot-row-excluded .integration-snapshot-name {
    color: var(--c-ink-muted);
}

.integration-snapshot-name {
    font-weight: 500;
    color: var(--c-ink-soft);
}

.integration-snapshot-account {
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
}


/* Stayed behind when the suggestion-card surface was cut to
   suggestions.css: each rule below overrides a shared button or pill
   group earlier in this sheet on source order at equal specificity,
   so it must sit after those groups in the cascade. */
.sc-integration {
    font-size: var(--fs-micro);
    padding: 1px var(--s-2);
}

.card-context-clear {
    font-size: var(--fs-ui);
    margin-left: auto;
}
.context-hint-action { font-size: var(--fs-ui); color: var(--c-oxblood); }
.context-hint-dismiss { font-size: var(--fs-ui); margin-left: auto; }

.readiness-route-link {
    color: var(--c-oxblood);
}


/* The two assistant-draft rules below stayed behind when the
   signed-in page family was cut to assistant.css: the button dresses
   a `.btn .btn-small` control and the meta line is a micro-label
   group member, and each must beat its group earlier in this sheet
   on source order at equal specificity.

   Quieter than the composer's own Submit, and deliberately: this
   control opens the task box, and the primary action of the whole
   surface is the button the person presses once they have read what is
   in it. */
.assistant-draft-btn {
    background: var(--c-paper-soft);
    border-color: var(--c-rule-strong);
    color: var(--c-ink);
}

/* The author-origin `display: inline-flex` on `.btn` outranks the user
   agent's own `[hidden]` rule, so withdrawing a sent item's send
   control (`control.hidden = sent` in `_paintAssistantDraftItem`)
   needs this explicit opt-out or the "hidden as well as disabled"
   invariant silently does nothing.  Same pattern as
   `.composer-integrations[hidden]` and `.support-chat-panel[hidden]`. */
.assistant-draft-btn[hidden] { display: none; }

/* Which draft this is, of how many — a tracked eyebrow, metadata tier
   like the clarification's meta line. */
.assistant-draft-item-meta {
    margin: 0;
    /* 0.16em, not --track-tag: the wordmark's exact tracking — a
       deliberate brand echo on the draft memo. */
    letter-spacing: 0.16em;
    color: var(--c-ink-muted);
}

/* ============================================================
 * Linked Claude accounts (/app-only, #/account)
 *
 * The account page's Claude-accounts section
 * (web/app/js/app.js `_renderClaudeAccountsSection` /
 * `_paintClaudeAccounts`, docs/APP.md#the-claude-accounts-ui-in-the-spa):
 * the pitch, the disclosure line, one row per linked account, and
 * the link dialog's numbered steps.
 *
 * Everything composes the established vocabulary — `.review-panel`
 * + `.review-row` for the list (so it reads as a sibling of the
 * usage-history ledger directly below it), `.pill-*` for the
 * textual status, the `.btn` family for every control, `.modal-*`
 * for the dialog — and every colour is a palette token.  Nothing
 * here is a new component; the section is a fourth thing on a page
 * that already had three.
 * ============================================================ */

/* The verbatim pitch reads as the section's lede; the disclosure
   beneath it is quieter but must stay legible text, so it takes
   --c-ink-muted (the AA-cleared secondary tier) rather than the
   decorative --c-ink-faint. */
.claude-accounts-terms {
    margin: var(--s-2) 0 0;
    font-size: var(--fs-ui);
    line-height: 1.55;
    color: var(--c-ink-muted);
    max-width: 720px;
}

.claude-accounts-body {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    margin-top: var(--s-4);
}

/* The savings line closes the section: what linking has actually been
   worth, in the same place that pitched it. Empty until the account
   page fills it (and never filled at all while the feature is off), so
   it must take no space of its own when it holds nothing. */
.claude-accounts-savings:empty {
    display: none;
}

.claude-accounts-savings-line {
    margin: var(--s-4) 0 0;
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--c-ink-soft);
}

/* The figure itself reads as a number, not as prose: tabular so it
   lines up with the balance above it, and a step darker than the line
   around it so the amount — not the sentence — is what the eye lands
   on. */
.claude-accounts-savings-amount {
    font-variant-numeric: tabular-nums;
    color: var(--c-ink);
    font-weight: 600;
}

/* One linked account. The row is a .review-row (1fr auto), so the
   identity line and meta sit left and Remove sits right. */
.claude-account-identity {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
    min-width: 0;
}

.claude-account-label {
    word-break: break-word;
}

/* The masked fingerprint tail. Monospaced and tabular so four
   characters read as an identifier rather than as a word, and
   muted because it discriminates between rows — it is not the
   thing being named. */
.claude-account-tail {
    font-family: var(--font-mono);
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
    background: var(--c-paper-soft);
    border-radius: var(--radius-sm);
    padding: 0 var(--s-2);
    white-space: nowrap;
}

.claude-account-meta {
    font-variant-numeric: tabular-nums;
}

/* The per-state line beneath the meta. Both states are written out
   in words above (the pill), so this carries the remedy — or, for a
   cooldown, says explicitly that there is none. */
.claude-account-note {
    margin: 0;
    font-size: var(--fs-ui);
    line-height: 1.5;
    color: var(--c-ink-soft);
}

.claude-account-note-attention {
    color: var(--c-status-failed);
}

.claude-account-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s-2);
    flex-shrink: 0;
}

.claude-accounts-actions {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-2);
}

/* Why the add control is disabled. Sits under the control rather
   than replacing it: a control that vanished at the cap would read
   as a feature that broke. */
.claude-accounts-cap-hint {
    margin: 0;
    font-size: var(--fs-ui);
    color: var(--c-ink-muted);
}

/* The link dialog's four steps. A real <ol> — the numbers are the
   content, not decoration, and they have to survive a screen
   reader. */
.claude-account-steps {
    list-style: decimal;
    margin: 0 0 var(--s-4);
    padding-left: var(--s-5);
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    font-size: var(--fs-body);
    line-height: 1.55;
    color: var(--c-ink-soft);
}

.claude-account-steps code {
    font-family: var(--font-mono);
    font-size: 0.88em;
    background: var(--c-paper-soft);
    border-radius: var(--radius-sm);
    padding: 0 var(--s-1);
}

.claude-account-steps a {
    color: var(--c-oxblood);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.claude-account-steps a:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: 2px;
}

/* The nickname field follows the token field; the gap is the modal
   body's own rhythm rather than a margin on .modal-field, which the
   preference dialogs share. */
.claude-account-label-field {
    margin-top: var(--s-4);
}

/* ============================================================
 * Responsive
 * ============================================================ */

@media (max-width: 720px) {
    .page { padding: var(--s-5) var(--s-4) var(--s-7); }
    .page-hero h1 { font-size: var(--fs-display); }
    /* Compound selector (0,2,0): must outrank .page-hero h1 (0,1,1)
       above — a bare .wordmark (0,1,0) would lose inside this query
       and the wordmark would jump back to 2rem. */
    .page-hero .wordmark { font-size: 1.4rem; }
    .page-hero p  { font-size: var(--fs-base); }

    /* The header gives up its sticky strip at phone widths — it would
       eat too much of a short viewport.  The session cluster is here
       too, folded inside the Account menu.  The border goes with the
       sticky strip: the scrolled hairline exists to terminate a fixed
       strip that content slides under, and a static header scrolls
       away with the page — a rule under it would flicker in at the
       first scrolled pixel and leave with the header, chrome noise on
       a boundary that no longer exists at these widths. */
    .app-header {
        position: static;
        margin: 0 auto var(--s-4);
        padding: 0;
        max-width: 880px;
        background: transparent;
        border-bottom: 0;
    }
    /* The menu's panel joins the flow here rather than dropping
       absolutely: the wrapped nav rows put the Account summary
       wherever the wrap left it — often at the row's left edge — so a
       right-anchored 16rem drop hung as much as ~186px off the left
       viewport edge with no scroll that could reach it.  Static, the
       panel opens in place under its summary like an accordion row,
       content-sized by the base rule's max-content width under the
       same viewport cap, and can never leave the viewport.  The
       header is static at these widths, so there is no fixed strip
       for the open panel to mis-align with. */
    .account-menu-panel {
        position: static;
        min-width: 0;
    }
    /* The brand mark's home anchor, link, chip and Sign out are tap
       targets here: give each the 44px floor the sheet's touch
       controls already use.  Centering each label inside its own
       taller box keeps the four on the row's shared baseline — the
       floor grows the hit area, not the type (without its row here
       the logo link kept a bare ~29px line box, ~2/3 shorter than
       every adjacent control). */
    .app-header-home,
    .page-nav-link,
    .page-nav-balance,
    .sign-out-btn {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }
    /* The Account menu's summary takes the same floor.  It carries the
       .page-nav-link class in markup, so the group above already
       reaches it — this (0,1,1) restatement is what keeps the floor on
       the element itself, ahead of the summary's own UA display, and
       it stays a separate rule because the group's selector text is
       pinned as-is by the nav-metrics suite. */
    .account-menu > summary {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }
    /* With the floor in place the box's bottom edge sits ~15px under
       the label, so the border-bottom underline the link and the chip
       use at wider widths would float detached from the text it marks.
       Here the border goes and the hover / keyboard-focus indicator is
       a text underline, offset to ride just under the glyphs.  The
       focus-visible ring is untouched; Sign out keeps its ghost fill. */
    .page-nav-link,
    .page-nav-balance {
        border-bottom: 0;
        text-underline-offset: 0.25em;
    }
    .page-nav-link:hover,
    .page-nav-link:focus-visible,
    .page-nav-balance:hover,
    .page-nav-balance:focus-visible {
        text-decoration: underline;
    }
    /* The current-page marker rides the same text underline the hover
       rules use here, in the marker's own oxblood.  Without this the
       zeroed border erased the marker's underline device and the
       hover/focus rules above never reach a non-interactive span, so
       "you are here" degraded to a darker grey alone — colour as the
       only cue, which WCAG 1.4.1 forbids. */
    .page-nav-link[aria-current="page"] {
        text-decoration: underline;
        text-decoration-color: var(--c-oxblood);
    }

    .card { padding: var(--s-4); }
    .panel-header { padding: var(--s-3) var(--s-4); }

    .composer-form { padding: var(--s-4); }
    .task-grid { grid-template-columns: 1fr; gap: var(--s-3); }
    .status-section-title { font-size: var(--fs-title); }
    .task-gallery { gap: var(--s-6); }

    .detail { gap: var(--s-6); }
    .detail-title { font-size: var(--fs-display); }
    .detail-section-head {
        flex-direction: column;
        align-items: stretch;
        gap: var(--s-3);
    }
    .review-row,
    .deliverable-row { padding: var(--s-3) var(--s-4); }
    /* Panel-row disclosures and the criteria summary line follow the
       rows' tighter mobile padding; the section-level summary already
       carries it. */
    .review-panel-summary,
    summary.review-disclosure-summary { padding: var(--s-3) var(--s-4); }
    .review-row {
        grid-template-columns: 1fr;
    }
    /* Best-practices carries a scoped, higher-specificity 3-column base
     * rule (marker + body + actions), and marker-bearing criterion rows
     * carry the same layout via .review-row-with-status; reset both here
     * so every review panel collapses to a single column. */
    .review-panel-best-practices .review-row,
    .review-row-with-status {
        grid-template-columns: 1fr;
    }
    .review-row-actions { justify-content: flex-end; }
    .deliverable-row {
        grid-template-columns: 40px 1fr;
    }
    .deliverable-icon { width: 40px; height: 40px; }
    .deliverable-actions {
        grid-column: 1 / -1;
        justify-content: flex-end;
    }
    /* Context-file rows: stack name over size so more of the
       uploaded filename survives before the ellipsis. */
    .context-file-meta {
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
    }
    .failure-banner { grid-template-columns: 1fr; }
    .failure-banner-actions { justify-content: flex-end; }
    .approval-banner { padding: var(--s-4); }
    .approval-banner-actions { flex-wrap: wrap; }
    /* Group name over the Edit control — side by side leaves the
       eyebrow one word wide on a phone. */
    .approval-preview-group-header {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--s-2);
    }
    .budget-banner { padding: var(--s-4); }
    .budget-banner-actions { flex-wrap: wrap; }
    /* Stat label over value: an 8.5rem label column plus a dollar
       figure does not fit a narrow viewport. */
    .budget-banner-stat {
        flex-direction: column;
        gap: 2px;
    }
    .budget-banner-stat dt { flex: none; padding-top: 0; }
    .modal { max-height: calc(100vh - 2rem); }
    .modal-header,
    .modal-body,
    .modal-actions { padding-left: var(--s-4); padding-right: var(--s-4); }

    .integrations-list { grid-template-columns: 1fr; }
    .composer-integrations-link { margin-left: 0; }

    .attach-chip-name { max-width: 140px; }
    .attach-meter-track { width: 88px; }

    /* One card per row below 720px: a two-column deck of 130px cards is
       a row of truncated titles, which is worse than scrolling. */
    .suggestion-deck-cards { grid-template-columns: 1fr; }
    .card-context-clear { margin-left: 0; }

    /* The strip's chips wrap to one per line rather than truncating the
       task names down to nothing. */
    .inflight-item-name { max-width: 100%; }
    .inflight-strip { margin-bottom: var(--s-4); }

    .support-chat-launcher { right: var(--s-4); bottom: var(--s-4); }

    /* At phone widths the labelled pill returns to the 56px disc: the
       widened label roughly doubled the fixed footprint over exactly
       the pages the overhaul made action-dense — at 390 it covered
       the approval card's Reject and the gallery's status pills at
       natural scroll positions (the rendered review's captures).  The
       label leaves the visual box but stays inside the button as its
       accessible name (the clipped visually-hidden declaration —
       display: none would silence it); height, offsets, and the lift
       arithmetic below are untouched, so the panel geometry pinned in
       tests/test_app_static.py::TestBrowserFloor still adds up. */
    .support-chat-launcher {
        width: 56px;
        padding: 0;
    }
    .support-chat-launcher-label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
        border: 0;
    }

    .support-chat-panel {
        left: var(--s-4);
        right: var(--s-4);
        width: auto;
        bottom: calc(var(--s-4) + 56px + var(--s-3));
    }

    /* On the assistant page the viewport-capped sheet keeps the reply
       slip's Send control near the bottom-right of the screen — exactly
       where the launcher floats.  Lift the launcher clear of the slip's
       44px action row (plus breathing room), and the panel above the
       lifted launcher, on this page only.  `page-assistant-thread` is a
       body-class latch the router toggles at the end of every dispatch,
       tracking whether the painted page carries the thread.  It replaces
       a parent-selecting pseudo-class over `[data-assistant-thread]`,
       which Firefox only reads from 121 — below that the dropped rule
       left the launcher covering the slip's Send
       (docs/APP.md#browser-floor). */
    body.page-assistant-thread .support-chat-launcher {
        bottom: calc(var(--s-4) + 44px + var(--s-3));
    }
    body.page-assistant-thread .support-chat-panel {
        bottom: calc(var(--s-4) + 44px + var(--s-3) + 56px + var(--s-3));
    }

    /* The sheet tightens its padding, the input well grows so the
       three-line placeholder does not clip, and the slip's controls
       clear the 44px touch floor. */
    .assistant-thread { padding: var(--s-4) var(--s-4) var(--s-3); }
    .assistant-input { min-height: 84px; }
    .assistant-form-actions .btn { min-height: 44px; }
    .page.page-conversing .assistant-thread {
        max-height: max(320px, calc(100vh - 13rem));
        max-height: max(320px, calc(100dvh - 13rem));
    }
}

/* Keyboard- and script-driven scroll-into-view must clear the sticky
   header strip (it only exists above 720px — the phone block sets the
   header static): without this, focusing a control near the top of the
   page parks it underneath the opaque strip.  4rem is sized to the
   single-line row (~3.2rem plus slack) the strip now holds across the
   whole sticky range: with the email, the balance chip and Sign out
   folded into the Account menu's panel, the row is the wordmark plus
   six short text items, narrower than any width the strip is sticky
   at.  The wraps that once demanded more — the raw email pushing a
   second line up to 1100px (hidden there by a since-retired media
   block), the chip's organization name doing the same just above
   720px (cleared by a since-retired 6rem band) — left the row with
   the cluster.  Overshoot costs only whitespace; an undershoot hides
   the focused control — the value is coupled to the row's real
   height, and changes with it. */
@media (min-width: 721px) {
    html { scroll-padding-top: 4rem; }
}

/* On genuinely short viewports — a landscape phone, or a portrait one
   with the keyboard up (`interactive-widget=resizes-content` shrinks
   100dvh to what remains above it) — the viewport cap would squeeze
   the turn list to a one-line ticker under the sheet's own fixed
   chrome.  Give the page scroll back instead: uncapped sheet, the turn
   list on its own bounded scroll, everything reachable the way the
   pre-sheet layout always was.  After the 720px block on purpose: at
   equal specificity this escape must win the cascade against both
   caps. */
@media (max-height: 560px) {
    .page.page-conversing .assistant-thread { max-height: none; }
    .page.page-conversing .assistant-turns {
        flex: 0 1 auto;
        max-height: 60vh;
        max-height: 60dvh;
    }
}

/* Motion opts out with the reader.  Every pulse and entrance here is
   ornament and stops outright; .gallery-spinner is meaning-bearing at
   its surviving sites — the integrations page, the composer's working
   set, task-detail loading, the account page and the credits panel,
   where it is the only sign a fetch is still running — so it keeps
   turning, slower.  (The task gallery's own cold load is the static
   .gallery-placeholder ledger rows: nothing there moves, so nothing
   needs a rule here.) */
@media (prefers-reduced-motion: reduce) {
    .fade-in,
    .modal-overlay,
    .support-chat-panel { animation: none; }
    .pill-in-progress::before,
    .attach-chip-uploading .attach-chip-state,
    .attach-chip-removing .attach-chip-state,
    .timeline-step-pulse,
    .inflight-item::before,
    .assistant-turn-pending .assistant-turn-body::before { animation: none; }
    .gallery-spinner { animation-duration: 1.8s; }
}

/* Printed page setup.  @page applies only in paged media, so a
   top-level rule is print-scoped by nature. */
@page { margin: 18mm 16mm; }

@media print {
    body { background: var(--c-paper); }
    .btn, .btn-icon { display: none; }
    .panel { box-shadow: none; border-color: var(--c-rule-strong); }
    .sc-card, .assistant-thread { box-shadow: none; }
    .composer { display: none; }
    .support-chat { display: none; }
    /* The header's link row and the Account menu are chrome; the
       .app-header itself stays, so its wordmark prints as letterhead. */
    .app-header .page-nav, .account-menu { display: none; }
    .modal-overlay { display: none; }
    .assistant-form { display: none; }
    .skip-link { display: none; }
    .detail-section, .card { break-inside: avoid; }
    h1, h2 { break-after: avoid; }
    .detail-section a[href^="http"]::after {
        content: " (" attr(href) ")";
        font-size: 0.85em;
        color: var(--c-ink-muted);
        overflow-wrap: anywhere;
    }
}
