/* =========================================================================
   Fingerprint v4 — "Instrument"
   Dark ink rail on warm paper, a retuned teal, iris for machine text,
   and the ridge-ticked gauge as the signature. CSS-first; no framework.
   ========================================================================= */

/* === DESIGN SYSTEM: tokens =========================================== */
:root{
  /* ---- paper & surfaces ---- */
  --paper:#FAF9F5;
  --paper-2:#F3F2EC;
  --surface:#FFFFFF;
  --sunken:#F5F4EF;

  /* ---- rail (dark) ---- */
  --rail:#101A18;
  --rail-2:#182422;
  --rail-3:#22302D;
  --rail-line:rgba(255,255,255,.08);
  --rail-ink:#EAEFED;
  --rail-ink-2:#8FA09B;
  --rail-ink-3:#5E6E6A;
  --edge:#26332D;   /* rail hairline — lifted rail tone (FP-UI-001) */

  /* ---- ink ---- */
  --ink:#131E1C;
  --ink-2:#485653;
  --ink-3:#788784;
  --ink-4:#A3AEAA;

  /* ---- lines ---- */
  --line:#E7E6E0;
  --line-2:#D8DAD4;
  --line-3:#C2C7C2;

  /* ---- accent (teal, retuned) ---- */
  --accent:#0E7A6B;
  --accent-deep:#0A574D;
  --accent-lit:#12A08B;
  --accent-tint:#E9F3F0;
  --accent-tint-2:#CFE6E0;

  /* ---- semantic ---- */
  --amber:#8F6A11;  --amber-bg:#FBF3E0;  --amber-line:#E7D3A3;
  --rust:#A6412A;   --rust-bg:#FBEDE8;   --rust-line:#EFCBBE;
  --iris:#4E4FA3;   --iris-bg:#EEEEFA;   --iris-line:#CFCFEE;

  /* ---- type faces ---- */
  --ui:"Instrument Sans",-apple-system,"Segoe UI",system-ui,sans-serif;
  --serif:"Newsreader","Iowan Old Style",Charter,Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,Menlo,monospace;

  /* ---- type scale ---- */
  --t-display:34px; --t-h2:22px;  --t-h3:15px;  --t-h4:13px;
  --t-body:15px;    --t-sm:13.5px;--t-label:11px;--t-micro:11.5px;
  --t-prose:18px;   --t-num:26px;

  /* ---- spacing (4px base) ---- */
  --s0:2px; --s1:4px; --s2:8px;  --s3:12px; --s4:16px; --s5:20px;
  --s6:24px;--s7:32px;--s8:40px; --s9:56px; --s10:72px;

  /* ---- radius ---- */
  --r1:6px; --r2:10px; --r3:14px; --r4:20px; --rp:999px;

  /* ---- LOCKED source-card sizing ----
     The "My writing" source cards must all be this tall and keep this
     shape/size. This is a deliberate design decision; do NOT change it
     without the owner's explicit confirmation. Define new card rules in
     the .sources-grid block below, scoped to .grid-writing. */
  --scard-h: 8rem;
  /* The "My classes" connect cards (Learning platform / Email) are locked to a
     single shared height so the two cards always render the same size and never
     grow when a school URL is revealed, a status changes, or a connection lands.
     8.75rem comfortably fits the tallest card state (connect + provider + URL). */
  --scard-class-h: 8.75rem;

  /* ---- elevation ---- */
  --sh1:0 1px 2px rgba(19,30,28,.05);
  --sh2:0 1px 2px rgba(19,30,28,.05), 0 6px 16px -6px rgba(19,30,28,.10);
  --sh3:0 2px 4px rgba(19,30,28,.05), 0 16px 32px -12px rgba(19,30,28,.16);
  --sh-ring:0 0 0 3px rgba(14,122,107,.16);
  --sh-lip:inset 0 1px 0 rgba(255,255,255,.14);

  /* ---- motion ---- */
  --t1:110ms; --t2:180ms; --t3:280ms; --t4:460ms;
  /* Routed navigation commits immediately; full loads keep one short fade. */
  /* Full-load content entrance. On a course/detail navigation the content is
     already rendered before this runs, so the fade is pure perceived latency:
     280ms (--t3) read as "still loading"; 120ms reads as "instant". */
  --t-fade:120ms;
  --e-out:cubic-bezier(.2,.8,.3,1);
  --e-spring:cubic-bezier(.34,1.32,.5,1);
  --e-io:cubic-bezier(.65,0,.35,1);

  /* ---- layout ---- */
  --rail-w:205px; --topbar-h:60px; --gutter:40px; --canvas-max:1440px;

  /* ---- the ridge motif ---- */
  --ridge:repeating-linear-gradient(90deg,currentColor 0 1px,transparent 1px 7px);
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; background:var(--paper); color:var(--ink);
  font-family:var(--ui); font-size:var(--t-body); line-height:1.6;
  font-feature-settings:"ss01" 1,"cv05" 1;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
a{color:var(--accent); text-decoration:none}
a:hover{color:var(--accent-deep)}
::selection{background:var(--accent-tint-2)}

code{font-family:var(--mono);font-size:.88em;background:var(--sunken);border:1px solid var(--line);padding:1px 6px;border-radius:5px;color:var(--ink-2)}
/* A document name in app text, not code — matches the rest of the UI. */
.fname{font-family:var(--ui);font-weight:520;letter-spacing:-.008em;color:var(--ink);word-break:break-word}

/* ---------- type roles ---------- */
.display{font-size:var(--t-display);line-height:1.08;font-weight:620;letter-spacing:-.03em;margin:0}
h2.sec{font-size:var(--t-h2);line-height:1.25;font-weight:600;letter-spacing:-.02em;margin:0}
h3.pt{font-size:var(--t-h3);line-height:1.35;font-weight:600;letter-spacing:-.008em;margin:0}
h4.sub{font-size:var(--t-h4);font-weight:600;letter-spacing:0;margin:0;color:var(--ink-2)}
.label{font-size:var(--t-label);font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-family:var(--ui)}
.micro{font-family:var(--ui);font-size:var(--t-micro);font-weight:520;letter-spacing:.02em;color:var(--ink-3);font-variant-numeric:tabular-nums}
.num{font-family:var(--ui);font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.prose,blockquote.prose{font-family:var(--serif);font-size:var(--t-prose);line-height:1.72;letter-spacing:-.004em;color:var(--ink)}
.dim{color:var(--ink-3)}
.small{font-size:var(--t-sm);line-height:1.5}

/* === SHELL ============================================================ */
.app{display:grid;grid-template-columns:var(--rail-w) minmax(0,1fr);min-height:100vh}
main{min-width:0}

/* The mobile nav scrim is an out-of-flow overlay. It must never become a grid
   item, or it steals column 1 and pushes the rail into column 2 and the page
   content into row 2 (off-screen). Always take it out of flow. */
.scrim{position:fixed;inset:0;z-index:45;background:rgba(19,30,28,.4);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);opacity:0;pointer-events:none;transition:opacity var(--t3)}
.app.nav-open .scrim{opacity:1;pointer-events:auto}

.rail{
  position:sticky;top:0;height:100vh;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none;
  background:var(--rail);color:var(--rail-ink);
  display:flex;flex-direction:column;padding:var(--s5) 0 var(--s2);
  box-shadow:8px 0 24px -18px rgba(19,30,28,.5), inset -1px 0 0 var(--edge);
}
.rail::-webkit-scrollbar{display:none}

.brand{display:flex;gap:var(--s4);align-items:center;padding:0 var(--s5) var(--s5)}
.brand a{display:flex;gap:var(--s4);align-items:center;color:inherit}
.brand a:hover{color:inherit}
.brand .mark{ /* the ridge mark itself, against the rail — no pad/container.
     The fine ridge gaps drop below device-pixel resolution below ~27px and the
     mark renders as a broken, dotted smudge, so it's displayed larger to stay
     crisp. Tuned to 85% of the original 40x55 → 34x47, then scaled ~2% smaller
     to 33x46 so it sits a touch lighter next to the larger wordmark. */
  width:33px;height:46px;flex:none;
  background:url("../static/rail-mark.svg") center/contain no-repeat;
}
.brand b{font-size:18.4px;font-weight:600;letter-spacing:-.02em;line-height:1.1;display:block}

.navgroup{padding:var(--s3) var(--s3) var(--s1)}
.navgroup>.label{padding:0 var(--s3) var(--s2);color:var(--rail-ink-3);font-size:10px;letter-spacing:.11em}
.nav a{
  position:relative;display:flex;align-items:center;gap:10px;
  padding:8px var(--s3);margin-bottom:1px;border-radius:8px;
  color:var(--rail-ink-2);font-size:14px;font-weight:450;line-height:1.2;
  transition:background var(--t2) var(--e-out),color var(--t2) var(--e-out),
             transform var(--t1) var(--e-out);
  -webkit-tap-highlight-color:transparent;touch-action:manipulation;
}
.nav a svg{width:17px;height:17px;flex:none;opacity:.72;transition:opacity var(--t2),transform var(--t2) var(--e-spring)}
.nav a:hover{background:var(--rail-2);color:var(--rail-ink)}
.nav a:hover svg{opacity:1;transform:translateX(1px)}
.nav a.on{background:var(--rail-3);color:#fff;font-weight:520}
.nav a.on svg{opacity:1;color:var(--accent-lit)}
.nav a.on::before{ /* active accent bar */
  content:"";position:absolute;left:4px;top:9px;bottom:9px;width:3px;
  border-radius:var(--rp);background:var(--accent-lit);
  animation:markIn var(--t1) var(--e-spring) both;
}
@keyframes markIn{from{transform:scaleY(.2);opacity:0}to{transform:scaleY(1);opacity:1}}
.nav a .count{margin-left:auto;font-family:var(--ui);font-size:10.5px;font-weight:600;letter-spacing:.02em;color:var(--rail-ink-3);background:var(--rail-2);padding:1px 6px;border-radius:var(--rp)}
.nav a.on .count{background:rgba(18,160,139,.18);color:#8FE3D3}

/* ---- press ----
   The only prior press vocabulary in this file is .btn:active{transform:
   translateY(1px)} at --t1. A nav item is a row in a vertical list, so vertical
   travel reads as the whole list shifting — press it inward instead. Transform
   ONLY, no colour: .nav a:active and .nav a.on are both (0,2,1), so a colour
   here would out-order .on by source position and dim the lit item's own label.
   :hover already supplies the background on pointer devices. */
.nav a:active{transform:scale(.96)}
.nav a:active svg{opacity:1;transform:translateX(1px) scale(.86)}

/* ---- pending ----
   The destination lights the moment it is pressed. Two jobs: the click is
   answered before the server replies, and the rail already shows the new module
   when the browser snapshots the OUTGOING page — so the fp-rail snapshots match
   on both sides and the rail genuinely does not move across the transition.
   Server state (.on / aria-current) is deliberately left alone: if the
   navigation is cancelled or redirects, nothing has lied about where the user
   is. The link carries aria-busy instead — account.js's idiom. */
.rail.is-navigating .nav a.on:not(.is-going){background:transparent;color:var(--rail-ink-2);font-weight:450}
.rail.is-navigating .nav a.on:not(.is-going) svg{opacity:.72;color:inherit}
.rail.is-navigating .nav a.on:not(.is-going)::before{content:none}
.nav a.is-going{background:var(--rail-3);color:#fff;font-weight:520}
.nav a.is-going svg{opacity:1;color:var(--accent-lit)}
.nav a.is-going::before{ /* same geometry as .nav a.on::before above */
  content:"";position:absolute;left:4px;top:9px;bottom:9px;width:3px;
  border-radius:var(--rp);background:var(--accent-lit);
  animation:markIn var(--t1) var(--e-spring) both;
}
/* The :not(.is-going) above is load-bearing: without it
   .rail.is-navigating .nav a.on (0,4,1) beats .nav a.is-going (0,2,1), and
   clicking the module you are already on would un-light it for the round trip.

   markIn fires once, under the cursor, on the outgoing page. The head script in
   base.html stamps .nav-settled on arrival so it does not fire a second time —
   that second play is the rail visibly "re-popping", and because
   ::view-transition-new is a LIVE representation it would run INSIDE the
   incoming snapshot, which is the one thing that would break the static rail. */
.nav-settled .nav a.on::before{animation:none}

.railfoot{margin-top:auto;padding:var(--s4) var(--s5) var(--s2);border-top:1px solid var(--rail-line)}

/* Identity chip (bottom-left) — shows the signed-in person's name and opens a
   small Account menu on click. The popup is position:fixed so the rail's
   overflow-y:auto never clips it; it's anchored to the button in JS. */
.profilemenu{position:relative}
.profilemenu .who{
  background:none;border:0;padding:0;margin:0;width:100%;cursor:pointer;font:inherit;line-height:1;
  display:flex;align-items:center;gap:var(--s3);text-align:left;color:var(--rail-ink-2);
}
.profilemenu .av{width:26px;height:26px;border-radius:50%;background:var(--rail-3);display:grid;place-items:center;font-size:11px;font-weight:600;color:var(--rail-ink);flex:none}
.profilemenu .who-meta{display:flex;flex-direction:column;justify-content:center;min-width:0;flex:1;line-height:1.2}
.profilemenu .who-name{color:var(--rail-ink);font-size:13px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.profilemenu .who .who-caret{color:var(--rail-ink-3);flex:none;transition:transform var(--t2) var(--e-out)}
.profilemenu.open .who .who-caret{transform:rotate(180deg)}
.profilemenu-pop{
  position:fixed;left:0;bottom:0;width:165px;z-index:55;
  background:var(--rail-2);border:1px solid var(--rail-line);border-radius:var(--r2);
  box-shadow:var(--sh2);padding:5px;display:none;
}
.profilemenu.open .profilemenu-pop{display:block}
.profilemenu-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:var(--rp);color:var(--rail-ink-2);font-size:13px;font-weight:450;text-decoration:none;white-space:nowrap}
.profilemenu-item svg{width:15px;height:15px;flex:none;opacity:.75}
.profilemenu-item:hover{background:var(--rail-3);color:var(--rail-ink)}
.profilemenu-item:hover svg{opacity:1}

/* ---------- account / profile page (working draft) ---------- */
.profileme{display:flex;align-items:center;gap:var(--s4)}
.profile-av{
  width:56px;height:56px;border-radius:14px;flex:none;
  background:var(--rail);color:var(--rail-ink);
  display:grid;place-items:center;
  font-family:var(--ui);font-size:18px;font-weight:600;letter-spacing:.02em;
  box-shadow:var(--sh-lip);
}
.profileme-meta{min-width:0}
.profileme-name{font-size:17px;font-weight:600;letter-spacing:-.01em;line-height:1.2}
.profileme-email{font-family:var(--ui);font-size:13px;color:var(--ink-3);margin-top:1px}
.profile-badge{flex:none}
.paycard{
  background:var(--rail);border-radius:var(--r3);padding:var(--s5);
  color:var(--rail-ink);box-shadow:var(--sh-lip);
}
.paycard-top{display:flex;align-items:center;justify-content:space-between;gap:var(--s4)}
.paycard-brand{font-size:15px;font-weight:620;letter-spacing:-.01em;color:#fff}
.paycard-number{
  font-family:var(--ui);font-size:20px;font-weight:600;letter-spacing:.14em;
  font-variant-numeric:tabular-nums;color:#fff;margin:var(--s5) 0 var(--s2);
}
.paycard-meta{font-family:var(--ui);font-size:12px;color:var(--rail-ink-3)}
.paycard-actions{margin-top:var(--s5);display:flex;gap:var(--s2)}

/* ---------- topbar ---------- */
.topbar{
  position:sticky;top:0;z-index:30;height:var(--topbar-h);
  display:flex;align-items:center;gap:var(--s4);
  padding:0 var(--gutter);
  background:rgba(250,249,245,.82);
  background:color-mix(in srgb,var(--paper) 82%,transparent);
  backdrop-filter:saturate(180%) blur(10px);
  -webkit-backdrop-filter:saturate(180%) blur(10px);
  border-bottom:1px solid var(--line);
}
.crumb{display:flex;align-items:center;gap:8px;font-size:var(--t-sm);color:var(--ink-3);min-width:0}
.crumb .grp{font-weight:600;font-size:var(--t-label);letter-spacing:.08em;text-transform:uppercase}
.crumb .now{color:var(--ink);font-weight:560;font-size:14px;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.iconbtn{
  width:32px;height:32px;display:none;place-items:center;border-radius:9px;
  border:1px solid var(--line-2);background:var(--surface);color:var(--ink-2);cursor:pointer;
}

/* ---------- canvas + page-enter choreography ---------- */
.canvas{
  padding:var(--s7) var(--gutter) var(--s10);max-width:var(--canvas-max);
  box-shadow:inset 16px 0 20px -16px rgba(0,0,0,.40);
}
/* The content area enters as ONE surface, not a cascade of blocks. Every direct
   child fades on the same clock with no per-child delay, so a module switch
   reads as the paper changing rather than the page reassembling — the stagger
   (0/45/90/130/165ms over --t4) was a 625ms rebuild on every single switch.
   Opacity only, deliberately: riseIn's translateY made every .canvas child a
   containing block for its position:fixed descendants, which is why the
   .modal-overlay opt-out below and account.html's inline animation:none had to
   exist. Opacity makes a stacking context but NOT a fixed-position containing
   block, so both are now belt-and-braces rather than load-bearing. */
.canvas>*{animation:canvasIn var(--t-fade) var(--e-out) both}
/* Routed canvases are static on insertion. */
html.fp-routed .canvas>*{animation:none}
.canvas:focus-visible,.canvas [tabindex="-1"]:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* Modal overlays inside the canvas (the first-run welcome prompt) are
   position:fixed and must not join the content entrance — they appear crisply
   via their own .open transition instead. */
.canvas>.modal-overlay{animation:none}
@keyframes canvasIn{from{opacity:0}to{opacity:1}}
/* riseIn is still used by .msg, where a per-message rise is correct. */
@keyframes riseIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}

/* Router shows this cue after 250ms and removes it at DOM commit. */
.nav-progress{
  position:fixed;top:var(--topbar-h);left:var(--rail-w);right:0;height:2px;z-index:46;
  overflow:hidden;opacity:0;pointer-events:none;
}
.nav-progress.on{opacity:1}
.nav-progress::before{
  content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--accent),transparent);
}
.nav-progress.on::before{animation:navSweep 1.1s var(--e-io) infinite}
@keyframes navSweep{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
@media (max-width:960px){.nav-progress{left:0}}
@media (prefers-reduced-motion:reduce){
  .nav-progress.on::before{animation:none;transform:none;opacity:.6}
}

/* The Cynthia (assistant) page keeps one flat paper surface. Its short, wide
   thread panel leaves empty space below, where the content-area inset shadow
   reads as a gradient seam between the top and bottom halves. Drop it on this
   page only — other pages and the rail keep their depth cues. */
.page-assistant .canvas{box-shadow:none}

/* Today is short and wide like the assistant page: the content-area inset
   shadow reads as a gradient seam across the lower empty space (the split
   "white card" the Cynthia fix already removed there). Drop it here too.
   The task/action detail page (action.html) is the same short, wide shape —
   its "Get this done" card leaves empty space below the fold, so the seam
   shows there too as a split white. Match the flat paper surface. */
.page-today .canvas,
.page-action .canvas{box-shadow:none}

/* Sources can end above the viewport bottom; keep the empty area on the same
   flat paper surface as the space around its cards.
   Generate is the same shape again: one short panel, and the brief box is only
   ever as tall as its own contents, so the seam below it showed as a line under
   the card. Flat paper here too. */
.page-sources .canvas,
.page-generate .canvas{box-shadow:none}

.pagehead{display:flex;align-items:flex-start;gap:var(--s5);margin-bottom:var(--s7)}
.pagehead .lede{max-width:62ch;color:var(--ink-2);font-size:var(--t-sm);margin:10px 0 0}
.pagehead .eyebrow{display:inline-flex;align-items:center;gap:7px;margin-bottom:10px}
.pagehead .eyebrow .label{color:var(--accent)}
.pagehead .eyebrow::before{content:"";width:14px;height:2px;border-radius:2px;background:var(--accent);opacity:.55}
.pagehead .actions{margin-left:auto;display:flex;gap:var(--s2);flex:none;padding-top:6px}

/* ---------- grids ---------- */
.grid{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:var(--s6);align-items:start}
.grid.wide{grid-template-columns:minmax(0,1fr)}
.cols2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s6)}
.cols3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s4)}
.stack{display:flex;flex-direction:column;gap:var(--s6)}
.stack.tight{gap:var(--s4)}

