/* Report Console — refined-utilitarian skin (2026-06-11 visual refresh).
   One plain CSS file, no build step. Inter is self-hosted (no CDN — Tailscale-only app).
   Colors are tokens wherever a token exists; the dark variant remaps :root and then
   hand-tunes a small set of AA-checked literals (badges, on-navy chrome) — see the
   comments in the dark block (K19: the old "every color is a token" claim overstated it). */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;             /* variable font — one latin file covers all weights */
  font-display: swap;
  src: url("fonts/inter-var-latin.260c81a4759b.woff2") format("woff2");
}

:root {
  /* K16: native widgets (date/month pickers, selects, scrollbars) follow the app's theme.
     Pinned to the RESOLVED theme rather than `light dark`, so choosing light on a dark-OS
     machine doesn't leave dark scrollbars and pickers behind (see the dark block below). */
  color-scheme: light;
  --brand:#1e3a5f;                  /* navy identity (header) */
  --brand-ink:#dbe6f4;              /* text on navy */
  --accent:#2563eb; --accent-strong:#1d4ed8;
  --ink:#0f172a; --soft:#5d6b80;  /* 5.09:1 on --bg, 4.94 on --code-bg (AA off-card too) */
  --bg:#f6f8fb; --surface:#ffffff; --line:#e2e8f0;
  /* --line is a DECORATIVE separator (cards, table rows) — WCAG 1.4.11 doesn't govern it.
     --control-line is the BOUNDARY of a UI component (inputs, selects, textareas, the
     neutral/ghost button tiers) and must clear 3:1 against BOTH --surface and --bg:
     light #7e8c9e = 3.42:1 on white, 3.22:1 on --bg (was --line at 1.23:1 — a fail). */
  --control-line:#7e8c9e;
  /* graphics tokens (badges, rails, borders) … */
  --ok:#16a34a; --warn:#d97706; --bad:#dc2626;
  /* …and darker -ink twins for SMALL TEXT — the plain tokens fail WCAG AA on white */
  --ok-ink:#15803d; --warn-ink:#b45309; --bad-ink:#b91c1c;
  --tint-accent:#eff6ff; --tint-ok:#f0fdf4; --tint-warn:#fffbeb; --tint-bad:#fef2f2;
  --code-bg:#f1f5f9; --press:#e2e8f0;
  /* Radius scale (named tiers, values = the ones already in use → no visual change):
     xs inline code chips · sm form controls/nav · md default panels · lg cards/pills. */
  --radius-xs:.25rem; --radius-sm:.35rem; --radius:.45rem; --radius-lg:.6rem;
  --shadow:0 1px 2px rgba(15,23,42,.06), 0 1px 3px rgba(15,23,42,.08);
  --scroll-shadow:rgba(15,23,42,.16);   /* table overflow edge fade */
  /* Attention accent (amber) — needs-you / stale / aging. Distinct from --accent (action)
     and --bad (fail/destructive). The -ink twin clears AA for small text on white/tints. */
  --attn-ink:#b45309; --attn-tint:#fffbeb;
  /* Type scale (documents the recurring sizes; values match what's in use). */
  --text-2xs:.7rem; --text-xs:.75rem; --text-sm:.85rem; --text-md:.9rem;
  --text-base:1rem; --text-lg:1.15rem; --text-xl:1.5rem;
  /* Job-log block (a dark console look in BOTH themes — deliberately not remapped). */
  --joblog-bg:#0b1426; --joblog-ink:#dbe6f4;
  /* Spacing scale — a 4/8px rhythm. (v6 J19 deleted an earlier --sp-* set because it had
     no adopters; this one is introduced together with its adopters, per that note.) */
  --sp-1:.25rem; --sp-2:.5rem; --sp-3:.75rem; --sp-4:1rem;
  --sp-5:1.5rem; --sp-6:2rem; --sp-7:3rem;
  /* Icon sizes. --icon-md is em-based so inline glyphs track their text; the sm/lg tiers
     are fixed for chrome (nav mark, entry cards, toolbar) where they shouldn't scale. */
  --icon-sm:1rem; --icon-md:1.15em; --icon-lg:1.6rem;
  --sidebar-w:15.5rem;
}

* { box-sizing:border-box; }
/* 16px base: anything smaller in an input makes iOS Safari auto-zoom on focus. */
body { margin:0; font:16px/1.55 Inter, system-ui, sans-serif; color:var(--ink);
       background:var(--bg); -webkit-text-size-adjust:100%; }
h1 { font-size:1.55rem; font-weight:700; letter-spacing:-.015em; }
h2 { font-size:1.1rem; font-weight:650; letter-spacing:-.01em; color:var(--ink); }

/* Micro-interactions: one shared 150ms rhythm, fully disabled on reduced motion. */
a, button, .badge, .btn-primary {
  transition: background-color .15s ease, color .15s ease,
              border-color .15s ease, box-shadow .15s ease; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration:.01ms !important; animation:none !important; }
}

/* Inline SVG icons (sprite in _icon_sprite.html). Stroke/fill via currentColor so they theme
   for free and inherit link/button color. Decorative — labels live on the surrounding element. */
.icon { width:var(--icon-md); height:var(--icon-md); flex:none; fill:none; stroke:currentColor;
        stroke-width:2; stroke-linecap:round; stroke-linejoin:round;
        vertical-align:-.18em; }
header strong .icon { width:1.25rem; height:1.25rem; vertical-align:-.24em; color:var(--brand-ink); }

header { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center;
         gap:.4rem 1rem; padding:.6rem max(1rem, env(safe-area-inset-right))
         .6rem max(1rem, env(safe-area-inset-left));
         background:var(--brand); color:#fff; }
header strong { font-weight:650; letter-spacing:-.01em; }
nav { display:flex; flex-wrap:wrap; gap:.2rem .4rem; align-items:center; }
/* 44px touch targets for nav links and all buttons (Apple HIG minimum). */
nav a { color:var(--brand-ink); text-decoration:none; padding:.6rem .5rem;
        min-height:44px; display:inline-flex; align-items:center; gap:.45rem;
        border-radius:var(--radius-sm); }
nav a:hover { background:rgba(255,255,255,.08); color:#fff; }
nav a:active { background:rgba(255,255,255,.16); color:#fff; }
nav a.active { background:rgba(255,255,255,.14); color:#fff;
        box-shadow:inset 0 -2px 0 #fff; }
/* Log out stays a POST form (CSRF) but reads as just another nav link. */
header form.inline button { background:transparent; border:none;
        color:var(--brand-ink); border-radius:var(--radius-sm);
        display:inline-flex; align-items:center; gap:.45rem; }
header form.inline button:hover { background:rgba(255,255,255,.08); color:#fff; }
form.inline { display:inline; }
/* The header title doubles as a home link — inherit the navy-header colors, no underline. */
header strong .brand-link { color:inherit; text-decoration:none; display:inline-flex;
        align-items:center; gap:.4rem; border-radius:var(--radius-sm); }

/* Keyboard skip-link: parked off-screen until focused, then pinned above the chrome so a
   keyboard user can jump past the nav. Snaps into place under reduced motion (transition off). */
.skip-link { position:fixed; left:.5rem; top:-4rem; z-index:1000;
        background:var(--surface); color:var(--accent-strong);
        border:1px solid var(--accent); border-radius:var(--radius);
        padding:.5rem .9rem; font-weight:600; text-decoration:none;
        transition:top .15s ease; }
.skip-link:focus { top:.5rem; }
main:focus { outline:none; }   /* programmatic target after skipping — suppress the full-page ring */

main { max-width:60rem; margin:0 auto;
       padding:1rem max(1rem, env(safe-area-inset-right))
       max(1.5rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); }
main a { color:var(--accent); }
main a:hover { color:var(--accent-strong); }

/* ── App shell: top bar on phones, left sidebar on wide desktop. ONE <nav>, reflowed by
   CSS — never duplicated (the tests assert a single aria-current="page" per page).

   A2: this breakpoint was 1024px, which made the layout get NARROWER as the screen got
   WIDER — the 15.5rem rail appeared while `main` was still capped at 60rem, so a card went
   472px → 364px crossing 1024px and only recovered at 1240px. The rail now waits until
   there is width to pay for it. (Media queries can't read a custom property, so this
   literal is the single source of truth for the shell breakpoint.) ─────────────────────── */
@media (min-width:1240px) {
  body.app { display:grid; grid-template-columns:var(--sidebar-w) 1fr; align-items:start; }
  body.app > header { position:sticky; top:0; height:100dvh; overflow-y:auto;
           flex-direction:column; flex-wrap:nowrap; align-items:stretch;
           justify-content:flex-start; gap:1rem; padding:1rem .8rem; }
  body.app > header strong { font-size:var(--text-base); padding:var(--sp-1) var(--sp-2); letter-spacing:-.01em; }
  nav { flex-direction:column; flex-wrap:nowrap; align-items:stretch; gap:.15rem; width:100%; }
  nav a { width:100%; }
  nav a.active { box-shadow:inset 3px 0 0 #fff; }   /* left indicator suits a vertical rail */
  .nav-logout { margin-top:auto; }                  /* pin log out to the rail's bottom */
  .nav-logout button { width:100%; justify-content:flex-start; }
  body.app > main { grid-column:2; }
}

button, input, select, textarea { font:inherit; }   /* keeps inputs at 16px — no iOS zoom */
button { min-height:44px; padding:.5rem 1rem; border:1px solid var(--control-line);
         border-radius:var(--radius); background:var(--surface); color:var(--ink);
         cursor:pointer; touch-action:manipulation; }
button:hover { border-color:var(--soft); }
button:active { background:var(--press); }
button[type="submit"] { background:var(--accent); border-color:var(--accent); color:#fff;
         font-weight:600; }
button[type="submit"]:hover { background:var(--accent-strong); border-color:var(--accent-strong); }
button[type="submit"]:active { background:var(--accent-strong); }
button[disabled], button[aria-disabled="true"] { opacity:.5; cursor:not-allowed; }
/* C4: aria-disabled keeps the control focusable so the REASON it's unavailable can
   be read. The click is refused in JS; this just matches the disabled look. */
button.danger { background:var(--bad); border-color:var(--bad); color:#fff; }
button.danger:hover, button.danger:active { background:var(--bad-ink); border-color:var(--bad-ink); }
.danger-zone { border:1px solid var(--bad); border-radius:var(--radius); padding:.4rem .8rem; }

/* Keyboard users get the same affordance pointer users get from :hover/:active. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible {
  outline:2px solid var(--accent); outline-offset:2px; }
/* The accent ring is only 2.23:1 against the navy header — use white there (11.5:1). */
header a:focus-visible, header button:focus-visible { outline-color:#fff; }

input:not([type="checkbox"]) { min-height:44px; padding:.45rem .6rem; width:100%;
        max-width:22rem; border:1px solid var(--control-line); border-radius:var(--radius-sm);
        background:var(--surface); color:var(--ink); }
input[type="checkbox"] { width:1.2rem; height:1.2rem; accent-color:var(--accent); }
/* The label IS the tap target for a 19px checkbox — keep it at the 44px minimum. */
label:has(input[type="checkbox"]) { min-height:44px; }
select { min-height:44px; padding:.45rem .6rem; border:1px solid var(--control-line);
        border-radius:var(--radius-sm); background:var(--surface); color:var(--ink); }
label { display:inline-flex; flex-wrap:wrap; align-items:center; gap:.4rem;
        padding:.25rem 0; }

/* Tables scroll inside their wrapper instead of stretching the page sideways. */
/* position:relative contains absolutely-positioned .vh headers (else they escape the
   clip and stretch the page sideways on mobile). */
/* B3: a table that scrolls sideways gave no sign that it did. Two fixed shadow layers and
   two scrolling cover layers: at an edge the cover sits over the shadow and hides it, so the
   fade shows only on the side that has more content. No JS, no scroll listener. */
.table-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; position:relative;
              border:1px solid var(--line); border-radius:var(--radius);
              background:
                linear-gradient(to right, var(--surface) 30%, transparent) left center / 2.5rem 100% no-repeat,
                linear-gradient(to left,  var(--surface) 30%, transparent) right center / 2.5rem 100% no-repeat,
                radial-gradient(farthest-side at 0 50%, var(--scroll-shadow), transparent) left center / .6rem 100% no-repeat,
                radial-gradient(farthest-side at 100% 50%, var(--scroll-shadow), transparent) right center / .6rem 100% no-repeat,
                var(--surface);
              background-attachment: local, local, scroll, scroll, scroll; }
table { border-collapse:collapse; width:100%; font-variant-numeric:tabular-nums; }
th { font-size:var(--text-xs); font-weight:600; text-transform:uppercase; letter-spacing:.05em;
     color:var(--soft); }
th, td { text-align:left; padding:.55rem .7rem; border-bottom:1px solid var(--line);
         overflow-wrap:break-word; }
tr:last-child td { border-bottom:none; }
td:first-child { white-space:nowrap; }
/* Long tables (history ledger, audit, experiments, attention): pin the header row to the
   top of the viewport so the column labels stay readable while scrolling. --surface bg keeps
   rows from showing through (and themes for free in dark mode). */
.table-wrap thead th { position:sticky; top:0; z-index:1; background:var(--surface); }

.badge { padding:.12rem .55rem; border-radius:var(--radius-lg); font-size:.8rem; font-weight:600;
         color:#fff; white-space:nowrap; }
.badge.generated { background:var(--soft); }
.badge.approved { background:var(--ok-ink); }      /* -ink shades: white text needs 4.5:1 */
.badge.approved_stale { background:var(--bad); }
.badge.post_failed { background:var(--bad); }        /* K4: failed Slack post, marker in .approved/failed/ */
.badge.posted { background:var(--accent); }
/* K22: History-ledger states derived from the audit trail (Copy-first flow) —
   copied = shipped via the clipboard (accent, like posted); reviewed = saved/edited. */
.badge.copied { background:var(--accent); }
.badge.reviewed { background:var(--ok-ink); }
.badge.missing { background:var(--warn-ink); }
.badge.in-progress { background:var(--warn-ink); }  /* J3: recap for the still-running week */
.chip-pass { color:var(--ok-ink); font-weight:600; }
.chip-fail { color:var(--bad-ink); font-weight:600; }
.mention { color:var(--accent); font-weight:600; }
.error, li.error { color:var(--bad-ink); }
.empty-state { color:var(--soft); font-style:italic; }

.messages { list-style:none; padding:0; margin:0 0 .3rem; }
.messages li { display:flex; align-items:center; gap:.5rem; padding:.4rem .4rem .4rem .7rem;
               border-radius:var(--radius); margin:.3rem 0;
               border:1px solid var(--line); background:var(--surface);
               animation:msg-in .2s ease-out; }
.messages li > .icon { flex:none; }                 /* status icon (alert / check) */
.messages .msg-text { flex:1 1 auto; min-width:0; } /* take the row; close button stays put */
/* Dismiss button: ghost, inherits the message's semantic color, 44px hit target. */
.messages .msg-close { flex:none; min-height:44px; min-width:44px; padding:0;
                       display:inline-flex; align-items:center; justify-content:center;
                       background:transparent; border:1px solid transparent; border-radius:var(--radius-sm);
                       color:inherit; opacity:.7; cursor:pointer; }
.messages .msg-close:hover { opacity:1; background:color-mix(in srgb, currentColor 12%, transparent); }
.messages .msg-close .icon { width:var(--icon-sm); height:var(--icon-sm); }
/* Leaving = fade + collapse before JS removes the node (skipped under reduced motion). */
.messages li.msg--leaving { opacity:0; transform:translateY(-.3rem);
                            transition:opacity .18s ease, transform .18s ease; }
@keyframes msg-in { from { opacity:0; transform:translateY(-.4rem); } to { opacity:1; transform:none; } }
.messages li.success { background:var(--tint-ok); border-color:var(--ok);
                       color:var(--ok-ink); }
.messages li.error   { background:var(--tint-bad); border-color:var(--bad);
                       color:var(--bad-ink); }
.messages li.warning { background:var(--tint-warn); border-color:var(--warn);
                       color:var(--warn-ink); }
.messages li.info    { background:var(--tint-accent); border-color:var(--accent);
                       color:var(--accent-strong); }

/* ── Cards (shared base: dashboard hero, action cards) ─────────────────────── */
.card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-lg);
        padding:.9rem 1.1rem; box-shadow:var(--shadow); }
.card.danger-zone { border-color:var(--bad); }   /* .card's border must not mute it */
.card h2 { margin:.2rem 0 .4rem; }
/* Actions page (#8): cards are grouped under an h2, so their own titles are h3 — styled as the
   card h2 they replaced. */
.card h3 { margin:.2rem 0 .4rem; font-size:1.1rem; font-weight:650; letter-spacing:-.01em; }
.action-group { margin-bottom:var(--sp-6); }
.action-group > h2 { margin:0 0 .15rem; }
.group-desc { color:var(--soft); font-size:var(--text-sm); margin:0 0 var(--sp-3); max-width:60ch; }
/* Dashboard deep links (#gen-weekly, #gen-monthly) must land below the sticky job strip. */
.action-group .card[id] { scroll-margin-top:6rem; }
/* Three cards per group: let them sit in one row as soon as the grid leaves single-column
   (the 1023px rule below) — at 21rem the third card of each group wrapped alone. */
@media (min-width:1024px) {
  .action-group .cards { grid-template-columns:repeat(auto-fit, minmax(18rem, 1fr)); }
}
/* Cards flow into as many columns as the width allows, instead of a hand-split pair of
   columns that left a void whenever the two sides held different card counts. */
.cards { display:grid; grid-template-columns:repeat(auto-fit, minmax(21rem, 1fr));
         gap:var(--sp-4); align-items:start; }
.cards > div { display:grid; gap:var(--sp-4); align-content:start; }
.action-desc { color:var(--soft); font-size:.9rem; margin:.2rem 0 .7rem; }
/* #9: the long card description sits behind "What it does" — the lead line carries the card. */
details.more > summary { font-size:var(--text-sm); color:var(--soft); font-weight:500;
        margin-bottom:var(--sp-2); }
details.more[open] > summary { margin-bottom:var(--sp-1); }
details.more .action-desc { margin-top:0; }
details > summary { min-height:44px; display:flex; align-items:center; gap:.3rem;
        cursor:pointer; color:var(--soft); font-weight:600; }
details[open] > summary { color:var(--ink); }
/* `display:flex` drops the browser's disclosure marker, which left every summary in the console
   ("Advanced", "What it does") looking like plain text. One chevron, drawn, rotated when open. */
details > summary::before { content:""; flex:none; width:.4rem; height:.4rem; margin-right:.1rem;
        border-right:2px solid currentColor; border-bottom:2px solid currentColor;
        transform:rotate(-45deg); transform-origin:60% 60%;
        transition:transform .15s ease; }
details[open] > summary::before { transform:rotate(45deg); }
details > summary::-webkit-details-marker { display:none; }   /* Safari keeps its own otherwise */
.vh { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0);
      white-space:nowrap; }

.badge.job-queued { background:var(--soft); }
.badge.job-running { background:var(--accent); }
.badge.job-done { background:var(--ok-ink); }
.badge.job-failed { background:var(--bad); }

/* ════════════════════════════════════════════════════════════════════════════
   Command-center dashboard (redesign, Option A). All token-driven → themes for free.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Lede: the standup panel (Phase 3) ──────────────────────────────────────
   A4 fix: the KIND label is the biggest thing here and the date reads as its value, so
   the words outrank the numbers. Previously the dashboard's only h1 was .vh and the card
   labels were .9rem — smaller than body text. */
.lede { background:var(--surface); border:1px solid var(--line);
        border-left:4px solid var(--soft); border-radius:var(--radius-lg);
        padding:var(--sp-4) var(--sp-5); margin:var(--sp-2) 0 var(--sp-5);
        box-shadow:var(--shadow); }
.lede--approved       { border-left-color:var(--ok);     background:var(--tint-ok); }
.lede--approved_stale { border-left-color:var(--bad);    background:var(--tint-bad); }
.lede--posted         { border-left-color:var(--accent); background:var(--tint-accent); }
.lede--generated      { border-left-color:var(--soft); }
.lede-head { display:flex; flex-wrap:wrap; align-items:baseline;
             justify-content:space-between; gap:var(--sp-2) var(--sp-4); }
.lede-title { display:flex; align-items:baseline; flex-wrap:wrap; gap:var(--sp-2);
              margin:0; font-size:var(--text-xl); font-weight:700; letter-spacing:-.02em; }
.lede-title .lede-icon { align-self:center; width:1.4rem; height:1.4rem; color:var(--soft); }
.lede-kind { color:var(--ink); }
.lede-date { color:var(--soft); font-weight:600; font-size:var(--text-lg);
             text-decoration:none; font-variant-numeric:tabular-nums; }
.lede-date:hover { color:var(--accent); text-decoration:underline; }
.lede-pills { display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
.lede .provenance { margin:var(--sp-2) 0 0; }
.lede-act { display:flex; align-items:center; flex-wrap:wrap;
            gap:var(--sp-2) var(--sp-4); margin:var(--sp-4) 0 0; }
/* C4: the auto-post warning was a title-only badge; it is readable text now. */
.lede-note { display:inline-flex; align-items:center; gap:var(--sp-1);
             color:var(--attn-ink); font-size:var(--text-sm); font-weight:600; }
.lede-note .icon { width:var(--icon-sm); height:var(--icon-sm); }
.lede-act .tally { margin-left:auto; }

/* Section headings — the dashboard's structure is now announced, not just aria-labelled. */
.section-h2 { margin:var(--sp-5) 0 var(--sp-2); font-size:var(--text-lg); }
.section-head { display:flex; flex-wrap:wrap; align-items:baseline;
                justify-content:space-between; gap:var(--sp-1) var(--sp-4); }
.section-head .section-h2 { margin-bottom:0; }
.section-note { margin:0 0 var(--sp-2); color:var(--soft); font-size:var(--text-sm); }
/* Icon beside a page h1 (experiments / attention, where an emoji used to sit). */
h1 .h1-icon { width:1.2em; height:1.2em; color:var(--soft); vertical-align:-.14em; }

.pipe { display:flex; align-items:center; gap:0; list-style:none;
        margin:var(--sp-3) 0 0; padding:0; min-width:0; flex-wrap:wrap;
        row-gap:var(--sp-2); }
.pipe-step { display:flex; align-items:center; gap:.35rem; position:relative; padding:0 .15rem; }
.pipe-step + .pipe-step { margin-left:1.2rem; }
.pipe-step + .pipe-step::before { content:""; position:absolute; left:-1.2rem; top:50%;
        width:1.2rem; height:2px; transform:translateY(-50%); background:var(--line); }
.pipe-step.is-done + .pipe-step::before { background:var(--ok-ink); }
.pipe-dot { width:.62rem; height:.62rem; border-radius:50%; background:var(--line); flex:none; }
.pipe-step.is-done .pipe-dot { background:var(--ok-ink); }
.pipe-step.is-now  .pipe-dot { background:var(--accent); box-shadow:0 0 0 3px var(--tint-accent); }
.pipe-lbl { font-size:.78rem; font-weight:600; letter-spacing:-.01em; color:var(--soft);
            white-space:nowrap; }
.pipe-step.is-done .pipe-lbl { color:var(--ok-ink); }
.pipe-step.is-now  .pipe-lbl { color:var(--accent-strong); }

/* J1: a hand-edited draft that FAILs the gate would ship unchecked via Copy — the strip's
   actionable warning (attention amber, distinct from action blue and fail red). */
.cmd-warn { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;
            background:var(--attn-tint); border:1px solid var(--attn-ink);
            color:var(--attn-ink); padding:.5rem .9rem; border-radius:var(--radius);
            margin:-.4rem 0 1rem; font-size:var(--text-sm); font-weight:600; }
.cmd-warn a { color:inherit; text-decoration:underline; }

.tally { display:flex; align-items:center; gap:.35rem; font-size:.78rem; font-weight:600;
         white-space:nowrap; background:var(--code-bg); border-radius:var(--radius-lg); padding:.25rem .6rem;
         font-variant-numeric:tabular-nums; }
.tally-pass { color:var(--ok-ink); }
.tally-fail { color:var(--bad-ink); }
.tally-sep  { color:var(--soft); }

/* ── Secondary artifact row: Daily digest · Weekly recap · Monthly report (3-up) ──
   B1: these were peers of the standup in a 2×2 grid. They are internal reads at three
   different cadences, so they sit below the lede as a lighter row instead. */
.report-row { display:grid; grid-template-columns:repeat(3, 1fr); gap:var(--sp-4);
              margin:0 0 var(--sp-5); align-items:start; }
.art-card { display:flex; flex-direction:column; gap:var(--sp-1);
            background:var(--surface); border:1px solid var(--line);
            border-radius:var(--radius-lg);
            padding:var(--sp-3) var(--sp-3); box-shadow:var(--shadow); }
.art-card--approved       { border-left-color:var(--ok); }
.art-card--approved_stale { border-left-color:var(--bad); }
.art-card--posted         { border-left-color:var(--accent); }
.art-card--generated      { border-left-color:var(--soft); }
.art-head { display:flex; align-items:center; justify-content:space-between; gap:.5rem; }
/* K15: the kinds are <h2>s (heading-nav reachable) styled as the same small labels —
   margin/size reset so the promotion changed semantics, not looks. */
/* A4: was .9rem — SMALLER than body text, under a 1.15rem date. The label that names the
   artifact now outranks the value it labels. */
.art-kind { display:inline-flex; align-items:center; gap:var(--sp-1); font-weight:650;
            font-size:var(--text-base); color:var(--ink); margin:0;
            letter-spacing:-.01em; }
.art-kind .icon { color:var(--soft); }
.art-date { font-size:var(--text-md); font-weight:600; letter-spacing:0; color:var(--soft);
            text-decoration:none; font-variant-numeric:tabular-nums; min-height:44px;
            display:inline-flex; align-items:center; }
.art-date:hover { color:var(--accent); text-decoration:underline; }
.art-card .provenance { margin:.1rem 0 .2rem; }
.art-card > .badge { align-self:flex-start; } /* a badge row must not stretch the card's width */
.art-act { display:flex; align-items:center; gap:.7rem; margin-top:auto; padding-top:.2rem; }
.art-act .btn-primary, .art-act .btn-tonal { font-size:.88rem; padding:.35rem .8rem; }
.art-act .btn-primary .icon, .art-act .btn-tonal .icon { margin-left:.2rem; }

/* ── Structured-view entry cards (Experiments · Stuck & aging) ──────────────── */
.entry-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; margin:0 0 1.3rem; }
.entry-card { display:flex; align-items:center; gap:.8rem; text-decoration:none;
              background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-lg);
              padding:.7rem .9rem; box-shadow:var(--shadow); color:var(--ink);
              min-height:44px; }
.entry-card:hover { border-color:var(--soft); }
.entry-icon { display:inline-flex; }
.entry-icon .icon { width:var(--icon-lg); height:var(--icon-lg); color:var(--accent); }
.entry-body { display:flex; flex-direction:column; line-height:1.2; flex:1 1 auto; min-width:0;
              gap:var(--sp-1); }
/* A4: the count used to sit above the label at 1.5rem, so "12" outranked "Experiments".
   Label first and larger; the count reads as its value. */
.entry-lbl { font-size:var(--text-base); font-weight:650; color:var(--ink);
             letter-spacing:-.01em; }
.entry-n { font-size:var(--text-lg); font-weight:700; letter-spacing:-.02em; color:var(--soft);
           font-variant-numeric:tabular-nums; }
.attn-text { color:var(--attn-ink); font-weight:600; }
.attn-text .icon { width:var(--icon-sm); height:var(--icon-sm); vertical-align:-.15em; }
.flag-blocked { margin-left:var(--sp-1); }
.empty-inline { color:var(--soft); font-style:italic; }   /* muted "no current snapshot" note */
.entry-go { color:var(--soft); flex:none; }

/* J4-v — "what's DUE but ungenerated" prompt on the weekly/monthly cards (attention amber,
   not alarm red — it's a nudge, not a failure). */
.due-hint { display:flex; align-items:center; gap:.35rem; flex-wrap:wrap; margin:.3rem 0 0;
            font-size:var(--text-sm); color:var(--attn-ink); }
.due-hint .icon { width:1em; height:1em; flex:none; }
.due-hint a { color:var(--attn-ink); text-decoration:underline; font-weight:600; }

/* Structured-view tables (Experiments / Stuck & aging): aging rows tinted amber, numeric cols. */
tr.row-attn td { background:var(--attn-tint); }
td.cell-num, th.cell-num { white-space:nowrap; font-variant-numeric:tabular-nums; }
/* Let the long name column wrap (override the global td:first-child nowrap) and cap its width so
   the numeric columns stay on-screen without horizontal scroll. */
table.data-table td:first-child { white-space:normal; min-width:13rem; max-width:24rem;
                                  overflow-wrap:anywhere; }

/* J6 — themed "not generated yet" page. */
.missing-card { max-width:34rem; }
.missing-card h1 { margin:0 0 .4rem; }
.missing-card h1 .sub { color:var(--soft); font-weight:500; font-size:.75em; }
.missing-card .missing-lead { color:var(--ink); margin:.2rem 0 1rem; }
.missing-card .btn-primary { margin:.2rem 0; }
.missing-card .quiet-link { justify-content:flex-start; padding-left:0; margin-top:.6rem; }

.recent-more { margin:.4rem 0 0; }
.recent-more > summary { font-size:.9rem; }
.recent-more h2 { font-size:1rem; margin:.6rem 0 .3rem; }

/* Generation-history list (as-generated page) — one row per snapshot run for a date. */
.gen-list { list-style:none; padding:0; margin:.3rem 0 0; }
.gen-list li { display:flex; flex-wrap:wrap; align-items:center; gap:.2rem .5rem;
               padding:.35rem 0; border-bottom:1px solid var(--line); }
.gen-list li:last-child { border-bottom:0; }
.gen-list li.is-current { background:var(--tint-accent); border-radius:var(--radius-sm);
                          padding-left:.5rem; padding-right:.5rem; }
.gen-list a { min-height:44px; display:inline-flex; align-items:center; font-weight:600;
              text-decoration:none; font-variant-numeric:tabular-nums; }

/* ── Today-First hero (dashboard) ──────────────────────────────────────────── */
.hero { background:var(--surface); border:1px solid var(--line);
        border-left:4px solid var(--soft); border-radius:var(--radius-lg);
        padding:.9rem 1.1rem; margin:.8rem 0 1.3rem; box-shadow:var(--shadow); }
.hero--approved        { border-left-color:var(--ok);     background:var(--tint-ok); }
.hero--approved_stale  { border-left-color:var(--bad);    background:var(--tint-bad); }
.hero--posted          { border-left-color:var(--accent); background:var(--tint-accent); }
.hero--missing         { border-left-color:var(--warn);   background:var(--tint-warn); }
.hero-head { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .7rem; }
.hero-date { font-size:1.3rem; font-weight:700; letter-spacing:-.01em;
             color:var(--ink); text-decoration:none; min-height:44px;
             display:inline-flex; align-items:center; }
.hero-date:hover { color:var(--accent); }
.hero-actions { display:flex; align-items:center; gap:.8rem; margin:.5rem 0 .1rem; }
.btn-primary { display:inline-flex; align-items:center; min-height:44px;
               padding:.5rem 1.1rem; background:var(--accent); color:#fff;
               border-radius:var(--radius); text-decoration:none; font-weight:600; }
.btn-primary:hover { background:var(--accent-strong); }

/* Button tiers — GLOBAL (K7). The old container-scoped rules (.report-actions .btn-tonal,
   .edit-controls .btn-neutral/.btn-ghost, kept below as no-op overrides) left two real
   mis-renders: the dashboard cards' <a class="btn-tonal"> was a bare 33px text link, and
   the monthly Re-finalize <button class="btn-neutral"> fell through to the filled
   button[type=submit] style — a secondary action rendering as the strongest on its row.
   The button-qualified selectors (0-1-1, later in the file) win that specificity tie. */
.btn-tonal, button.btn-tonal, .btn-neutral, button.btn-neutral, .btn-ghost, button.btn-ghost {
  display:inline-flex; align-items:center; justify-content:center; min-height:44px;
  padding:.5rem 1.1rem; border:1px solid transparent; border-radius:var(--radius);
  text-decoration:none; cursor:pointer; }
.btn-tonal, button.btn-tonal { background:var(--tint-accent); border-color:var(--accent);
                               color:var(--accent-strong); font-weight:600; }
.btn-tonal:hover, button.btn-tonal:hover { background:var(--accent); border-color:var(--accent); color:#fff; }
.btn-tonal:disabled, button.btn-tonal:disabled { opacity:.5; cursor:not-allowed; }
.btn-neutral, button.btn-neutral { background:var(--surface); border-color:var(--control-line);
                                   color:var(--ink); font-weight:500; }
.btn-neutral:hover, button.btn-neutral:hover { background:var(--press); }
.btn-ghost, button.btn-ghost { background:transparent; border-color:var(--control-line);
                               color:var(--soft); font-weight:500; }
.btn-ghost:hover, button.btn-ghost:hover { border-color:var(--soft); color:var(--ink); }
.btn-ghost:disabled, button.btn-ghost:disabled { opacity:.5; cursor:not-allowed; }
.btn-primary:active { background:var(--accent-strong); }
.quiet-link { color:var(--soft); min-height:44px; min-width:44px; display:inline-flex;
              align-items:center; justify-content:center; padding:.6rem .4rem; }
.row-date a { display:inline-flex; min-height:44px; align-items:center;
              text-decoration:none; font-weight:600; color:var(--accent); }
.row-date .gen-hint { display:block; white-space:normal; }
.digest-list { list-style:none; padding:0; }
.digest-list a { display:inline-flex; min-height:44px; align-items:center;
                 padding:.4rem .2rem; }

/* Single full-width editor column (the live editor renders inline, so no side preview).
   min-width:0 stops long unbreakable validator tokens from widening the page on phones. */
.editor-wrap { max-width:58rem; min-width:0; }
textarea { width:100%; border:1px solid var(--control-line); border-radius:var(--radius-sm);
           background:var(--surface); color:var(--ink); }
textarea#editor { min-height:30rem; font:16px/1.5 ui-monospace, monospace; }
.preview { border:1px solid var(--line); border-radius:var(--radius); padding:0 1rem;
           background:var(--surface);
           overflow:auto; overflow-wrap:break-word; word-break:break-word; }
.preview a { overflow-wrap:anywhere; }
/* Standalone (view-only) preview: cap the prose measure near the 65–75ch band. */
.preview-solo { max-width:46rem; }

/* Inline live-preview markdown editor (CodeMirror 6, vendored bundle). Themed via the app
   tokens so the dark variant maps for free. The .cm-lp-* classes come from our live-preview
   plugin (editor-src/src/index.js): it hides the markdown markers off the active line and
   renders bold/headings/links in place — but the document stays literal markdown. */
/* Toolbar (formatting + Write/Markdown toggle) sits flush on top of the editor and stays
   pinned while you scroll a long draft, so formatting + save state are always reachable. */
.md-toolbar { display:flex; align-items:center; justify-content:space-between; gap:.5rem;
              flex-wrap:wrap; padding:.3rem .4rem; background:var(--code-bg);
              border:1px solid var(--line); border-bottom:0; border-radius:var(--radius-sm) var(--radius-sm) 0 0;
              position:sticky; top:0; z-index:5; }
.md-tools { display:flex; align-items:center; gap:.1rem; }
.md-right { display:flex; align-items:center; gap:.6rem; }
.md-right .save-status { font-size:.8rem; }
.md-btn { min-height:0; height:2rem; width:2.2rem; padding:0; margin:0;
          display:inline-flex; align-items:center; justify-content:center;
          background:transparent; border:1px solid transparent; border-radius:var(--radius-sm);
          color:var(--ink); cursor:pointer; touch-action:manipulation; }
.md-btn:hover { background:var(--press); }
.md-btn:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
.md-btn .icon { width:1.15rem; height:1.15rem; }
.md-sep { width:1px; height:1.3rem; background:var(--line); margin:0 .3rem; }
/* C6 - the Ctrl/Cmd-S binding lived only in draft.js; surface it beside the save status.
   Shown only where there is a real keyboard to press it with. */
.md-kbd { display:none; align-items:center; gap:var(--sp-1); color:var(--soft);
          font-size:var(--text-2xs); }
.md-kbd .icon { width:var(--icon-sm); height:var(--icon-sm); }
.md-kbd kbd { font:inherit; font-family:ui-monospace, monospace; background:var(--surface);
              border:1px solid var(--control-line); border-bottom-width:2px;
              border-radius:var(--radius-xs); padding:0 var(--sp-1); }
/* `pointer: fine` reports false wherever no mouse is attached yet (headless, some
   laptops, a plugged-in-later mouse), which hid the hint from the very desktops it is
   for. Show it unless the primary input is actually coarse. */
@media (min-width:48rem) and (not (pointer: coarse)) { .md-kbd { display:inline-flex; } }
.seg-toggle { display:inline-flex; border:1px solid var(--control-line); border-radius:var(--radius);
              overflow:hidden; background:var(--surface); }
.seg-opt { min-height:0; height:1.9rem; padding:0 .8rem; margin:0; border:0;
           background:transparent; color:var(--soft); font-weight:600; font-size:.82rem;
           cursor:pointer; touch-action:manipulation; }
.seg-opt + .seg-opt { border-left:1px solid var(--line); }
.seg-opt:hover { background:var(--press); }
.seg-opt:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.seg-opt[aria-pressed="true"] { background:var(--accent); color:#fff; }
/* Touch devices (iPhone via Tailscale): meet the 44px minimum tap target. */
@media (pointer: coarse) {
  .md-btn { height:44px; width:44px; }
  .md-btn .icon { width:1.25rem; height:1.25rem; }
  .seg-opt { height:44px; padding:0 1rem; }
  /* K8: undo the desktop 2rem compaction — restore/discard must be tappable. */
  .draft-restore button { height:auto; min-height:44px; }
}
/* Restore-unsaved-draft banner (local autosave). */
.draft-restore { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
                 background:var(--tint-accent); border:1px solid var(--accent);
                 color:var(--ink); padding:.5rem .8rem; border-radius:var(--radius);
                 margin:.6rem 0; font-size:.9rem; }
.draft-restore button { min-height:0; height:2rem; padding:0 .8rem; font-size:var(--text-sm); }
.cm-editor { border:1px solid var(--control-line); border-radius:var(--radius-sm);
             background:var(--surface); color:var(--ink); }
/* In the edit pane the toolbar sits flush on top, so square the editor's top corners
   (the hidden <textarea> sits between them in the DOM, so an adjacent selector won't do). */
.editor-wrap .cm-editor { border-top-left-radius:0; border-top-right-radius:0; }
.cm-editor.cm-focused { outline:2px solid var(--accent); outline-offset:-1px; }
/* No max-height: the editor grows with content so the PAGE scrolls as one (no nested
   scroll region). The sticky toolbar keeps tools in reach. */
.cm-editor .cm-scroller { font:16px/1.7 Inter, system-ui, sans-serif; min-height:30rem; }
/* Markdown (raw) mode reads better monospace — alignment of `- `, `·`, links, code. */
.cm-editor.cm-raw .cm-scroller { font-family:ui-monospace, monospace; }
.cm-editor .cm-content { padding:.7rem .4rem; caret-color:var(--ink); }
.cm-editor .cm-cursor { border-left-color:var(--ink); }
.cm-editor .cm-activeLine { background:color-mix(in srgb, var(--accent) 7%, transparent); }
.cm-editor .cm-selectionBackground,
.cm-editor.cm-focused .cm-selectionBackground { background:var(--press); }
/* Inline-rendered markdown (markers hidden off the active line). */
.cm-lp-h1 { font-size:1.5em;  font-weight:700; line-height:1.3; }
.cm-lp-h2 { font-size:1.25em; font-weight:700; line-height:1.3; }
.cm-lp-h3 { font-size:1.12em; font-weight:700; }
.cm-lp-h4, .cm-lp-h5, .cm-lp-h6 { font-weight:700; }
.cm-lp-strong { font-weight:700; }
.cm-lp-em { font-style:italic; }
.cm-lp-strike { text-decoration:line-through; color:var(--soft); }
.cm-lp-code { font-family:ui-monospace, monospace; background:var(--code-bg);
              border-radius:var(--radius-xs); padding:0 .2rem; }
.cm-lp-link { color:var(--accent); text-decoration:underline; cursor:text; }
.cm-lp-mention { color:var(--accent); font-weight:600; } /* <@U…> rendered as @Nick (mirrors .mention) */
/* Lists. The `-`/`*` marker is replaced by a • (◦ nested) widget — always, cursor line
   included (see the comment in editor-src/src/index.js). The hanging indent is what makes a
   wrapped bullet read as a list item: without it a long line wraps flush to the left margin.
   Nested items keep their literal leading spaces, so their continuation lines align to the
   top-level indent rather than the nested one — close enough, don't measure per line. */
/* `.cm-editor` prefix required: CodeMirror's own `.cm-line { padding:0 2px 0 6px }` is
   injected into <head> AFTER this sheet, so a bare `.cm-lp-li` loses the cascade at equal
   specificity and only the negative indent lands — which pulls the bullet off the left edge. */
.cm-editor .cm-lp-li { padding-left:calc(6px + 1.2em); text-indent:-1.2em; }
.cm-lp-bullet { color:var(--soft); }

/* ── Link popover (write mode) ───────────────────────────────────────────────
   Write mode never reveals a link's `](url)` on the caret's line — report bullets end in
   ~70-char Airtable URLs and the reflow made the text jump under the cursor. The URL lives
   here instead: a popover anchored over the link (hover, or caret inside), so it overlays
   the line rather than displacing it. Google Docs / Notion, not Obsidian.
   `.cm-editor` prefixes: CodeMirror injects its own `.cm-tooltip` rules into <head> AFTER
   this sheet, so a bare selector loses the cascade at equal specificity (same reason as
   `.cm-lp-li` above). */
.cm-editor .cm-tooltip.cm-tooltip-link {
  display:flex; align-items:center; gap:var(--sp-1);
  background:var(--surface); color:var(--ink);
  border:1px solid var(--control-line); border-radius:var(--radius-sm);
  box-shadow:var(--shadow); padding:.25rem .3rem; font-size:var(--text-sm);
  max-width:min(34rem, calc(100vw - 2rem));
}
.cm-tooltip-link .cm-lp-tiptext {
  font-family:ui-monospace, monospace; font-size:var(--text-xs); color:var(--soft);
  padding:0 .25rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.cm-tooltip-link .cm-lp-tiptext.is-empty { font-style:italic; font-family:inherit; }
.cm-tooltip-link .cm-lp-tipurl {
  font:inherit; font-family:ui-monospace, monospace; font-size:var(--text-xs);
  color:var(--ink); background:var(--surface);
  border:1px solid var(--control-line); border-radius:var(--radius-xs);
  padding:.2rem .35rem; min-width:min(22rem, 60vw);
}
.cm-tooltip-link .cm-lp-tipurl:focus-visible { outline:2px solid var(--accent); outline-offset:-1px; }
.cm-tooltip-link .cm-lp-tipbtn {
  display:inline-flex; align-items:center; justify-content:center;
  width:1.75rem; height:1.75rem; min-height:0; padding:0; flex:none;
  background:none; border:0; border-radius:var(--radius-xs);
  color:var(--soft); cursor:pointer;
}
.cm-tooltip-link .cm-lp-tipbtn:hover { background:var(--press); color:var(--ink); }
.cm-tooltip-link .cm-lp-tipbtn:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
.cm-tooltip-link .cm-lp-tipbtn .icon { width:var(--icon-sm); height:var(--icon-sm); }

/* ── Report editor hierarchy (draft / digest pages) ─────────────────────────
   Two tiers: report-level actions (Copy / Regenerate) up top, edit-level
   actions (Save / Approve) demoted below the editor. Button weight encodes
   rank: filled primary → tonal → neutral → ghost. Tier classes are
   container-scoped (specificity 0,2,0) so they outrank the global
   button[type=submit] accent rule in BOTH light and dark themes. */
.report-head { display:flex; align-items:flex-start; justify-content:space-between;
               gap:1rem; flex-wrap:wrap; }
.report-head h1 { margin:0; }
.report-head .sub { color:var(--soft); font-weight:500; }
.report-head .provenance { margin:.3rem 0 0; }
.status-pills { display:flex; gap:.4rem; align-items:center; padding-top:.35rem; }

.report-actions { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
                  margin:.9rem 0 .5rem; }
.report-actions .hint { color:var(--soft); font-size:var(--text-sm); }
.report-actions .btn-tonal { background:var(--tint-accent); border-color:var(--accent);
                             color:var(--accent-strong); font-weight:600; }
.report-actions .btn-tonal:hover { background:var(--accent); border-color:var(--accent); color:#fff; }
.report-actions .btn-tonal:disabled { opacity:.5; cursor:not-allowed; }

.edit-controls { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap;
                 margin-top:.8rem; padding-top:.8rem; border-top:1px solid var(--line); }
.edit-controls .btn-neutral { background:var(--surface); border-color:var(--control-line);
                              color:var(--ink); font-weight:500; }
.edit-controls .btn-neutral:hover { background:var(--press); }
.edit-controls .approve-wrap { margin-left:auto; display:flex; align-items:center; gap:.6rem; }
.edit-controls .approve-hint { color:var(--soft); font-size:.78rem; max-width:18rem; text-align:right; }
.edit-controls .btn-ghost { background:transparent; border-color:var(--control-line);
                            color:var(--soft); font-weight:500; }
.edit-controls .btn-ghost:hover { border-color:var(--soft); color:var(--ink); }
.edit-controls .btn-ghost:disabled { opacity:.5; cursor:not-allowed; }

pre.joblog { background:var(--joblog-bg); color:var(--joblog-ink); padding:1rem;
             border-radius:var(--radius); max-height:28rem; overflow:auto; font-size:13px; }
.stale-banner { background:var(--tint-bad); border:1px solid var(--bad);
                color:var(--bad-ink); padding:.6rem 1rem; border-radius:var(--radius);
                margin:.6rem 0; }
/* Links inside the alert inherit its ink + underline, so the recovery action reads as a link
   without a clashing blue inside the red banner. */
.stale-banner a { color:inherit; text-decoration:underline; font-weight:600; }
.provenance { color:var(--soft); font-size:var(--text-sm); margin:.2rem 0 .8rem; }
.provenance code, .gen-hint code, .hero code { background:var(--code-bg);
                padding:.05rem .3rem; border-radius:var(--radius-xs); }
.edited-badge { color:var(--warn-ink); font-weight:600; margin-left:var(--sp-1);
                white-space:nowrap; display:inline-flex; align-items:center; gap:.2em; }
.edited-badge .icon { width:.95em; height:.95em; }
.gen-hint { color:var(--soft); font-size:.8rem; white-space:nowrap; }
p.gen-hint { white-space:normal; }  /* prose hints (diff legend) must wrap at 320px */
ul.nested { margin:.1rem 0 0; padding-left:1.4rem; list-style:circle; }
ul li { padding:.15rem 0; }

/* Validation issues render as a bordered panel (it's a live region draft.js rewrites). */
ul#issues { list-style:none; margin:.6rem 0 0; padding:0; }
ul#issues:has(li) { border:1px solid var(--line); border-radius:var(--radius);
                    padding:.5rem .8rem; background:var(--surface); }
ul#issues li.error { padding:.15rem 0; overflow-wrap:anywhere; }
.save-status { color:var(--ok-ink); font-weight:600; }
.save-status.unsaved { color:var(--warn-ink); }
ul#issues.stale { opacity:.55; }   /* live-validate unreachable → results are stale */
.approve-status { background:var(--tint-accent); border:1px solid var(--accent);
                  color:var(--accent-strong); padding:.6rem 1rem;
                  border-radius:var(--radius); margin:.6rem 0; }
.approve-status .job-head { display:flex; align-items:center; gap:.4rem; }
.approve-status .icon { color:var(--accent-strong); }
/* Indeterminate progress bar for a running approve/regenerate job (no real %; just
   "working"). Transform-only sliver; reduced motion (below) freezes it to a full track. */
.job-progress { position:relative; height:3px; margin-top:.5rem; overflow:hidden;
                border-radius:var(--radius-xs);
                background:color-mix(in srgb, var(--accent) 22%, transparent); }
.job-progress::after { content:""; position:absolute; inset:0 auto 0 0; width:40%;
                       border-radius:inherit; background:var(--accent);
                       animation:job-slide 1.4s ease-in-out infinite; }
@keyframes job-slide { 0% { transform:translateX(-110%); } 100% { transform:translateX(360%); } }

/* Inline async spinner (Save / Approve / Copy / Regenerate). Transform-only, so the
   global reduced-motion rule leaves a static partial ring — a visible busy mark, never a
   frozen-looking blank. Sized in em so it tracks the button's text. */
.spinner { display:inline-block; width:.85em; height:.85em; margin-right:.45em;
           vertical-align:-.1em; border:2px solid currentColor; border-top-color:transparent;
           border-radius:50%; animation:spin .6s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .job-progress::after { width:100%; }   /* static full track instead of a moving sliver */
}

form.login { display:grid; gap:.35rem; }
form.login label { padding-bottom:0; font-weight:600; }
form.login input { max-width:none; margin-bottom:.5rem; }
form.login .btn-primary { width:100%; justify-content:center; border:none;
        cursor:pointer; margin-top:.3rem; }
.login-card { max-width:24rem; margin:8vh auto 0; }
.login-card h1 { margin-top:.2rem; }
.login-card > .btn-primary { width:100%; justify-content:center; }

/* ── Audit log: action badges, filter bar, pager ───────────────────────────── */
.badge.audit-save    { background:var(--accent); }
.badge.audit-approve { background:var(--ok-ink); }
.badge.audit-job     { background:var(--warn-ink); }
.badge.audit-cancel  { background:var(--bad); }
.badge.audit-login, .badge.audit-other { background:var(--soft); }
.filter-bar { display:flex; flex-wrap:wrap; align-items:end; gap:.6rem 1rem;
        margin:.4rem 0 1rem; }
.filter-bar label { display:grid; gap:.2rem; font-weight:600; font-size:var(--text-sm);
        color:var(--soft); }
.pager { display:flex; align-items:center; gap:1rem; margin:.8rem 0; }
.pager a { display:inline-flex; min-height:44px; align-items:center;
        text-decoration:none; font-weight:600; }

/* ── Archive: listings, name filter, breadcrumb ────────────────────────────── */
.breadcrumb { color:var(--soft); font-size:.9rem; margin:.4rem 0 0; }
.breadcrumb a { text-decoration:none; }
.file-list { list-style:none; padding:0; margin:.2rem 0 0; }
.file-list a { display:inline-flex; min-height:44px; align-items:center;
        text-decoration:none; overflow-wrap:anywhere; }
.archive-filter input { max-width:28rem; }

/* ── Generated snapshot: view toggle + diff ────────────────────────────────── */
.view-toggle { display:inline-flex; border:1px solid var(--control-line); border-radius:var(--radius-lg);
        overflow:hidden; margin:.9rem 0 .7rem; background:var(--surface); }
.view-toggle a { display:inline-flex; min-height:44px; align-items:center;
        padding:.5rem 1rem; text-decoration:none; color:var(--soft); font-weight:600; }
.view-toggle a + a { border-left:1px solid var(--line); }
.view-toggle a.active { background:var(--accent); color:#fff; }
.diff { border:1px solid var(--line); border-radius:var(--radius);
        background:var(--surface); overflow-x:auto;
        font:13px/1.6 ui-monospace, monospace; }
.diff-line { padding:.05rem .8rem; white-space:pre-wrap; overflow-wrap:anywhere; }
.diff-add  { background:var(--tint-ok);  color:var(--ok-ink); }
.diff-del  { background:var(--tint-bad); color:var(--bad-ink); }
.diff-hunk { background:var(--code-bg);  color:var(--soft); }
.diff-key  { padding:.05rem .45rem; border-radius:var(--radius-xs); font-weight:600; }

/* ── Dark variant (used at night/early morning) ────────────────────────────
   The theme is RESOLVED in base.html's head script, which always stamps a concrete
   data-theme="light|dark" before first paint — from localStorage if the operator
   pinned one, else from prefers-color-scheme. So every rule below needs a single
   selector instead of a prefers-color-scheme block duplicated for the manual
   override. theme.js re-resolves when the OS flips while the choice is "system".
   Trade-off: with JS off the console stays light. Acceptable — the editor,
   clipboard, job stream and filters already require JS. ────────────────────── */
:root[data-theme="dark"] {
  color-scheme: dark;
  --brand:#152a47; --brand-ink:#c6d6ea;
  --accent:#5b93f5; --accent-strong:#8ab2f8;
  --ink:#e2e8f0; --soft:#97a4b6;
  --bg:#0b1220; --surface:#151e31; --line:#2b3a52;
  /* #5f7593 = 3.53:1 on --surface, 3.97:1 on --bg (--line was 1.45:1 — a 1.4.11 fail). */
  --control-line:#5f7593;
  --ok:#22c55e; --warn:#f59e0b; --bad:#ef4444;
  --ok-ink:#5fd58a; --warn-ink:#f1b25b; --bad-ink:#f58c8c;
  --tint-accent:#16243d; --tint-ok:#12281b; --tint-warn:#2a2008; --tint-bad:#2c1414;
  --code-bg:#22304a; --press:#23314c;
  --shadow:0 1px 2px rgba(0,0,0,.4);
  --scroll-shadow:rgba(0,0,0,.55);
  /* Attention amber, lightened for dark surfaces (mirrors --warn-ink's dark treatment). */
  --attn-ink:#f1b25b; --attn-tint:#2a2008;
}
/* Mid-tone ok/warn/bad tokens need DARK text in dark mode (white fails on them).
   The slate badge needs WHITE (6.31:1 — dark ink was 2.93); the blue is lightened
   to #5d8bf0 so its dark ink clears AA (5.73:1, was 4.41 on #3b76e8). */
:where([data-theme="dark"]) .badge.approved { background:var(--ok); color:#06270f; }
:where([data-theme="dark"]) .badge.missing { background:var(--warn); color:#2a1c02; }
:where([data-theme="dark"]) .badge.in-progress { background:var(--warn); color:#2a1c02; }
:where([data-theme="dark"]) .badge.approved_stale { background:var(--bad); color:#2d0707; }
:where([data-theme="dark"]) .badge.post_failed { background:var(--bad); color:#2d0707; }
:where([data-theme="dark"]) .badge.posted { background:#5d8bf0; color:#061129; }
:where([data-theme="dark"]) .badge.copied { background:#5d8bf0; color:#061129; }
:where([data-theme="dark"]) .badge.reviewed { background:var(--ok); color:#06270f; }
:where([data-theme="dark"]) .badge.generated { background:#536174; color:#fff; }
:where([data-theme="dark"]) .badge.job-queued { background:#536174; color:#fff; }
:where([data-theme="dark"]) .badge.job-running { background:#5d8bf0; color:#061129; }
:where([data-theme="dark"]) .badge.job-done { background:var(--ok); color:#06270f; }
:where([data-theme="dark"]) .badge.job-failed { background:var(--bad); color:#2d0707; }
:where([data-theme="dark"]) .badge.audit-save { background:#5d8bf0; color:#061129; }
:where([data-theme="dark"]) .badge.audit-approve { background:var(--ok); color:#06270f; }
:where([data-theme="dark"]) .badge.audit-job { background:var(--warn); color:#2a1c02; }
:where([data-theme="dark"]) .badge.audit-cancel { background:var(--bad); color:#2d0707; }
:where([data-theme="dark"]) .badge.audit-login, :where([data-theme="dark"]) .badge.audit-other { background:#536174; color:#fff; }
:where([data-theme="dark"]) .view-toggle a.active { background:#2f6ae0; color:#fff; }
:where([data-theme="dark"]) button[type="submit"], :where([data-theme="dark"]) .btn-primary { background:#2f6ae0; border-color:#2f6ae0; color:#fff; }
:where([data-theme="dark"]) button[type="submit"]:hover, :where([data-theme="dark"]) .btn-primary:hover { background:#4a80ea; border-color:#4a80ea; }
/* …but a ghost-tier submit (the jobs table's `cancel`) must stay ghost: the rule above ties
   .btn-ghost on specificity and wins by source order, painting it as the filled primary. */
:where([data-theme="dark"]) button.btn-ghost[type="submit"] { background:transparent;
        border-color:var(--control-line); color:var(--soft); }
:where([data-theme="dark"]) button.btn-ghost[type="submit"]:hover { background:transparent;
        border-color:var(--soft); color:var(--ink); }
:where([data-theme="dark"]) button.danger { background:#c93535; border-color:#c93535; color:#fff; }

/* Phones + tablets + the narrow-desktop band: stack the side-by-side grids, shrink the
   editor. Raised 900px → 1023px so the two-column grids only appear once the content
   column is genuinely wide enough for them (see the A2 note on the shell breakpoint). */
@media (max-width: 1023px) {
  .cards { grid-template-columns:1fr; }
  .report-row { grid-template-columns:1fr; }
  .entry-grid { grid-template-columns:1fr; }
  textarea#editor { min-height:18rem; }
  .cm-editor .cm-scroller { min-height:18rem; }
  .preview { max-height:24rem; }
  main { padding-top:.75rem; }
  h1 { font-size:1.4rem; }
  .view-toggle { display:grid; grid-template-columns:1fr 1fr; width:100%; }
  .view-toggle a { justify-content:center; text-align:center; padding:.5rem .4rem; }
}

/* ── Public shell (login / logged out / lockout / 403 / 404) ─────────────────
   A3: these pages have no nav, but they used to render the same <header>, so the wide
   layout painted a 15.5rem navy rail with nothing in it. base.html now emits a centred
   brand mark instead and tags <body class="public">. */
.public-brand { display:flex; justify-content:center; margin:var(--sp-6) 0 0; }
.public-brand .brand-link { color:var(--ink); text-decoration:none; font-weight:650;
        display:inline-flex; align-items:center; gap:var(--sp-2);
        font-size:var(--text-lg); letter-spacing:-.01em; }
.public-brand .icon { width:1.35rem; height:1.35rem; color:var(--accent); }
body.public main { max-width:26rem; }
body.public .login-card, body.public .card { margin-top:var(--sp-5); }

/* ── Page width tiers ───────────────────────────────────────────────────────
   B3: `main` was globally capped at 60rem — a prose measure. That is right for the
   editors and the dashboard, but it forced the 5–7 column ledgers (audit, archive,
   experiments, attention, jobs) to scroll horizontally on a 1920px monitor. Table pages
   opt into the wide tier. */
main.main--wide { max-width:78rem; }

/* ── Theme switch (D4) — a 3-state segmented control in the nav rail ────────── */
.theme-switch { display:inline-flex; gap:0; align-self:flex-start;
        border:1px solid rgba(255,255,255,.22); border-radius:var(--radius-sm);
        overflow:hidden; margin:var(--sp-2) 0 0; }
.theme-switch button { min-height:36px; min-width:40px; padding:0; margin:0; border:0;
        border-radius:0; background:transparent; color:var(--brand-ink);
        display:inline-flex; align-items:center; justify-content:center;
        cursor:pointer; touch-action:manipulation; }
.theme-switch button + button { border-left:1px solid rgba(255,255,255,.22); }
.theme-switch button:hover { background:rgba(255,255,255,.10); color:#fff; }
.theme-switch button[aria-pressed="true"] { background:rgba(255,255,255,.20); color:#fff; }
.theme-switch .icon { width:var(--icon-sm); height:var(--icon-sm); }
/* Touch: the 36px chrome control needs the full 44px target on a phone. */
@media (pointer: coarse) { .theme-switch button { min-height:44px; min-width:44px; } }
@media (min-width:1240px) { .theme-switch { align-self:stretch; } .theme-switch button { flex:1; } }


/* ── Forms (C3) ─────────────────────────────────────────────────────────────
   The Actions forms were <br>-separated implicit labels with a `placeholder` standing in
   for helper text and no grouping. Now: explicit label/for, a hint paragraph wired by
   aria-describedby, and a fieldset per option group. */
.field { display:grid; gap:var(--sp-1); margin:0 0 var(--sp-3); max-width:22rem; }
.field > label { display:block; padding:0; font-weight:600; font-size:var(--text-sm); }
.field > input, .field > select { margin:0; }
.field[hidden] { display:none; }   /* the Other-model field starts hidden (actions.js reveals it) */
/* A <select> sizes itself to its widest option — the model ids blew past the card on a phone.
   Match the input rule so it fills the field instead. */
.field > select { width:100%; max-width:22rem; }
.hint { margin:0; color:var(--soft); font-size:var(--text-sm); line-height:1.45; }
.hint code { background:var(--code-bg); padding:.05rem .3rem; border-radius:var(--radius-xs); }
/* Optional/required markers — 'required' must not be colour-only, so it reads as a word. */
.opt, .req { font-weight:500; font-size:var(--text-2xs); text-transform:uppercase;
             letter-spacing:.05em; padding:.05rem .35rem; border-radius:var(--radius-xs); }
.opt { color:var(--soft); background:var(--code-bg); }
.req { color:var(--bad-ink); background:var(--tint-bad); }
.check { display:flex; align-items:flex-start; gap:var(--sp-2); min-height:44px;
         padding:var(--sp-1) 0; font-size:var(--text-sm); }
.check input { margin-top:.15rem; }
details.adv > summary { margin:var(--sp-2) 0; }
details.adv fieldset { border:1px solid var(--line); border-radius:var(--radius);
                       padding:var(--sp-2) var(--sp-3); margin:0 0 var(--sp-3); min-width:0; }
details.adv legend { padding:0 var(--sp-1); font-size:var(--text-sm); font-weight:650;
                     color:var(--soft); }
details.adv fieldset .field:last-child { margin-bottom:0; }

/* ── Shared destructive confirmation (C1) ───────────────────────────────────
   Replaces window.confirm (regen + monthly companions), the required checkbox
   (post-to-team), the two-click label swap (Copy) and the nothing-at-all (job cancel). */
.confirm-row { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
               gap:var(--sp-2) var(--sp-4); width:100%; margin:var(--sp-2) 0 0;
               padding:var(--sp-3); border:1px solid var(--attn-ink);
               background:var(--attn-tint); color:var(--attn-ink);
               border-radius:var(--radius); }
.confirm-msg { margin:0; flex:1 1 16rem; font-size:var(--text-sm); font-weight:600; }
.confirm-actions { display:flex; align-items:center; gap:var(--sp-2); flex:none; }
.confirm-actions button { min-height:40px; padding:var(--sp-2) var(--sp-4);
                          font-size:var(--text-sm); }
@media (pointer: coarse) { .confirm-actions button { min-height:44px; } }

/* Job row controls — both ghost tier, so `cancel` is no longer the loudest thing in the
   table (C2: button[type=submit] is globally the filled accent button). */
/* Jobs table: the args summary is raw argv (monthly jobs carry ~150 chars of CSV paths), and
   .gen-hint is nowrap — so it stretched the Action column until State/Exit scrolled off-screen.
   Give it its own line and let it break anywhere (paths have no spaces to break at). */
.job-action { min-width:9rem; }
.job-action .job-args { display:block; white-space:normal; overflow-wrap:anywhere;
        font-family:ui-monospace, monospace; font-size:var(--text-xs); margin-top:.15rem; }
/* Phones: six columns can't fit, so each job becomes a stacked card instead of a table that
   hides State/Exit off-screen. When · State · Exit on top, Action full-width, By + log below. */
@media (max-width:40rem) {
  #jobs-table thead { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }
  #jobs-table, #jobs-table tbody { display:block; }
  #jobs-table tr { display:grid; grid-template-columns:1fr auto auto; align-items:center;
        grid-template-areas:"when state exit" "action action action" "by ctl ctl";
        column-gap:var(--sp-2); padding:.6rem .7rem; border-bottom:1px solid var(--line); }
  #jobs-table tr:last-child { border-bottom:none; }
  #jobs-table td { padding:0; border:0; }
  #jobs-table .job-when { grid-area:when; }
  #jobs-table .job-state { grid-area:state; }
  #jobs-table .job-exit { grid-area:exit; }
  #jobs-table .job-action { grid-area:action; min-width:0; margin:.3rem 0 .15rem; }
  #jobs-table .job-by { grid-area:by; color:var(--soft); font-size:var(--text-sm); }
  #jobs-table .job-controls { grid-area:ctl; display:flex; justify-content:flex-end; gap:var(--sp-2); }
  #jobs-table .job-controls .inline { margin-left:0; }
  #jobs-table td.empty-state { grid-column:1 / -1; }
  #jobs-table tr.job-logrow { display:block; padding-top:0; }
  #jobs-table tr.job-logrow > td { display:block; }
  #jobs-table tr.is-watched td:first-child { box-shadow:none; }
  #jobs-table tr.is-watched { background:var(--tint-accent); box-shadow:inset 3px 0 0 var(--accent);
        border-bottom:0; }
}
/* Not display:flex on the <td> itself — that drops it out of table layout and its bottom
   border misaligns with the row. Nowrap keeps watch log + cancel side by side. */
.job-controls { white-space:nowrap; }
.job-controls .inline { display:inline; margin-left:var(--sp-2); }
.job-controls .btn-ghost { min-height:36px; padding:var(--sp-1) var(--sp-3);
                           font-size:var(--text-sm); }
@media (pointer: coarse) { .job-controls .btn-ghost { min-height:44px; } }
/* Failure reason under a failed job's label (job_display.error_line). */
.job-error { display:block; color:var(--bad-ink); font-size:var(--text-xs); margin-top:.15rem;
             overflow-wrap:anywhere; }
/* Inline log: one row moved under the watched job (actions.js). */
#jobs-table tr.is-watched td { background:var(--tint-accent); }
#jobs-table tr.is-watched td:first-child { box-shadow:inset 3px 0 0 var(--accent); }
.job-logrow > td { background:var(--tint-accent); padding-top:0; }
.job-logrow { scroll-margin-top:8rem; }   /* clear the sticky status strip */
.job-logrow pre.joblog { margin:0; max-height:22rem; white-space:pre-wrap; overflow-wrap:anywhere; }
.job-log-head { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2);
                flex-wrap:wrap; margin-bottom:var(--sp-2); }
.job-log-title { font-size:var(--text-sm); font-weight:600; }
.job-log-bar { display:flex; gap:var(--sp-2); }
.job-log-bar .btn-ghost { min-height:36px; padding:var(--sp-1) var(--sp-3); font-size:var(--text-sm); }
@media (pointer: coarse) { .job-log-bar .btn-ghost { min-height:44px; } }

.table-note { color:var(--soft); font-size:var(--text-sm); margin:var(--sp-2) 0 0; }

/* Status strip: the live or just-finished job, pinned to the top while you scroll the forms. */
.job-strip-slot { position:sticky; top:env(safe-area-inset-top, 0px); z-index:5; }
.job-strip-slot:not(:empty) { margin-bottom:var(--sp-3); }
.job-strip { display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap;
             background:var(--tint-accent); border:1px solid var(--accent); border-radius:var(--radius);
             padding:var(--sp-2) var(--sp-3); box-shadow:var(--shadow); }
.job-strip--done   { background:var(--tint-ok);  border-color:var(--ok); }
.job-strip--failed { background:var(--tint-bad); border-color:var(--bad); }
.job-strip--done > .icon   { color:var(--ok-ink); }
.job-strip--failed > .icon { color:var(--bad-ink); }
.job-strip-text { flex:1 1 9rem; min-width:0; margin:0; line-height:1.35; }
.job-strip-sub { display:block; color:var(--soft); font-size:var(--text-sm); overflow-wrap:anywhere; }
.job-strip-elapsed { font-variant-numeric:tabular-nums; }
.job-strip-actions { display:flex; gap:var(--sp-2); }
.job-strip-actions .btn-ghost { min-height:44px; display:inline-flex; align-items:center; }
.job-strip-dot { width:.6rem; height:.6rem; border-radius:50%; background:var(--accent); flex:none;
                 box-shadow:0 0 0 3px var(--tint-accent), 0 0 0 4px var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .job-strip--running .job-strip-dot { animation:strip-pulse 1.6s ease-in-out infinite; }
}
@keyframes strip-pulse { 50% { opacity:.35; } }

/* Standup card: the dates the script will actually use (pipeline.standup_plan + actions.js). */
.date-preview { background:var(--tint-accent); border-radius:var(--radius); margin:var(--sp-3) 0 0;
                padding:var(--sp-2) var(--sp-3); font-size:var(--text-sm); }
.date-preview.is-warn { background:var(--tint-warn); border:1px solid var(--warn); }
.date-preview.is-warn strong { color:var(--warn-ink); }

/* Duplicate-run note above a report form's submit button (actions.js). */
.dup-note { background:var(--tint-warn); border:1px solid var(--warn); border-radius:var(--radius);
            padding:var(--sp-2) var(--sp-3); font-size:var(--text-sm); margin:var(--sp-3) 0; }
.dup-note strong { color:var(--warn-ink); }

.chip-pass .icon, .chip-fail .icon { width:.95em; height:.95em; vertical-align:-.12em; }

/* C4: the visible reason an action is unavailable (was a title= on a disabled button,
   which is unreachable by keyboard because disabled controls aren't focusable). */
.approve-why { margin:var(--sp-1) 0 0; width:100%; text-align:right;
               color:var(--attn-ink); font-size:var(--text-2xs); font-weight:600; }
/* ── Sortable column headers (Phase 5) ──────────────────────────────────────
   Only on tables that render their complete dataset — see sort.js. The header stays a
   real <button> so it is keyboard-operable; aria-sort carries the state. */
th:has(.th-sort) { padding:0; }
.th-sort { display:flex; align-items:center; gap:var(--sp-1); width:100%;
           min-height:0; margin:0; padding:var(--sp-2) var(--sp-3);
           background:transparent; border:0; border-radius:0; cursor:pointer;
           font:inherit; color:inherit; text-align:left; }
.th-sort:hover { background:var(--press); color:var(--ink); }
.th-sort:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.sort-mark { width:.9rem; height:.9rem; opacity:.35; flex:none; }
th[aria-sort="ascending"] .sort-mark,
th[aria-sort="descending"] .sort-mark { opacity:1; color:var(--accent); }
th[aria-sort="descending"] .sort-mark { transform:rotate(180deg); }
th[aria-sort="ascending"], th[aria-sort="descending"] { color:var(--ink); }
@media (pointer: coarse) { .th-sort { min-height:44px; } }
/* ── Print: weekly/monthly reports are management-shareable documents — print the CONTENT,
   not the app chrome (J17). On edit pages the hidden server render (#render-cache) becomes
   the printed body in place of the editor. */
@media print {
  header, .skip-link, .messages, .report-actions, .edit-controls, .md-toolbar,
  ul#issues, #issues-summary, .stale-banner, .cmd-warn, .approve-status,
  .draft-restore, .recent-more, .entry-grid, .filter-bar, .pager { display:none !important; }
  body { display:block; background:#fff; }
  main { max-width:none; padding:0; }
  .editor-wrap { display:none; }
  #render-cache[hidden] { display:block; }
  .preview, .preview-solo { max-width:none; max-height:none; overflow:visible;
                            border:none; padding:0; }
}