/* ---------- utilities ---------- */
.u-row{display:flex;align-items:center;gap:var(--s3)}
.u-row.top{align-items:flex-start}
.u-col{display:flex;flex-direction:column;gap:var(--s2)}
.u-between{display:flex;align-items:center;justify-content:space-between;gap:var(--s3)}
.u-wrap{flex-wrap:wrap}
.u-grow{flex:1;min-width:0}
.u-right{margin-left:auto}
.u-mt2{margin-top:var(--s2)}.u-mt4{margin-top:var(--s4)}.u-mt6{margin-top:var(--s6)}
.u-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.toolbar{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap}
.actions{display:flex;align-items:center;gap:var(--s2)}

/* === COMPONENTS ======================================================== */

/* ---------- panel ---------- */
.panel{
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r3);
  box-shadow:var(--sh1);overflow:hidden;
  transition:box-shadow var(--t3) var(--e-out),border-color var(--t3) var(--e-out);
}
.panel:hover{box-shadow:var(--sh2)}
.panelhead{
  display:flex;align-items:center;gap:var(--s3);
  padding:var(--s4) var(--s5);border-bottom:1px solid var(--line);
  background:linear-gradient(var(--surface),var(--surface)) padding-box;
}
.panelhead h3{font-size:var(--t-h3);font-weight:600;letter-spacing:-.008em;margin:0}
.panelhead .note{margin-left:auto;font-family:var(--ui);font-size:12.5px;font-weight:520;letter-spacing:-.006em;color:var(--ink-3);font-variant-numeric:tabular-nums}
.panelbody{padding:var(--s5)}
.panelbody.flush{padding:0}
.panelfoot{padding:var(--s3) var(--s5);border-top:1px solid var(--line);background:var(--sunken)}
/* accent variants */
.panel.is-danger{border-color:var(--rust-line)}
.panel.is-danger .panelhead{background:var(--rust-bg);border-bottom-color:var(--rust-line)}
.panel.is-danger .panelhead h3{color:var(--rust)}
.panel.is-warn{border-color:var(--amber-line)}
.panel.is-warn .panelhead{background:var(--amber-bg);border-bottom-color:var(--amber-line)}
.panel.is-warn .panelhead h3{color:var(--amber)}
.panel.is-ai{border-color:var(--iris-line)}
.panel.is-ai .panelhead{background:var(--iris-bg);border-bottom-color:var(--iris-line)}
.panel.is-ai .panelhead h3{color:var(--iris)}
.pill-count{font-family:var(--ui);font-size:11px;font-weight:600;letter-spacing:.04em;padding:1px 8px;border-radius:var(--rp);background:var(--sunken);color:var(--ink-3);border:1px solid var(--line)}
.panel.is-danger .pill-count{background:#fff;border-color:var(--rust-line);color:var(--rust)}
.panel.is-warn .pill-count{background:#fff;border-color:var(--amber-line);color:var(--amber)}

/* ---------- buttons ---------- */
.btn{
  --bh:38px;
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  height:var(--bh);padding:0 16px;border-radius:var(--r2);border:1px solid transparent;
  font-family:var(--ui);font-size:14px;font-weight:540;letter-spacing:-.006em;
  cursor:pointer;white-space:nowrap;background:none;color:var(--ink);
  transition:background var(--t2) var(--e-out),border-color var(--t2),color var(--t2),
             box-shadow var(--t2),transform var(--t1) var(--e-out);
}
.btn svg{width:16px;height:16px;flex:none}
.btn:active{transform:translateY(1px)}
.btn:focus-visible{outline:none;box-shadow:var(--sh-ring)}
.btn-primary{background:var(--accent);color:#fff}
.btn-primary:hover{background:var(--accent-deep);color:#fff;box-shadow:0 3px 10px -3px rgba(10,87,77,.45)}
.btn-ghost{background:var(--surface);border-color:var(--line-2);color:var(--ink-2)}
.btn-ghost:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-tint)}
.btn-quiet{color:var(--ink-3);padding:0 10px}
.btn-quiet:hover{color:var(--ink);background:var(--paper-2)}
.btn.danger{border-color:var(--rust-line);color:var(--rust);background:var(--surface)}
.btn.danger:hover{background:var(--rust-bg);border-color:var(--rust)}
.btn-sm{--bh:30px;padding:0 11px;font-size:13px;border-radius:8px}
.btn-sm svg{width:14px;height:14px}
.btn-icon{--bh:32px;width:32px;padding:0;border-radius:8px;color:var(--ink-3)}
.btn-icon:hover{background:var(--paper-2);color:var(--ink)}
.btn[disabled],.btn[aria-disabled=true]{opacity:.45;pointer-events:none}
.btn.is-busy{pointer-events:none;position:relative;color:transparent}
.btn.is-busy::after{
  content:"";position:absolute;width:15px;height:15px;border-radius:50%;
  border:2px solid currentColor;border-top-color:transparent;color:#fff;
  animation:spin .7s linear infinite;
}
.btn-ghost.is-busy::after{color:var(--accent)}
.regenerate-button{position:relative}
.regenerate-label{display:inline-flex;align-items:center;gap:7px}
.regenerate-button>.fp-load{display:none;position:absolute;width:18px;height:25px}
.regenerate-button[aria-busy=true]{opacity:1}
.regenerate-button[aria-busy=true]>.regenerate-label{visibility:hidden}
.regenerate-button[aria-busy=true]>.fp-load{display:block}
.regenerate-button>.fp-load .fp-live{stroke:currentColor}
.regenerate-button>.fp-load .fp-ghost{stroke:rgba(255,255,255,.25)}
.regenerate-error[hidden],.regenerate-retry[hidden]{display:none}
.pagehead .actions:has(form[data-regenerate]){flex-wrap:wrap;max-width:100%}
form[data-regenerate]{display:flex;flex-wrap:wrap;gap:var(--s2);min-width:0;max-width:100%}
@keyframes spin{to{transform:rotate(360deg)}}

/* segmented switch */
.seg{
  display:inline-flex;padding:3px;gap:2px;
  background:rgba(19,30,28,.05);border:1px solid var(--line-2);border-radius:var(--r2);
  box-shadow:inset 0 1px 2px rgba(19,30,28,.05);
}
.seg-btn{
  display:inline-flex;align-items:center;justify-content:center;
  height:28px;padding:0 13px;border:0;border-radius:7px;background:none;cursor:pointer;
  font-family:var(--ui);font-size:13px;font-weight:520;color:var(--ink-3);
  transition:color var(--t2),background var(--t2) var(--e-out),box-shadow var(--t2);
}
.seg-btn:hover{color:var(--ink)}
.seg-btn.on{background:var(--surface);color:var(--ink);box-shadow:var(--sh1)}
/* A segmented switch that slides: one white pill moves between the halves, so a
   change reads as a single control changing state rather than two buttons
   lighting up independently. Classes' Active/Archived switch and Generate's
   Upload/Paste switch are the same control, and each page's module measures the
   pill from the live half it sits under — never a fixed width — so it stays
   right whatever the two words happen to measure. */
.seg--slide{position:relative;isolation:isolate}
.seg--slide .seg-btn{position:relative;z-index:1;background:none;box-shadow:none}
.seg--slide .seg-btn.on{background:none;box-shadow:none}
.seg-pill{
  position:absolute;z-index:0;top:3px;left:3px;height:28px;width:0;
  background:var(--surface);border-radius:7px;box-shadow:var(--sh1);
  transition:transform var(--t3) var(--e-out),width var(--t3) var(--e-out);
}
.seg-pill.is-ready{width:var(--pill-w,0);transform:translateX(var(--pill-x,0))}
/* The switch is a state change; the white pill carries the only motion. */
@media (prefers-reduced-motion:reduce){.seg-pill{transition:none}}

/* ---------- inputs ---------- */
.field{display:flex;flex-direction:column;gap:6px}
.field>.label{color:var(--ink-2)}
.field .hint{font-size:12.5px;color:var(--ink-3)}
.fieldrow{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--s4)}
/* The reviewed answer: a read-only block that shows exactly what will be sent.
   Deliberately not an input — a control the student can type into is a control
   that can differ from what they reviewed. It wraps, and it scrolls past 18rem
   so a long answer cannot push the actions off the screen (SUBMIT-REVIEW.md
   §3.3, §6.1). No new colour, radius or spacing value: the tokens below are the
   whole vocabulary, and a new hue in a review panel would read as another app. */
.review-answer{
  background:var(--sunken);
  border:1px solid var(--line);
  border-radius:var(--r2);
  padding:var(--s4);
  max-height:18rem;
  overflow:auto;
  white-space:pre-wrap;
  overflow-wrap:anywhere;
  font-family:var(--ui);
  font-size:var(--t-body);
  line-height:1.6;
  color:var(--ink-2);
}
input[type=text],input[type=email],input[type=password],input[type=number],input[type=date],input[type=search],select,textarea{
  width:100%;font-family:var(--ui);font-size:14.5px;line-height:1.5;color:var(--ink);
  background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r2);
  padding:10px 12px;box-shadow:inset 0 1px 2px rgba(19,30,28,.03);
  transition:border-color var(--t2),box-shadow var(--t2),background var(--t2);
}
input::placeholder,textarea::placeholder{color:var(--ink-4)}
input:hover,select:hover,textarea:hover{border-color:var(--line-3)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:var(--sh-ring)}
.search-field{position:relative}
.search-field input[type=search]{padding-right:40px}
/* The custom icon button sits where Chrome/Safari/Edge draw the native
   clear-search "x" once text is typed — without this they'd overlap. */
.search-field input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
.search-icon-btn{
  position:absolute;right:4px;top:50%;transform:translateY(-50%);width:30px;height:30px;
  display:grid;place-items:center;border:0;border-radius:8px;background:none;color:var(--ink-3);
  cursor:pointer;transition:color var(--t2),background var(--t2);
}
.search-icon-btn svg{width:16px;height:16px}
.search-icon-btn:hover{color:var(--accent);background:var(--accent-tint)}
.search-icon-btn:focus-visible{outline:none;box-shadow:var(--sh-ring)}
textarea{min-height:120px;resize:vertical}
textarea.writing{font-family:var(--serif);font-size:16.5px;line-height:1.65}
select{
  appearance:none;padding-right:34px;cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--ink-3) 50%),linear-gradient(135deg,var(--ink-3) 50%,transparent 50%);
  background-position:calc(100% - 17px) calc(50% + 1px),calc(100% - 12px) calc(50% + 1px);
  background-size:5px 5px,5px 5px;background-repeat:no-repeat;
}
/* A compact select for in-card control rows (e.g. the Email source card's
   provider picker). The default select is ~42px tall, which is taller than the
   30px buttons beside it and stretches the card past the rest of the row. */
select.sel-sm{height:30px;padding:4px 30px 4px 12px;font-size:13px;line-height:1.5}
.drop{
  position:relative;display:grid;place-items:center;gap:6px;text-align:center;
  padding:var(--s7) var(--s5);border:1.5px dashed var(--line-3);border-radius:var(--r3);
  background:var(--sunken);cursor:pointer;
  transition:border-color var(--t2),background var(--t2),transform var(--t2) var(--e-out);
}
.drop:hover,.drop.over{border-color:var(--accent);background:var(--accent-tint);transform:translateY(-1px)}
.drop svg{width:22px;height:22px;color:var(--ink-3)}
.drop:hover svg{color:var(--accent)}
.drop input[type=file]{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden}
/* What the box says while it is holding nothing: the icon, the invitation, and
   the line a refusal lands in. It is one block so the received state can remove
   all three at once. */
.drop-idle{display:grid;justify-items:center;gap:6px}
/* A brief in the box changes what the box is: the dashed edge goes solid and the
   box lists the documents it is holding. module-generate.js writes the rows for
   files it takes itself, and a refusal goes into the hint line in rust. */
.drop.has-file{border-style:solid;border-color:var(--accent);background:var(--accent-tint)}
.drop.has-file svg{color:var(--accent)}
.drop [data-drop-hint].is-file{color:var(--accent-deep);font-weight:520}
.drop [data-drop-hint].is-bad{color:var(--rust)}
/* A brief is several documents, and the box has to say which ones it is holding
   — a filename is not a hint line. Once it holds something the zone stacks from
   the top and the idle line is gone; each document is a row, the name is the
   loudest thing in the box, and the row can be taken back out. */
.drop:has(.drop-list:not([hidden])){place-items:stretch;text-align:left;cursor:default}
.drop:has(.drop-list:not([hidden])) .drop-idle{display:none}
.drop-list{display:grid;gap:8px;width:100%}
.drop-list[hidden]{display:none}
.drop-list .doc{
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;
  padding:9px 10px;background:var(--paper);border:1px solid var(--line-2);border-radius:var(--r2);
}
.drop-list .doc-mark svg{width:16px;height:16px;color:var(--accent-deep)}
.drop-list .doc-text{min-width:0;display:grid;gap:2px}
/* The line that says the box received it: body size and real ink, not a caption.
   A long filename is truncated inside its row rather than stacking the row. */
.drop-list .doc-name{
  font-size:14px;font-weight:560;letter-spacing:-.01em;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.drop-list .doc-meta{font-size:12px;color:var(--ink-3);letter-spacing:-.004em}
.drop-list .doc-x{
  display:grid;place-items:center;width:26px;height:26px;padding:0;cursor:pointer;
  border:1px solid transparent;border-radius:var(--r2);background:transparent;color:var(--ink-3);
}
.drop-list .doc-x:hover{border-color:var(--line-2);color:var(--ink);background:var(--sunken)}
.drop-list .doc-x[hidden]{display:none}
.drop-list .doc-x svg{width:14px;height:14px}
/* The way to add a second one is part of the same box, under the documents that
   are in it, so the zone reads as a list rather than a control with a stack. */
.drop-list .doc-add{
  display:flex;align-items:center;justify-content:center;gap:7px;padding:8px 10px;
  font-size:13px;font-weight:520;color:var(--accent-deep);
  border:1.5px dashed var(--line-3);border-radius:var(--r2);
}
.drop-list .doc-add[hidden]{display:none}
.drop-list .doc-add svg{width:14px;height:14px}
/* What the run could not take, said inside the box in the rust the app uses for
   everything the student has to act on. */
.drop-note{margin:8px 0 0;font-size:12.5px;line-height:1.45;color:var(--rust);letter-spacing:-.004em}
.drop-note[hidden]{display:none}
.divider{display:flex;align-items:center;gap:var(--s3);margin:var(--s5) 0;color:var(--ink-4)}
.divider::before,.divider::after{content:"";height:1px;flex:1;background:var(--line)}
.divider span{font-size:var(--t-label);letter-spacing:.08em;text-transform:uppercase;font-weight:600}

/* ---------- the unified list ---------- */
.list{display:flex;flex-direction:column}
.row{
  position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;
  align-items:center;gap:var(--s4);padding:13px var(--s5);
  border-bottom:1px solid var(--line);
  transition:background var(--t2) var(--e-out);
}
.list .row:last-child{border-bottom:0}
.row::before{ /* hover bar slides in */
  content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--ccolor,var(--accent));
  transform:scaleX(0);transform-origin:left;transition:transform var(--t2) var(--e-out);
}
.row:hover{background:var(--sunken)}
.row:hover::before{transform:scaleX(1)}
.row.c1:hover .lead,.row.c2:hover .lead,.row.c3:hover .lead,.row.c4:hover .lead,.row.c5:hover .lead,
.row.c6:hover .lead,.row.c7:hover .lead,.row.c8:hover .lead,.row.c9:hover .lead,.row.c10:hover .lead{background:var(--ccolor,var(--line-2))}
.row .lead{width:3px;height:26px;border-radius:var(--rp);background:var(--line-2);flex:none}
.row.urgent .lead{background:var(--rust)}
.row.soon .lead{background:var(--amber)}
.row.ok .lead{background:var(--accent)}
.row .lead.c1{background:#05743C}.row .lead.c2{background:#1463C2}.row .lead.c3{background:#C05A0C}
.row .lead.c4{background:#CB2D7A}.row .lead.c5{background:#6C35C9}.row .lead.c6{background:#9A780B}
.row .lead.c7{background:#037B70}.row .lead.c8{background:#1B6B98}.row .lead.c9{background:#5A42B5}
.row .lead.c10{background:#A2660F}
.row.c1{--ccolor:#05743C}.row.c2{--ccolor:#1463C2}.row.c3{--ccolor:#C05A0C}
.row.c4{--ccolor:#CB2D7A}.row.c5{--ccolor:#6C35C9}.row.c6{--ccolor:#9A780B}
.row.c7{--ccolor:#037B70}.row.c8{--ccolor:#1B6B98}.row.c9{--ccolor:#5A42B5}
.row.c10{--ccolor:#A2660F}
.row.urgent::before{background:var(--rust)} .row.soon::before{background:var(--amber)}
.row .title{font-size:14.5px;font-weight:520;letter-spacing:-.008em;line-height:1.35}
.row .meta{font-size:12.5px;color:var(--ink-3);margin-top:3px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.row .meta .sep{color:var(--ink-4)}
.reveal-desc{margin-top:3px}
.reveal-desc summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:540;color:var(--ink-3)}
.reveal-desc summary::-webkit-details-marker{display:none}
.reveal-desc summary::after{content:"▸";font-size:11px;color:var(--ink-4);transition:transform var(--t2) var(--e-out)}
.reveal-desc[open] summary::after{transform:rotate(90deg)}
.reveal-desc .meta{margin-top:4px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.row .end{display:flex;align-items:center;gap:var(--s3);flex:none}
.row .add-details{grid-column:2 / -1;display:flex;gap:var(--s3);align-items:center;margin-top:2px}
.row .add-details input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--line);border-radius:var(--rp);background:var(--sunken);color:var(--ink-2);font-family:var(--ui);font-size:13px}
.row .add-details input::placeholder{color:var(--ink-4)}
.row .add-details button{flex:none}
.row .when{font-family:var(--ui);font-size:12px;font-weight:560;letter-spacing:-.01em;color:var(--ink-2);font-variant-numeric:tabular-nums;text-align:right}
.row .when b{display:block;font-weight:600;font-size:12.5px;letter-spacing:-.01em}
.row .when.past{color:var(--rust)} .row .when.today{color:var(--amber)}
.row.done .title{text-decoration:line-through;color:var(--ink-4)}
.row .rowtools{display:flex;gap:4px;opacity:0;transform:translateX(4px);transition:opacity var(--t2),transform var(--t2) var(--e-out)}
.row:hover .rowtools,.row:focus-within .rowtools{opacity:1;transform:none}

/* course token */
.course{
  display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:6px;
  font-family:var(--ui);font-size:10.5px;font-weight:600;letter-spacing:.05em;
  background:var(--sunken);color:var(--ink-2);border:1px solid var(--line);
}
/* A 10-hue, red-free palette (green/blue/orange/pink/purple/yellow plus
   shades) so many classes each get a distinct yet cohesive token. */
.course.c1{background:#D9F2E4;color:#05743C;border-color:#AEE2C6}
.course.c2{background:#D6E8FE;color:#1463C2;border-color:#AED0F5}
.course.c3{background:#FBE5CC;color:#C05A0C;border-color:#F0C79E}
.course.c4{background:#FBD6E6;color:#CB2D7A;border-color:#F0B0CF}
.course.c5{background:#E8DBFC;color:#6C35C9;border-color:#CFB9F1}
.course.c6{background:#FBF1C0;color:#9A780B;border-color:#EDD683}
.course.c7{background:#CCEFED;color:#037B70;border-color:#A1DDD6}
.course.c8{background:#D9ECF7;color:#1B6B98;border-color:#AFD2E6}
.course.c9{background:#E1DBF8;color:#5A42B5;border-color:#C7BDF0}
.course.c10{background:#F9EDCB;color:#A2660F;border-color:#EDD394}

/* ---------- tables ---------- */
table.data,table.inner{width:100%;border-collapse:separate;border-spacing:0;font-size:13.5px}
table.data th{
  position:sticky;top:var(--topbar-h);z-index:2;text-align:left;
  padding:9px var(--s4);background:var(--sunken);color:var(--ink-3);
  font-size:var(--t-label);font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  border-bottom:1px solid var(--line);border-top:1px solid var(--line);
}
table.data th:first-child,table.data td:first-child{padding-left:var(--s5)}
table.data th:last-child,table.data td:last-child{padding-right:var(--s5)}
table.data td{padding:11px var(--s4);border-bottom:1px solid var(--line);vertical-align:middle}
table.data tbody tr{transition:background var(--t2) var(--e-out)}
table.data tbody tr:hover{background:var(--sunken)}
table.data tbody tr:last-child td{border-bottom:0}
table.data .n,table.data th.n{text-align:right;font-family:var(--ui);font-variant-numeric:tabular-nums}
table.data th.n{font-family:var(--ui)}
table.data tr.muted td{color:var(--ink-4)}
/* Short in-flow tables (e.g. billing history) opt out of the sticky header —
   a sticky th on a short, page-scrolled table overlaps its own first row. */
table.data.plain th{position:static}
table.data th a{color:inherit;display:inline-flex;align-items:center;gap:4px}
table.data th a:hover{color:var(--ink)}
table.data th .sorted{color:var(--accent)}
.tablewrap{overflow-x:auto}
table.inner th{
  text-align:left;padding:8px var(--s4);color:var(--ink-3);
  font-size:var(--t-label);font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  border-bottom:1px solid var(--line);
}
table.inner td{padding:9px var(--s4);border-bottom:1px solid var(--line);vertical-align:middle}
table.inner tr:last-child td{border-bottom:0}
table.inner th.num,table.inner td.num{text-align:right;font-variant-numeric:tabular-nums}

/* ---------- stat cards ---------- */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--s3)}
.card{
  padding:var(--s4);border:1px solid var(--line);border-radius:var(--r2);background:var(--surface);
  transition:transform var(--t2) var(--e-out),box-shadow var(--t2),border-color var(--t2);
}
.card:hover{transform:translateY(-2px);box-shadow:var(--sh2);border-color:var(--line-2)}
.card .k{font-size:var(--t-label);font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3)}
.card .v{font-family:var(--ui);font-size:var(--t-num);font-weight:600;letter-spacing:-.02em;line-height:1.15;margin-top:8px;font-variant-numeric:tabular-nums}
.card .u{font-size:12px;color:var(--ink-3);margin-left:4px;font-family:var(--ui)}
.card .trend{font-size:12px;color:var(--ink-3);margin-top:4px}

/* ---------- stat row ---------- */
.stat{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s4);padding:9px 0;border-bottom:1px solid var(--line)}
.stat:last-child{border-bottom:0}
.stat .k{font-size:13px;color:var(--ink-3)}
.stat .v{font-family:var(--ui);font-size:14.5px;font-weight:560;letter-spacing:-.015em;line-height:1.3;color:var(--ink);font-variant-numeric:tabular-nums}

/* ---------- clickable "at a glance" summary (Today) ---------- */
a.card{display:block;color:inherit;text-decoration:none;cursor:pointer}
a.stat{color:inherit;text-decoration:none;cursor:pointer}
/* Group panels targeted by a summary link are revealed below the sticky topbar
   and briefly flashed so the jump is obvious. */
.panel[id^="bucket-"]{scroll-margin-top:calc(var(--topbar-h) + var(--s3))}
.panel:target{outline:4px solid rgba(166,65,42,0);outline-offset:2px;animation:target-flash 1.4s ease}
@keyframes target-flash{0%,100%{outline-color:rgba(166,65,42,0)}25%,70%{outline-color:rgba(166,65,42,.38)}}

/* ---------- data chart picker ---------- */
.chart-picker{display:flex;flex-wrap:wrap;gap:var(--s2)}
.chart-chip{
  appearance:none;border:1px solid var(--line-2);background:var(--surface);color:var(--ink-2);
  font-family:var(--ui);font-size:12px;font-weight:560;padding:6px 12px;border-radius:var(--rp);
  cursor:pointer;transition:border-color var(--t2),color var(--t2),background var(--t2);
}
.chart-chip:hover{border-color:var(--accent-tint-2);color:var(--ink)}
.chart-chip.selected{background:var(--accent-tint);color:var(--accent-deep);border-color:var(--accent-tint-2)}

/* The Data page's four "How you write" cards are the chart selector — clicking
   one shows its visual. ``--sel`` adds the button resets the shared ``.card``
   needs plus a clear selected state. */
.card--sel{cursor:pointer;text-align:left;appearance:none;font-family:var(--ui);color:inherit}
.card--sel.selected{border-color:var(--accent-tint-2);box-shadow:var(--sh2)}
.card--sel.selected .k{color:var(--accent-deep)}
.chart-note{color:var(--ink-2);font-size:13px;font-weight:560;letter-spacing:-.01em}

/* ---------- tags & flags ---------- */
.tag{
  display:inline-flex;align-items:center;gap:5px;height:21px;padding:0 8px;border-radius:6px;
  font-family:var(--ui);font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  background:var(--sunken);color:var(--ink-2);border:1px solid var(--line);
}
.tag.teal{background:var(--accent-tint);color:var(--accent-deep);border-color:var(--accent-tint-2)}
.tag.ochre{background:var(--amber-bg);color:var(--amber);border-color:var(--amber-line)}
.tag.rust{background:var(--rust-bg);color:var(--rust);border-color:var(--rust-line)}
.tag.iris{background:var(--iris-bg);color:var(--iris);border-color:var(--iris-line)}
.flag{display:inline-flex;align-items:center;gap:4px;font-family:var(--ui);font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3)}
.flag::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor}
.flag.ok{color:var(--accent)} .flag.warn{color:var(--amber)} .flag.iris{color:var(--iris)}

/* ---------- notices ---------- */
/* The row is centred on one line, which is what the stretch default got wrong in
   both directions. A notice is taller than its type whenever a taller child sits
   beside the message — a 30px action button against a 21.6px line of 13.5px type
   is the common case — and the default left the message at the top of that box
   while the button beside it stayed in the middle, so an amber banner read as if
   its words had slipped upwards. Where the message is the tallest child instead,
   the stretch pins a child that has a height of its own — the 17px icon — to the
   start of the row: on the Sources banner, whose sentence wraps to two lines at
   1280, the icon sat 12.09px above the centre line while the copy ran past the
   middle. The icon, the message and every action now share the banner's own
   centre line, and a message long enough to wrap centres as a block. */
.notice{
  display:flex;align-items:center;gap:var(--s3);padding:13px var(--s4);border-radius:var(--r2);
  border:1px solid var(--line-2);background:var(--surface);font-size:13.5px;color:var(--ink-2);
  border-left-width:3px;border-left-color:var(--ink-4);
  animation:noticeIn var(--t3) var(--e-out) both;
  margin-bottom:var(--s4);
}
@keyframes noticeIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.notice>svg{width:17px;height:17px;flex:none;color:var(--ink-3)}
/* The leading icon carried a 1px top margin that lined it up with the first
   line of a top-aligned notice. With the row centred that nudge would instead
   drop the icon below the centre line, so it is gone. The selector stays
   scoped to the direct child, which is what keeps it off icons in nested
   centring boxes (.notice-x, .notice-badge, a .btn inside a notice). */
.notice b{color:var(--ink);font-weight:600}
.notice.warn{background:var(--amber-bg);border-color:var(--amber-line);border-left-color:var(--amber);color:#6E5210}
.notice.warn>svg{color:var(--amber)}
.notice.dupe{background:var(--rust-bg);border-color:var(--rust-line);border-left-color:var(--rust);color:#83341F}
.notice.dupe>svg{color:var(--rust)}
.notice.info{background:var(--accent-tint);border-color:var(--accent-tint-2);border-left-color:var(--accent);color:var(--accent-deep)}
.notice.info>svg{color:var(--accent)}
/* The connection banner wraps to two lines on a narrow sidebar; its centring is
   the base notice's now. */
#source-connection-status{flex-wrap:wrap}
#source-connection-status[hidden],#source-connection-status [hidden]{display:none}
/* The action is pushed to the far end of the row; its centring is the base
   notice's. The dismiss control is the same, one size down. */
.notice .act{margin-left:auto}
.notice-x{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;flex:none;margin-left:auto;border:0;border-radius:6px;background:transparent;color:inherit;opacity:.55;cursor:pointer;padding:0}
.notice-x:hover{opacity:1;background:rgba(0,0,0,.06)}
.notice-x svg{width:15px;height:15px;flex:none}
.notice ul{margin:var(--s2) 0 0;padding-left:1.2rem}
/* A self-contained banner card (e.g. the Canvas session-expired reminder):
   a rounded, bordered card with a tinted icon badge and an action button,
   with no left accent bar — the "contained" treatment. Both of these used to
   ask to be centred because the base notice did not; that is the base now, so
   what is left is the tighter gap they share. */
.notice--row,.notice--contained{gap:var(--s2)}
/* A notice whose message and action belong together: the action follows the
   text at --s3 instead of being pushed to the far right by .act's auto margin,
   so "Canvas didn't finish syncing." keeps its Resync button in reach. The
   dismiss button remains the only thing pinned to the right edge. */
.notice-line{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap;min-width:0}
.notice-line .act{margin-left:0}
.notice--contained{
  padding:11px var(--s3);border:1px solid var(--line-2);color:var(--ink);
}
.notice--contained strong{font-weight:600;color:var(--ink);line-height:1.3}
.notice-badge{
  display:inline-flex;align-items:center;justify-content:center;
  width:30px;height:30px;flex:none;border-radius:9px;
  background:var(--amber-bg);border:1px solid var(--amber-line);color:var(--amber);
}
.notice-badge svg{width:16px;height:16px;color:var(--amber)}

/* ---------- empty state ---------- */
.empty{
  position:relative;overflow:hidden;text-align:center;padding:var(--s9) var(--s5);
  border:1px dashed var(--line-2);border-radius:var(--r3);background:var(--surface);
}
.empty::before{ /* ambient ridge */
  content:"";position:absolute;inset:auto 0 -40px 0;height:120px;color:var(--accent);opacity:.06;
  background:repeating-radial-gradient(circle at 50% 100%,transparent 0 9px,currentColor 9px 10px);
  pointer-events:none; /* decoration only — never cover the bottom half of buttons */
}
.empty h3{font-size:17px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
.empty p{margin:0 auto var(--s5);max-width:44ch;color:var(--ink-3);font-size:13.5px}

/* === SIGNATURE + SPECIALS ============================================== */

/* ---------- THE GAUGE ---------- */
.gauge{display:grid;grid-template-columns:150px minmax(0,1fr) 84px;align-items:center;gap:var(--s4);padding:11px 0;border-bottom:1px solid var(--line)}
.gauge:last-child{border-bottom:0}
.gauge .gname{font-size:13.5px;color:var(--ink-2);letter-spacing:-.005em}
.gauge .track{
  position:relative;height:26px;border-radius:var(--r1);background:var(--sunken);
  border:1px solid var(--line);overflow:hidden;
}
.gauge .ridges{position:absolute;inset:0;color:var(--ink-4);opacity:.22;background:var(--ridge)}
.gauge .band{ /* target range */
  position:absolute;top:0;bottom:0;background:var(--accent-tint-2);
  border-left:1px solid var(--accent-tint-2);border-right:1px solid var(--accent-tint-2);
  opacity:0;animation:bandIn var(--t4) var(--e-out) 220ms forwards;
}
@keyframes bandIn{to{opacity:1}}
.gauge .band::after{content:"";position:absolute;inset:0;color:var(--accent);opacity:.16;background:var(--ridge)}
.gauge .marker{
  position:absolute;top:-1px;bottom:-1px;width:3px;border-radius:2px;background:var(--accent);
  box-shadow:0 0 0 3px rgba(14,122,107,.14);
  animation:markerSlide 820ms var(--e-out) both;
}
@keyframes markerSlide{from{left:0;opacity:0}to{opacity:1}}
.gauge.off .marker{background:var(--amber);box-shadow:0 0 0 3px rgba(143,106,17,.16)}
.gauge .gval{font-family:var(--ui);font-size:13px;font-weight:560;text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
.gauge.off .gval{color:var(--amber)}
.gauge:hover .track{border-color:var(--line-3)}

/* ---------- verdict ring ---------- */
.verdict{display:flex;align-items:center;gap:var(--s6)}
.score{position:relative;width:104px;height:104px;flex:none}
.score svg{transform:rotate(-90deg)}
.score .bg{stroke:var(--line);stroke-width:8;fill:none}
.score .fg{stroke:var(--accent);stroke-width:8;fill:none;stroke-linecap:round;
  stroke-dasharray:283;animation:ringDraw 1100ms var(--e-out) both}
@keyframes ringDraw{from{stroke-dashoffset:283}}
.score.is-warn .fg{stroke:var(--amber)} .score.is-bad .fg{stroke:var(--rust)}
.score .val{position:absolute;inset:0;display:grid;place-content:center;text-align:center}
.score .val b{font-family:var(--ui);font-size:27px;font-weight:650;letter-spacing:-.03em;display:block;animation:fadeUp var(--t4) var(--e-out) 400ms both}
.score .val span{font-size:10px;letter-spacing:.09em;text-transform:uppercase;font-weight:600;color:var(--ink-3)}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.verdict .say h3{font-size:20px;font-weight:600;letter-spacing:-.02em;margin:0}
.verdict .say p{margin:6px 0 0;color:var(--ink-2);font-size:13.5px;max-width:48ch}

/* ---------- detector report card ---------- */
/* The ridge scan a card runs while the work it reports on is being written.
   These two rules were inline on generate.html, which is the whole reason the
   Autopilot pages had no loading state to show: one surface's read animation
   lived inside one template. The Generate card still switches its own loader
   off with [hidden]; the Autopilot cards are switched by [data-regenerating]. */
.gz-loader{display:grid;place-items:center;gap:var(--s4);padding:var(--s4) 0}
.gz-loader[hidden]{display:none}
.gz-loading{font-size:13px;font-weight:520;color:var(--ink-3);letter-spacing:-.006em;margin:0}

/* A regeneration takes the card over. The old number and the passages behind it
   describe text that is being replaced, so neither is shown beside a read that
   has already started. Declared after `.gz-loader` so the pending read is off
   until regeneration.js marks the page; the score draws in when the mark comes
   off, because a new node inserted under `display:none` starts its animation
   when it is first shown. */
.detector-pending{display:none}
[data-regenerating] .detector-pending{display:grid}
[data-regenerating] [data-detector-report],
[data-regenerating] [data-detector-evidence]{display:none}

/* ---------- prose / writing ---------- */
.prosemeta{display:flex;align-items:center;gap:var(--s3);margin-bottom:var(--s3)}
.prose{position:relative;padding-left:var(--s5);margin:0;border-left:2px solid var(--line-2)}
.prose::before{content:"";position:absolute;left:-2px;top:0;width:2px;height:44px;background:var(--accent);border-radius:2px;
  animation:accentDrop 620ms var(--e-out) both}
@keyframes accentDrop{from{height:0;opacity:0}to{opacity:1}}
.prose p{margin:0 0 1.05em}.prose p:last-child{margin-bottom:0}
.prose.ai{border-left-color:var(--iris-line)}      /* machine-authored text reads iris */
.prose.ai::before{display:none}
.prose mark{background:var(--amber-bg);border-bottom:1.5px solid var(--amber-line);padding:1px 2px;border-radius:2px;color:inherit}
.prose ins{background:var(--accent-tint);text-decoration:none;border-bottom:1.5px solid var(--accent-tint-2)}
.prose del{color:var(--ink-4)}
.prose-sm{font-size:16px;line-height:1.6}
.prose.brief{white-space:pre-line}  /* keep the briefing's line breaks/grouping */

/* ---------- chat ---------- */
.thread{display:flex;flex-direction:column}
.msg{display:grid;grid-template-columns:82px minmax(0,1fr);gap:var(--s4);margin-bottom:var(--s4);animation:riseIn var(--t3) var(--e-out) both}
.msg .who{font-size:var(--t-label);font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);padding-top:4px}
.msg.assistant .who{color:var(--iris)}
.msg .body{font-size:14.5px;line-height:1.6;white-space:pre-line}
.msg.assistant .body{font-family:var(--serif);font-size:15px;line-height:1.55;border-left:2px solid var(--iris-line);padding-left:var(--s4)}
.composer{display:flex;flex-direction:row;align-items:center;gap:var(--s3);padding:var(--s3) var(--s4);border:1px solid var(--line-2);border-radius:var(--r3);background:var(--surface);box-shadow:var(--sh1);transition:box-shadow var(--t2),border-color var(--t2)}
.composer:focus-within{border-color:var(--accent);box-shadow:var(--sh-ring)}
.composer textarea{border:0;box-shadow:none;padding:0;min-height:0;flex:1;background:none;resize:none;overflow-y:auto}
.composer textarea:focus{box-shadow:none}
.composer-actions{display:flex;flex-direction:row;align-items:center;gap:var(--s2);flex:none}
/* Dictate and attach carry icons and nothing else, so the three controls read as
   one evenly spaced cluster: with text labels the gaps between them measured the
   same but looked uneven, because each label's own length sat inside its padding. */
.composer-actions .btn-icon{--bh:30px;width:30px}
.composer-actions .btn-icon svg{width:16px;height:16px}
/* .btn-icon declares its colour after .btn-primary does, so the listening state
   has to ask for its white icon explicitly. */
.composer-actions .btn-icon.btn-primary{color:#fff}
/* …and its *hover* background for the same reason: .btn-icon:hover sets
   --paper-2 and is declared after .btn-primary:hover, so without this the white
   icon above would sit on a near-white button (1.12:1) — and the pointer is on
   this button the moment dictation starts. */
.composer-actions .btn-icon.btn-primary:hover{background:var(--accent-deep)}
.attachments{display:flex;flex-direction:row;flex-wrap:wrap;gap:var(--s2);margin-bottom:var(--s3)}
/* The chip row is emptied and hidden between turns, but `display:flex` is an
   author rule and the `hidden` attribute is only the UA sheet's `display:none`:
   the author rule wins, so an "empty" chip row still paid its own 12px margin.
   Measured on the Cynthia page, that put the composer 33px below the panel's top
   edge and 21px above its bottom — the off-centre box this fixes. A hidden
   container takes no room at all. */
.attachments[hidden]{display:none}
.attachment{display:inline-flex;align-items:center;gap:var(--s2);padding:2px 4px 2px var(--s3);border:1px solid var(--line-2);border-radius:var(--r2);background:var(--surface);font-size:13px;color:var(--ink-2);max-width:100%}
.attachment span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}
.attachment-remove{border:0;background:none;color:var(--ink-3);font-size:12px;padding:2px 6px;cursor:pointer}
.attachment-remove:hover{color:var(--accent)}
.typing{display:inline-flex;gap:4px;align-items:center}
.typing i{width:5px;height:5px;border-radius:50%;background:var(--iris);opacity:.4;animation:blink 1.1s var(--e-io) infinite}
.typing i:nth-child(2){animation-delay:.15s} .typing i:nth-child(3){animation-delay:.3s}
@keyframes blink{0%,100%{opacity:.25;transform:translateY(0)}50%{opacity:1;transform:translateY(-2px)}}
.dots{display:inline-flex;gap:3px;align-items:center;vertical-align:middle;margin-left:5px;line-height:1}
.dots i{width:4px;height:4px;border-radius:50%;background:currentColor;opacity:.3;animation:blink 1.1s var(--e-io) infinite}
.dots i:nth-child(2){animation-delay:.15s} .dots i:nth-child(3){animation-delay:.3s}

/* ---------- accordion (measurement) ---------- */
.measurement{border-bottom:1px solid var(--line)}
.measurement:last-child{border-bottom:0}
.measurement>summary{
  list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--s3);
  padding:13px var(--s5);transition:background var(--t2) var(--e-out);
}
.measurement>summary::-webkit-details-marker{display:none}
.measurement>summary:hover{background:var(--sunken)}
.measurement>summary .chev{width:14px;height:14px;color:var(--ink-4);transition:transform var(--t2) var(--e-out)}
.measurement[open]>summary .chev{transform:rotate(90deg);color:var(--accent)}
.measurement[open]>summary{background:var(--sunken)}
.measurement .mname{font-size:14px;font-weight:520}
.measurement .mval{margin-left:auto;font-family:var(--ui);font-size:13px;font-weight:560;font-variant-numeric:tabular-nums}
.mbody{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--t3) var(--e-out)}
.measurement[open] .mbody{grid-template-rows:1fr}
.mbody>div{overflow:hidden}
.mbody .inner{padding:var(--s4) var(--s5) var(--s6);border-top:1px solid var(--line);background:var(--surface)}
.defs{display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px var(--s4);font-size:13.5px}
.defs dt{color:var(--ink-3);font-size:var(--t-label);font-weight:600;letter-spacing:.07em;text-transform:uppercase;padding-top:3px}
.defs dd{margin:0}
.cloud{display:flex;flex-wrap:wrap;gap:6px}

/* ---------- collapsible field (generate "or paste it") ---------- */
.assignmentbox{border:1px solid var(--line);border-radius:var(--r2);background:var(--surface)}
.assignmentbox>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:var(--s3);padding:11px var(--s4);transition:background var(--t2) var(--e-out)}
.assignmentbox>summary::-webkit-details-marker{display:none}
.assignmentbox>summary:hover{background:var(--sunken)}
.assignmentbox>summary .chev{width:14px;height:14px;color:var(--ink-4);transition:transform var(--t2) var(--e-out)}
.assignmentbox>summary .label{color:var(--ink-2)}
.assignmentbox[open]>summary .chev{transform:rotate(90deg);color:var(--accent)}
.assignmentbox[open]>summary{border-bottom:1px solid var(--line);background:var(--sunken)}
.assignmentbox .assignmentbody{padding:var(--s4)}
/* "or paste it" toggle: a quiet ghost button that turns teal on hover. */
.assignmentbox>summary.paste-toggle{
  display:inline-flex;align-items:center;gap:6px;padding:6px 11px;border-radius:8px;
  border:1px solid var(--line-2);background:var(--surface);color:var(--ink-2);
  font-family:var(--ui);font-size:13px;font-weight:540;letter-spacing:-.006em;
  transition:background var(--t2) var(--e-out),border-color var(--t2),color var(--t2);
}
.assignmentbox>summary.paste-toggle:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-tint)}
.assignmentbox>summary.paste-toggle .chev{width:14px;height:14px;color:var(--ink-4);transition:transform var(--t2) var(--e-out)}
.assignmentbox[open]>summary.paste-toggle .chev{transform:rotate(90deg);color:var(--accent)}
.assignmentbox[open]>summary.paste-toggle{background:var(--accent-tint);border:1px solid var(--line-2)}

/* ---------- source / course cards ---------- */
.sources-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(272px,100%),1fr));gap:var(--s4)}
/* "My writing" sources — uniform, locked card height. See the --scard-h note.
   `height` (not `min-height`) so adding a Connect drawer or longer copy can
   never stretch the card or the row; the drawer scrolls inside the leftover
   space via the .foot overflow rules below. */
.grid-writing .scard{height:var(--scard-h)}
.grid-writing .scard .foot{min-height:0;overflow-y:auto}
.grid-writing .scard .creds{max-height:6.5rem;overflow-y:auto}
/* Class cards start at the writing-card height and grow for setup or status. */
.grid-classes{align-items:start}
.grid-classes .scard{min-height:var(--scard-h)}
.grid-classes .scard .creds{max-height:9rem;overflow-y:auto}
/* The two connected-source groups (Learning platform / Email) are LOCKED to one
   shared height so they always match and never change when a state changes or
   the school URL field is revealed. `height` not `min-height`, and the foot
   scrolls inside the leftover space rather than stretching the card. */
.grid-classes .source-group{height:var(--scard-class-h)}
.grid-classes .source-group .source-provider-panel{min-height:0}
.grid-classes .source-group .source-provider-panel .foot{min-height:0;overflow-y:auto}
/* Once a provider is connected the chooser is hidden: the card already knows
   which one it is, so the select is only shown while choosing/connecting. */
.grid-classes .source-group.connected .source-provider-choice{display:none}
/* The two "My classes" groups are a compact chooser, not status panels. Keep
   the provider state available to assistive technology, but do not let a
   connected/error sentence push the controls onto another row. */
.grid-classes .source-group .source-provider-panel .c-id .sub{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
.grid-classes .source-group .source-provider-panel{gap:var(--s2)}
/* the connected-Outlook school-mail feed stays capped so the card keeps its
   locked height (and the Canvas/Zoom cards in the same row stretch to match) */
.grid-classes .scard .scard-feed{max-height:6rem;overflow-y:auto}
.scard{
  position:relative;display:flex;flex-direction:column;gap:var(--s1);
  padding:var(--s3);border:1px solid var(--line);border-radius:var(--r3);background:var(--surface);
  box-shadow:var(--sh1);overflow:hidden;
  transition:transform var(--t2) var(--e-out),box-shadow var(--t2),border-color var(--t2);
}
.scard:hover{transform:translateY(-2px);box-shadow:var(--sh2);border-color:var(--line-2)}
.scard::after{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform var(--t3) var(--e-out)}
.scard:hover::after{transform:scaleX(1)}
.scard .top{display:flex;align-items:flex-start;gap:12px}
.scard .glyph{width:38px;height:38px;border-radius:var(--r2);display:grid;place-items:center;background:var(--sunken);border:1px solid var(--line);color:var(--ink-3);flex:none}
.scard .glyph svg{width:19px;height:19px}
.scard h4{font-size:15px;font-weight:600;letter-spacing:-.01em;margin:0}
/* The status line is ours, but it can still carry text from a library we do
   not control. Cap it at two lines and break unspaced strings (a Chromium
   command line has none) so a card can never grow to fit an error message and
   shove the rest of the grid around. Two, not three: the card height is fixed,
   so a third line had nowhere to go but under the clip. */
.scard .sub{
  font-size:12.5px;line-height:1.35;color:var(--ink-3);margin:2px 0 0;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  overflow:hidden;overflow-wrap:anywhere;
}
.scard .c-id{min-width:0}
.scard .facts{display:flex;gap:6px;flex-wrap:wrap;margin-top:1px}
.scard .foot{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:var(--s1)}
.scard.connected{border-color:var(--accent-tint-2)}
.scard.connected .glyph{background:var(--accent-tint);border-color:var(--accent-tint-2);color:var(--accent)}

/* First platform import: a quiet checklist, then a Classes review handoff. */
.onboarding{
  margin:0 0 var(--s6);padding:var(--s5) var(--s6);
  border:1px solid var(--accent-tint-2);border-radius:var(--r3);
  background:var(--surface);box-shadow:var(--sh1);
}
.onboarding__intro{display:flex;align-items:center;justify-content:space-between;gap:var(--s6)}
.onboarding__intro>div{min-width:0}
.onboarding__intro .label{color:var(--accent)}
.onboarding h2{margin:var(--s1) 0 var(--s2);font-size:19px;font-weight:600;letter-spacing:-.02em}
.onboarding p{margin:0;max-width:68ch;color:var(--ink-2);font-size:var(--t-sm);line-height:1.5}
.onboarding__intro>form,.onboarding__intro>.btn{flex:none}
.onboarding__steps{
  list-style:none;display:flex;flex-wrap:wrap;gap:var(--s3) var(--s6);
  padding:var(--s4) 0 0;margin:var(--s4) 0 0;border-top:1px solid var(--line);
}
.onboarding__steps li{display:flex;align-items:center;gap:var(--s2);color:var(--ink-3);font-size:12.5px;line-height:1.4}
.onboarding__number{
  display:grid;place-items:center;flex:none;width:25px;height:25px;
  border:1px solid var(--line-2);border-radius:50%;font-size:12px;font-weight:600;
  font-variant-numeric:tabular-nums;
}
.onboarding__number svg{width:14px;height:14px}
.onboarding__steps .is-current{color:var(--ink);font-weight:600}
.onboarding__steps .is-current .onboarding__number{background:var(--accent);border-color:var(--accent);color:#fff}
.onboarding__steps .is-done{color:var(--accent)}
.onboarding__steps .is-done .onboarding__number{background:var(--accent-tint);border-color:var(--accent-tint-2)}
.onboarding--manual{border-color:var(--line)}
.platform-card,#learning-platform-card,#add-class{scroll-margin-top:calc(var(--topbar-h) + var(--s6))}
.platform-card{gap:var(--s1)}
.platform-card .top{position:relative;padding-right:40px}
.platform-card .c-id{flex:1}
#canvas-sync-loader{position:absolute;right:0;top:50%;transform:translateY(-50%)}
#canvas-sync-loader[hidden]{display:none}
#canvas-sync-loader .fp-load{width:32px;height:44px}
.platform-card .foot{padding-top:0}
.canvas-retry{display:flex;align-items:center;flex-wrap:wrap;gap:var(--s1) var(--s2);width:100%}
.canvas-retry[hidden]{display:none}
.canvas-retry .small{flex:1 1 160px;line-height:1.5}
.platform-connect{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--s1) var(--s2);width:100%;min-width:0}
.platform-choice{min-width:0}
.platform-choice select{height:30px;min-width:0;font-size:13px;padding:4px 30px 4px 9px}
.platform-host{grid-column:1/-1;min-width:0}
.platform-host input{height:30px;font-size:13px;padding:4px 9px}
/* A category owns the card; each provider keeps its independent sign-in state. */
.source-group{gap:var(--s2)}
.source-provider-choice{margin:0;min-width:0}
.source-provider-panel{display:flex;flex-direction:column;gap:var(--s4);min-width:0;flex:1}
.source-provider-panel[hidden]{display:none}
.source-provider-panel>.top{min-height:38px;align-items:flex-start}
.source-provider-panel .c-id{flex:1}
.source-provider-panel .foot{margin-top:auto}
.source-provider-panel .platform-connect,.provider-connect{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center;width:100%;min-width:0}
.source-provider-panel .platform-host{flex:1 1 100%}
.source-provider-panel .platform-host[hidden]{display:none}
/* A card renders its school-URL field hidden while it is empty, because the
   Connect button reveals it (source-groups.js) — so the first paint matches the
   settled card. Without scripting there is no reveal, so the field has to stay:
   has-js is stamped on <html> by base.html's head bootstrap. */
html:not(.has-js) .source-provider-panel .platform-host[hidden]{display:block}
.provider-connect input[type=text]{height:30px;padding:4px 9px;font-size:13px;min-width:0}
/* Progressive enhancement keeps each independent form intact. Empty school
   addresses appear below the action row after Connect; without JS the field is
   visible from the start and the provider chooser submits its ordinary GET
   form. */
.source-group-ready .source-provider-panel .foot{padding-top:0;gap:var(--s2)}
.source-group-ready .platform-connect,.source-group-ready .provider-connect{display:contents}
.source-group-ready .source-provider-choice{flex:1 1 110px;order:1}
.source-group-ready .source-provider-choice select{
  background-color:var(--sunken);border-color:var(--line);border-radius:8px;
  color:var(--ink-2);box-shadow:none;
}
.source-group-ready .source-provider-choice select:hover{border-color:var(--line-2)}
.source-group-ready .source-provider-choice select:focus{border-color:var(--accent);box-shadow:var(--sh-ring)}
.source-group-ready .platform-host{order:2}
.source-group-ready .canvas-retry{order:2}
.source-provider-panel .sub{-webkit-line-clamp:unset;display:block}
.source-group .platform-card .top{padding-right:0}
.source-group #canvas-sync-loader{position:static;transform:none;flex:none;margin-top:-3px}
.existing-email{border-top:1px solid var(--line);padding-top:var(--s2);font-size:12.5px}
.existing-email summary{cursor:pointer;color:var(--ink-2)}
.existing-email>.small{margin:var(--s2) 0;line-height:1.5}
.existing-email .scard-feed{margin-bottom:var(--s2)}
.existing-email .u-row{flex-wrap:wrap}
@media(max-width:640px){
  .onboarding{padding:var(--s4)}
  .onboarding__intro{align-items:flex-start;flex-direction:column;gap:var(--s3)}
  .onboarding__steps{flex-direction:column;gap:var(--s3)}
}

/* in-card "connect by pasting your own credentials" prompt */
.creds{width:100%;margin-top:2px}
.creds>summary{list-style:none;cursor:pointer;font-size:12.5px;color:var(--ink-3);padding:2px 0}
.creds>summary::-webkit-details-marker{display:none}
.creds[open]>summary{color:var(--accent)}
.creds form{width:100%}

/* a source card's "Connect" reveal: pick a connection method */
.creds-options{display:flex;flex-direction:column;gap:var(--s2);margin-top:var(--s2);width:100%}
.creds-options .creds-option{border:1px solid var(--line);border-radius:var(--r2);padding:var(--s2)}
.creds-option.soon{opacity:.85;background:var(--sunken);display:flex;flex-direction:column;gap:2px}
.creds-option.soon b{font-size:13px;font-weight:600}
.creds-option.soon .small{margin:0}

/* a "Connect" summary drawn as a real primary button, matching the plain
   Connect buttons on Google Drive / OneDrive instead of a grey text link */
.creds > summary.creds-btn{
  list-style:none;
  display:inline-flex;align-items:center;justify-content:center;
  height:30px;padding:0 11px;border-radius:8px;border:1px solid transparent;
  font-family:var(--ui);font-size:13px;font-weight:540;letter-spacing:-.006em;
  color:#fff;background:var(--accent);cursor:pointer;white-space:nowrap;
  transition:background var(--t2) var(--e-out),box-shadow var(--t2);
}
.creds > summary.creds-btn:hover{background:var(--accent-deep);box-shadow:0 3px 10px -3px rgba(10,87,77,.45)}
.creds[open] > summary.creds-btn{color:#fff;background:var(--accent)}

/* hide a native file input behind a styled "choose" label (GoodNotes) */
.file-hidden{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;padding:0;margin:-1px}

/* visually hidden but still read by screen readers */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* mode cards (Cynthia)
   Four labels, none of them a sentence, so each card is sized to the word in it
   instead of the word floating in a box the tallest card set. */
.modes{display:flex;flex-wrap:wrap;gap:var(--s2)}
.suggbar{display:flex;align-items:center;gap:var(--s3);margin:var(--s2) 0 var(--s3)}
.suggtoggle{display:inline-flex;align-items:center;gap:var(--s2);background:none;border:0;padding:0;color:var(--ink);cursor:pointer;font:inherit}
.suggtoggle .chev{color:var(--ink-3);transition:transform var(--t2)}
/* the base SVG is a DOWN chevron, so collapsed must sit unrotated ("open me")
   and the flip belongs to the expanded state — rotating on "false" pointed the
   arrow up while the drawer was shut, exactly backwards. Same open-state-only
   rule as the .measurement / .assignmentbox summary chevrons. */
.suggtoggle[aria-expanded="true"] .chev{transform:rotate(180deg)}
.modes.hidden{display:none}
.mode{
  /* Centred in a box the size of its own label, rather than top-aligned in a
     grid cell stretched to the tallest one — which is what left a one-line
     label sitting at the top of its slack. */
  display:inline-flex;align-items:center;min-height:40px;
  padding:8px 16px;border:1px solid var(--line);border-radius:var(--r2);background:var(--surface);cursor:pointer;
  color:var(--ink);text-decoration:none;text-align:left;
  /* A white card on warm paper needs the shadow to lift off it, or the row
     reads as four pale rectangles rather than four things to press. */
  box-shadow:var(--sh1);
  transition:border-color var(--t2),background var(--t2),transform var(--t2) var(--e-out),box-shadow var(--t2);
}
.mode:hover{transform:translateY(-1px);box-shadow:var(--sh2);color:var(--ink)}
.mode.on{border-color:var(--accent);background:var(--accent-tint);box-shadow:0 0 0 1px var(--accent),var(--sh2)}
.mode b{display:block;font-size:13.5px;font-weight:620;letter-spacing:-.008em;line-height:1.3}

/* No skeleton styles: there is no asynchronous consumer. */

/* boundary (assistant disclaimer) */
.boundary{margin-top:var(--s6);padding-top:var(--s4);border-top:1px solid var(--line);font-size:13px;color:var(--ink-3);max-width:46rem}
.boundary p{margin:0;line-height:1.6}

/* codeblock (manifest CSV) */
.codeblock{font-family:var(--mono);font-size:12px;line-height:1.7;background:var(--rail);color:#CFE0DB;border-radius:var(--r2);padding:14px 16px;overflow-x:auto;white-space:pre;margin:var(--s3) auto;text-align:left}

/* setup list (assistant no-key) */
.setup{counter-reset:s;list-style:none;padding:0;margin:var(--s3) auto var(--s4);display:flex;flex-direction:column;gap:var(--s3);text-align:left;max-width:56ch}
.setup li{counter-increment:s;position:relative;padding-left:34px;font-size:13.5px;color:var(--ink-2)}
.setup li::before{content:counter(s,decimal-leading-zero);position:absolute;left:0;top:0;font-family:var(--ui);font-size:11px;font-weight:600;color:var(--accent);border:1px solid var(--accent-tint-2);background:var(--accent-tint);border-radius:6px;padding:1px 6px}
.setup li b{color:var(--ink);font-weight:600}

/* folder / import helper rows retained from v3 (restyled) */
.folderrow{display:flex;gap:var(--s3);align-items:flex-end;flex-wrap:wrap}
.folderrow label{display:flex;flex-direction:column;gap:6px;font-family:var(--ui);font-size:var(--t-label);font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);flex:1;min-width:14rem}
.importbar{display:flex;gap:var(--s4);align-items:flex-end;flex-wrap:wrap;margin-bottom:var(--s3)}
.mode-actions{display:flex;gap:var(--s3);flex-wrap:wrap;align-items:center}
.smart-import-table{min-width:0}
.smart-import-table th:last-child,.smart-import-table td:last-child{width:1%;white-space:nowrap;padding-left:var(--s4)}
.smart-import-table td:first-child{max-width:0;overflow-wrap:anywhere}
.smart-import-table td.muted,.smart-import-table tr.muted td{color:var(--ink-4)}
/* Smart-import result header — the "what it found" / "N worth importing"
   summary is centred, then the row itself. */
.smart-import-summary{text-align:center;margin:var(--s3) 0 var(--s4)}
.smart-import-summary .subhead{margin:0 0 var(--s2)}
.smart-import-summary p{margin:0}
/* Exclude: fade the row out before it's removed, so a tap reads as a
   deliberate action instead of a jarring instant disappear. */
.smart-import-table tr.is-excluded{animation:sm-exclude-out 200ms var(--e-io) forwards}
@keyframes sm-exclude-out{from{opacity:1}to{opacity:0}}
/* Smart import list scrolls in its own region; the Save action is a separate row
   pinned to the bottom of the modal, never scrolled with the document list. */
.smart-import-form{display:flex;flex-direction:column;min-height:0}
.smart-import-scroll{overflow-y:auto;max-height:min(46vh,340px);min-height:0}
.smart-import-footer{position:sticky;bottom:0;flex:none;margin-top:var(--s3);padding-top:var(--s3);border-top:1px solid var(--line);background:var(--surface);display:flex;justify-content:flex-end}
.subhead{font-size:var(--t-label);font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2);margin:var(--s5) 0 var(--s3)}
.doclist{margin-top:var(--s5)}
.cardact{margin-top:var(--s4)}

/* ---------- responsive: the rail no longer disappears ---------- */
@media (max-width:1180px){:root{--gutter:28px;--rail-w:196px}.grid{grid-template-columns:minmax(0,1fr) 320px}}
@media (max-width:960px){
  :root{--gutter:20px}
  .app{grid-template-columns:1fr}
  .rail{
    position:fixed;z-index:50;top:0;left:0;width:272px;
    transform:translateX(-100%);transition:transform var(--t3) var(--e-out);
    box-shadow:0 0 60px rgba(19,30,28,.35);
  }
  .app.nav-open .rail{transform:none}
  /* FP-UI-001: on this breakpoint the rail becomes an off-canvas drawer, so the
     rail/canvas seam treatment (hairline + cast shadow) no longer applies. Kill
     the cast shadow so it doesn't read as a stray band at the viewport's left
     edge. (The rail's own box-shadow is overridden above, dropping the hairline.) */
  .canvas{box-shadow:none}
  .iconbtn{display:grid}
  .topbar{padding:0 var(--gutter);gap:var(--s3)}
  .grid,.cols2{grid-template-columns:minmax(0,1fr)}
  .pagehead{flex-direction:column;gap:var(--s4)}
  .pagehead .actions{margin-left:0}
  :root{--t-display:27px}
  .gauge{grid-template-columns:minmax(0,1fr) 70px;grid-template-areas:"n v" "t t";row-gap:8px}
  .gauge .gname{grid-area:n}.gauge .gval{grid-area:v}.gauge .track{grid-area:t}
  .msg{grid-template-columns:minmax(0,1fr);gap:6px}
}
@media (max-width:600px){
  .canvas{padding:var(--s5) var(--gutter) var(--s9)}
  .row{grid-template-columns:auto minmax(0,1fr);row-gap:8px}
  .row .end{grid-column:2;justify-content:flex-start}
  .cards{grid-template-columns:repeat(2,minmax(0,1fr))}
}

/* ---------- accessibility floor ---------- */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}

/* ---------- Cynthia async 'thinking' bubble ---------- */
.msg.thinking .who{color:var(--iris)}
.msg.thinking .body{font-family:var(--ui);font-size:13.5px;font-weight:520;letter-spacing:.01em;color:var(--ink-3)}
.msg.thinking .body::after{content:"…";display:inline-block;margin-left:2px;animation:thinkPulse 1.2s var(--e-io) infinite}
@keyframes thinkPulse{0%,100%{opacity:.25}50%{opacity:1}}

/* ---------- Autopilot lean card (v2: the wordmark joins the sweep) ---------- */
/* The Autopilot palette is defined at :root too, so `.ap__row` can be reused
   outside the card (the "Prep in Advance" panel) without losing its colors. */
:root{
  --ap-surface:#FFFFFF;--ap-ink:#14171A;--ap-ink-seen:#5F625E;--ap-muted:#6E7370;
  --ap-faint:#9AA09B;--ap-line:#E5E2DA;--ap-line-soft:#EFEDE7;--ap-track:#DFDCD4;
  --ap-label:#BCC0BA;--ap-teal-lit:#12A47B;--ap-cycle:5s;
  --ap-teal:#1D9E75;--ap-teal-deep:#0F6E56;--ap-teal-soft:#E7F3EE;--ap-hover:#F5FAF8;--ap-chip:#F1EFE8;
}
.ap{
  --ap-surface:#FFFFFF;--ap-ink:#14171A;--ap-ink-seen:#5F625E;--ap-muted:#6E7370;
  --ap-faint:#9AA09B;--ap-line:#E5E2DA;--ap-line-soft:#EFEDE7;--ap-track:#DFDCD4;
  --ap-label:#BCC0BA;--ap-teal-lit:#12A47B;--ap-cycle:5s;
  --ap-teal:#1D9E75;--ap-teal-deep:#0F6E56;--ap-teal-soft:#E7F3EE;--ap-hover:#F5FAF8;--ap-chip:#F1EFE8;
  background:var(--ap-surface);border:1px solid var(--ap-line);border-radius:14px;overflow:hidden;
  font-family:inherit;margin-bottom:var(--s6);
}
.ap__head{padding:24px 22px}
.ap__mark{position:relative;display:inline-flex;align-items:center;gap:13px}
.ap__strokes{position:relative;width:30px;height:18px;flex:none}
.ap__strokes i{position:absolute;left:0;height:2px;border-radius:1px;background:var(--ap-track)}
.ap__strokes i:nth-child(1){top:2px;width:30px}
.ap__strokes i:nth-child(2){top:8px;width:20px}
.ap__strokes i:nth-child(3){top:14px;width:25px}
/* The lit layer spans the FULL width of the mark (inset: 0) as a row with the
   same gap as the base, so its clip-path percentages share the beam's `left`
   coordinate space and the duplicate strokes land exactly on the originals.
   Shrinking it to just the icon width runs the strokes on the wrong clock. */
.ap__lit{position:absolute;inset:0;display:inline-flex;align-items:center;gap:13px;animation:ap-window var(--ap-cycle) linear infinite}
.ap__lit .ap__strokes i{background:var(--ap-teal)}
.ap__beam{position:absolute;top:-3px;left:0;width:1.5px;height:26px;background:var(--ap-teal);animation:ap-beam var(--ap-cycle) linear infinite}
/* Loading-state label: title case, lightly tracked, sitting quiet until the
   beam reaches it. Each letter is its own <b> so it lights whole rather than
   being sliced mid-glyph. The lift is colour only inside the letterform. */
.ap__word{font-size:14.5px;font-weight:500;letter-spacing:.05em;white-space:nowrap}
.ap__word b{font-weight:inherit;color:var(--ap-label);animation:ap-letter var(--ap-cycle) linear infinite}
/* The wordmark sweep: the beam crosses the mark, the lit strokes follow the
   window it has passed, and each letter lights as it is reached. All three share
   one 5s linear cycle, so they stay in step for the whole pass. The ease is baked
   into the position values — don't change one animation's stops without mirroring
   the others, and don't swap linear for an easing function, or the glow desyncs
   from the line again. (today.html gives every letter its own animation-delay
   from the same --ap-cycle; prefers-reduced-motion turns all of it off below.) */
@keyframes ap-window{0%{clip-path:inset(0 100% 0 0)}8%{clip-path:inset(0 97% 0 0)}20%{clip-path:inset(0 85% 0 0)}33%{clip-path:inset(0 62% 0 20%)}46%{clip-path:inset(0 38% 0 44%)}58%{clip-path:inset(0 15% 0 67%)}66%{clip-path:inset(0 0 0 82%)}72%,100%{clip-path:inset(0 0 0 100%)}}
@keyframes ap-beam{0%{left:0%;opacity:0}8%{left:3%;opacity:1}20%{left:15%}33%{left:38%}46%{left:62%}58%{left:85%}66%{left:100%;opacity:1}72%,100%{left:100%;opacity:0}}
@keyframes ap-letter{0%{color:var(--ap-label)}1%{color:var(--ap-teal-lit)}6%{color:var(--ap-teal-lit)}20%{color:var(--ap-label)}100%{color:var(--ap-label)}}
.ap__row{display:flex;align-items:center;gap:12px;width:100%;padding:16px 22px;border:0;border-top:1px solid var(--ap-line-soft);background:none;font:inherit;text-align:left;color:inherit;cursor:pointer;text-decoration:none;transition:background .12s ease}
.ap__row:hover{background:var(--ap-hover)}
/* A row with no destination is not a link, so it does not answer the pointer:
   the alert row that reports a failed pass has nowhere to send the student. */
.ap__row:not([href]){cursor:default}
.ap__row:not([href]):hover{background:none}
/* No position here any more: it existed only as the containing block for a
   strike that lived inside the title, and the mark-as-done line is the row's now. */
.ap__what{flex:1;min-width:0;font-size:16px;font-weight:500;letter-spacing:-.01em;color:var(--ap-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ap__row[data-seen="true"] .ap__what{color:var(--ap-ink-seen);font-weight:400}
.ap__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.ap__row:focus-visible{outline:2px solid var(--ap-teal-deep);outline-offset:-2px}
.ap__foot{display:flex;gap:16px;padding:14px 22px;border-top:1px solid var(--ap-line-soft)}
.ap__foot a{font-size:12px;color:var(--ap-faint);text-decoration:none}
.ap__foot a:hover{color:var(--ap-teal-deep)}
.ap__grouphead{display:flex;align-items:center;gap:8px;padding:11px 22px 7px;border-top:1px solid var(--ap-line)}
.ap__grouptitle{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ap-muted)}
/* Prep in Advance groups by how far out the item is (soonest first, one "8+
   days" bucket). A subhead labels each due-day band; the row that starts a band
   leans on the subhead's top border instead of drawing its own. */
.ap__subhead{display:flex;align-items:center;gap:8px;padding:14px 22px 2px;border-top:1px solid var(--ap-line);font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ap-muted)}
.ap__subhead:first-child{border-top:0}
.ap__subhead + .ap__item .ap__row{border-top:0}
.ap__subhead + .ap__row{border-top:0}
.ap__subhead > .pill-count{color:var(--ap-muted)}
.ap__group .ap__item:first-of-type .ap__row,
.ap__group .ap__row:first-of-type{border-top:0}
/* Both groups share one row anatomy so the panel reads as a single list:
   a bold title, a left rule in the course colour (see .ap__rule below), and a
   right rail carrying the due-date heat. "Needs your attention" is the to-do
   list; the "Accomplished" rows keep the green source chip ("Ready") so you
   can see what Autopilot already produced. Clicking an item opens its own
   get-it-done page, not the class hub. */
.ap__group--attention .ap__grouptitle{color:var(--amber)}
.ap__row--todo{position:relative;--todo-pad-l:20px;--todo-pad-r:58px;padding-left:var(--todo-pad-l)}
.ap__row--todo:hover{background:var(--ap-hover)}
.ap__row--todo .ap__what{font-weight:600;color:var(--ap-ink)}
.ap__row--todo[data-seen="true"] .ap__what{color:var(--ap-ink-seen)}
/* Left rule: course colour, and nothing else. Absolute so it spans the row
   height without affecting layout; colour comes from .c1..c10 below, falling
   back to a neutral line for rows with no course (signals, failures). */
.ap__rule{position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:0 3px 3px 0;background:var(--ap-line)}
/* Right rail: due-date heat. Only the first four tiers get the filled chip;
   --rust/--rust-bg, --amber and the muted tokens are the existing paired
   light/dark tokens, not a hex with opacity. */
.ap__pts{font-size:12.5px;color:var(--ap-faint);flex:none;min-width:52px;text-align:right}
/* The derived reading scope ("pp. 1-40"): the one thing on the row the
   student acts on, so it keeps its chip on small screens too. */
.ap__scope{flex:none;max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px 9px;border-radius:999px;background:var(--ap-teal-soft);color:var(--ap-teal-deep);font-size:12.5px;font-weight:600;letter-spacing:-.01em}
.ap__row[data-seen="true"] .ap__scope{opacity:.72}
/* "What's due" on the action page and the class hub: the same answer, sized to
   be the first thing the eye lands on inside the panel. */
.scope-label{margin:6px 0 0;font-size:18px;font-weight:650;letter-spacing:-.01em;color:var(--ap-teal-deep,#0f766e)}
.row__scope{display:inline-block;margin-left:8px;padding:2px 8px;border-radius:999px;background:var(--ap-teal-soft,#e6f5f2);color:var(--ap-teal-deep,#0f766e);font-size:12px;font-weight:600;white-space:nowrap}
.ap__rail{display:inline-flex;align-items:center;gap:7px;flex:none;font-size:13px;font-weight:500;letter-spacing:-.005em;white-space:nowrap;color:var(--ap-muted)}
.ap__rail svg{width:17px;height:17px;flex:none}
.ap__rail.t1{color:var(--rust);background:var(--rust-bg);border-radius:8px;padding:3px 10px}
.ap__rail.t2{color:var(--amber)}
.ap__rail.t3{color:var(--ap-ink-seen)}
.ap__rail.t4,.ap__rail.none{color:var(--ap-faint)}
/* Course rule colours — mapped onto the existing course tile accents so the
   left rule reads like the class-grid tile for the same subject. */
.ap__rule.c1{background:#05743C}.ap__rule.c2{background:#1463C2}.ap__rule.c3{background:#C05A0C}
.ap__rule.c4{background:#CB2D7A}.ap__rule.c5{background:#6C35C9}.ap__rule.c6{background:#9A780B}
.ap__rule.c7{background:#037B70}.ap__rule.c8{background:#1B6B98}.ap__rule.c9{background:#5A42B5}
.ap__rule.c10{background:#A2660F}
/* Each feed row is wrapped so it can carry a "⋯" action menu — open on Canvas
   and mark as done — without nesting a button inside the row's link. */
.ap__item{position:relative}
/* Prep in Advance rows pair the feed row with an "add details" form inside the
   same .ap__item, which makes the item taller than the row. The action menu
   must centre on the row, not the whole item, so the row + menu are wrapped in
   their own positioned box that spans just the row. */
.ap__rowwrap{position:relative}
/* The action menu's gutter is also where the mark-as-done line stops, so both
   read the same length instead of hard-coding 58px/48px in two places. */
.ap__row--actions{padding-right:var(--todo-pad-r,58px)}
.ap__menu{position:absolute;top:50%;right:12px;transform:translateY(-50%);z-index:30}
/* When a menu opens, it must stack above every sibling row's "⋯" trigger and
   popup, or the neighbouring rows' dots paint through the open panel (all the
   .ap__menu boxes share z-index:30, so DOM order decides who wins — the row
   after the open one wins). Raising the open menu above the rest keeps the
   popup an opaque clean panel and leaves the neighbour dots sitting where they
   belong, on their own rows, hidden behind it. */
.ap__menu.open{z-index:50}
/* Let open row menus escape the card and paint above the next animated canvas
   section. Keep the usual rounded clipping when no menu is open. */
.ap:has(.ap__menu.open),
.panel[data-prep-panel]:has(.ap__menu.open){position:relative;z-index:1;overflow:visible}
.panel[data-prep-panel]:has(.ap__menu.open)>.panelhead{border-radius:var(--r3) var(--r3) 0 0}
.ap__more{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:var(--ap-faint);cursor:pointer;font-size:16px;line-height:1}
.ap__more:hover{background:var(--ap-hover);color:var(--ap-teal-deep)}
/* Once the menu opens, the trigger "⋯" must not peek back out above (or at the
   top corner of) the popup — the open menu already shows its own items, so a
   second set of dots reads as the menu re-printing its trigger. Hide it. */
.ap__menu.open .ap__more{visibility:hidden}
.ap__pop{position:absolute;top:calc(100% + 4px);right:0;min-width:176px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r3);box-shadow:var(--sh3);padding:4px;display:none;flex-direction:column;gap:2px}
.ap__menu.open .ap__pop{display:flex}
/* When the row sits low enough that the menu would run off the bottom of the
   viewport, bindRowActions flips it open upward so it stays on screen. */
.ap__menu.open--up .ap__pop{top:auto;bottom:calc(100% + 4px)}
.ap__item-act{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:0;border-radius:7px;background:none;color:var(--ap-ink);font-family:var(--ui);font-size:13.5px;font-weight:500;text-align:left;text-decoration:none;cursor:pointer;white-space:nowrap}
.ap__item-act svg{width:15px;height:15px;flex:none;color:var(--ap-muted)}
.ap__item-act:hover{background:var(--ap-hover)}
.ap__item-act:hover svg{color:var(--ap-teal-deep)}
/* "Prep in Advance" rows reuse the Autopilot row outside the card. */
.ap__rows{display:flex;flex-direction:column}
.ap__rows .ap__row:first-of-type{border-top:0}
.ap__rows .add-details{display:flex;gap:var(--s3);align-items:center;padding:2px 22px 14px}
.ap__rows .add-details input{flex:1;min-width:0;padding:7px 10px;border:1px solid var(--ap-line);border-radius:8px;background:var(--ap-hover);color:var(--ap-ink);font-family:var(--ui);font-size:13px}
.ap__rows .add-details input::placeholder{color:var(--ap-faint)}
/* A "needs a look" row's plain-language reason, with the one action that clears it. */
.ap__note{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2) var(--s3);padding:2px 22px 14px;font-size:13px;line-height:1.4;color:var(--ap-muted)}
/* Mark-as-done completion animation. Pressing "Mark as done" runs ONE short
   180ms beat — the teal sweep and the line land immediately, then the row
   collapses and fades in place — and the panel is refetched from the server as
   soon as the toggle is accepted and that beat is done, so the row's real state
   (and the counts) are the server's truth. The class ladder is
   is-sweeping + is-complete -> is-done, driven by markDone/playMarkDone in
   module-today.js. Keep the draws at or under the flourish budget in that file
   or the row collapses mid-draw.

   The line does the whole ending. It is drawn across the row — under the title,
   through the scope chip and the points, out to the row's right padding — and
   nothing replaces the due-date rail: the rail simply fades out with the rest
   of the row's furniture, so the last thing on screen is one clean strike
   before the row collapses. A "Done ✓" pill used to swap in over the rail;
   it was the only element of the ending that had to be read rather than seen,
   it held the row at full height for a beat it did not need, and the row leaves
   Today on the refetch anyway. */
.ap__sweepclip{position:absolute;inset:0;overflow:hidden;pointer-events:none;border-radius:inherit}
.ap__sweep{position:absolute;top:0;bottom:0;left:0;width:120%;transform:translateX(-120%);background:linear-gradient(90deg, rgba(18,164,123,0), rgba(18,164,123,.16) 45%, rgba(18,164,123,.32) 50%, rgba(18,164,123,.16) 55%, rgba(18,164,123,0))}
.ap__row.is-sweeping{background:var(--ap-teal-soft)}
.ap__row.is-sweeping .ap__sweep{transform:translateX(120%);transition:transform .16s cubic-bezier(.4,0,.2,1)}
/* The teal sweep needs the .ap__sweepclip/.ap__sweep markup, which only the two
   Autopilot feed loops carry: a "Prep in Advance" row marking done in place gets
   the line and the tinted row but not the gradient sweep. */
/* The line spans the row's padded content box, so it starts and stops exactly
   where the row's own content does: the same two lengths the row is padded by
   (--todo-pad-l/--todo-pad-r, restated at the mobile breakpoint below), which is
   why the reach is a calc rather than a percentage of the row. It crosses the
   scope chip and the rail, and it paints above them because they are static
   content while the line is positioned — measured, not assumed: dropping the
   z-index changes no pixel today. The z-index is kept as the explicit guard for
   the day a chip is given a position of its own, and to stay under the row's
   action menu (z-index:30). */
.ap__strike{position:absolute;left:var(--todo-pad-l,20px);top:50%;height:1.5px;margin-top:-.75px;width:0;z-index:2;background:var(--ap-teal-deep);pointer-events:none;transition:width .16s cubic-bezier(.4,0,.2,1)}
.ap__row.is-sweeping .ap__strike,
.ap__row.is-done .ap__strike{width:calc(100% - var(--todo-pad-l,20px) - var(--todo-pad-r,58px))}
.ap__row.is-sweeping .ap__what,
.ap__row.is-done .ap__what{color:var(--ap-ink-seen);font-weight:400}
.ap__row.is-complete .ap__rule,
.ap__row.is-done .ap__rule{background:var(--ap-teal)}
.ap__row.is-complete .ap__rule{box-shadow:0 0 0 4px var(--ap-teal-soft)}
.ap__row.is-complete .ap__scope,
.ap__row.is-done .ap__scope{opacity:.55}
.ap__row.is-complete .ap__pts,
.ap__row.is-done .ap__pts{opacity:0}
/* The rail fades rather than being covered by a replacement chip, so the row's
   right-hand column empties out instead of swapping one badge for another. */
.ap__row.is-complete .ap__rail,
.ap__row.is-done .ap__rail{opacity:0;transition:opacity .16s ease}
/* "Mark as done" button busy state: an inline spinner swaps in for the check
   icon and the label tints teal while the POST is in flight. */
.ap__item-act.is-busy .ap__act-ic{display:none}
.ap__item-act.is-busy .ap__act-label{color:var(--ap-teal-deep)}
.ap__spin{display:none;width:14px;height:14px;flex:none;border:2px solid var(--ap-line);border-top-color:var(--ap-teal);border-radius:50%;animation:ap-spin .7s linear infinite}
.ap__item-act.is-busy .ap__spin{display:inline-block}
@keyframes ap-spin{to{transform:rotate(360deg)}}
@media (max-width:560px){
  .ap__head{padding:20px 16px}
  .ap__row{padding:15px 16px;gap:10px}
  .ap__row--actions{padding-right:var(--todo-pad-r,48px)}
  .ap__grouphead{padding:9px 16px 5px}
  .ap__scope{max-width:46%;font-size:12px;padding:2px 7px}
  .ap__pts{display:none}
  .ap__row--todo{--todo-pad-l:16px;--todo-pad-r:48px;padding-left:var(--todo-pad-l)}
  .ap__menu{right:8px}
}
@media (prefers-reduced-motion:reduce){
  .ap__lit{animation:none;clip-path:none;opacity:.85}
  .ap__word b{animation:none}
  .ap__beam{display:none}
  .ap__row{transition:none}
  .ap__sweep,.ap__strike,.ap__rule,.ap__rail,.ap__what{transition:none !important}
  .ap__spin{animation:none}
}

/* =========================================================================
   Classes card redesign — "next-up band" hub cards
   Ports the standalone class-card mockup (classes-redesign.html) onto the
   app's tokens: equal-height rows, a tinted subject tile, meta chips, a
   dated "Next up" band, and a quieter, right-aligned Remove.
   ========================================================================= */
.class-grid{grid-auto-rows:1fr}
.class-card{height:100%}
.class-card .titles{min-width:0;flex:1}
.class-card h4.ctitle{
  font-size:16.5px;font-weight:620;letter-spacing:-.011em;line-height:1.28;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
.class-card h4.ctitle a{color:var(--ink);text-decoration:none}
.class-card h4.ctitle a:hover{color:var(--accent-deep);text-decoration:underline;text-underline-offset:2px}
.class-card .sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Each card carries its course's tint, and the "Next up" band below is a wash
   of it. This is the one place the colour lives now: the card no longer ships a
   monogram tile, so the tint marks the whole card instead of a 38px square.
   `--shade` is the same value the old tile used as its background. */
.class-card.c1{--shade:#D9F2E4}
.class-card.c2{--shade:#D6E8FE}
.class-card.c3{--shade:#FBE5CC}
.class-card.c4{--shade:#FBD6E6}
.class-card.c5{--shade:#E8DBFC}
.class-card.c6{--shade:#FBF1C0}
.class-card.c7{--shade:#CCEFED}
.class-card.c8{--shade:#D9ECF7}
.class-card.c9{--shade:#E1DBF8}
.class-card.c10{--shade:#F9EDCB}

/* meta chips replace the label/value stat rows */
.class-card .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}
.class-card .chip{
  font-size:11.5px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;
  padding:3px 9px;border-radius:var(--rp);border:1px solid var(--line);
  background:var(--sunken);color:var(--ink-2);white-space:nowrap;
}
.class-card .chip-count{background:var(--accent-tint);color:var(--accent-deep);border-color:var(--accent-tint-2)}
.class-card .chip-count.zero{background:var(--sunken);color:var(--ink-3);border-color:var(--line)}
.class-card .chip-archived{background:var(--amber-bg);color:var(--amber);border-color:var(--amber-line)}

.class-card .spacer{flex:1;min-height:var(--s3)}

/* The next-up band: a soft course-tinted wash that fades out to the right,
   instead of a status-toned left rule. The edge is deliberately soft — a hard
   1px rule next to the card border read as a second border, and the tint is
   already the card's own colour rather than a status colour. `--shade` is set
   by the .c1..c10 classes on the card, so the wash always matches the course.
   The band carries no status rail any more (owner's call, 2026-09-11): a slim
   colour tick inside a band that is already tinted read as a second mark on the
   same edge, and the card is calmer without it. Overdue still announces itself
   in words — the label switches to "Overdue" — but "due soon" is no longer
   signalled in the band at all. */
.class-card .next{
  padding:9px 10px 9px 16px;border-left:0;border-radius:var(--r2);
  background:var(--shade,var(--sunken));
  display:flex;align-items:flex-start;
}
/* color-mix keeps the wash derived from one token per course rather than a
   second hand-picked palette; browsers without it fall back to the flat
   --shade above, which is the same hue and just a touch stronger. */
@supports (background:color-mix(in srgb,red 50%,transparent)){
  .class-card .next{
    background:linear-gradient(90deg,
      color-mix(in srgb,var(--shade,var(--sunken)) 45%,transparent) 0%,
      rgba(255,255,255,0) 90%);
  }
}
.class-card .next.empty{background:linear-gradient(90deg,var(--sunken) 0%,rgba(255,255,255,0) 90%)}
.class-card .next.empty .nval{color:var(--ink-3);font-weight:500}
.class-card .next .nbody{min-width:0;flex:1}
.class-card .next .nlabel{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3);display:block}
.class-card .next .nval{font-size:15px;font-weight:620;color:var(--ink);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
.class-card .next .nwhen{font-size:13px;color:var(--ink-2);margin-top:2px;display:block}

/* action row pinned to the card bottom */
.class-card .foot{padding-top:var(--s3);border-top:1px solid var(--line)}
.class-card .foot-remove .btn{color:var(--rust)}
.class-card .foot-remove .btn:hover{opacity:1;color:var(--rust);background:var(--rust-bg)}

/* Archived actions stay together at the lower left, where the active card's
   Open action starts. */
.class-card .foot-actions{display:flex;align-items:center;gap:var(--s2)}
.class-card .foot-actions .foot-remove{margin-left:0}

/* Archive is pushed to the card's bottom-right, away from Open */
.class-card .foot-archive{margin-left:auto}

/* Archive is an icon button that reveals its label on hover and nudges up */
.class-card .archive-btn{width:32px;padding:0;justify-content:center;position:relative;transform:translateY(0)}
.class-card .archive-btn .archive-label{
  position:absolute;left:calc(100% + 7px);top:50%;transform:translateY(-50%);
  padding:4px 8px;border-radius:6px;background:var(--ink);color:var(--surface);
  font-size:11.5px;font-weight:560;white-space:nowrap;opacity:0;pointer-events:none;
  transition:opacity var(--t2) var(--e-out),transform var(--t2) var(--e-out);
}
.class-card .archive-btn .archive-label::before{
  content:"";position:absolute;right:100%;top:50%;transform:translateY(-50%);
  border:5px solid transparent;border-right-color:var(--ink);
}
.class-card .archive-btn:hover,.class-card .archive-btn:focus-visible{transform:translateY(-2px)}
.class-card .archive-btn:hover .archive-label,.class-card .archive-btn:focus-visible .archive-label{
  opacity:1;transform:translateY(-50%) translateX(2px);
}
.class-card .archive-btn svg{transition:transform var(--t2) var(--e-out)}
.class-card .archive-btn:hover svg{transform:translateY(-1px)}
.class-card .archive-btn.archive-btn--review{width:auto;padding:0 10px;gap:5px}

/* Reset semester — destructive, so it reads like Remove: a red bordered pill */
.btn-reset{
  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;
  font-family:var(--ui);font-size:13.5px;font-weight:600;white-space:nowrap;
  color:var(--rust);background:transparent;
  border:1px solid var(--rust-line);border-radius:var(--rp);
  padding:7px 15px;opacity:.82;
  transition:background var(--t2),opacity var(--t2),border-color var(--t2),color var(--t2);
}
.btn-reset:hover{opacity:1;background:var(--rust-bg);border-color:var(--rust);color:var(--rust)}
.btn-reset:focus-visible{outline:2px solid var(--rust);outline-offset:2px}

/* =========================================================================
   Canvas connector card (1C) — a status dot, a name, and opt-in write access
   toggles. Ported from canvas-1c-3b.html onto the app's tokens.
   ========================================================================= */
/* status dot replaces the tile glyph for the Canvas card */
.c-dot{width:9px;height:9px;border-radius:50%;background:var(--line-2);flex:none}
.scard .c-dot{margin-top:5px}
.c-dot.is-on{background:var(--accent)}
.c-dot.is-busy{background:var(--amber);animation:pulse-dot 1.2s var(--e-io) infinite}
@keyframes pulse-dot{0%,100%{opacity:.35}50%{opacity:1}}
.scard .c-id .sub{margin-top:2px}
/* Canvas sync buffer — the spinner shown while a background sync pulls courses
   and deadlines. Shared by the Classes Canvas panel and the Sources card. */
.canvas-sync-spinner{width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;display:inline-block;flex:none;animation:canvas-sync-spin .8s linear infinite}
@keyframes canvas-sync-spin{to{transform:rotate(360deg)}}
/* write-access permission rows */
.c-write{margin-top:2px;border-top:1px solid var(--line);padding-top:var(--s3)}
.c-write .prow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0}
.c-write .prow+.prow{border-top:1px solid var(--line)}
.c-write .p-txt{flex:1;min-width:0}
.c-write .p-l{font-size:13.5px;font-weight:500;color:var(--ink);margin:0}
.c-write .p-s{font-size:12px;color:var(--ink-3);margin:2px 0 0;line-height:1.45}
.cap-error{font-size:12.5px;color:var(--rust);margin:var(--s1) 0 0;padding-top:var(--s1);border-top:1px dashed var(--rust-line)}
/* switch toggle — role="switch", applies immediately, no save button */
.tgl{width:38px;height:22px;border-radius:11px;border:1px solid var(--line-2);background:var(--line);
  position:relative;padding:0;flex:none;cursor:pointer;transition:background var(--t2) var(--e-out),border-color var(--t2)}
.tgl i{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--surface);
  box-shadow:0 1px 2px rgba(0,0,0,.14);transition:left var(--t2) var(--e-out)}
.tgl[aria-checked="true"]{background:var(--accent);border-color:var(--accent)}
.tgl[aria-checked="true"] i{left:18px}
.tgl:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .tgl,.tgl i{transition:none}
}

/* ---------- fingerprint loader: ridge scan ----------------------------
   Traced ridge centrelines from fingerprint-logo. Each ridge animates on
   its own dash, so bands can light independently. The 0% frame is the
   FULLY DRAWN print: with animations paused (reduced motion, print, a
   screenshot) the loader still reads as the mark, never as an empty box.
   Markup: templates/_loader.html
--------------------------------------------------------------------- */
.fp-load{display:block;flex:none;width:112px;height:154px}
.fp-load--sm{width:72px;height:99px}
.fp-load--lg{width:160px;height:221px}
.fp-load g{fill:none;stroke-width:6.5;stroke-linecap:round;stroke-linejoin:round}
.fp-load .fp-ghost{stroke:var(--line)}
.fp-load .fp-live{stroke:var(--accent-lit)}
.fp-load .fp-live path{
  animation-duration:1.872s;
  animation-timing-function:var(--e-io);
  animation-iteration-count:infinite;
  animation-fill-mode:both;
}

/* per-ridge dash length + phase. Generated — do not hand-edit. */
.fp-load .fp-live path:nth-child(1){stroke-dasharray:247;animation-name:fp-r0;animation-delay:0s}
.fp-load .fp-live path:nth-child(2){stroke-dasharray:572;animation-name:fp-r1;animation-delay:0s}
.fp-load .fp-live path:nth-child(3){stroke-dasharray:273;animation-name:fp-r2;animation-delay:0s}
.fp-load .fp-live path:nth-child(4){stroke-dasharray:209;animation-name:fp-r3;animation-delay:0.014s}
.fp-load .fp-live path:nth-child(5){stroke-dasharray:308;animation-name:fp-r4;animation-delay:0.168s}
.fp-load .fp-live path:nth-child(6){stroke-dasharray:71;animation-name:fp-r5;animation-delay:0.159s}
.fp-load .fp-live path:nth-child(7){stroke-dasharray:513;animation-name:fp-r6;animation-delay:0.156s}
.fp-load .fp-live path:nth-child(8){stroke-dasharray:136;animation-name:fp-r7;animation-delay:0.161s}
.fp-load .fp-live path:nth-child(9){stroke-dasharray:125;animation-name:fp-r8;animation-delay:0.332s}
.fp-load .fp-live path:nth-child(10){stroke-dasharray:220;animation-name:fp-r9;animation-delay:0.321s}
.fp-load .fp-live path:nth-child(11){stroke-dasharray:157;animation-name:fp-r10;animation-delay:0.324s}
.fp-load .fp-live path:nth-child(12){stroke-dasharray:132;animation-name:fp-r11;animation-delay:0.333s}
.fp-load .fp-live path:nth-child(13){stroke-dasharray:440;animation-name:fp-r12;animation-delay:0.463s}
.fp-load .fp-live path:nth-child(14){stroke-dasharray:166;animation-name:fp-r13;animation-delay:0.466s}
.fp-load .fp-live path:nth-child(15){stroke-dasharray:310;animation-name:fp-r14;animation-delay:0.474s}
.fp-load .fp-live path:nth-child(16){stroke-dasharray:278;animation-name:fp-r15;animation-delay:0.487s}
.fp-load .fp-live path:nth-child(17){stroke-dasharray:41;animation-name:fp-r16;animation-delay:0.623s}
.fp-load .fp-live path:nth-child(18){stroke-dasharray:139;animation-name:fp-r17;animation-delay:0.633s}
.fp-load .fp-live path:nth-child(19){stroke-dasharray:409;animation-name:fp-r18;animation-delay:0.629s}
.fp-load .fp-live path:nth-child(20){stroke-dasharray:54;animation-name:fp-r19;animation-delay:0.626s}
.fp-load .fp-live path:nth-child(21){stroke-dasharray:228;animation-name:fp-r20;animation-delay:0.8s}
.fp-load .fp-live path:nth-child(22){stroke-dasharray:135;animation-name:fp-r21;animation-delay:0.803s}
.fp-load .fp-live path:nth-child(23){stroke-dasharray:127;animation-name:fp-r22;animation-delay:0.783s}
.fp-load .fp-live path:nth-child(24){stroke-dasharray:171;animation-name:fp-r23;animation-delay:0.794s}
.fp-load .fp-live path:nth-child(25){stroke-dasharray:172;animation-name:fp-r24;animation-delay:0.951s}
@media (prefers-reduced-motion:reduce){.fp-load .fp-live path:nth-child(n){animation:none}}

@keyframes fp-r0{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-247}52.001%{stroke-dashoffset:247}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r1{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-572}52.001%{stroke-dashoffset:572}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r2{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-273}52.001%{stroke-dashoffset:273}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r3{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-209}52.001%{stroke-dashoffset:209}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r4{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-308}52.001%{stroke-dashoffset:308}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r5{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-71}52.001%{stroke-dashoffset:71}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r6{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-513}52.001%{stroke-dashoffset:513}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r7{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-136}52.001%{stroke-dashoffset:136}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r8{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-125}52.001%{stroke-dashoffset:125}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r9{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-220}52.001%{stroke-dashoffset:220}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r10{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-157}52.001%{stroke-dashoffset:157}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r11{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-132}52.001%{stroke-dashoffset:132}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r12{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-440}52.001%{stroke-dashoffset:440}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r13{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-166}52.001%{stroke-dashoffset:166}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r14{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-310}52.001%{stroke-dashoffset:310}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r15{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-278}52.001%{stroke-dashoffset:278}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r16{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-41}52.001%{stroke-dashoffset:41}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r17{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-139}52.001%{stroke-dashoffset:139}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r18{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-409}52.001%{stroke-dashoffset:409}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r19{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-54}52.001%{stroke-dashoffset:54}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r20{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-228}52.001%{stroke-dashoffset:228}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r21{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-135}52.001%{stroke-dashoffset:135}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r22{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-127}52.001%{stroke-dashoffset:127}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r23{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-171}52.001%{stroke-dashoffset:171}94%,100%{stroke-dashoffset:0}}
@keyframes fp-r24{0%,12%{stroke-dashoffset:0}46%,52%{stroke-dashoffset:-172}52.001%{stroke-dashoffset:172}94%,100%{stroke-dashoffset:0}}

/* ---------- branded boot splash ----------------------------------------
   Full-viewport dark reveal on first entry each session. The loader's 0% frame
   is the fully drawn mark, so with reduced motion / print it still reads as the
   logo. .done (set by the base.html splash script) fades it out and frees the
   page. Markup: the #splash block in templates/base.html.
-------------------------------------------------------------------------- */
.splash{
  position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
  padding:var(--s6);
  background:var(--rail);
  background:radial-gradient(120% 120% at 50% 38%, #182422 0%, var(--rail) 62%);
  transition:opacity var(--t4) var(--e-out), visibility var(--t4) var(--e-out);
}
.splash-box{
  display:flex;flex-direction:column;align-items:center;gap:var(--s6);text-align:center;
  opacity:0;transform:translateY(10px) scale(.985);
}
.splash.open .splash-box{
  opacity:1;transform:none;transition:opacity var(--t4) var(--e-out), transform var(--t4) var(--e-spring);
}
.splash .fp-load{margin:0 auto}
.splash-brand b{display:block;font-size:28px;font-weight:620;letter-spacing:-.03em;line-height:1;color:#fff}
.splash-brand span{display:block;margin-top:10px;font-family:var(--ui);font-size:11px;font-weight:550;letter-spacing:.16em;text-transform:uppercase;color:var(--rail-ink-3)}
.splash.done{opacity:0;visibility:hidden;pointer-events:none}
/* The splash is server-rendered into EVERY response as the first element in
   <body> and is opaque from parse — position:fixed, inset:0, z-index:100, an
   opaque dark radial-gradient, and no initial opacity:0. The only thing that
   removes it on a repeat visit is a script at the BOTTOM of <body>, after the
   SVG sprite, the confirm modal and the feedback dialog include. On any parse
   delay that is a full-viewport dark flash on every navigation in a session —
   very likely the literal referent of "it refreshes the whole page". The head
   script stamps .splash-seen before <body> parses, so the node never paints;
   base.html's own removal still runs and is still correct. */
.splash-seen .splash{display:none}
/* The boot splash draws the mark in ONCE — the accent bands light up outward
   from the core and then hold, a single reveal. It never does the shared
   loader's "draw on / wipe / draw on again" pass (which reads as running
   twice). The shared fp-r* keyframes stay as the seamless infinite loop for
   the Drafting panel loader; the splash swaps the animation-name for a
   one-way draw-in (fp-sdraw) and pins it to one iteration. The under-layer is
   a dim tint of the teal (not the light --line) so the mark reads as the
   single-color logo, not a two-tone. */
@keyframes fp-sdraw{from{stroke-dashoffset:var(--dlen)}78%,100%{stroke-dashoffset:0}}
.splash .fp-ghost{stroke:rgba(14,122,107,.22)}
.splash .fp-load .fp-live path{animation-name:fp-sdraw;animation-iteration-count:1}
/* per-ridge dash length, matching each ridge's stroke-dasharray, so the accent
   draws each ridge in from its own start instead of one shared offset. */
.splash .fp-live path:nth-child(1){--dlen:247px}
.splash .fp-live path:nth-child(2){--dlen:572px}
.splash .fp-live path:nth-child(3){--dlen:273px}
.splash .fp-live path:nth-child(4){--dlen:209px}
.splash .fp-live path:nth-child(5){--dlen:308px}
.splash .fp-live path:nth-child(6){--dlen:71px}
.splash .fp-live path:nth-child(7){--dlen:513px}
.splash .fp-live path:nth-child(8){--dlen:136px}
.splash .fp-live path:nth-child(9){--dlen:125px}
.splash .fp-live path:nth-child(10){--dlen:220px}
.splash .fp-live path:nth-child(11){--dlen:157px}
.splash .fp-live path:nth-child(12){--dlen:132px}
.splash .fp-live path:nth-child(13){--dlen:440px}
.splash .fp-live path:nth-child(14){--dlen:166px}
.splash .fp-live path:nth-child(15){--dlen:310px}
.splash .fp-live path:nth-child(16){--dlen:278px}
.splash .fp-live path:nth-child(17){--dlen:41px}
.splash .fp-live path:nth-child(18){--dlen:139px}
.splash .fp-live path:nth-child(19){--dlen:409px}
.splash .fp-live path:nth-child(20){--dlen:54px}
.splash .fp-live path:nth-child(21){--dlen:228px}
.splash .fp-live path:nth-child(22){--dlen:135px}
.splash .fp-live path:nth-child(23){--dlen:127px}
.splash .fp-live path:nth-child(24){--dlen:171px}
.splash .fp-live path:nth-child(25){--dlen:172px}
@media (prefers-reduced-motion:reduce){
  .splash{transition:none}
  .splash.open .splash-box{transition:none}
}

/* ---------- plan & usage meters (profile / generate / check) ---------- */
.meter{display:flex;flex-direction:column;gap:6px}
.meter-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s4)}
.meter-label{font-size:13px;color:var(--ink-2);font-weight:560}
.meter-count{font-family:var(--ui);font-size:13px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.meter-count b{color:var(--ink);font-weight:600}
.meter-track{height:8px;border-radius:var(--rp);background:var(--sunken);overflow:hidden}
.meter-fill{
  height:100%;border-radius:var(--rp);transform-origin:left;
  background:linear-gradient(90deg,var(--accent),var(--accent-lit));
  animation:meterFill var(--t4) var(--e-out) both;
}
.meter-fill.is-full{background:linear-gradient(90deg,var(--amber),var(--rust))}
@keyframes meterFill{from{width:0}to{width:var(--pct,0)}}

/* plan switcher (profile) */
.plan-switch{display:flex;gap:var(--s2)}
.plan-option{
  position:relative;display:flex;flex-direction:column;gap:2px;
  padding:var(--s3) var(--s4);border:1px solid var(--line-2);border-radius:var(--r2);
  cursor:pointer;transition:border-color var(--t2),background var(--t2) var(--e-out),box-shadow var(--t2);
}
.plan-option:hover{border-color:var(--line-3)}
.plan-option.on{border-color:var(--accent);background:var(--accent-tint);box-shadow:var(--sh-ring)}
.plan-option input{position:absolute;opacity:0;pointer-events:none}
.plan-option-name{font-weight:600;font-size:14px;color:var(--ink)}
.plan-option-price{font-family:var(--ui);font-size:12px;color:var(--ink-3)}

@media (prefers-reduced-motion:reduce){
  .meter-fill{animation:none}
}

/* Where a scraped due date came from, quoted from the page it was read on. */
.task-provenance{margin:10px 0 0;font-size:13px;color:var(--ink-4)}

/* ============================================================================
   AUTH — split screen (login / signup)

   Left: the brand panel. The real ridge mark (templates/_authmark.html) draws
   itself once on load, then a slow band of light passes over it every few
   seconds — movement you notice, not movement that pulls at you.
   Right: the form, on app tokens.

   Everything here is scoped to .auth-* / #fpRidges, so no in-app surface moves.
   ========================================================================= */
.auth-shell{
  min-height:100vh;
  display:grid;
  grid-template-columns:minmax(0,1.04fr) minmax(0,1fr);
  background:var(--paper);
}
@supports (min-height:100dvh){.auth-shell{min-height:100dvh}}

/* ---------- left: brand panel ---------- */
.auth-brandside{
  position:relative;overflow:hidden;
  display:flex;flex-direction:column;justify-content:space-between;
  padding:var(--s8);
  background:radial-gradient(118% 88% at 20% 8%,
    var(--rail-3) 0%,var(--rail-2) 32%,var(--rail) 66%,#080E0D 100%);
  color:var(--rail-ink);
}
/* a still glow under the print for depth, then a vignette so the ridges settle
   into the panel instead of ending at a hard edge */
.auth-brandside::before{
  content:"";position:absolute;left:50%;top:50%;
  width:min(560px,88%);aspect-ratio:1;transform:translate(-50%,-50%);
  background:radial-gradient(circle,rgba(18,196,140,.10) 0%,rgba(18,196,140,0) 68%);
  pointer-events:none;
}
.auth-brandside::after{
  content:"";position:absolute;inset:0;
  background:radial-gradient(74% 58% at 50% 50%,transparent 30%,rgba(6,11,10,.68) 100%);
  pointer-events:none;
}
.auth-lockup{
  position:relative;z-index:2;
  display:inline-flex;align-items:center;gap:var(--s3);
  font-size:15.5px;font-weight:600;letter-spacing:-.012em;color:var(--rail-ink);
}
/* same ridge mark and same 34x47 as the in-app rail brand: below ~27px the fine
   ridge gaps fall under device-pixel resolution and the mark reads as a smudge */
.auth-lockup img{width:34px;height:47px;flex:none;display:block}
/* log in: the small mark up top is dropped — the ridge print below is already
   the mark, so a second one reads as a duplicate. The wordmark stands alone,
   larger and centred over the print, so the panel is one lockup, not two. */
.auth-lockup--wordmark{
  align-self:center;justify-content:center;gap:0;
  font-size:28px;letter-spacing:-.026em;
}
/* the wordmark runs a touch larger on the login page only, so the shared
   auth-lockup sizing on the other auth pages stays as-is */
.auth-shell--login .auth-lockup--wordmark{font-size:41px}
.auth-tag{position:relative;z-index:2;max-width:33ch;margin-bottom:var(--s3)}
.auth-tag .rule{width:48px;height:3px;border-radius:2px;background:var(--accent-lit);margin-bottom:16px}
.auth-tag p{margin:0;font-size:14.5px;line-height:1.62;color:var(--rail-ink-2)}
/* Close (✕) affordance for the standalone contact page — pinned to the
   top-right of the viewport so a stray click can always be dismissed. */
.auth-close{position:fixed;top:var(--s5);right:var(--s5);z-index:30;display:grid;place-items:center;width:36px;height:36px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:var(--ink-2);font-size:15px;text-decoration:none;cursor:pointer;transition:background .14s ease,color .14s ease}
.auth-close:hover{background:var(--paper-2);color:var(--ink)}

/* ---------- the mark ---------- */
.authmark{
  position:absolute;z-index:1;
  left:50%;top:50%;transform:translate(-50%,-50%);
  width:min(330px,54%);
}
.authmark svg{display:block;width:100%;height:auto}
.authmark__ink{stroke:#2C7F69;stroke-width:5.2}
.authmark__scan{position:absolute;inset:0}
.authmark__scan use{stroke:#6BEFC1;stroke-width:5.2}

/* each ridge draws itself, top of the pad downward (--i is the draw order) */
#fpRidges path{
  stroke-dasharray:1;stroke-dashoffset:1;
  animation:ridgeDraw 1.1s var(--e-io) both;
  animation-delay:calc(var(--i) * 46ms + 140ms);
}
@keyframes ridgeDraw{to{stroke-dashoffset:0}}

/* the light band: a masked copy of the same ridges, gliding once every 9s */
.authmark__scan{
  --band:linear-gradient(to bottom,
    transparent 0%,rgba(0,0,0,.5) 26%,#000 50%,rgba(0,0,0,.5) 74%,transparent 100%);
  -webkit-mask-image:var(--band);mask-image:var(--band);
  -webkit-mask-size:100% 42%;mask-size:100% 42%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  animation:ridgeScan 9s var(--e-io) 2.4s infinite;
  opacity:.9;
}
@keyframes ridgeScan{
  0%,6%{-webkit-mask-position:50% -120%;mask-position:50% -120%}
  58%,100%{-webkit-mask-position:50% 220%;mask-position:50% 220%}
}

/* ---------- right: the form ---------- */
.auth-formside{display:flex;align-items:center;justify-content:center;padding:var(--s8) var(--s7)}
.auth-form{width:100%;max-width:376px;animation:authLift var(--t4) var(--e-out) both}
@keyframes authLift{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.auth-form h1{margin:0 0 8px;font-size:31px;line-height:1.08;letter-spacing:-.03em;font-weight:600;color:var(--ink)}
.auth-form .lede{margin:0 0 26px;color:var(--ink-3);font-size:14.5px;line-height:1.55}
/* no lede on log in: the heading takes over the lede's breathing room so the
   first field doesn't crowd it */
.auth-form--nolede h1{margin-bottom:26px}
.auth-form .notice{margin-bottom:18px}
.auth-form .divider{margin:18px 0}
.auth-form .btn-primary,
.auth-form .btn-ghost{width:100%;--bh:44px;font-size:14.5px}
.auth-form .btn-primary{margin-top:20px}

/* The age-confirmation screens (signup gate, restricted account) ask exactly one
   question — heading, box, button — so the shared rhythm leaves holes: the
   --nolede heading gap of 26px stands in for a lede these screens don't have,
   and the field's 15px does not collapse into the button's 20px (the button is
   inline-flex, and inline-level margins never collapse), adding up to 35px under
   a single 20px checkbox. Measured target: 16px above the box, 20px below it
   (the button already has its 20px from the shared rule above). */
.auth-form--confirm h1{margin-bottom:16px}
.auth-form--confirm .auth-field{margin-bottom:0}
.auth-form--confirm .auth-field label{font-size:14.5px}
/* account rights sit under the question, quiet and out of the way: they are an
   exit, not a second call to action */
.auth-rights{margin-top:26px;padding-top:18px;border-top:1px solid var(--line)}
.auth-rights-row{display:flex;flex-wrap:wrap;gap:2px 4px;align-items:center}
.auth-rights-row .btn{width:auto;--bh:36px;padding:0 14px;font-size:13.5px}
.auth-rights .auth-foot{margin-top:2px;text-align:left}

/* sentence-case labels here (not the app's micro caps): this surface is the
   first thing a new user sees, so it reads as prose, not as instrumentation */
.auth-field{margin-bottom:15px}
.auth-labelrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:7px}
.auth-field label{font-size:13.5px;font-weight:540;color:var(--ink);letter-spacing:-.004em}
.auth-control{position:relative}
.auth-field input[type=text],
.auth-field input[type=email],
.auth-field input[type=password]{height:44px;padding:0 13px;font-size:14.5px}
.auth-field.has-peek input{padding-right:66px}
.peek{
  position:absolute;right:5px;top:5px;height:34px;padding:0 10px;
  border:0;border-radius:8px;background:none;
  font-family:var(--ui);font-size:12.5px;font-weight:560;color:var(--ink-3);
  cursor:pointer;transition:color var(--t2),background var(--t2);
}
.peek:hover{color:var(--ink);background:var(--paper-2)}
.peek:focus-visible{outline:none;box-shadow:var(--sh-ring)}

/* field-level notes, shown only once a field is actually wrong */
.auth-note{display:none;margin:7px 0 0;font-size:12.5px;color:var(--rust)}
.auth-field.invalid input{border-color:var(--rust);box-shadow:0 0 0 3px rgba(166,65,42,.13)}
.auth-field.invalid .auth-note{display:block}
.auth-field .hint{margin:7px 0 0;font-size:12.5px;color:var(--ink-3)}

.auth-ghostlink{
  background:none;border:0;padding:0;
  font-family:var(--ui);font-size:12.5px;color:var(--ink-3);
  cursor:pointer;border-bottom:1px solid transparent;transition:color var(--t2),border-color var(--t2);
}
.auth-ghostlink:hover{color:var(--accent);border-bottom-color:rgba(14,122,107,.32)}
.auth-ghostlink:focus-visible{outline:none;box-shadow:var(--sh-ring);border-radius:4px}
.auth-hint{
  display:none;margin:9px 0 0;padding:9px 11px;
  font-size:12.5px;line-height:1.5;color:var(--ink-2);
  background:var(--sunken);border:1px solid var(--line);border-radius:var(--r1);
}
.auth-hint.open{display:block}

.auth-foot{margin:24px 0 0;text-align:center;font-size:13.5px;color:var(--ink-3)}
.auth-foot a{color:var(--accent);font-weight:600;text-decoration:none;border-bottom:1px solid rgba(14,122,107,.28)}
.auth-foot a:hover{border-bottom-color:var(--accent)}

/* ---------- calm mode ---------- */
@media (prefers-reduced-motion:reduce){
  #fpRidges path{animation:none;stroke-dashoffset:0}
  .authmark__scan{animation:none;-webkit-mask-position:50% 50%;mask-position:50% 50%;opacity:.55}
  .auth-form{animation:none;opacity:1;transform:none}
}

/* ---------- narrow ---------- */
@media (max-width:900px){
  .auth-shell{grid-template-columns:1fr;grid-template-rows:auto 1fr}
  .auth-brandside{min-height:184px;padding:var(--s6)}
  .auth-tag{display:none}
  .authmark{width:236px;left:auto;right:-52px;top:auto;bottom:-104px;transform:none}
  .auth-brandside::before{width:330px;left:auto;right:-10px;top:auto;bottom:-90px;transform:none}
  .auth-brandside::after{background:radial-gradient(84% 92% at 78% 96%,transparent 22%,rgba(6,11,10,.6) 100%)}
  .auth-formside{padding:var(--s7) var(--s5) var(--s9);align-items:flex-start}
  .auth-form{max-width:420px}
  .auth-form h1{font-size:27px}
  /* the print slides to the bottom-right here, so the wordmark has nothing to
     centre over — it goes back to the top-left corner */
  .auth-lockup--wordmark{align-self:flex-start;font-size:23px}
  .auth-shell--login .auth-lockup--wordmark{font-size:29px}
}

/* =========================================================================
   Account overlay (Account · Settings · Billing)
   Replaces Profile, Settings, and the standalone Autopilot page. Scoped to
   .acct-*; reuses the app's design tokens so it reads native to Fingerprint.
   ========================================================================= */
/* The overlay covers the page it opened from — the white canvas to the right of
   the rail and below the topbar — not the whole laptop screen. That keeps the
   dialog centred on the page (so it never looks shoved left under the dark
   rail) and leaves the app's own chrome crisp instead of dimming it into a
   band that reads as the layout being cut off at the top.
   `animation:none` opts out of the .canvas>* rise-in: that keyframe animates a
   transform, which would make this fixed layer resolve against the canvas box
   and jump as it finished. */
/* Center the dialog, and when the viewport is shorter than the dialog let the
   overlay scroll so the header (the "Account" title) is never clipped off the
   top. `margin:auto` (not plain align-items:center) keeps it centred when it
   fits and reachable when it doesn't. */
.acct-overlay{position:fixed;top:var(--topbar-h);right:0;bottom:0;left:var(--rail-w);z-index:90;display:flex;align-items:flex-start;justify-content:center;overflow-y:auto;padding:var(--s5);animation:none}
.acct-scrim{position:absolute;inset:0;background:rgba(19,30,28,.46);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
.acct-modal{position:relative;z-index:1;display:flex;flex-direction:column;margin:auto;width:min(980px,100%);height:min(660px,100%);background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:var(--sh3);overflow:hidden}
.acct-head{padding:22px 26px 0;flex:none}
.acct-headrow{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.acct-title{margin:0;font-family:var(--ui);font-size:23px;font-weight:800;letter-spacing:-.025em;color:var(--ink)}
.acct-sub{margin:3px 0 0;font-size:13.5px;color:var(--ink-3)}
.acct-close{display:grid;place-items:center;width:32px;height:32px;flex:none;border:1px solid var(--line);border-radius:9px;background:var(--surface);color:var(--ink-2);font-size:15px;text-decoration:none;cursor:pointer}
.acct-close:hover{background:var(--paper-2)}

.acct-tabs{display:flex;gap:2px;margin-top:16px;border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:none}
.acct-tabs::-webkit-scrollbar{display:none}
.acct-tab{appearance:none;border:0;background:none;padding:10px 12px 12px;font-family:var(--ui);font-size:14.5px;font-weight:600;color:var(--ink-3);cursor:pointer;position:relative;white-space:nowrap;letter-spacing:-.01em}
.acct-tab:hover{color:var(--ink-2)}
.acct-tab[aria-selected="true"]{color:var(--ink)}
.acct-tab[aria-selected="true"]::after{content:"";position:absolute;left:12px;right:12px;bottom:-1px;height:2px;background:var(--accent);border-radius:2px 2px 0 0}

.acct-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:22px 26px 28px}
.acct-panel{display:none;animation:acct-in .16s ease-out}
.acct-panel.active{display:block}
@keyframes acct-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}

.acct-group{border:1px solid var(--line);border-radius:12px;background:var(--surface);overflow:hidden;margin-bottom:18px}
.acct-grouplabel{margin:0 0 8px;font-family:var(--ui);font-size:13px;font-weight:700;color:var(--ink-2);letter-spacing:-.01em}
.acct-grouplabel:not(:first-child){margin-top:24px}
.acct-row{display:flex;align-items:center;gap:18px;padding:15px 18px;border-bottom:1px solid var(--paper-2)}
.acct-row:last-child{border-bottom:0}
.acct-rowmain{flex:1;min-width:0}
.acct-rowtitle{font-family:var(--ui);font-size:14.5px;font-weight:600;letter-spacing:-.01em;color:var(--ink)}
.acct-rowtitle-lg{font-size:17px}
.acct-rowdesc{font-size:13px;color:var(--ink-3);margin-top:2px;line-height:1.45}
.acct-avatarrow{display:flex;align-items:center;gap:16px;padding:18px}
.acct-avatar{width:58px;height:58px;border-radius:50%;background:var(--rail-2);color:var(--rail-ink);display:grid;place-items:center;font-family:var(--ui);font-size:22px;font-weight:700;flex:none}
.acct-fieldwrap{padding:0 18px 18px;border-top:1px solid var(--paper-2)}
.acct-fieldwrap{padding-top:18px}
.acct-field{display:flex;flex-direction:column;gap:6px;width:100%}
.acct-fieldlabel{font-family:var(--ui);font-size:12.5px;font-weight:600;color:var(--ink-2)}
.acct-input{font-family:var(--ui);font-size:14.5px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--ink);width:100%}
.acct-input:focus{outline:2px solid var(--accent);outline-offset:0;border-color:transparent}

/* Time zone: a header and the field, nothing between them, and the field's own
   list hanging directly beneath it. The card cannot clip (the list is taller
   than the card) and the list is the field's exact width, so it never lands
   off-centre the way a browser-drawn popup does. */
.acct-group.acct-tzcard{overflow:visible;padding:15px 18px 18px}
.acct-tz{position:relative;margin-top:10px}
.acct-tz .acct-input{padding-right:34px}
.acct-tz-caret{position:absolute;top:50%;right:10px;transform:translateY(-50%);display:grid;place-items:center;width:20px;height:20px;color:var(--ink-3);cursor:pointer}
.acct-tz-list{position:absolute;z-index:6;top:calc(100% + 2px);left:0;right:0;max-height:min(280px,45vh);overflow-y:auto;overscroll-behavior:contain;padding:4px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 12px 28px rgba(19,30,28,.16)}
.acct-tz-list[hidden]{display:none}
.acct-tz.is-up .acct-tz-list{top:auto;bottom:calc(100% + 2px)}
.acct-tz-opt{padding:8px 10px;border-radius:7px;font-family:var(--ui);font-size:13.5px;color:var(--ink);cursor:pointer}
.acct-tz-opt[hidden]{display:none}
.acct-tz-opt[data-active]{background:var(--paper-2)}
.acct-tz-opt[aria-selected="true"]{color:var(--accent);font-weight:600}
/* "No zone by that name." stands where the list would be — outside the listbox,
   which may hold nothing but options. */
.acct-tz-none{position:absolute;z-index:6;top:calc(100% + 2px);left:0;right:0;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 12px 28px rgba(19,30,28,.16);font-family:var(--ui);font-size:13px;color:var(--ink-3)}
.acct-tz.is-up .acct-tz-none{top:auto;bottom:calc(100% + 2px)}
.acct-tz-none[hidden]{display:none}
.acct-pill{font-size:11.5px;font-weight:700;padding:3px 9px;border-radius:20px;background:var(--accent-tint);color:var(--accent);white-space:nowrap}

/* Toggle: 50×29 track, 23px knob (spec). */
.acct-switch{position:relative;display:inline-block;width:50px;height:29px;flex:none;cursor:pointer}
.acct-switch input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.acct-track{position:absolute;inset:0;border-radius:20px;background:var(--line-2);transition:background .16s ease;pointer-events:none}
.acct-track::after{content:"";position:absolute;top:3px;left:3px;width:23px;height:23px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(19,30,28,.28);transition:transform .16s cubic-bezier(.3,.8,.4,1)}
.acct-switch input:checked + .acct-track{background:var(--accent)}
.acct-switch input:checked + .acct-track::after{transform:translateX(21px)}
.acct-switch input:focus-visible + .acct-track{outline:2px solid var(--accent);outline-offset:3px}

/* Segmented control. */
.acct-seg{display:flex;background:var(--paper-2);border-radius:10px;padding:4px;gap:2px;flex:none}
.acct-segbtn{position:relative;font-family:var(--ui);font-size:13.5px;font-weight:600;color:var(--ink-3);padding:6px 14px;border-radius:7px;cursor:pointer;white-space:nowrap}
.acct-segbtn input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.acct-segbtn:hover{color:var(--ink)}
.acct-segbtn:has(input:checked){background:var(--accent);color:#fff}
.acct-segbtn:focus-within{outline:2px solid var(--accent);outline-offset:2px}

/* Sits under its card, lined up with that card's own text column: the 1px border
   and the 18px row padding are what place the titles it belongs with, and lining
   the action up with them reads as deliberate. At the card's outer edge the rule
   under the label starts a pixel or two left of the label itself — an underline
   covers the glyph's side bearing — which read as a ragged left edge rather than
   as an alignment. `appearance` and the explicit box reset stop a UA button style
   from indenting or centring it further. */
.acct-quiet{appearance:none;-webkit-appearance:none;display:block;width:fit-content;margin:0 0 18px 19px;padding:0;border:0;background:none;font-family:var(--ui);font-size:12px;font-weight:500;text-align:left;color:var(--ink-3);cursor:pointer;text-decoration:underline;text-underline-offset:2px}
.acct-quiet:hover{color:var(--accent)}
.acct-selectwrap{position:relative;display:inline-flex;align-items:center;flex:none}
.acct-select{appearance:none;font-family:var(--ui);font-size:13.5px;font-weight:600;color:var(--ink);padding:8px 30px 8px 14px;border:1px solid var(--line);border-radius:8px;background:var(--surface);cursor:pointer}
.acct-select:focus{outline:2px solid var(--accent);outline-offset:0}
.acct-caret{position:absolute;right:10px;pointer-events:none;color:var(--ink-3)}

.acct-invoice{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid var(--paper-2);font-size:13.5px;color:var(--ink)}
.acct-invoice:last-child{border-bottom:0}
.acct-invoice span{color:var(--ink-3)}
.acct-danger{color:var(--rust);border-color:var(--rust-line)}
.acct-danger:hover{background:var(--rust-bg)}

/* Save bar — slides up only when dirty. Kept in-flow (relative) and animated via
   max-height + opacity + translate so the show/hide never flips position or
   snaps height:0↔auto (which read as a jerk). When hidden it collapses to zero
   height and releases its grip on the layout. */
.acct-savebar{flex:none;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px;padding:14px 26px;border-top:1px solid var(--line);background:var(--paper);max-height:0;overflow:hidden;opacity:0;transform:translateY(8px);visibility:hidden;pointer-events:none;transition:max-height .22s ease,opacity .18s ease,transform .22s ease,visibility 0s linear .22s}
.acct-savebar.show{max-height:120px;opacity:1;transform:none;visibility:visible;pointer-events:auto;transition:max-height .22s ease,opacity .18s ease,transform .22s ease,visibility 0s}
.acct-savemsg{font-family:var(--ui);font-size:13.5px;color:var(--ink-2);font-weight:600}
.acct-savebtns{display:flex;gap:10px}

/* Typed delete confirmation. */
.acct-confirm{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:rgba(19,30,28,.5)}
.acct-confirm[hidden]{display:none}
.acct-confirmbox{width:min(400px,90vw);background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--sh3);padding:var(--s6)}
.acct-confirmbox h3{margin:0;font-family:var(--ui);font-size:var(--t-h2);font-weight:600;color:var(--ink)}
.acct-confirmbox p{margin:var(--s2) 0 var(--s4);font-family:var(--ui);color:var(--ink-2);font-size:var(--t-body);line-height:1.5}
.acct-confirmbox .acct-input{margin-bottom:var(--s4)}
.acct-confirmbtns{display:flex;gap:var(--s2);justify-content:flex-end}

.acct-autobody.dim{opacity:.42;pointer-events:none}
.acct-action{display:inline-flex;gap:8px;margin:0}

/* At 960px the rail turns into an off-canvas drawer and the topbar grows a menu
   button, so the overlay takes the whole viewport: there is no permanent rail to
   sit beside, and leaving the menu button exposed would let it open the drawer
   underneath this layer, where nothing but a dead tap comes of it. (Has to live
   after the base rule above to win on source order.) */
@media (max-width:960px){
  .acct-overlay{top:0;left:0}
}

/* Under 860px the overlay becomes a full-screen sheet. */
@media (max-width:860px){
  .acct-overlay{inset:0;padding:0}
  .acct-modal{width:100%;height:100%;border-radius:0;border:0}
  .acct-row{flex-wrap:wrap}
  .acct-seg{overflow-x:auto;max-width:100%}
}
@media (prefers-reduced-motion:reduce){
  .acct-panel{animation:none}
  .acct-track,.acct-track::after,.acct-savebar{transition:none}
}
