/* Hallmark · genre: atmospheric · macrostructure: app-dashboard · design-system: design.md · designed-as-app
   Hallmark · pre-emit critique: P4 H5 E4 S4 R5 V4 · verified live 2026-08-20 (Today, Week, Classes, Study, GPA, Checklist)
   Lopes HQ — 2026-08-20 "poster dark" reskin: palette unchanged (near-black +
   indigo/violet, Cooper: "same same colors"), the difference is typographic —
   huge condensed uppercase display type (Avenir Next Condensed) with italic
   serif accent lines, per the vibe-only reference. Class colors keep the
   lifted-for-dark swatches; chips always carry a text label. */

:root {
  color-scheme: dark;
  --paper: #0e0a1a;          /* dark violet window plane (FileSorter backdrop) */
  --paper-deep: #05060a;     /* deeper sidebar well */
  --sheet: rgba(19, 18, 32, 0.55);        /* translucent cards over the backdrop */
  --sheet-raised: rgba(30, 28, 48, 0.6);
  --ink: #eef0f8;
  --ink-2: #a9adc4;
  /* Lifted from #696e86, which measured 3.65:1 on cards and 3.41:1 on raised
     cards — under the 4.5:1 needed for the small text this is mostly used for.
     Same hue and saturation, ~8% lighter; now ≥4.57:1 on every surface. */
  --muted: #7e839a;
  --rule: rgba(255, 255, 255, 0.07);
  --rule-strong: rgba(255, 255, 255, 0.12);
  --accent: #7c86f2;
  --accent-ink: #ffffff;
  --accent-soft: rgba(124, 134, 242, 0.13);
  --accent-grad: linear-gradient(135deg, #8b93f8, #9f6ef0);
  --good: #3ecf8e;
  --good-text: #3ecf8e;
  --warning: #e3b34c;
  --critical: #e5636c;
  --critical-text: #e5636c;
  --s1: #6cb6f5; --s2: #e0925c; --s3: #3ecf9a; --s4: #e3b34c;
  --s5: #e574b8; --s6: #45c4b0; --s7: #b48af5; --s8: #e5636c;
  --s9: #ff7a5c;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 12px 32px rgba(0, 0, 0, 0.35);
  --serif: ui-serif, "Iowan Old Style", Georgia, "Times New Roman", serif;
  --sans: ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  /* Interface chrome that should stay sans whatever reading font is chosen.
     body.body-serif and friends redefine --sans, which is right for body
     copy but turns labels and controls serif along with it. */
  --ui-sans: ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  /* Display face for the poster voice — condensed, heavy, uppercase. Ships
     with macOS; Futura as the first fallback keeps the condensed feel. */
  --display: "Avenir Next Condensed", "Futura", var(--sans);
  /* Spacing / type scale. Both are expressed against a multiplier so the two
     Appearance controls have exactly one place to write: density moves spacing,
     type size moves type, and nothing has to enumerate the tokens one by one.
     A body class sets the multiplier; every token below follows. */
  --dens: 1;
  --tscale: 1;
  /* Radius / motion / line-height had no tokens at all — every rule hardcoded
     its own. These cover the shared components; deliberately NOT scaled by
     density, since a rounder corner is not a denser one. */
  --radius: 16px; --radius-sm: 8px; --radius-xs: 7px; --radius-pill: 999px;
  /* ===== Shared motion system =====
     One duration/easing/distance/scale vocabulary for every interactive
     surface, so a button press, a card hover, and a modal open all feel
     like the same app instead of eight one-off transitions. Existing
     --motion-fast/--motion/--motion-slow (already load-bearing on the hub,
     dropdown chevron, and a few panels) are kept as-is and now sit inside
     this same scale rather than beside it.
       hover/press ......... --dur-press   (120-180ms, immediate)
       small UI transition . --dur-small   (180-250ms, crisp)  = --motion-fast
       panel / modal ........ --dur-panel   (250-350ms, smooth) = --motion
       page / view .......... --dur-view    (300-450ms, deliberate)
       background / ambient . --dur-ambient (4-12s, nearly imperceptible)
     Leaving is quicker than entering (something dismissed doesn't need to
     be savored) and eases in rather than out. Entering eases out — fast
     start, gentle settle. --ease-spring is for drag/physical motion only,
     never for a plain hover or fade. */
  --motion-fast: 0.18s; --motion: 0.32s; --motion-slow: 0.5s;
  --dur-press: 0.14s;
  --dur-small: var(--motion-fast);   /* 0.18s */
  --dur-panel: var(--motion);        /* 0.32s */
  --dur-view: 0.38s;
  --dur-ambient: 8s;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-spring: cubic-bezier(0.22, 1.4, 0.36, 1);
  --dist-sm: 8px;
  --dist-md: 16px;
  --scale-press: 0.98;
  --scale-modal-from: 0.98;
  --leading: 1.45;
  /* Backdrop bloom strength, so "atmospheric / flat" is one number. */
  --bloom: 1;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
html { background: var(--paper); }
body {
  margin: 0;
  /* Dark violet base, lit as a room rather than decorated with blobs. Three
     corner blooms used to overlap into a muddy wash that read as light leaks;
     this is one key light from above with a faint floor bounce, a vignette to
     pull the corners down so the centre reads as the lit part, and fine grain
     on top. The grain matters more than it looks: wide, dark gradients band
     into visible steps on 8-bit displays, and a few percent of noise is what
     keeps them reading as smooth instead of cheap. The seeded starfields live
     on body::before / ::after so they can twinkle in counter-phase. */
  background:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='140'%20height='140'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='saturate'%20values='0'/%3E%3C/filter%3E%3Crect%20width='140'%20height='140'%20filter='url(%23n)'%20opacity='0.05'/%3E%3C/svg%3E") 0 0 / 140px 140px repeat,
    radial-gradient(ellipse 130% 105% at 50% 38%, transparent 42%, rgba(3, 2, 9, 0.6) 100%),
    radial-gradient(1150px 640px at 50% -14%, rgba(150, 112, 235, calc(0.19 * var(--bloom))), transparent 68%),
    radial-gradient(950px 430px at 50% 110%, rgba(124, 96, 210, calc(0.12 * var(--bloom))), transparent 70%);
  /* Scale tokens live here, not on :root: a custom property resolves its own
     var() references against the element it is declared on, and the density /
     text-size classes land on <body>. Declared upstream they would always
     resolve the multiplier as 1 and the controls would do nothing. */
  --space-1: calc(4px * var(--dens)); --space-2: calc(8px * var(--dens));
  --space-3: calc(12px * var(--dens)); --space-4: calc(16px * var(--dens));
  --space-5: calc(24px * var(--dens)); --space-6: calc(32px * var(--dens));
  --text-xs: calc(11px * var(--tscale)); --text-sm: calc(12.5px * var(--tscale));
  --text-base: calc(14px * var(--tscale)); --text-lg: calc(17px * var(--tscale));
  --text-xl: calc(24px * var(--tscale)); --text-2xl: calc(34px * var(--tscale));
  --text-3xl: calc(46px * var(--tscale));
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
button { font: inherit; color: inherit; }
body::before, body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-repeat: repeat;
}
body::before { background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221400%22%20height%3D%22900%22%3E%3Ccircle%20cx%3D%22895.2%22%20cy%3D%2222.5%22%20r%3D%220.68%22%20fill%3D%22white%22%20opacity%3D%220.15%22%2F%3E%3Ccircle%20cx%3D%221031.1%22%20cy%3D%22609.0%22%20r%3D%221.29%22%20fill%3D%22white%22%20opacity%3D%220.09%22%2F%3E%3Ccircle%20cx%3D%22590.7%22%20cy%3D%2226.8%22%20r%3D%220.62%22%20fill%3D%22white%22%20opacity%3D%220.26%22%2F%3E%3Ccircle%20cx%3D%2237.2%22%20cy%3D%22179.0%22%20r%3D%221.05%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%22308.6%22%20cy%3D%22530.3%22%20r%3D%221.21%22%20fill%3D%22white%22%20opacity%3D%220.06%22%2F%3E%3Ccircle%20cx%3D%221128.1%22%20cy%3D%22628.3%22%20r%3D%220.74%22%20fill%3D%22white%22%20opacity%3D%220.12%22%2F%3E%3Ccircle%20cx%3D%221340.1%22%20cy%3D%22302.9%22%20r%3D%220.49%22%20fill%3D%22white%22%20opacity%3D%220.1%22%2F%3E%3Ccircle%20cx%3D%221186.5%22%20cy%3D%22543.4%22%20r%3D%221.21%22%20fill%3D%22white%22%20opacity%3D%220.34%22%2F%3E%3Ccircle%20cx%3D%22750.7%22%20cy%3D%22875.8%22%20r%3D%220.78%22%20fill%3D%22white%22%20opacity%3D%220.28%22%2F%3E%3Ccircle%20cx%3D%221161.2%22%20cy%3D%22556.7%22%20r%3D%221.26%22%20fill%3D%22white%22%20opacity%3D%220.29%22%2F%3E%3Ccircle%20cx%3D%22986.4%22%20cy%3D%2241.2%22%20r%3D%220.63%22%20fill%3D%22white%22%20opacity%3D%220.17%22%2F%3E%3Ccircle%20cx%3D%22111.7%22%20cy%3D%22209.5%22%20r%3D%220.5%22%20fill%3D%22white%22%20opacity%3D%220.17%22%2F%3E%3Ccircle%20cx%3D%22890.0%22%20cy%3D%22328.3%22%20r%3D%220.77%22%20fill%3D%22white%22%20opacity%3D%220.14%22%2F%3E%3Ccircle%20cx%3D%22373.8%22%20cy%3D%22843.0%22%20r%3D%221.05%22%20fill%3D%22white%22%20opacity%3D%220.3%22%2F%3E%3Ccircle%20cx%3D%22239.6%22%20cy%3D%22656.2%22%20r%3D%220.56%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%221385.3%22%20cy%3D%22576.0%22%20r%3D%220.96%22%20fill%3D%22white%22%20opacity%3D%220.33%22%2F%3E%3Ccircle%20cx%3D%221180.0%22%20cy%3D%22698.4%22%20r%3D%220.63%22%20fill%3D%22white%22%20opacity%3D%220.07%22%2F%3E%3Ccircle%20cx%3D%22441.6%22%20cy%3D%22241.0%22%20r%3D%220.61%22%20fill%3D%22white%22%20opacity%3D%220.43%22%2F%3E%3Ccircle%20cx%3D%221226.9%22%20cy%3D%22283.2%22%20r%3D%221.06%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%221280.4%22%20cy%3D%22413.0%22%20r%3D%220.66%22%20fill%3D%22white%22%20opacity%3D%220.16%22%2F%3E%3Ccircle%20cx%3D%22785.9%22%20cy%3D%22236.5%22%20r%3D%220.98%22%20fill%3D%22white%22%20opacity%3D%220.41%22%2F%3E%3Ccircle%20cx%3D%22559.2%22%20cy%3D%22197.4%22%20r%3D%221.4%22%20fill%3D%22white%22%20opacity%3D%220.26%22%2F%3E%3Ccircle%20cx%3D%22127.3%22%20cy%3D%2242.4%22%20r%3D%220.51%22%20fill%3D%22white%22%20opacity%3D%220.3%22%2F%3E%3Ccircle%20cx%3D%221108.9%22%20cy%3D%22379.9%22%20r%3D%220.46%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%221394.6%22%20cy%3D%22476.2%22%20r%3D%221.37%22%20fill%3D%22white%22%20opacity%3D%220.4%22%2F%3E%3Ccircle%20cx%3D%2216.1%22%20cy%3D%22648.6%22%20r%3D%221.08%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%22373.6%22%20cy%3D%22576.9%22%20r%3D%220.51%22%20fill%3D%22white%22%20opacity%3D%220.23%22%2F%3E%3Ccircle%20cx%3D%22635.2%22%20cy%3D%22858.4%22%20r%3D%221.28%22%20fill%3D%22white%22%20opacity%3D%220.16%22%2F%3E%3Ccircle%20cx%3D%22700.8%22%20cy%3D%22160.8%22%20r%3D%221.31%22%20fill%3D%22white%22%20opacity%3D%220.4%22%2F%3E%3Ccircle%20cx%3D%22417.8%22%20cy%3D%22575.1%22%20r%3D%221.01%22%20fill%3D%22white%22%20opacity%3D%220.12%22%2F%3E%3Ccircle%20cx%3D%221067.5%22%20cy%3D%22485.4%22%20r%3D%221.18%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%220.8%22%20cy%3D%22291.7%22%20r%3D%220.42%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%221230.2%22%20cy%3D%22748.5%22%20r%3D%220.71%22%20fill%3D%22white%22%20opacity%3D%220.08%22%2F%3E%3Ccircle%20cx%3D%221229.2%22%20cy%3D%22852.3%22%20r%3D%220.49%22%20fill%3D%22white%22%20opacity%3D%220.25%22%2F%3E%3Ccircle%20cx%3D%2296.9%22%20cy%3D%22684.5%22%20r%3D%221.17%22%20fill%3D%22white%22%20opacity%3D%220.11%22%2F%3E%3Ccircle%20cx%3D%22665.4%22%20cy%3D%22494.8%22%20r%3D%220.67%22%20fill%3D%22white%22%20opacity%3D%220.4%22%2F%3E%3Ccircle%20cx%3D%22592.4%22%20cy%3D%22190.6%22%20r%3D%220.94%22%20fill%3D%22white%22%20opacity%3D%220.34%22%2F%3E%3Ccircle%20cx%3D%22281.6%22%20cy%3D%22280.5%22%20r%3D%221.4%22%20fill%3D%22white%22%20opacity%3D%220.31%22%2F%3E%3Ccircle%20cx%3D%22613.3%22%20cy%3D%22465.8%22%20r%3D%220.52%22%20fill%3D%22white%22%20opacity%3D%220.15%22%2F%3E%3Ccircle%20cx%3D%22473.3%22%20cy%3D%22529.5%22%20r%3D%220.63%22%20fill%3D%22white%22%20opacity%3D%220.15%22%2F%3E%3Ccircle%20cx%3D%2299.4%22%20cy%3D%22568.0%22%20r%3D%220.63%22%20fill%3D%22white%22%20opacity%3D%220.41%22%2F%3E%3Ccircle%20cx%3D%221203.5%22%20cy%3D%2263.8%22%20r%3D%220.64%22%20fill%3D%22white%22%20opacity%3D%220.32%22%2F%3E%3Ccircle%20cx%3D%22299.9%22%20cy%3D%22119.1%22%20r%3D%221.34%22%20fill%3D%22white%22%20opacity%3D%220.28%22%2F%3E%3Ccircle%20cx%3D%22661.7%22%20cy%3D%22706.2%22%20r%3D%221.21%22%20fill%3D%22white%22%20opacity%3D%220.13%22%2F%3E%3Ccircle%20cx%3D%22135.7%22%20cy%3D%22387.9%22%20r%3D%220.82%22%20fill%3D%22white%22%20opacity%3D%220.24%22%2F%3E%3Ccircle%20cx%3D%221020.7%22%20cy%3D%22606.0%22%20r%3D%221.38%22%20fill%3D%22white%22%20opacity%3D%220.1%22%2F%3E%3Ccircle%20cx%3D%22563.7%22%20cy%3D%22305.4%22%20r%3D%221.26%22%20fill%3D%22white%22%20opacity%3D%220.16%22%2F%3E%3Ccircle%20cx%3D%22266.3%22%20cy%3D%22403.8%22%20r%3D%220.82%22%20fill%3D%22white%22%20opacity%3D%220.17%22%2F%3E%3Ccircle%20cx%3D%22349.7%22%20cy%3D%22830.9%22%20r%3D%220.84%22%20fill%3D%22white%22%20opacity%3D%220.4%22%2F%3E%3Ccircle%20cx%3D%22770.5%22%20cy%3D%2245.5%22%20r%3D%221.4%22%20fill%3D%22white%22%20opacity%3D%220.39%22%2F%3E%3Ccircle%20cx%3D%221356.6%22%20cy%3D%22833.7%22%20r%3D%221.25%22%20fill%3D%22white%22%20opacity%3D%220.12%22%2F%3E%3Ccircle%20cx%3D%22679.9%22%20cy%3D%22192.4%22%20r%3D%220.8%22%20fill%3D%22white%22%20opacity%3D%220.08%22%2F%3E%3Ccircle%20cx%3D%22530.6%22%20cy%3D%22886.8%22%20r%3D%220.67%22%20fill%3D%22white%22%20opacity%3D%220.37%22%2F%3E%3Ccircle%20cx%3D%22637.0%22%20cy%3D%22380.7%22%20r%3D%221.36%22%20fill%3D%22white%22%20opacity%3D%220.45%22%2F%3E%3Ccircle%20cx%3D%22778.1%22%20cy%3D%22646.6%22%20r%3D%220.55%22%20fill%3D%22white%22%20opacity%3D%220.18%22%2F%3E%3Ccircle%20cx%3D%221356.2%22%20cy%3D%22521.3%22%20r%3D%220.94%22%20fill%3D%22white%22%20opacity%3D%220.35%22%2F%3E%3Ccircle%20cx%3D%2280.0%22%20cy%3D%22525.8%22%20r%3D%220.9%22%20fill%3D%22white%22%20opacity%3D%220.39%22%2F%3E%3Ccircle%20cx%3D%22220.4%22%20cy%3D%22864.7%22%20r%3D%220.48%22%20fill%3D%22white%22%20opacity%3D%220.13%22%2F%3E%3Ccircle%20cx%3D%22833.0%22%20cy%3D%22607.7%22%20r%3D%220.64%22%20fill%3D%22white%22%20opacity%3D%220.11%22%2F%3E%3Ccircle%20cx%3D%221246.4%22%20cy%3D%22221.6%22%20r%3D%220.99%22%20fill%3D%22white%22%20opacity%3D%220.3%22%2F%3E%3Ccircle%20cx%3D%22586.9%22%20cy%3D%22525.3%22%20r%3D%220.92%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%22286.0%22%20cy%3D%22644.6%22%20r%3D%220.64%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%22940.4%22%20cy%3D%22270.0%22%20r%3D%220.72%22%20fill%3D%22white%22%20opacity%3D%220.35%22%2F%3E%3Ccircle%20cx%3D%22101.6%22%20cy%3D%22412.5%22%20r%3D%221.4%22%20fill%3D%22white%22%20opacity%3D%220.45%22%2F%3E%3Ccircle%20cx%3D%22102.6%22%20cy%3D%22191.8%22%20r%3D%220.67%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3C%2Fsvg%3E'); animation: twinkle-a 7s ease-in-out infinite; }
body::after  { background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%221400%22%20height%3D%22900%22%3E%3Ccircle%20cx%3D%22864.9%22%20cy%3D%22479.9%22%20r%3D%220.77%22%20fill%3D%22white%22%20opacity%3D%220.29%22%2F%3E%3Ccircle%20cx%3D%22232.0%22%20cy%3D%22741.9%22%20r%3D%220.78%22%20fill%3D%22white%22%20opacity%3D%220.37%22%2F%3E%3Ccircle%20cx%3D%221290.4%22%20cy%3D%22276.9%22%20r%3D%221.39%22%20fill%3D%22white%22%20opacity%3D%220.14%22%2F%3E%3Ccircle%20cx%3D%22918.8%22%20cy%3D%22821.1%22%20r%3D%220.51%22%20fill%3D%22white%22%20opacity%3D%220.38%22%2F%3E%3Ccircle%20cx%3D%22558.2%22%20cy%3D%2258.8%22%20r%3D%221.1%22%20fill%3D%22white%22%20opacity%3D%220.2%22%2F%3E%3Ccircle%20cx%3D%22565.0%22%20cy%3D%22721.5%22%20r%3D%221.2%22%20fill%3D%22white%22%20opacity%3D%220.32%22%2F%3E%3Ccircle%20cx%3D%221165.5%22%20cy%3D%22535.0%22%20r%3D%220.86%22%20fill%3D%22white%22%20opacity%3D%220.43%22%2F%3E%3Ccircle%20cx%3D%221114.0%22%20cy%3D%22282.4%22%20r%3D%221.09%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%22640.0%22%20cy%3D%22238.4%22%20r%3D%220.6%22%20fill%3D%22white%22%20opacity%3D%220.09%22%2F%3E%3Ccircle%20cx%3D%22735.6%22%20cy%3D%22556.8%22%20r%3D%220.98%22%20fill%3D%22white%22%20opacity%3D%220.08%22%2F%3E%3Ccircle%20cx%3D%22570.5%22%20cy%3D%22587.6%22%20r%3D%221.02%22%20fill%3D%22white%22%20opacity%3D%220.32%22%2F%3E%3Ccircle%20cx%3D%22889.0%22%20cy%3D%22177.3%22%20r%3D%220.68%22%20fill%3D%22white%22%20opacity%3D%220.24%22%2F%3E%3Ccircle%20cx%3D%221283.1%22%20cy%3D%22718.4%22%20r%3D%220.71%22%20fill%3D%22white%22%20opacity%3D%220.37%22%2F%3E%3Ccircle%20cx%3D%22855.1%22%20cy%3D%22389.0%22%20r%3D%221.19%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%221086.4%22%20cy%3D%22499.9%22%20r%3D%221.05%22%20fill%3D%22white%22%20opacity%3D%220.39%22%2F%3E%3Ccircle%20cx%3D%22299.1%22%20cy%3D%22877.0%22%20r%3D%220.93%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%221085.4%22%20cy%3D%2245.4%22%20r%3D%220.89%22%20fill%3D%22white%22%20opacity%3D%220.35%22%2F%3E%3Ccircle%20cx%3D%22637.3%22%20cy%3D%22808.9%22%20r%3D%221.2%22%20fill%3D%22white%22%20opacity%3D%220.25%22%2F%3E%3Ccircle%20cx%3D%221053.1%22%20cy%3D%22656.0%22%20r%3D%220.75%22%20fill%3D%22white%22%20opacity%3D%220.28%22%2F%3E%3Ccircle%20cx%3D%22796.6%22%20cy%3D%22231.6%22%20r%3D%220.47%22%20fill%3D%22white%22%20opacity%3D%220.11%22%2F%3E%3Ccircle%20cx%3D%2282.5%22%20cy%3D%22153.1%22%20r%3D%221.22%22%20fill%3D%22white%22%20opacity%3D%220.13%22%2F%3E%3Ccircle%20cx%3D%221333.7%22%20cy%3D%22750.2%22%20r%3D%221.28%22%20fill%3D%22white%22%20opacity%3D%220.28%22%2F%3E%3Ccircle%20cx%3D%22597.9%22%20cy%3D%22545.8%22%20r%3D%221.28%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%22601.9%22%20cy%3D%22667.0%22%20r%3D%220.56%22%20fill%3D%22white%22%20opacity%3D%220.31%22%2F%3E%3Ccircle%20cx%3D%22486.2%22%20cy%3D%2261.6%22%20r%3D%220.75%22%20fill%3D%22white%22%20opacity%3D%220.32%22%2F%3E%3Ccircle%20cx%3D%22841.3%22%20cy%3D%22645.6%22%20r%3D%220.59%22%20fill%3D%22white%22%20opacity%3D%220.25%22%2F%3E%3Ccircle%20cx%3D%22858.4%22%20cy%3D%22497.5%22%20r%3D%220.56%22%20fill%3D%22white%22%20opacity%3D%220.34%22%2F%3E%3Ccircle%20cx%3D%221179.7%22%20cy%3D%22353.7%22%20r%3D%220.59%22%20fill%3D%22white%22%20opacity%3D%220.36%22%2F%3E%3Ccircle%20cx%3D%22874.9%22%20cy%3D%22375.8%22%20r%3D%220.56%22%20fill%3D%22white%22%20opacity%3D%220.24%22%2F%3E%3Ccircle%20cx%3D%22976.7%22%20cy%3D%22289.5%22%20r%3D%220.43%22%20fill%3D%22white%22%20opacity%3D%220.29%22%2F%3E%3Ccircle%20cx%3D%22897.8%22%20cy%3D%22276.2%22%20r%3D%221.34%22%20fill%3D%22white%22%20opacity%3D%220.27%22%2F%3E%3Ccircle%20cx%3D%221093.1%22%20cy%3D%22786.6%22%20r%3D%220.63%22%20fill%3D%22white%22%20opacity%3D%220.18%22%2F%3E%3Ccircle%20cx%3D%22616.1%22%20cy%3D%22341.2%22%20r%3D%221.04%22%20fill%3D%22white%22%20opacity%3D%220.12%22%2F%3E%3Ccircle%20cx%3D%221148.5%22%20cy%3D%22686.3%22%20r%3D%220.64%22%20fill%3D%22white%22%20opacity%3D%220.38%22%2F%3E%3Ccircle%20cx%3D%22120.2%22%20cy%3D%22624.6%22%20r%3D%220.74%22%20fill%3D%22white%22%20opacity%3D%220.39%22%2F%3E%3Ccircle%20cx%3D%22385.3%22%20cy%3D%22474.4%22%20r%3D%220.99%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%22195.4%22%20cy%3D%22773.0%22%20r%3D%220.9%22%20fill%3D%22white%22%20opacity%3D%220.21%22%2F%3E%3Ccircle%20cx%3D%22895.3%22%20cy%3D%22102.3%22%20r%3D%220.64%22%20fill%3D%22white%22%20opacity%3D%220.08%22%2F%3E%3Ccircle%20cx%3D%221107.1%22%20cy%3D%22191.2%22%20r%3D%220.93%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%22812.0%22%20cy%3D%2292.7%22%20r%3D%221.26%22%20fill%3D%22white%22%20opacity%3D%220.43%22%2F%3E%3Ccircle%20cx%3D%22708.2%22%20cy%3D%22531.9%22%20r%3D%221.04%22%20fill%3D%22white%22%20opacity%3D%220.31%22%2F%3E%3Ccircle%20cx%3D%22694.8%22%20cy%3D%2214.3%22%20r%3D%220.55%22%20fill%3D%22white%22%20opacity%3D%220.44%22%2F%3E%3Ccircle%20cx%3D%221281.3%22%20cy%3D%22584.4%22%20r%3D%221.02%22%20fill%3D%22white%22%20opacity%3D%220.42%22%2F%3E%3Ccircle%20cx%3D%221029.3%22%20cy%3D%2218.8%22%20r%3D%221.19%22%20fill%3D%22white%22%20opacity%3D%220.12%22%2F%3E%3Ccircle%20cx%3D%221052.6%22%20cy%3D%22583.0%22%20r%3D%221.11%22%20fill%3D%22white%22%20opacity%3D%220.37%22%2F%3E%3C%2Fsvg%3E'); animation: twinkle-b 9s ease-in-out infinite; }
@keyframes twinkle-a { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@keyframes twinkle-b { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  body::before, body::after { animation: none; }
}
.serif { font-family: var(--serif); }

/* ============ Chassis ============ */
.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* ---- Sidebar ---- */
.sidebar {
  background: rgba(5, 4, 12, 0.45);
  border-right: 1px solid var(--rule-strong);
  display: flex;
  flex-direction: column;
  padding: 22px 0 14px;
  user-select: none;
}
.brand { padding: 0 20px 18px; }
.eyebrow {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}
.wordmark {
  font-family: var(--display);
  font-size: 32px;
  font-weight: 800;
  text-transform: uppercase;
  margin: 2px 0 0;
  line-height: 1.05;
  letter-spacing: 0.015em;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.brand-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }

#nav { display: flex; flex-direction: column; gap: 3px; padding: var(--space-2) 14px 0; }
#nav button {
  appearance: none;
  background: none;
  border: none;
  border-radius: 8px;
  text-align: left;
  padding: 8px 13px;
  font-family: var(--display);
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-2);
  cursor: pointer;
  position: relative;
  transition: background 0.15s ease, color 0.15s ease;
}
#nav button:hover { background: var(--accent-soft); color: var(--ink); }
#nav button.active {
  background: var(--sheet);
  color: var(--accent);
  font-weight: 650;
  box-shadow: var(--shadow);
}
#nav button.active::before {
  content: "";
  position: absolute;
  left: -14px;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--accent-grad);
}
#nav button .nav-count {
  float: right;
  font-size: 11px;
  font-weight: 650;
  color: var(--critical-text);
  font-variant-numeric: tabular-nums;
}

.side-foot { margin-top: auto; padding: 14px 20px 4px; border-top: 1px solid var(--rule); }
.countdown { font-family: var(--serif); }
.countdown .big {
  font-family: var(--display);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}
.countdown .sub { font-size: 12px; color: var(--ink-2); font-family: var(--sans); margin-top: 2px; }
.side-actions { display: flex; gap: 6px; margin-top: 12px; }
.side-btn {
  appearance: none;
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  padding: 4px 9px;
  font-size: 11.5px;
  color: var(--ink-2);
  cursor: pointer;
}
.side-btn:hover { color: var(--ink); border-color: var(--muted); }
.side-btn.theme { margin-left: auto; }

/* ---- Main ---- */
#main { overflow-y: auto; flex: 1; }
#view-root { max-width: 1020px; margin: 0 auto; padding: 30px 34px 60px; }

.view-head { margin-bottom: 20px; }
.view-head h2 {
  font-family: var(--display);
  font-size: 40px;
  font-weight: 800;
  text-transform: uppercase;
  margin: 0;
  line-height: 1.02;
  letter-spacing: 0.005em;
  text-wrap: balance;
}
.view-head .sub { color: var(--ink-2); margin-top: 3px; max-width: 64ch; }
.view-head .head-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.deck-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.deck-actions-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.deck-actions-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-right: 2px;
}

/* ============ Paper panels ============ */
.panel {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-4) calc(18px * var(--dens));
  box-shadow: var(--shadow);
}
.panel.ruled { background-image: none; }
.panel h3 {
  margin: 0 0 var(--space-3);
  font-family: var(--display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  display: flow-root;
}
.panel h3 .h3-action { float: right; letter-spacing: 0; text-transform: none; margin-left: 14px; }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.stack { display: flex; flex-direction: column; gap: var(--space-4); }

/* ============ Buttons, inputs ============ */
.btn {
  appearance: none;
  background: var(--accent-grad);
  color: var(--accent-ink);
  border: none;
  border-radius: var(--radius-sm);
  padding: calc(7px * var(--dens)) calc(15px * var(--dens));
  font-family: var(--display);
  font-size: calc(13.5px * var(--tscale));
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  cursor: pointer;
  transition: filter var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
}
.btn:hover { filter: brightness(1.1); }
.btn:active { transform: scale(var(--scale-press)); filter: brightness(1.05); }
.btn.small { padding: calc(4px * var(--dens)) calc(10px * var(--dens)); font-size: var(--text-sm); }
.ghost {
  appearance: none;
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-xs);
  padding: calc(6px * var(--dens)) calc(12px * var(--dens));
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--dur-press) var(--ease-out), border-color var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.ghost:hover { color: var(--ink); border-color: var(--muted); }
.ghost:active { transform: scale(var(--scale-press)); }
.ghost.small { padding: calc(3px * var(--dens)) calc(9px * var(--dens)); font-size: var(--text-xs); }
.ghost.danger:hover { color: var(--critical-text); border-color: var(--critical-text); }
.icon-btn {
  appearance: none;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 15px;
  cursor: pointer;
  padding: 3px 6px;
  border-radius: 5px;
  line-height: 1;
  flex: none;
  transition: color var(--dur-press) var(--ease-out), background var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.icon-btn:hover { color: var(--critical-text); background: var(--accent-soft); }
.icon-btn:active { transform: scale(var(--scale-press)); }
.link-btn {
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
  font-size: 12.5px;
}
.link-btn:hover { text-decoration: underline; }

input, select, textarea {
  font: inherit;
  color: var(--ink);
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  padding: 6px 9px;
  transition: border-color var(--dur-press) var(--ease-out), box-shadow var(--dur-press) var(--ease-out);
}
input:focus-visible, select:focus-visible, textarea:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  transition: outline-color var(--dur-press) var(--ease-out);
}
input[type="checkbox"] {
  accent-color: var(--accent);
  width: 16px;
  height: 16px;
  padding: 0;
  flex: none;
}
label.f { display: flex; flex-direction: column; gap: 3px; font-size: 11.5px; color: var(--muted); }
.form-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }

.day-toggles { display: flex; gap: 4px; }
.day-toggles button {
  appearance: none;
  font-size: 12.5px;
  font-weight: 650;
  width: 32px;
  height: 30px;
  border-radius: 6px;
  border: 1px solid var(--rule-strong);
  background: var(--sheet-raised);
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-press) var(--ease-out), border-color var(--dur-press) var(--ease-out),
              color var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
}
.day-toggles button:active { transform: scale(var(--scale-press)); }
.day-toggles button.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* ============ Rows / lists ============ */
ul.rows { list-style: none; margin: 0; padding: 0; }
ul.rows li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--rule);
}
ul.rows li:last-child { border-bottom: none; }
.grow { flex: 1; min-width: 0; }
.row-title { font-weight: 550; overflow-wrap: anywhere; }
.row-title.done { text-decoration: line-through; color: var(--muted); font-weight: 450; }
.row-meta { font-size: var(--text-xs); color: var(--ink-2); overflow-wrap: anywhere; }
.time-col { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--ink-2); white-space: nowrap; }
.empty { color: var(--muted); padding: 6px 0; font-size: 13px; }

.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--c, var(--rule-strong)); }

.class-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 650;
  color: var(--ink-2);
  white-space: nowrap;
}
.class-tag .dot { width: 8px; height: 8px; }

.pill {
  font-size: 10.5px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
  flex: none;
}
.pill.overdue { background: var(--critical); color: #fff; }
.pill.today { background: var(--accent); color: var(--accent-ink); }
.pill.soft { background: var(--accent-soft); color: var(--accent); }

/* ============ Today ============ */
.hero {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 18px 20px;
  border-left: 3px solid var(--accent);
}
.hero .big-line { font-family: var(--serif); font-size: 24px; font-weight: 700; }
.hero .sub { color: var(--ink-2); margin-top: 2px; }
.hero-side { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
.hero-side label { font-size: 11px; color: var(--muted); }

.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
.stat {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 14px 16px;
  box-shadow: var(--shadow);
}
.stat .v { font-family: var(--display); font-size: 32px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat .v .warn { font-size: 12px; color: var(--critical-text); font-weight: 650; font-family: var(--sans); }
.stat .k { font-size: 11.5px; color: var(--muted); margin-top: 1px; }

.meter { height: 5px; border-radius: 3px; background: var(--rule); overflow: hidden; margin-top: 8px; }
.meter > div { height: 100%; border-radius: 3px; background: var(--accent); transition: width 0.3s ease; }

.today-class-links { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 3px; }

/* ============ Link chips ============ */
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
  max-width: 100%;
}
.chip:hover { border-color: var(--accent); background: var(--accent-soft); }
.chip .x { color: var(--muted); font-weight: 400; margin-left: 2px; }
.chip .x:hover { color: var(--critical-text); }
.chip.add { color: var(--ink-2); border-style: dashed; }
.chip span.lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ============ Weekly schedule (day-focused) ============
   One day's classes as a poster-style list rather than a Mon-Fri grid of
   absolutely-positioned blocks — the day tabs reuse .class-tabs/.class-tab
   verbatim (same component the class-detail page and Settings already use).
   Each row leads with a giant hour numeral — the same big-digits treatment
   as the Focus Timer elsewhere in the app — rather than a fixed-width time
   string: a previous version hardcoded a pixel width sized against a
   browser-preview font and it silently overflowed onto the class name once
   rendered in the real app's actual "Avenir Next Condensed". Nothing here
   has a hardcoded width; every column either sizes to its own content
   (the hour, the duration) or is the flexible middle column with
   min-width: 0 + ellipsis, so it can never spill into a neighbor. */
.class-tabs + .day-schedule,
.class-tabs + p.empty { margin-top: 14px; }
.day-schedule { display: flex; flex-direction: column; gap: 8px; }
.day-sched-row {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 20px;
  width: 100%;
  padding: 14px 20px;
  border: none;
  border-left: 3px solid var(--c, var(--accent));
  border-radius: 10px;
  background: color-mix(in srgb, var(--c, var(--accent)) 12%, var(--sheet-raised));
  text-align: left;
  cursor: pointer;
  transition: filter var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
}
.day-sched-row:hover { filter: brightness(1.15); }
.day-sched-row:active { transform: scale(var(--scale-press)); }
.day-sched-hour {
  flex: none;
  text-align: center;
}
.day-sched-hour b {
  display: block;
  font-family: var(--display);
  font-size: 40px;
  font-weight: 800;
  line-height: 0.9;
  color: var(--c, var(--accent));
}
.day-sched-ampm {
  display: block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 2px;
}
.day-sched-info { flex: 1; min-width: 0; }
.day-sched-code {
  display: block;
  font-family: var(--display);
  font-size: 17px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.day-sched-sub {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 13px;
  color: var(--ink-2);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.day-sched-dur {
  flex: none;
  text-align: right;
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
}
.day-sched-dur b {
  display: block;
  font-family: var(--display);
  font-size: 22px;
  font-weight: 700;
  color: var(--ink);
}
.schedule-unscheduled { margin: 10px 0 0; }

/* ============ Classes ============ */
.class-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-4); }
.class-card {
  position: relative;
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: 16px;
  padding: 14px 16px 14px 22px;
  box-shadow: var(--shadow);
  cursor: pointer;
  overflow: hidden;
  text-align: left;
  transition: border-color var(--dur-small) var(--ease-out), transform var(--dur-small) var(--ease-out),
              box-shadow var(--dur-small) var(--ease-out), opacity var(--dur-small) var(--ease-out);
}
.class-card::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 7px;
  background: var(--c, var(--rule-strong));
}
.class-card:hover {
  border-color: var(--rule-strong);
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3), 0 20px 40px rgba(0, 0, 0, 0.3);
}
.class-card:active { transform: translateY(-1px) scale(var(--scale-press)); }
.class-card .code { font-family: var(--display); font-size: 19px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; }
.class-card .cname { color: var(--ink-2); font-size: 12.5px; margin-top: 1px; overflow-wrap: anywhere; }
.class-card .meta { font-size: 12px; color: var(--muted); margin-top: 8px; line-height: 1.55; }
.class-card .badges { margin-top: 8px; display: flex; gap: 6px; flex-wrap: wrap; }
.mini-badge {
  font-size: 10.5px;
  font-weight: 650;
  color: var(--ink-2);
  background: var(--accent-soft);
  border-radius: 999px;
  padding: 2px 8px;
}
.class-card.add-card {
  border-style: dashed;
  border-color: var(--rule-strong);
  color: var(--ink-2);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 110px;
  font-weight: 600;
  background: none;
}
.class-card.add-card::before { display: none; }
.class-card.add-card:hover { color: var(--accent); border-color: var(--accent); }

/* Class detail */
.back-row { margin-bottom: 12px; }
.detail-head {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 18px 20px;
  border-left: 7px solid var(--c, var(--rule-strong));
}
.detail-head .code { font-family: var(--display); font-size: 30px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.01em; line-height: 1.1; }
.detail-head .cname { color: var(--ink-2); }
.detail-head .meta { font-size: 12.5px; color: var(--muted); margin-top: 6px; line-height: 1.6; }
.detail-head .actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
a.mail { color: var(--accent); text-decoration: none; font-weight: 600; }
a.mail:hover { text-decoration: underline; }

.file-row-name { font-weight: 550; overflow-wrap: anywhere; }
.file-actions { display: flex; gap: 2px; flex: none; }
.file-actions .icon-btn { font-size: 13px; color: var(--ink-2); }
.file-actions .icon-btn:hover { color: var(--accent); background: var(--accent-soft); }
.file-actions .icon-btn.del:hover { color: var(--critical-text); }

/* ============ Assignments ============ */
.group-h {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 18px 0 6px;
}
.group-h.first { margin-top: 0; }
.group-h.danger { color: var(--critical-text); }

/* ============ Dining (collapsible location groups) ============ */
.dining-group-h {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin: 18px 0 6px;
  padding: 4px 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
  cursor: pointer;
}
.dining-group-h:first-child { margin-top: 0; }
.dining-group-h:hover { color: var(--ink-2); }
.dining-group-h .row-meta {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  flex: none;
}
.dining-group-chevron { flex: none; font-size: 10px; width: 10px; text-align: center; }

/* ============ Week ahead ============ */
.week-day { padding: 12px 0; border-bottom: 1px solid var(--rule); }
.week-day:last-child { border-bottom: none; padding-bottom: 0; }
.week-day:first-child { padding-top: 0; }
.week-day-head {
  margin: 0 0 6px;
  font-family: var(--display);
  font-size: 15px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.week-day.is-today .week-day-head { color: var(--accent); }
.week-day .empty { padding: 0; }

/* ============ Calendar ============ */
.calendar-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.calendar-actions { display: flex; gap: 7px; flex: none; align-items: center; }
/* Week/Month switch. Padded off from the month arrows beside it so the two
   controls don't read as one row of five equal buttons. */
.cal-mode-toggle { display: flex; gap: 4px; }
.calendar-actions .cal-mode-toggle + button { margin-left: 5px; }
.calendar-legend { display: flex; gap: 14px; flex-wrap: wrap; margin: -8px 0 12px; color: var(--muted); font-size: 11px; }
.calendar-legend span::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--c); }
.calendar-legend .class { --c: var(--s7); }
.calendar-legend .assignment { --c: var(--s4); }
.calendar-legend .exam { --c: var(--critical); }
.calendar-legend .milestone { --c: var(--s1); }
.calendar-legend .campus { --c: var(--s6); }
.calendar-legend .personal { --c: var(--s5); }
.calendar-legend .email { --c: var(--s3); }
.calendar-legend .pending { --c: var(--warn); }
.calendar-scroll { padding: 0; overflow-x: auto; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, minmax(88px, 1fr)); min-width: 620px; }
.calendar-weekday { padding: 8px 10px; border-bottom: 1px solid var(--rule-strong); color: var(--muted); font-family: var(--display); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.calendar-day { min-height: 116px; padding: 7px; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.calendar-day:nth-child(7n) { border-right: none; }
.calendar-day.outside { opacity: 0.42; }
.calendar-date { width: 24px; height: 24px; margin-bottom: 4px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-2); font-size: 12px; font-variant-numeric: tabular-nums; }
.calendar-day.is-today .calendar-date { background: var(--accent-grad); color: #fff; font-weight: 700; }
.calendar-item { display: block; width: 100%; margin: 2px 0; padding: 3px 5px; overflow: hidden; border: 0; border-left: 2px solid var(--c); border-radius: 4px; background: color-mix(in srgb, var(--c) 10%, transparent); color: var(--ink); font-size: 10.5px; line-height: 1.25; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
button.calendar-item { cursor: pointer; }
button.calendar-item:hover { background: color-mix(in srgb, var(--c) 20%, transparent); }
.calendar-item span { margin-right: 4px; color: var(--c); font-size: 9px; font-weight: 700; text-transform: uppercase; }
.calendar-item.class { --c: var(--s7); }
.calendar-item.assignment { --c: var(--s4); }
.calendar-item.exam { --c: var(--critical); }
.calendar-item.milestone { --c: var(--s1); }
.calendar-item.campus { --c: var(--s6); }
.calendar-item.personal { --c: var(--s5); }
.calendar-item.email { --c: var(--s3); }
.calendar-item.pending { --c: var(--warn); }
.calendar-item.is-done { opacity: 0.48; text-decoration: line-through; }
.calendar-more { padding: 2px 5px; color: var(--muted); font-size: 10px; }
@media (max-width: 700px) {
  .calendar-head { align-items: flex-start; flex-direction: column; }
}

/* ============ Study ============ */
.workspace-view { position:relative; max-width:1280px!important; padding:28px 30px 64px!important; }
.workspace-week{--workspace-accent:#9f8cff;--workspace-hot:#7bc7ff}.workspace-calendar{--workspace-accent:#79c7ff;--workspace-hot:#f7d86a}.workspace-classes,.workspace-class{--workspace-accent:#f39acb;--workspace-hot:#9f8cff}.workspace-lectures{--workspace-accent:#86c8ff;--workspace-hot:#b292ff}.workspace-assignments{--workspace-accent:#ffd46b;--workspace-hot:#ff9fbf}.workspace-campus{--workspace-accent:#80ddc2;--workspace-hot:#8cbcff}.workspace-gpa{--workspace-accent:#b496ff;--workspace-hot:#76d6ff}.workspace-checklist{--workspace-accent:#ffd46b;--workspace-hot:#91dfb6}.workspace-email{--workspace-accent:#9b92ff;--workspace-hot:#f2a1ca}.workspace-review{--workspace-accent:#f2a1ca;--workspace-hot:#ffd46b}.workspace-contacts{--workspace-accent:#80ddc2;--workspace-hot:#9c9aff}.workspace-settings{--workspace-accent:#b496ff;--workspace-hot:#8cc9ff}
.workspace-view::before { content:""; position:absolute; z-index:-1; width:650px; height:440px; top:-180px; right:-240px; border-radius:50%; background:radial-gradient(closest-side,rgba(159,110,240,.17),transparent 72%); pointer-events:none; }
.workspace-view .view-head { position:relative; margin:8px 0 28px; padding:0 0 18px; border-bottom:1px solid var(--rule); }
.workspace-view .view-head::before { display:block; margin-bottom:7px; color:var(--accent); font-size:10px; font-weight:800; letter-spacing:.15em; text-transform:uppercase; }
.workspace-week .view-head::before { content:"GCUHQ / Weekly command"; }.workspace-calendar .view-head::before { content:"GCUHQ / Schedule"; }.workspace-classes .view-head::before { content:"GCUHQ / Course rooms"; }.workspace-lectures .view-head::before { content:"GCUHQ / Lecture library"; }.workspace-assignments .view-head::before { content:"GCUHQ / Work queue"; }.workspace-campus .view-head::before { content:"GCUHQ / Campus life"; }.workspace-gpa .view-head::before { content:"GCUHQ / Academic standing"; }.workspace-checklist .view-head::before { content:"GCUHQ / Launch checklist"; }.workspace-email .view-head::before { content:"GCUHQ / Mail room"; }.workspace-review .view-head::before { content:"GCUHQ / Weekly signal"; }.workspace-contacts .view-head::before { content:"GCUHQ / People to call"; }.workspace-settings .view-head::before { content:"GCUHQ / Control room"; }
.workspace-view .view-head h2 { max-width:13ch; font-size:clamp(46px,6vw,72px); line-height:.9; letter-spacing:.005em; }
.workspace-view .view-head .sub { margin-top:10px; max-width:58ch; font-family:var(--serif); font-size:15px; font-style:italic; line-height:1.4; }
.workspace-view .calendar-head { align-items:flex-end; }.workspace-view .calendar-head .calendar-actions { align-self:flex-end; }
.workspace-view .panel{border-radius:20px;box-shadow:0 4px 0 rgba(7,5,17,.22),0 15px 30px rgba(7,4,18,.22)}.workspace-hero { position:relative; overflow:hidden; border:2px solid color-mix(in srgb,var(--workspace-accent) 55%,rgba(255,255,255,.1)); border-radius:24px; background:radial-gradient(420px 240px at 100% 0%,color-mix(in srgb,var(--workspace-accent) 20%,transparent),transparent 70%),rgba(25,22,45,.82); box-shadow:0 5px 0 color-mix(in srgb,var(--workspace-accent) 20%,transparent),0 20px 42px rgba(7,4,18,.3); }
.workspace-hero>*{position:relative;z-index:1}
.workspace-hero::after { content:""; position:absolute; width:160px; height:160px; right:-68px; top:-74px; border:1px solid rgba(217,201,255,.18); border-radius:50%; box-shadow:0 0 0 25px rgba(169,137,246,.025); pointer-events:none; }
.workspace-view .panel { border-color:rgba(255,255,255,.09); }
.workspace-view .panel h3 { color:var(--ink-2); }
.workspace-view .grid-2 { gap:18px; }.workspace-view .stack { gap:18px; }
.workspace-view .class-tabs { margin-bottom:18px; padding:6px; border:1px solid color-mix(in srgb,var(--workspace-accent) 28%,var(--rule)); border-radius:16px; background:rgba(12,10,24,.44); box-shadow:inset 0 1px rgba(255,255,255,.04); }
.workspace-view .class-tab,.workspace-view .btn,.workspace-view .ghost { border-radius:999px; }.workspace-view .class-tab.active{background:color-mix(in srgb,var(--workspace-accent) 26%,var(--sheet-raised));color:var(--ink);box-shadow:0 3px 0 color-mix(in srgb,var(--workspace-accent) 22%,transparent)}.workspace-view .btn{background:color-mix(in srgb,var(--workspace-accent) 55%,#695ce0);border-color:color-mix(in srgb,var(--workspace-accent) 72%,white)}.workspace-view .ghost:hover{border-color:var(--workspace-hot);background:color-mix(in srgb,var(--workspace-hot) 12%,transparent)}
.workspace-week .week-day { position:relative; padding:17px 0 17px 19px; border-left:2px solid rgba(124,134,242,.18); }.workspace-week .week-day.is-today { border-left-color:var(--accent); background:linear-gradient(90deg,rgba(124,134,242,.08),transparent 55%); }
.workspace-calendar .calendar-legend { margin:0 0 14px; padding:9px 12px; border:1px solid var(--rule); border-radius:10px; background:rgba(12,10,24,.32); }.workspace-calendar .calendar-day { min-height:130px; }
.workspace-calendar .calendar-scroll.workspace-hero { overflow-x:auto; overflow-y:hidden; }
.workspace-classes .class-grid { gap:14px; }.workspace-classes .class-card { min-height:148px; padding:18px 19px 17px 25px; border-radius:21px; background:radial-gradient(190px 120px at 100% 0%,color-mix(in srgb,var(--c) 13%,transparent),transparent 78%),var(--sheet); box-shadow:0 4px 0 color-mix(in srgb,var(--c) 18%,transparent),0 14px 26px rgba(7,4,18,.2); }.workspace-classes .class-card .code { font-size:24px; }.workspace-classes .class-card:hover { border-color:color-mix(in srgb,var(--c) 55%,var(--rule-strong)); }
.workspace-class .detail-head { min-height:152px; align-items:flex-end; border-left-width:8px; background:radial-gradient(330px 190px at 100% 0%,color-mix(in srgb,var(--c) 18%,transparent),transparent 74%),rgba(25,22,45,.76); }.workspace-class .detail-head::before { content:"COURSE WORKSPACE"; position:absolute; top:18px; left:20px; color:var(--c); font-size:10px; font-weight:800; letter-spacing:.14em; }.workspace-class .detail-head > div:first-child { padding-top:18px; }.workspace-class .detail-head .code { font-size:clamp(32px,5vw,52px); }
.workspace-lectures .lask { margin-bottom:18px; border-color:rgba(190,165,255,.28); background:radial-gradient(380px 180px at 100% 0%,rgba(124,134,242,.16),transparent 75%),rgba(25,22,45,.76); }.workspace-lectures .lask h3 { color:var(--accent); }.workspace-lectures #lec-search { padding:12px 14px; border-radius:10px; background:rgba(8,7,16,.44); }
.workspace-assignments .group-h { margin:24px 0 8px; padding-left:12px; border-left:2px solid var(--accent); }.workspace-assignments .group-h.danger { border-left-color:var(--critical); }.workspace-assignments .workspace-hero { padding:20px; }
.workspace-campus .app-tile { min-height:100px; padding:16px; border-radius:14px; background:radial-gradient(150px 90px at 100% 0%,rgba(124,134,242,.11),transparent 75%),var(--sheet-raised); transition:transform var(--dur-small) var(--ease-out),border-color var(--dur-small) var(--ease-out),background var(--dur-small) var(--ease-out); }.workspace-campus .app-tile:hover { transform:translateY(-2px); }.workspace-campus .app-tile-name { font-family:var(--display); font-size:18px; text-transform:uppercase; letter-spacing:.025em; }
.workspace-gpa .gpa-grid { padding:8px 0 14px; }.workspace-gpa .gpa-value { font-size:clamp(48px,5vw,70px); }.workspace-gpa .proj { border-color:rgba(190,165,255,.24); background:radial-gradient(360px 190px at 100% 0%,rgba(159,110,240,.13),transparent 75%),var(--sheet); }
.workspace-checklist .workspace-hero { padding:24px; }.workspace-checklist .meter { height:12px; border-radius:999px; overflow:hidden; background:rgba(255,255,255,.08); }.workspace-checklist .meter > div { background:var(--accent-grad); }
.workspace-email .email-inbox,.workspace-email .email-reader,.workspace-email .email-compose { border-color:rgba(190,165,255,.22); box-shadow:0 18px 45px rgba(7,4,18,.25); }.workspace-email .email-toolbar { background:radial-gradient(340px 140px at 100% 0%,rgba(124,134,242,.14),transparent 75%),rgba(19,18,32,.72); }.workspace-email .email-organize { border-left:3px solid var(--s6); }
.workspace-review .review-score-panel { min-height:156px; padding:24px; border-color:rgba(190,165,255,.26); background:radial-gradient(330px 190px at 100% 0%,rgba(159,110,240,.15),transparent 75%),rgba(25,22,45,.76); }.workspace-review .review-score { width:118px; height:118px; }.workspace-review .review-band { font-size:28px; }
.workspace-contacts .panel { border-left:4px solid var(--workspace-accent); }.workspace-contacts .rows > li { padding:14px 0; }.workspace-settings .class-tabs { margin-bottom:18px; }.workspace-settings .health-fold { border-radius:14px; background:rgba(9,8,19,.28); }
.study-workspace { max-width:1280px!important; padding:24px 30px 64px!important; }
.study-stage{position:relative}.study-stage:before{content:"";position:absolute;z-index:-1;width:620px;height:430px;top:-150px;right:-190px;border-radius:50%;background:radial-gradient(closest-side,rgba(159,110,240,.19),transparent 72%)}
.study-stage-head,.study-library-head,.study-focus-head,.study-section-head,.study-now,.study-dashboard{display:flex;align-items:center;justify-content:space-between;gap:var(--space-5)}.study-stage-head{align-items:flex-end;margin:10px 0 28px}.study-stage-head h2{margin:4px 0 0;font-family:var(--display);font-size:clamp(44px,6.5vw,74px);font-weight:800;line-height:.9;text-transform:uppercase}.study-stage-head p,.study-section-head p{margin:10px 0 0;color:var(--ink-2);max-width:48ch}.study-stage-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.study-now{position:relative;overflow:hidden;min-height:150px;padding:22px 26px;border:1px solid rgba(190,165,255,.32);border-radius:20px;background:linear-gradient(115deg,rgba(124,134,242,.23),rgba(159,110,240,.1) 55%,rgba(19,18,32,.42)),var(--sheet);box-shadow:0 20px 52px rgba(8,4,20,.28)}.study-now:after{content:"";position:absolute;width:220px;height:220px;right:-74px;top:-104px;border:1px solid rgba(217,201,255,.25);border-radius:50%;box-shadow:0 0 0 32px rgba(169,137,246,.035),0 0 0 64px rgba(169,137,246,.025)}.study-now-mark{position:relative;z-index:1;width:88px;height:88px;border-radius:26px;display:grid;place-content:center;flex:none;text-align:center;color:#fff;background:var(--accent-grad);box-shadow:0 12px 30px rgba(133,111,240,.32)}.study-now-mark span{display:block;font-family:var(--display);font-size:41px;font-weight:800;line-height:.8}.study-now-mark small{margin-top:6px;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.study-now-copy{position:relative;z-index:1;flex:1;min-width:0}.study-now-copy h3,.study-section-head h3,.study-library h3,.study-focus h3{margin:3px 0 0;font-family:var(--display);font-size:24px;font-weight:700;line-height:1;text-transform:uppercase;color:var(--ink)}.study-now-copy p{margin:7px 0 0;color:var(--ink-2);max-width:56ch}.study-now-action{position:relative;z-index:1;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.study-section-head{align-items:flex-end;margin:34px 0 12px}.study-section-head p{font-family:var(--serif);font-style:italic;text-align:right}.study-mode-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:10px}.study-mode-card{appearance:none;position:relative;min-height:212px;overflow:hidden;display:flex;flex-direction:column;align-items:flex-start;padding:18px;color:var(--ink);text-align:left;border:1px solid var(--rule-strong);border-radius:16px;background:rgba(22,19,42,.72);box-shadow:var(--shadow);cursor:pointer;transition:transform .16s ease,border-color .16s ease,background .16s ease}.study-mode-card:after{content:"";position:absolute;width:154px;height:154px;right:-66px;bottom:-80px;border:1px solid rgba(255,255,255,.14);border-radius:50%}.study-mode-card:hover{transform:translateY(-3px);border-color:var(--mode,var(--accent));background:color-mix(in srgb,var(--mode,var(--accent)) 13%,var(--sheet-raised))}.study-mode-card:focus-visible{outline:3px solid var(--accent);outline-offset:3px}.mode-flash{--mode:#a98af5}.mode-test{--mode:#7caff4}.mode-match{--mode:#e398bd}.mode-guide{--mode:#45c4b0}.mode-quiz{--mode:#f0b35a}.mode-index{color:var(--mode);font-size:10px;font-weight:800;letter-spacing:.13em}.mode-glyph{width:48px;height:48px;margin-top:16px;display:grid;place-items:center;border:1px solid color-mix(in srgb,var(--mode) 52%,transparent);border-radius:13px;color:var(--mode);font-family:var(--display);font-size:20px;font-weight:800;background:color-mix(in srgb,var(--mode) 12%,transparent)}.mode-body{margin-top:15px;display:flex;flex-direction:column;gap:4px}.mode-body strong{font-family:var(--display);font-size:21px;font-weight:700;line-height:1;text-transform:uppercase}.mode-body small{color:var(--ink-2);font-size:12px;line-height:1.35}.mode-action{margin-top:auto;color:var(--mode);font-size:11px;font-weight:800;letter-spacing:.1em;text-transform:uppercase}.mode-action:after{content:"  →"}
.study-dashboard{align-items:start;margin-top:26px}.study-library{flex:1;min-width:0;padding:20px}.study-library-head{align-items:flex-start}.study-library-stack{margin-top:20px}.study-deck-search{width:100%;margin-top:18px}.study-library .lib-section+.lib-section{margin-top:22px;padding-top:18px}.study-library .rows>li{padding:11px 0}.study-library .deck-group-head{margin-bottom:6px}.study-focus{position:sticky;top:20px;width:min(100%,320px);padding:20px;background:linear-gradient(160deg,rgba(42,36,79,.78),rgba(19,18,32,.68))}.study-focus-head{align-items:flex-start}.focus-pulse{width:10px;height:10px;margin-top:3px;border-radius:50%;background:var(--good);box-shadow:0 0 0 5px rgba(62,207,142,.13)}.study-focus .timer-face{margin-top:26px;font-size:clamp(58px,6vw,74px)}.study-focus .timer-controls{margin-top:18px}.study-focus .timer-set{padding-bottom:16px;border-bottom:1px solid var(--rule)}.study-focus .streak-row{margin-top:16px}
/* Focus Timer and the six Study Library sections all sit as siblings in
   one grid (not a two-column split) — a 0.9fr/1.1fr pairing forced a short
   Focus Timer card and a much taller Study Library column to share row
   height, so most of a scroll went past dead space beside the timer.
   auto-fill collapses to one column on its own once the container is too
   narrow for a second 340px card, the same way .class-grid already does. */
.lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--space-4); align-items: start; }
.timer-face {
  font-family: var(--display);
  font-size: 66px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  text-align: center;
  line-height: 1.05;
  margin: 8px 0 0;
}
.timer-controls { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 12px; }
.timer-set {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 14px;
  color: var(--muted);
  font-size: 12px;
  align-items: center;
  flex-wrap: wrap;
}
.timer-set select { font-size: 12px; padding: 3px 6px; }
.sessions-note { text-align: center; color: var(--good-text); font-size: 12.5px; margin-top: 10px; font-weight: 600; }

.streak-row {
  display: flex;
  gap: 18px;
  justify-content: center;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}
.streak-stat { text-align: center; }
.streak-num { display: block; font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.streak-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.spark-row { display: flex; gap: 6px; justify-content: center; margin-top: 12px; }
.spark-bar { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 20px; }
.spark-track { width: 12px; height: 26px; display: flex; align-items: flex-end; }
.spark-fill { width: 100%; border-radius: 3px 3px 0 0; background: var(--accent-grad); min-height: 3px; }
.spark-lbl { font-size: 9.5px; color: var(--muted); }

.deck-row-count { font-size: 12px; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }

.flash-card {
  border: 1px solid var(--rule-strong);
  border-radius: 14px;
  background: var(--paper-deep);
  min-height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 22px;
  font-size: 17px;
  font-family: var(--serif);
  cursor: pointer;
  user-select: none;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow);
  transition: transform var(--dur-press) var(--ease-out), border-color var(--dur-press) var(--ease-out);
}
.flash-card:active { transform: scale(var(--scale-press)); }
.flash-hint { text-align: center; font-size: 11.5px; color: var(--muted); margin-top: 7px; }
.flash-reveal-row { display: flex; justify-content: center; margin-top: 12px; }
.flash-actions { display: flex; gap: 8px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
.flash-actions .grade-again:hover { color: var(--critical-text); border-color: var(--critical-text); }
.flash-actions .grade-hard:hover { color: var(--warning); border-color: var(--warning); }

.match-progress { height: 5px; border-radius: 999px; background: var(--rule); overflow: hidden; margin-top: 10px; max-width: 320px; }
.match-progress-fill { height: 100%; background: var(--accent-grad); border-radius: 999px; }

.match-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.match-tile {
  appearance: none;
  position: relative;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  padding: 16px 14px;
  min-height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 13.5px;
  font-family: var(--serif);
  cursor: pointer;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow);
  transition: transform var(--dur-press) var(--ease-out), box-shadow var(--dur-small) var(--ease-out),
              border-color var(--dur-small) var(--ease-out), background var(--dur-small) var(--ease-out),
              color var(--dur-small) var(--ease-out), opacity var(--dur-panel) var(--ease-out);
}
.match-tile:hover:not(:disabled) {
  border-color: var(--accent);
  background: var(--accent-soft);
  transform: translateY(-3px);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
}
.match-tile:active:not(:disabled) { transform: scale(var(--scale-press)); }
.match-tile.is-selected {
  background: var(--accent-grad);
  border-color: transparent;
  color: var(--accent-ink);
  font-weight: 600;
  transform: translateY(-3px);
  box-shadow: 0 12px 28px rgba(124, 134, 242, 0.4);
}
.match-tile.is-matched {
  border-color: var(--good-text);
  background: rgba(62, 207, 142, 0.08);
  color: var(--good-text);
  opacity: 0.7;
  cursor: default;
  box-shadow: none;
}
.match-tile.is-matched::after {
  content: "✓";
  position: absolute;
  top: 6px;
  right: 8px;
  font-size: 11px;
  font-family: var(--sans);
}

/* ============ Campus ============ */
.app-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.app-tile {
  appearance: none;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 9px;
  padding: 12px 14px;
  text-align: left;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.app-tile:hover { border-color: var(--accent); background: var(--accent-soft); }
.app-tile-name { font-weight: 650; color: var(--accent); }
.app-tile-sub { font-size: 11.5px; color: var(--muted); }
.hours-input {
  width: 230px;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  padding: 4px 8px;
}
.row-title.clickable { cursor: pointer; }
.row-title.clickable:hover { color: var(--accent); }

/* ============ Campus map (visual) ============ */
.cm-stage {
  position: relative;
  overflow: hidden;
  height: calc(100vh - 190px);
  min-height: 460px;
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  background: var(--sheet-raised);
  cursor: grab;
  touch-action: none;
}
.cm-stage:active { cursor: grabbing; }
.cm-canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.cm-img { display: block; width: 100%; height: 100%; user-select: none; pointer-events: none; }
.cm-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  z-index: 2;
}
.cm-pin-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  background: var(--accent);
}
.cm-pin-academic .cm-pin-dot { background: #f0648f; }
.cm-pin-housing .cm-pin-dot { background: #9175b5; }
.cm-pin-dining .cm-pin-dot { background: #fcb74e; }
.cm-pin-services .cm-pin-dot { background: #6b7280; }
.cm-pin-athletics .cm-pin-dot { background: #9cca3c; }
.cm-pin-home .cm-pin-dot { width: 22px; height: 22px; background: var(--accent); box-shadow: 0 0 0 5px var(--accent-soft), 0 1px 4px rgba(0, 0, 0, 0.45); }
.cm-pin-home .cm-pin-label { background: var(--accent); font-size: 12px; }
.cm-pin-hasclass .cm-pin-dot { width: 20px; height: 20px; box-shadow: 0 0 0 3px #fff, 0 0 0 5px var(--accent), 0 1px 4px rgba(0, 0, 0, 0.45); }
.cm-pin-hasclass .cm-pin-label { background: var(--accent); font-weight: 800; }
.cm-pin-custom .cm-pin-dot { background: #fff; box-shadow: 0 0 0 2px #333, 0 1px 4px rgba(0, 0, 0, 0.45); }
.cm-pin-custom .cm-pin-label { background: #333; }
.cm-route { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.cm-route polyline {
  fill: none;
  stroke: var(--accent);
  stroke-width: 0.35;
  stroke-dasharray: 1 0.8;
  stroke-linecap: round;
}
.cm-search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 8px;
  overflow: hidden;
}
.cm-search-results:empty { display: none; border: none; }
.cm-search-row {
  appearance: none;
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule);
  padding: 8px 12px;
  text-align: left;
  font-size: 13px;
  color: var(--ink);
  cursor: pointer;
}
.cm-search-row:last-child { border-bottom: none; }
.cm-search-row:hover { background: var(--accent-soft); color: var(--accent); }
.cm-zoom-btn {
  appearance: none;
  min-width: 40px;
  min-height: 34px;
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid var(--rule-strong);
  background: var(--sheet-raised);
  color: var(--ink);
  cursor: pointer;
}
.cm-zoom-btn:hover { border-color: var(--accent); background: var(--accent-soft); }
.cm-pin-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 12px;
  font-weight: 700;
  color: #fff;
  background: rgba(0, 0, 0, 0.72);
  padding: 3px 7px;
  border-radius: 5px;
  white-space: nowrap;
  pointer-events: none;
}
.cm-pin-sub { font-size: 9.5px; font-weight: 500; opacity: 0.85; }

.campus-map-preview {
  position: relative;
  overflow: hidden;
  height: 240px;
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  background: var(--sheet-raised);
  cursor: pointer;
  margin-bottom: 10px;
}
.campus-map-preview:hover { border-color: var(--accent); }
.campus-map-preview .cm-canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
.campus-map-preview-hint {
  position: absolute;
  right: 8px;
  bottom: 8px;
  font-size: 10.5px;
  font-weight: 600;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  padding: 3px 8px;
  border-radius: 999px;
  pointer-events: none;
}

/* ============ Exam countdown strip ============ */
.exam-strip { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 2px; }
.exam-chip {
  flex: none;
  min-width: 130px;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  padding: 10px 12px;
  cursor: pointer;
  text-align: left;
}
.exam-chip:hover { border-color: var(--accent); }
.exam-chip-days {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.exam-chip-title { font-weight: 650; margin-top: 3px; overflow-wrap: anywhere; }
.exam-chip-class { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.exam-chip.warm { border-color: var(--warning); }
.exam-chip.warm .exam-chip-days { color: var(--warning); }
.exam-chip.hot { border-color: var(--critical); background: rgba(229, 99, 108, 0.08); }
.exam-chip.hot .exam-chip-days { color: var(--critical-text); }

/* ============ Grade tracker ============ */
.gcat {
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 10px 12px;
  margin-bottom: 8px;
  background: var(--sheet-raised);
}
.gcat-head { display: flex; align-items: center; gap: 7px; }
.gcat-name { font-weight: 650; }
.gcat-w { width: 54px; padding: 3px 6px; font-size: 12.5px; text-align: center; }
.gcat-avg { margin-left: auto; font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.score-chip { cursor: default; color: var(--ink); font-variant-numeric: tabular-nums; }
.score-add { display: inline-flex; align-items: center; gap: 4px; }
.score-add input { width: 52px; padding: 3px 6px; font-size: 12.5px; }
.grade-summary { display: flex; align-items: baseline; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
.grade-big {
  font-family: var(--display);
  font-size: 38px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.grade-letter { color: var(--ink-2); font-weight: 600; }
.whatif { margin-top: 10px; border-top: 1px solid var(--rule); padding-top: 10px; }
.whatif-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  padding: 3px 0;
  color: var(--ink-2);
}
.whatif-row span:last-child { color: var(--ink); font-variant-numeric: tabular-nums; }
.trend-wrap { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
.trend-svg { width: 100%; height: 56px; display: block; overflow: visible; }
.trend-svg polyline { fill: none; stroke: var(--accent); stroke-width: 2; }
.trend-svg circle { fill: var(--accent); }
.trend-note { font-size: 11.5px; color: var(--muted); margin-top: 4px; text-align: right; font-variant-numeric: tabular-nums; }
.trend-empty { font-size: 12px; color: var(--muted); margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }

.notes-area {
  width: 100%;
  resize: vertical;
  min-height: 80px;
  font-family: inherit;
  line-height: 1.5;
}

/* ============ Workload forecast ============ */
.hrs-input {
  width: 48px;
  padding: 3px 6px;
  font-size: 12px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  flex: none;
}
.hrs-set {
  width: 42px;
  padding: 2px 5px;
  font-size: 11.5px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  margin: 0 2px;
}
.workload-headline {
  font-weight: 650;
  margin: 0 0 10px;
  font-size: 13.5px;
}
.workload-headline.ok { color: var(--good-text); }
.workload-headline.warn { color: var(--critical-text); }

/* ============ GPA ============ */
.gpa-grid { display: flex; gap: 28px; flex-wrap: wrap; }
.gpa-value {
  font-family: var(--display);
  font-size: 56px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.gpa-value.dim { color: var(--ink-2); font-size: 40px; }
.gpa-sub-label { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 4px; }
.gpa-note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }
.gpa-prior { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
.gpa-live { color: var(--accent); }
.gpa-live .link-btn { margin-left: 2px; }
.gpa-goal-row { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
.gpa-goal-row input { width: 80px; }
.gpa-goal-note { font-size: 12.5px; color: var(--ink-2); margin-top: 8px; line-height: 1.5; }
.gpa-goal-note.warn { color: var(--critical-text); }
.grade-sel { width: 64px; }

/* GPA projection — the Monte Carlo panel. Reads as one more panel in the GPA
   view, not a dashboard bolted on: same rules, same type, accent for the
   likely middle and muted for the tails. */
.proj { margin-top: var(--space-4); }
.proj-head { display: flex; gap: 32px; flex-wrap: wrap; align-items: flex-end; }
.proj-stat { padding-bottom: 4px; }
.proj-stat-val {
  font-family: var(--display);
  font-size: 26px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
  color: var(--ink-2);
}
.proj-stat-val.good { color: var(--good-text); }
.proj-stat-val.warn { color: var(--critical-text); }
.proj-chart-wrap { margin-top: 18px; }
.proj-chart { width: 100%; height: 108px; display: block; overflow: visible; }
.proj-chart rect.in { fill: var(--accent); fill-opacity: 0.85; }
.proj-chart rect.out { fill: var(--muted); fill-opacity: 0.4; }
.proj-chart line { stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.proj-chart line.med { stroke: var(--ink); }
.proj-chart line.tgt { stroke: var(--warning); stroke-dasharray: 4 3; }
.proj-axis {
  display: flex;
  justify-content: space-between;
  font-size: var(--text-xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-top: 4px;
}
.proj-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: var(--text-xs);
  color: var(--muted);
  margin-top: 8px;
}
.proj-legend .k { width: 10px; height: 2px; border-radius: 1px; margin-left: 10px; }
.proj-legend .k:first-child { margin-left: 0; }
.proj-legend .k.med { background: var(--ink); }
.proj-legend .k.tgt { background: var(--warning); }
.proj-legend .k.in { background: var(--accent); height: 8px; border-radius: 2px; }
.proj-note { font-size: var(--text-sm); color: var(--ink-2); line-height: 1.55; margin: 12px 0 0; }
.proj-note strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.proj-lev { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule); }
.proj-lev-head {
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-xs);
  color: var(--muted);
  margin-bottom: 6px;
}
.proj-swing {
  font-family: var(--display);
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.proj-lev-note { margin-top: 8px; line-height: 1.5; }
.proj-math { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--rule); }
.proj-math summary {
  cursor: pointer;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.proj-math summary:hover { color: var(--ink-2); }
.proj-math ul { margin: 10px 0 0; padding-left: 18px; }
.proj-math li { font-size: var(--text-sm); color: var(--ink-2); line-height: 1.55; margin-bottom: 6px; }

.syl-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  max-height: 320px;
  overflow-y: auto;
}
.syl-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--rule);
}
.syl-row input[type="date"] { width: 128px; flex: none; font-size: 12px; }
.syl-row input[type="text"] { flex: 1; min-width: 0; }
/* Halo import review — same row rhythm as the syllabus reviewer, with the
   extra columns Halo gives us (time, points, add/update/skip). Wraps rather
   than squeezing, since the modal is narrower than this row wants to be. */
.halo-row { flex-wrap: wrap; }
.halo-row input[type="time"] { width: 104px; flex: none; font-size: 12px; }
.halo-row .halo-pts { width: 62px; flex: none; font-size: 12px; }
.halo-row .halo-act { flex: none; font-size: 11.5px; max-width: 132px; }
.halo-row .halo-act-new {
  flex: none;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--good-text);
  min-width: 46px;
}
/* Undated rows are the ones the parser refuses to guess for — they stay
   unchecked and are visibly marked so they can't be imported by accident. */
.halo-row.needs-review {
  background: color-mix(in srgb, var(--warning) 9%, transparent);
  border-left: 2px solid var(--warning);
  padding-left: 6px;
}
.halo-grade-row .halo-score {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
  color: var(--ink);
}
.halo-grade-row select { flex: none; max-width: 160px; font-size: 12px; }
.syl-exam {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11.5px;
  color: var(--ink-2);
  white-space: nowrap;
  flex: none;
}

/* ============ Modal ============ */
#modal-root:empty { display: none; }
.modal-veil {
  position: fixed;
  inset: 0;
  background: rgba(20, 12, 30, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  animation: modal-veil-in var(--dur-panel) var(--ease-out) both;
}
.modal-veil.closing { animation: modal-veil-out var(--dur-small) var(--ease-in) both; }
@keyframes modal-veil-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-veil-out { to { opacity: 0; } }
.modal {
  background: #1e1c30;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 16px;
  box-shadow: 0 24px 72px rgba(5, 4, 12, 0.72);
  width: min(560px, calc(100vw - 60px));
  max-height: calc(100vh - 80px);
  overflow-y: auto;
  padding: 20px 22px;
  animation: modal-in var(--dur-panel) var(--ease-out) both;
}
.modal-veil.closing .modal { animation: modal-out var(--dur-small) var(--ease-in) both; }
@keyframes modal-in {
  from { opacity: 0; transform: scale(var(--scale-modal-from)); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes modal-out {
  to { opacity: 0; transform: scale(var(--scale-modal-from)); }
}
.modal.modal-wide {
  width: min(720px, calc(100vw - 60px));
}
.modal.modal-reader {
  width: min(820px, calc(100vw - 60px));
  max-height: calc(100vh - 40px);
}
.modal h4 {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin: 0 0 14px;
}
.modal .modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 18px;
}
.modal p.confirm-text { margin: 0; color: var(--ink-2); }
.btn.danger { background: var(--critical); color: #fff; }

/* ============ Toast ============ */
#toast-root {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 60;
  align-items: center;
}
.toast {
  background: var(--ink);
  color: var(--paper);
  font-size: 13px;
  font-weight: 550;
  border-radius: 999px;
  padding: 8px 18px;
  box-shadow: 0 6px 20px rgba(15, 8, 25, 0.3);
  animation: toast-in var(--dur-small) var(--ease-out) both;
}
.toast.leaving { animation: toast-out var(--dur-press) var(--ease-in) both; }
.toast.error { background: var(--critical); color: #fff; }
.toast.ok { background: var(--good-text); color: #fff; }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(var(--dist-sm)); }
  to { opacity: 1; transform: none; }
}
@keyframes toast-out {
  to { opacity: 0; transform: translateY(var(--dist-sm)) scale(0.96); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ============ Lectures ============ */
.rec-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--accent-grad);
  color: var(--accent-ink);
  border: none;
  border-radius: 999px;
  padding: 7px 16px 7px 12px;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(0,0,0,0.15);
  transition: filter 0.12s ease, transform 0.08s ease;
}
.rec-btn:hover { filter: brightness(1.12); }
.rec-btn:active { transform: scale(0.97); }
.rec-btn .rec-dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; flex: none; }
.rec-btn.is-recording {
  background: var(--critical);
  color: #fff;
}
.rec-btn.is-recording .rec-dot { animation: rec-pulse 1.2s ease-in-out infinite; }
@keyframes rec-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }

.rec-bar-btn {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  background: var(--critical);
  color: #fff;
  border: none;
  border-radius: 9px;
  padding: 9px 12px;
  margin-bottom: 12px;
  font-weight: 650;
  font-size: 12.5px;
  cursor: pointer;
  text-align: left;
}
.rec-bar-btn:hover { filter: brightness(1.1); }
.rec-bar-btn .rec-dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; flex: none; animation: rec-pulse 1.2s ease-in-out infinite; }
.rec-bar-btn .grow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-bar-btn .rec-bar-time { font-variant-numeric: tabular-nums; opacity: 0.85; flex: none; }

.lec-notice {
  margin: 0 0 14px;
  padding: 10px 14px;
  border-radius: 9px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
}
.lec-storage { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--rule); }

/* ============ Class tabs ============ */
.class-tabs {
  display: flex;
  gap: 2px;
  margin-top: 16px;
  border-bottom: 1px solid var(--rule-strong);
  overflow-x: auto;
}
.class-tab {
  appearance: none;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: calc(9px * var(--dens)) calc(14px * var(--dens));
  margin-bottom: -1px;
  color: var(--muted);
  font-weight: 600;
  font-size: calc(13px * var(--tscale));
  white-space: nowrap;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: color var(--dur-small) var(--ease-out), border-color var(--dur-small) var(--ease-out);
}
.class-tab:hover { color: var(--ink-2); }
.class-tab.active { color: var(--ink); border-bottom-color: var(--accent); }
.class-tab-count {
  font-size: 11px;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--rule-strong);
  color: var(--ink-2);
}
.class-tab.active .class-tab-count { background: var(--accent-soft); color: var(--accent); }

/* ============ Settings / health ============ */
.health-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  margin-top: 6px; align-self: flex-start;
}
.health-dot.ok { background: var(--good); }
.health-dot.warn { background: var(--warning); }
.health-dot.bad { background: var(--critical); }
.health-rows li { align-items: flex-start; }
.archived-head {
  margin: 26px 0 10px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.class-card.is-archived { opacity: 0.62; }
.class-card.is-archived:hover { opacity: 1; }

/* ============ Study library ============ */
.due-hero {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
.deck-group + .deck-group { margin-top: 18px; }
.lib-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}
.lib-subgroup + .lib-subgroup { margin-top: 12px; }
.deck-group-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 2px;
}
.deck-group-head .class-tag { text-transform: none; letter-spacing: 0; font-size: 11.5px; }
.deck-group.is-archived { opacity: 0.6; }
.deck-group.is-archived:hover { opacity: 1; }
.mastery-bar {
  display: flex;
  height: 4px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--rule);
  margin-top: 6px;
  max-width: 260px;
}
.mastery-bar span { display: block; height: 100%; }
.mastery-bar .m-mastered { background: var(--good); }
.mastery-bar .m-learning { background: var(--accent); }
.mastery-bar .m-new { background: var(--rule-strong); }
.flash-source {
  text-align: center;
  font-size: 12px;
  color: var(--muted);
  margin-top: 6px;
}
.warn { color: var(--critical-text); font-weight: 650; }

/* ============ Practice test ============ */
.pt-panel { max-width: 560px; margin: 0 auto; text-align: center; padding: 32px 28px; background: var(--paper-deep); }
.pt-type {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
}
.pt-prompt { font-family: var(--serif); font-size: 22px; font-weight: 700; margin-bottom: 20px; }
.pt-claim {
  font-size: 16px;
  padding: 14px 18px;
  border-radius: 10px;
  background: var(--sheet-raised);
  border: 1px solid var(--rule);
  margin-bottom: 16px;
}
.pt-options { display: flex; flex-direction: column; gap: 8px; }
.pt-option {
  appearance: none;
  text-align: left;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 9px;
  padding: 11px 16px;
  color: var(--ink);
  font-size: 14px;
  cursor: pointer;
}
.pt-option:hover:not(:disabled) { border-color: var(--accent); }
.pt-option:disabled { cursor: default; }
.pt-option.correct { border-color: var(--good); background: rgba(62, 207, 142, 0.12); color: var(--good-text); }
.pt-option.wrong { border-color: var(--critical); background: rgba(229, 99, 108, 0.12); color: var(--critical-text); }
.pt-written-answer { text-align: left; }
.pt-correct-text { font-family: var(--serif); font-size: 18px; font-weight: 700; margin-top: 4px; }
.pt-result { font-size: 18px; font-weight: 700; }
.pt-result.correct { color: var(--good-text); }
.pt-result.wrong { color: var(--critical-text); }
.src-chip {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  white-space: nowrap;
  flex: none;
}
.src-loc {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent);
  margin-top: 12px;
}
.src-loc:first-child { margin-top: 0; }
.syl-row .src-chip { align-self: center; }
.transcript-box {
  white-space: pre-wrap;
  max-height: 50vh;
  overflow-y: auto;
  background: var(--sheet-raised);
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: 14px 16px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink-2);
}
.modal h5 {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 16px 0 8px;
}
.lec-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 6px; }
.lec-read-row { margin: 14px 0 4px; }
.transcript-reader {
  white-space: pre-wrap;
  font-size: 15.5px;
  line-height: 1.75;
  color: var(--ink);
  padding: 6px 2px 10px;
}
.transcript-reader .word-seek {
  cursor: pointer;
  border-radius: 3px;
}
.transcript-reader .word-seek:hover {
  background: var(--accent-soft);
}
.notes-preview {
  margin: 0 0 8px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink-2);
}
.notes-editor-full {
  width: 100%;
  min-height: 58vh;
  font-size: 15.5px;
  line-height: 1.75;
  color: var(--ink);
  background: var(--paper-deep);
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  padding: 16px 18px;
  resize: vertical;
}
.lec-summary p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }
.lec-summary p:last-child { margin-bottom: 0; }
.lec-keypoints { margin-bottom: 14px; }
.lec-keypoints ul, .lec-keypoints-list { margin: 0; padding-left: 20px; display: grid; gap: 6px; }
.lec-keypoints li, .lec-keypoints-list li { font-size: 14px; line-height: 1.5; color: var(--ink); }
.lec-keypoints li::marker, .lec-keypoints-list li::marker { color: var(--accent); }
.lec-summary-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.lec-summary-head h5 { margin: 0; }
.seg { display: inline-flex; padding: 3px; border-radius: 999px; background: var(--paper-deep); border: 1px solid var(--rule-strong); }
.seg-btn { appearance: none; border: 0; background: transparent; color: var(--muted); font: inherit; font-size: 12px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; padding: 6px 14px; border-radius: 999px; cursor: pointer; min-height: 30px; transition: background .18s, color .18s; }
.seg-btn:hover { color: var(--ink); }
.seg-btn.on { background: var(--accent); color: var(--accent-ink); }
.seg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.notes-editor-label { margin: 14px 0 6px; }
/* Phone hand-off card in the Lectures view. Reads as a quiet utility panel,
   not a second hero — the library is still the point of the screen. */
.phone-inbox { margin-bottom: 16px; }
.phone-inbox h3 { margin: 0 0 6px; font-family: var(--display); font-size: 15px; text-transform: uppercase; letter-spacing: .08em; }
.phone-inbox .rows li { gap: 10px; flex-wrap: wrap; }
.phone-inbox select { min-height: 34px; max-width: 190px; }
.phone-inbox .modal-actions { gap: 8px; }
.lec-jump { max-height: 180px; overflow-y: auto; }
.lec-jump .rows li { padding: 5px 0; }

/* Ask about this assignment — a running thread, not a form. */
.assign-chat { border-top: 1px solid var(--rule); padding-top: 10px; }
.assign-chat h5 { margin: 0 0 4px; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
.assign-chat-thread { max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
.chat-turn { display: flex; flex-direction: column; gap: 4px; }
.chat-q {
  align-self: flex-end;
  max-width: 85%;
  background: var(--accent-soft);
  color: var(--ink);
  border-radius: 12px 12px 2px 12px;
  padding: 7px 12px;
  font-size: 13px;
}
.chat-a {
  align-self: flex-start;
  max-width: 85%;
  background: var(--sheet-raised);
  border: 1px solid var(--rule-strong);
  border-radius: 12px 12px 12px 2px;
  padding: 7px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-2);
}

/* ============ Opening sequence ============ */
.lhq-launch {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
  color: var(--ink);
  background:
    radial-gradient(circle at 50% 46%, rgba(124, 134, 242, 0.15), transparent 31%),
    var(--paper);
  animation: lhq-curtain 0.58s 1.82s cubic-bezier(.7, 0, .3, 1) both;
}
.lhq-launch::before {
  content: "";
  position: absolute;
  width: 430px;
  height: 430px;
  border: 1px solid rgba(124, 134, 242, 0.13);
  border-radius: 50%;
  opacity: 0;
  transform: scale(.68);
  animation: lhq-ring 1.05s .08s cubic-bezier(.16, 1, .3, 1) both;
}
.lhq-launch-stage { display: grid; place-items: center; gap: 18px; transform: translateY(-7px); }
.lhq-launch-mark { width: min(30vw, 200px); height: min(30vw, 200px); overflow: visible; }
/* The app's own mark, drawn rather than shown: the ring sweeps itself
   around, the crossbar lands, and a gloss passes over the finished shape —
   the same bevelled sheen the icon has, arrived at rather than presented. */
.lhq-launch-glow {
  fill: url(#lhqLaunchGradient);
  opacity: 0;
  filter: blur(26px);
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(.6);
  animation: lhq-glow 1.5s .06s cubic-bezier(.16, 1, .3, 1) forwards;
}
.lhq-launch-ring {
  fill: none;
  stroke: url(#lhqLaunchGradient);
  stroke-width: 30;
  stroke-dasharray: 334.6 400;
  stroke-dashoffset: 334.6;
  animation: lhq-draw .9s .12s cubic-bezier(.3, .8, .35, 1) forwards;
}
.lhq-launch-bar {
  fill: url(#lhqLaunchGradient);
  transform-box: fill-box;
  transform-origin: left center;
  transform: scaleX(0);
  animation: lhq-bar .42s .82s cubic-bezier(.2, .9, .3, 1.25) forwards;
}
.lhq-launch-gloss {
  fill: rgba(255, 255, 255, .55);
  transform: translateX(0) rotate(18deg);
  animation: lhq-gloss .75s 1.02s cubic-bezier(.4, 0, .3, 1) forwards;
}
.lhq-launch-copy { text-align: center; opacity: 0; transform: translateY(12px); animation: lhq-copy .52s .76s ease-out forwards; }
.lhq-launch-copy strong { display: block; font-family: var(--display); font-size: 38px; font-weight: 800; text-transform: uppercase; line-height: 1.05; letter-spacing: 0.02em; }
.lhq-launch-copy span { display: block; margin-top: 8px; color: var(--muted); font-size: 11.5px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
body.launching > .app { animation: lhq-workspace 1.05s 1.42s cubic-bezier(.16, 1, .3, 1) both; }
@keyframes lhq-ring { to { opacity: 1; transform: scale(1); } }
@keyframes lhq-glow {
  0%   { opacity: 0;   transform: scale(.6); }
  45%  { opacity: .75; transform: scale(1.04); }
  100% { opacity: .32; transform: scale(1); }
}
@keyframes lhq-draw { to { stroke-dashoffset: 0; } }
@keyframes lhq-bar { to { transform: scaleX(1); } }
@keyframes lhq-gloss { from { transform: translateX(0) rotate(18deg); } to { transform: translateX(330px) rotate(18deg); } }
@keyframes lhq-copy { to { opacity: 1; transform: none; } }
@keyframes lhq-curtain { to { opacity: 0; transform: scale(1.035); visibility: hidden; } }
@keyframes lhq-workspace { from { filter: blur(9px); transform: scale(.982); opacity: .55; } to { filter: none; transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .lhq-launch { display: none; }
  body.launching > .app { animation: none; filter: none; transform: none; opacity: 1; }
}

/* ============ Home (Today v4 — poster) ============
   The front page is a poster, not a report: an italic serif date line over a
   huge condensed greeting, today's schedule as the single main block, and one
   next-deadline card. Checklist, due-lists, and launcher tiles live in their
   own tabs. Pre-semester the poster carries a clearly-worded day-one countdown. */
.home-poster {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding-top: 22px;
}
.home-poster::before {
  content: "";
  position: absolute;
  top: -150px;
  left: -90px;
  width: 520px;
  height: 400px;
  background: radial-gradient(closest-side, rgba(124, 134, 242, 0.15), transparent 70%);
  pointer-events: none;
}
.home-head { position: relative; }
.home-date {
  font-family: var(--serif);
  font-style: italic;
  font-size: 17px;
  color: var(--ink-2);
  letter-spacing: 0.01em;
}
.home-greet {
  font-family: var(--display);
  font-size: clamp(52px, 8.5vw, 84px);
  font-weight: 800;
  text-transform: uppercase;
  line-height: 0.94;
  letter-spacing: 0.005em;
  margin: 8px 0 0;
}
.home-sec { position: relative; }
.home-label {
  font-family: var(--display);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}

/* Schedule — rule-led rows, class color as a left bar, big condensed type */
.sched { display: flex; flex-direction: column; border-top: 1px solid var(--rule-strong); }
.sched-row {
  appearance: none;
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule-strong);
  display: flex;
  align-items: center;
  gap: 22px;
  text-align: left;
  padding: 16px 8px 16px 16px;
  cursor: pointer;
  position: relative;
  transition: background 0.15s ease;
}
.sched-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 3px;
  border-radius: 2px;
  background: var(--c, var(--rule-strong));
}
.sched-row:hover { background: var(--accent-soft); }
.sched-row.past { opacity: 0.45; }
.sr-time {
  font-family: var(--display);
  font-size: 20px;
  font-weight: 600;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  min-width: 148px;
  flex: none;
}
.sr-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.sr-code {
  font-family: var(--display);
  font-size: 26px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 1.05;
  overflow-wrap: anywhere;
}
.sched-row.live .sr-code {
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.sr-name { font-size: 13px; color: var(--ink-2); }
.sr-room { font-size: 12px; color: var(--muted); }
.sr-now {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--good-text);
  border: 1px solid var(--good);
  border-radius: 999px;
  padding: 3px 11px;
  flex: none;
}
.sched-empty {
  color: var(--muted);
  font-family: var(--serif);
  font-style: italic;
  font-size: 14px;
  padding: 16px 2px;
  border-top: 1px solid var(--rule-strong);
  border-bottom: 1px solid var(--rule-strong);
}

/* Next deadline — one card, one number */
.next-dl {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  background:
    radial-gradient(120% 200% at 100% 0%, rgba(124, 134, 242, 0.12), transparent 55%),
    var(--sheet);
  border: 1px solid var(--rule-strong);
  border-radius: 18px;
  padding: 18px 26px;
  cursor: pointer;
  text-align: left;
  box-shadow: var(--shadow);
  transition: border-color 0.15s ease;
}
.next-dl:hover { border-color: var(--accent); }
.dl-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dl-kind {
  font-family: var(--display);
  font-size: 11.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--accent);
}
.dl-title {
  font-family: var(--display);
  font-size: 26px;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.05;
  overflow-wrap: anywhere;
}
.dl-meta { font-size: 13px; color: var(--ink-2); }
.dl-side { flex: none; text-align: right; display: flex; flex-direction: column; align-items: flex-end; }
.dl-num {
  font-family: var(--display);
  font-size: 46px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.dl-num.word { font-size: 26px; line-height: 1.3; }
.dl-num.crit { background: none; color: var(--critical-text); -webkit-text-fill-color: var(--critical-text); }
.dl-lbl {
  font-size: var(--text-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-top: 4px;
}
.all-clear { font-family: var(--serif); font-style: italic; color: var(--muted); font-size: 14px; padding: 4px 2px; }

/* ================= Today's plan ================= */
.plan-count {
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin-left: 8px;
  font-variant-numeric: tabular-nums;
}
.plan-rows .plan-block { align-items: center; gap: 12px; }
/* Start time + length, condensed like the rest of the poster type so the
   plan reads as a schedule rather than a checklist. */
.plan-when {
  flex: none;
  width: 74px;
  font-family: var(--display);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ink);
  line-height: 1.1;
}
.plan-when span {
  display: block;
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-top: 2px;
}
.plan-block.is-done .plan-when,
.plan-block.is-skipped .plan-when { color: var(--muted); }
.plan-block.is-done { opacity: 0.62; }
.plan-block.is-skipped { opacity: 0.45; }
.plan-state {
  flex: none;
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.plan-block.is-done .plan-state { color: var(--good-text); }
.plan-noest {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}

/* ================= Weekly review ================= */
.review-score-panel { display: flex; align-items: center; gap: 20px; }
.review-score {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 108px;
  height: 108px;
  border-radius: 50%;
  border: 2px solid var(--rule-strong);
}
.review-score-num {
  font-family: var(--display);
  font-size: 40px;
  font-weight: 800;
  line-height: 1;
  color: var(--ink);
}
.review-score-of {
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 4px;
}
.review-score.low { border-color: color-mix(in srgb, var(--good) 55%, transparent); box-shadow: 0 0 26px color-mix(in srgb, var(--good) 18%, transparent); }
.review-score.medium { border-color: color-mix(in srgb, var(--warning) 60%, transparent); box-shadow: 0 0 26px color-mix(in srgb, var(--warning) 20%, transparent); }
.review-score.high { border-color: color-mix(in srgb, var(--critical) 65%, transparent); box-shadow: 0 0 30px color-mix(in srgb, var(--critical) 24%, transparent); }
.review-band {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--ink);
  margin-bottom: 4px;
}
.review-rows .review-risk { align-items: flex-start; gap: 14px; padding: 13px 0; }
/* Severity shows as a left edge rather than a colored blob — same restraint
   as .hub-bubble.alert, so a real emergency still stands out. */
.review-risk { border-left: 3px solid var(--rule-strong); padding-left: 12px; }
.review-risk.sev-high { border-left-color: var(--critical); }
.review-risk.sev-med { border-left-color: var(--warning); }
.review-risk.sev-low { border-left-color: var(--accent); }
.review-why {
  font-family: var(--serif);
  font-style: italic;
  color: var(--ink-2);
  font-size: 13.5px;
  margin: 4px 0 5px;
  max-width: 62ch;
}
.review-action { font-size: 13px; color: var(--ink); margin-top: 7px; }
.review-action-label {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: 6px;
}
.review-btns { flex: none; display: flex; align-items: center; gap: 6px; }
.recovery-list { margin: 0; padding-left: 20px; }
.recovery-list li { margin-bottom: 10px; line-height: 1.45; }
.recovery-list li strong { display: block; font-weight: 600; color: var(--ink); }
.recovery-list li span { font-size: 12.5px; color: var(--muted); }
.review-spark { display: flex; align-items: flex-end; gap: 10px; height: 82px; }
.review-bar { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; }
.review-bar-fill { width: 100%; max-width: 34px; border-radius: 4px 4px 0 0; background: var(--accent); }
.review-bar-fill.low { background: var(--good); }
.review-bar-fill.medium { background: var(--warning); }
.review-bar-fill.high { background: var(--critical); }
.review-bar span { font-size: 10px; color: var(--muted); margin-top: 6px; }

/* ================= Lecture → study material ================= */
.sm-list { margin-top: 10px; }
.sm-row { align-items: center; gap: 12px; }
.sm-row .btn.small { flex: none; min-width: 96px; }
.sm-done {
  color: var(--good-text);
  font-size: 12px;
  margin-left: 4px;
  display: inline-block;
  animation: sm-done-in var(--dur-press) var(--ease-spring) both;
}
@keyframes sm-done-in {
  from { opacity: 0; transform: scale(0.4); }
  to { opacity: 1; transform: scale(1); }
}
.sm-list .row-meta { max-width: 46ch; }

/* Pre-semester countdown — the number is labeled, not floating */
.count-poster {
  display: flex;
  align-items: center;
  gap: 28px;
  background:
    radial-gradient(120% 200% at 0% 0%, rgba(124, 134, 242, 0.14), transparent 55%),
    var(--sheet);
  border: 1px solid var(--rule-strong);
  border-radius: 18px;
  padding: 22px 30px;
  box-shadow: var(--shadow);
}
.cp-num {
  font-family: var(--display);
  font-size: 92px;
  font-weight: 800;
  line-height: 0.9;
  font-variant-numeric: tabular-nums;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  flex: none;
}
.cp-title {
  font-family: var(--display);
  font-size: 24px;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.05;
}
.cp-meta { font-family: var(--serif); font-style: italic; color: var(--ink-2); margin-top: 3px; }
.cp-copy input[type="date"] { margin-top: 10px; }
.cp-miles { margin-top: 12px; border-top: 1px solid var(--rule); padding-top: 10px; }
.cp-mile { display: flex; align-items: baseline; gap: 10px; font-size: 13.5px; padding: 2px 0; }
.cp-mile-when {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
  min-width: 96px;
  flex: none;
}

/* ============ Motion (view-enter) ============
   Two primitives only: a fade+rise when a view is entered, staggered across
   the view's top-level blocks (and the home poster's inner blocks). The
   .view-anim class is added by go() and removed ~700ms later, so data
   re-renders and the Today minute tick never replay it. */
@keyframes view-in { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes view-fade { from { opacity: 0; } to { opacity: 1; } }
#view-root.view-anim > * { animation: view-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
#view-root.view-anim > *:nth-child(2) { animation-delay: 0.08s; }
#view-root.view-anim > *:nth-child(3) { animation-delay: 0.16s; }
#view-root.view-anim > *:nth-child(4) { animation-delay: 0.24s; }
#view-root.view-anim > *:nth-child(n+5) { animation-delay: 0.32s; }
#view-root.view-anim > .home-poster { animation: none; }
/* Same opt-out for the AI stage, for a different reason: the orb's canvas
   composites with mix-blend-mode, and an ancestor animating opacity or
   transform creates a stacking context, which isolates that blending. The
   canvas then has nothing to blend against and renders as an opaque black
   square for exactly as long as the entrance animation runs. The stage's
   children animate individually instead — all except the orb, which must
   have no animated ancestor at all. */
#view-root.view-anim > .ai-stage { animation: none; }
.view-anim .ai-stage > .ai-hello,
.view-anim .ai-stage > .ai-pill,
.view-anim .ai-stage > .ai-talk,
.view-anim .ai-stage > .ai-secondary {
  animation: view-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.view-anim .ai-stage > .ai-pill,
.view-anim .ai-stage > .ai-talk { animation-delay: 0.12s; }
.view-anim .ai-stage > .ai-secondary { animation-delay: 0.18s; }
.view-anim .home-poster > * { animation: view-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
.view-anim .home-poster > *:nth-child(2) { animation-delay: 0.12s; }
.view-anim .home-poster > *:nth-child(3) { animation-delay: 0.24s; }
.view-anim .home-poster > *:nth-child(4) { animation-delay: 0.36s; }
@media (prefers-reduced-motion: reduce) {
  #view-root.view-anim > *, .view-anim .home-poster > * { animation: view-fade 0.12s ease-out both; animation-delay: 0s; }
}

/* ============ Appearance & layout (customization round) ============ */
/* Every control here works the same way the original font preset did: a class
   on <body> redefines a variable the rest of the stylesheet already reads.
   Nothing downstream needs to know these exist. */

/* --- Display face (the poster voice) --- */
body.font-serif { --display: ui-serif, "Iowan Old Style", Georgia, "Times New Roman", serif; }
body.font-system { --display: ui-sans-serif, -apple-system, "Segoe UI", sans-serif; }
body.disp-futura { --display: "Futura", "Avenir Next Condensed", var(--sans); }
body.disp-optima { --display: "Optima", "Avenir Next", var(--sans); }
body.disp-copperplate { --display: "Copperplate", "Avenir Next Condensed", var(--sans); }
body.disp-avenir { --display: "Avenir Next", var(--sans); }

/* --- Reading face. The original preset never touched this; body copy was
       always --sans regardless of which "font" was chosen. --- */
body.body-serif { --sans: ui-serif, "Iowan Old Style", Georgia, serif; }
body.body-ny { --sans: "New York", ui-serif, Georgia, serif; }
body.body-georgia { --sans: Georgia, ui-serif, "Times New Roman", serif; }

/* --- Accent is fixed to the purple identity everywhere (design.md — the
       purple/black identity is chosen, not incidental); there is no per-user
       accent-color picker any more, so this never drifts to another hue. --- */

/* --- Density and type size. One multiplier each; :root does the rest. --- */
body.dens-compact { --dens: 0.72; }
body.dens-roomy { --dens: 1.2; }
body.text-sm { --tscale: 0.92; }
body.text-lg { --tscale: 1.12; }

/* --- Backdrop. Flat drops the blooms; quiet halves them. --- */
body.bg-flat { --bloom: 0; }
body.bg-quiet { --bloom: 0.5; }
body.stars-off::before, body.stars-off::after { display: none; }

/* --- Motion. "Calm" mirrors the OS reduced-motion rule rather than
       duplicating the twelve specific blocks it already overrides. --- */
body.motion-calm * { transition: none !important; animation: none !important; }
body.motion-extra { --motion: 0.46s; --motion-slow: 0.72s; }
body.motion-extra #view-root.view-anim > * { animation-duration: 0.72s; }
body.motion-extra .panel { transition: transform var(--motion-fast) ease-out; }
body.motion-extra .panel:hover { transform: translateY(-2px); }

/* --- Hub bubble size --- */
body.hub-lg { --hub-bubble-scale: 1.15; }
body.hub-sm { --hub-bubble-scale: 0.85; }
.swatch-row { display: flex; gap: 8px; }
.swatch-row button {
  appearance: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid transparent;
  background: var(--c);
  cursor: pointer;
  padding: 0;
}
.swatch-row button:hover { transform: scale(1.1); }
.swatch-row button.on { border-color: var(--ink); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.25); }
.home-order-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 2px;
  border-bottom: 1px solid var(--rule);
}
.home-order-row:last-of-type { border-bottom: none; }
.home-order-row .grow { flex: 1; }
.home-order-row button[disabled] { opacity: 0.35; cursor: default; }


/* ============ Hub navigation ============
   The sidebar is gone. Navigation is a full-screen hub: the wordmark orb in
   the middle, sections as bubbles that pop out of it with a staggered spring.
   Views zoom out of the bubble that opened them. Esc / the topbar mini-orb
   reopens the hub; clicking the big orb (or navigating) closes it. */
.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px;
  border-bottom: 1px solid var(--rule);
  background: rgba(10, 8, 20, 0.5);
  flex: none;
}
.mini-orb {
  appearance: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  font-family: var(--display);
  font-weight: 800;
  font-size: 13px;
  color: #fff;
  background: radial-gradient(circle at 32% 28%, rgba(159, 110, 240, 0.7), rgba(19, 18, 32, 0.9) 75%);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 0 18px rgba(139, 147, 248, 0.3);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.mini-orb:hover { transform: scale(1.08); box-shadow: 0 0 30px rgba(139, 147, 248, 0.5); }
.mini-orb:active { transform: scale(0.95); }
/* Quick-nav: the app's most-used sections sit directly in the topbar so
   getting to them doesn't require knowing the orb opens a full-screen hub
   first. The orb (and this strip's own "More") still open that hub for
   everything else — this is additive, not a replacement. Hidden below
   760px: a cramped row of nav buttons is worse than just using the orb,
   which is still the fully-functional way in at any width. */
.quick-nav { display: flex; align-items: center; gap: 2px; }
.quick-nav button {
  appearance: none;
  background: none;
  border: 1px solid transparent;
  border-radius: 7px;
  color: var(--ink-2);
  padding: 6px 10px;
  font-size: 12.5px;
  font-weight: 600;
  font-family: var(--display);
  letter-spacing: 0.03em;
  cursor: pointer;
  transition: color var(--dur-press) var(--ease-out), background var(--dur-press) var(--ease-out),
              border-color var(--dur-press) var(--ease-out);
}
.quick-nav button:hover { color: var(--ink); border-color: var(--rule-strong); }
.quick-nav button.current { color: var(--ink); background: var(--accent-soft); }
.quick-nav-more { color: var(--muted) !important; }
.crumb {
  font-family: var(--display);
  font-size: 15px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-2);
}
.crumb.quick-nav-covered { display: none; }
/* Same specificity as the rule above (two classes each) — this MUST come
   after it in source order, or the unconditional display:none wins even
   while this media query matches and silently hides the crumb with
   nothing left to say "you're here" once quick-nav is also hidden below. */
@media (max-width: 760px) {
  .quick-nav { display: none; }
  .crumb.quick-nav-covered { display: block; }
}
.topbar-record {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: none;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  padding: 6px 14px 6px 11px;
  font-family: var(--display);
  font-size: 12.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.topbar-record .rec-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--critical); flex: none; }
.topbar-record:hover { border-color: var(--critical); color: var(--ink); }
.topbar .countdown.top { margin-left: auto; display: flex; align-items: baseline; gap: 8px; }
.topbar .countdown.top .big { font-size: 18px; }
.topbar .countdown.top .sub { margin-top: 0; }
.topbar-search {
  appearance: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--rule-strong);
  color: var(--ink-2);
  font-size: 18px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.topbar-search:hover { border-color: var(--accent); color: var(--ink); transform: scale(1.1); }
.search-group { margin-bottom: 14px; }
.search-group:last-child { margin-bottom: 0; }
.search-group h5 { margin: 0 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-2); }
.search-row { width: 100%; text-align: left; }

/* Hub ⇄ view switching is a plain swap: no fades, no zoom, no transitions.
   Cooper, 2026-08-21, after three rounds of increasingly baroque attempts to
   make the cross-fade behave ("stop trying to get around this... super
   simple"): opacity transitions on .app raced every DOM rewrite and the
   compositor, producing a stuck background, then a double blink, then a
   delay. `display:none` has none of those failure modes — the element is
   simply not rendered, so there is no frame to get stuck and nothing to
   animate. Do NOT reintroduce a transition here. */
.hub {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
}
body.hub-open .app { display: none; }
.hub[hidden] { display: none; }
/* ================= Hub stage =================
   The sun's JOB survives (show what's next, click to go there) but it is
   typography now, not a glowing disc — the arc dock owns the lower third,
   so the hero had to stop being a circle competing with eleven other
   circles. sunFocus() still supplies the content and the click target. */
.hub-horizon {
  position: absolute;
  left: 50%;
  bottom: -74vw;
  width: 156vw;
  height: 100vw;
  transform: translateX(-50%);
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 0%, rgba(124, 134, 242, 0.11), rgba(14, 10, 26, 0) 60%);
}
.hub-stage {
  position: absolute;
  inset: 0 0 38% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 24px;
  cursor: pointer;
}
.stage-date {
  font-family: var(--serif);
  font-style: italic;
  font-size: 16px;
  color: var(--ink-2);
  margin-bottom: 14px;
}
.stage-eyebrow {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
  text-shadow: 0 0 18px color-mix(in srgb, var(--accent) 60%, transparent);
}
.stage-big {
  font-family: var(--display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(44px, 8.6vw, 104px);
  line-height: 0.92;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--ink);
  text-shadow: 0 0 60px rgba(176, 130, 255, 0.28);
  transition: text-shadow 0.2s ease;
}
.hub-stage:hover .stage-big { text-shadow: 0 0 74px rgba(176, 130, 255, 0.45); }
.stage-sub { font-family: var(--serif); font-style: italic; font-size: 18px; color: var(--ink-2); margin-top: 14px; }
.stage-meta { display: flex; gap: 26px; margin-top: 30px; flex-wrap: wrap; justify-content: center; }
.meta-item { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.meta-num { font-family: var(--display); font-size: 26px; font-weight: 800; line-height: 1; color: var(--ink); }
.meta-num.warn { color: var(--warning); }
.meta-num.bad { color: var(--critical); }
.meta-num.ok { color: var(--good-text); }
.meta-lbl { font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }

/* ================= Hub dock ================= */
/* bottom: 3vh (not 0) so the row and its labels sit clear of the very
   bottom edge on a tall/full-screen window instead of hugging it. */
.hub-dock { position: absolute; left: 0; right: 0; bottom: 3vh; height: 40%; pointer-events: none; }
/* positionHubBubbles() reads THIS element's clientWidth to place every
   bubble and draw the connecting arc — capping it at a fixed 1180px meant
   the whole row (and the line under it) stopped growing past a ~1255px
   window while .hub-horizon behind it kept scaling in vw, so on a wide or
   full-screen window the line visibly fell short of the backdrop it sits
   on. 1900px is still a ceiling (an ultrawide monitor shouldn't spread
   bubbles absurdly far apart), just one wide enough to actually track a
   normal full-screen window instead of an old MacBook's. */
.hub-dock-inner { position: relative; width: min(1900px, 92vw); height: 100%; margin: 0 auto; }
/* Horizon line, generated in JS from the SAME parabola that places the
   bubbles, so they sit on it by construction at any section count. An
   eyeballed CSS ellipse drifted out of alignment the moment the curve
   depth or the number of sections changed. */
.hub-arc { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.hub-arc polyline { fill: none; stroke: rgba(190, 165, 255, 0.26); stroke-width: 1; }
.hub-arc polyline.glow { stroke: rgba(124, 134, 242, 0.16); stroke-width: 7; filter: blur(5px); }
.hub-dock-label {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  text-shadow: 0 1px 10px rgba(5, 6, 10, 0.9), 0 0 3px rgba(5, 6, 10, 0.8);
  transition: text-shadow 0.2s ease;
}
.hub-dock-sub {
  font-size: 10px;
  letter-spacing: 0.05em;
  color: var(--muted);
  white-space: nowrap;
  margin-top: -5px;
  min-height: 12px;
}
.hub-slot:hover .hub-dock-label {
  text-shadow: 0 1px 10px rgba(5, 6, 10, 0.9), 0 0 16px color-mix(in srgb, var(--c, transparent) 70%, transparent);
}
.hub-bubble { pointer-events: auto; }
.hub-slot {
  position: absolute;
  left: var(--x, 50%);
  bottom: var(--y, 0px);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  pointer-events: auto;
  transform: translateX(-50%) scale(0);
  transition: transform 0.45s cubic-bezier(0.22, 1.4, 0.36, 1);
  transition-delay: var(--d, 0s);
}
.hub.open .hub-slot { transform: translateX(-50%) scale(1); }
/* Dock magnification — the lift lives on the slot, the scale on the bubble,
   so the label rises with it without being stretched. */
.hub.open .hub-slot:hover { transform: translateX(-50%) translateY(-14px) scale(1); transition-delay: 0s; }
.hub-slot:has(.hub-bubble:active) { transform: translateX(-50%) translateY(-10px) scale(0.95); transition-delay: 0s; }
.hub-bubble {
  width: var(--sz, 62px);
  height: var(--sz, 62px);
  border-radius: 50%;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--fs, 13px);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink);
  background:
    radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--c, transparent) 14%, transparent), transparent 65%),
    rgba(19, 18, 32, 0.72);
  border: 1px solid color-mix(in srgb, var(--c, #ffffff) 38%, transparent);
  /* --u is this section's urgency, 0-1, written by renderSidebar(). It
     drives glow strength as well as size, so "needs you" reads instantly
     without having to compare planet diameters against each other. */
  box-shadow: 0 0 calc(12px + 26px * var(--u, 0)) color-mix(in srgb, var(--c, transparent) calc((10 + 34 * var(--u, 0)) * 1%), transparent), var(--shadow);
  cursor: pointer;
  flex-direction: column;
  gap: 1px;
  transition: border-color 0.15s ease, box-shadow 0.25s ease, transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.hub.open .hub-slot:hover .hub-bubble { transform: scale(1.2); }
.hub-bubble.ring-1 { background: rgba(19, 18, 32, 0.6); border-color: color-mix(in srgb, var(--c, #ffffff) 26%, transparent); }
.hub-bubble.ring-2 { background: rgba(19, 18, 32, 0.52); border-color: color-mix(in srgb, var(--c, #ffffff) 22%, transparent); }
.hub-bubble:hover { border-color: var(--c, var(--accent)); box-shadow: 0 0 30px color-mix(in srgb, var(--c, transparent) 46%, transparent), var(--shadow); }
.hub-bubble.active { border-color: var(--c, var(--accent)); box-shadow: 0 0 22px color-mix(in srgb, var(--c, transparent) 34%, transparent), var(--shadow); }
/* display:contents — invisible to the flex layout, exists purely as a
   render target so renderSidebar() can rewrite the stat/badge on every tick
   without touching (and wiping) the icon sibling next to it. */
.hub-bubble-content { display: contents; }
/* The icon per section — hairline, not a logo. It's the whole visible
   content of the bubble now (2026-08-24: no more big centered number
   competing with it — see renderSidebar()), so it stays at one steady
   opacity instead of dimming for a stat that no longer renders here. */
.hub-bubble-icon {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 42%;
  height: 42%;
  color: var(--c, var(--ink-2));
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.hub.open .hub-slot:hover .hub-bubble-icon { opacity: 0.65; }
/* Fires only when something genuinely needs him — overdue work, a class
   starting inside 30 minutes, a stuck transcription. Deliberately rare, so
   it never becomes wallpaper he stops seeing. */
.hub-bubble.alert {
  border-color: color-mix(in srgb, var(--c, #ffffff) 85%, #ffffff);
  box-shadow: 0 0 34px color-mix(in srgb, var(--c, transparent) 55%, transparent),
              0 0 0 1px color-mix(in srgb, var(--c, transparent) 45%, transparent), var(--shadow);
}
/* Corner notification bubble — the only thing that ever overlays the icon.
   Numbered when hubStatus() has a count (overdue items, low accounts…);
   .dot is the bare, smaller version for an alert with nothing to count
   (a class in session, an empty Contacts list pre-semester). */
.hub-bubble .n {
  position: absolute;
  top: -4px;
  right: 2px;
  min-width: 20px;
  height: 20px;
  border-radius: 10px;
  padding: 0 5px;
  display: grid;
  place-items: center;
  font-size: 11px;
  background: var(--accent-grad);
  color: #fff;
}
.hub-bubble .n.dot {
  min-width: 10px;
  width: 10px;
  height: 10px;
  padding: 0;
  top: -2px;
  right: 4px;
}
.hub-foot {
  position: absolute;
  bottom: 22px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
}
@media (prefers-reduced-motion: reduce) {
  .hub-slot { transition-duration: 0.01s; transition-delay: 0s; }
}

/* ================= assistant fly transition ================= */
/* One-shot overlay div, created and removed by JS around a normal instant
   view swap — see playFlyTransition(). z-index 45: above the docked panel
   (40), below the modal veil (50), so a modal can still interrupt it. */
@keyframes fly-warp {
  0%   { opacity: 0; transform: scale(0.85); filter: blur(0px); }
  35%  { opacity: 1; transform: scale(1.15); filter: blur(2px); }
  60%  { opacity: 1; transform: scale(1.3); filter: blur(6px); }
  100% { opacity: 0; transform: scale(1.6); filter: blur(0px); }
}
.fly-overlay {
  position: fixed;
  inset: 0;
  z-index: 45;
  pointer-events: none;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 50%,
      rgba(226, 214, 255, 0.95),
      rgba(168, 128, 246, 0.75) 30%,
      rgba(88, 60, 150, 0.5) 55%,
      rgba(15, 10, 26, 0.85) 80%);
  animation: fly-warp 1.05s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* Hyperspace streaks — thin spokes radiating from center, rushing outward.
   A conic gradient of hairline stops is one element instead of dozens of
   positioned divs, and it reads as motion lines once it scales past the
   viewport. Picks up the same starfield language as the app backdrop. */
.fly-overlay::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 180vmax;
  height: 180vmax;
  margin: -90vmax 0 0 -90vmax;
  background: repeating-conic-gradient(
    from 0deg,
    rgba(255, 255, 255, 0) 0deg 1.6deg,
    rgba(255, 255, 255, 0.5) 1.6deg 1.9deg,
    rgba(255, 255, 255, 0) 1.9deg 4deg);
  mask-image: radial-gradient(circle, transparent 4%, #000 22%, #000 46%, transparent 70%);
  -webkit-mask-image: radial-gradient(circle, transparent 4%, #000 22%, #000 46%, transparent 70%);
  animation: fly-streaks 1.05s cubic-bezier(0.3, 0, 0.1, 1) forwards;
}
@keyframes fly-streaks {
  0%   { opacity: 0; transform: scale(0.06) rotate(0deg); }
  30%  { opacity: 0.9; }
  100% { opacity: 0; transform: scale(1) rotate(9deg); }
}
/* Arrival: the destination view settles in as you land. Deliberately a
   one-shot transform on #view-root ALONE — never a cross-fade between the
   outgoing and incoming views, which is the pattern that caused stuck
   frames and double blinks before the app moved to instant view swaps. */
@keyframes fly-arrive {
  from { opacity: 0; transform: scale(0.965) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
#view-root.flying-in { animation: fly-arrive 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
@media (prefers-reduced-motion: reduce) {
  .fly-overlay, .fly-overlay::before { animation-duration: 0.01s; }
  #view-root.flying-in { animation-duration: 0.01s; }
}

/* ================= Sol captions ================= */
/* Sits in the page's own bottom gutter rather than floating over content:
   body.sol-captioning adds matching bottom padding to #main, so the caption
   never covers what Sol just flew you to. Stops short of the docked panel
   when that's open, for the same reason. */
.sol-caption {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 44;
  padding: 13px 22px 15px;
  background: linear-gradient(to top, rgba(10, 8, 20, 0.97) 60%, rgba(10, 8, 20, 0));
  border-top: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--ink);
  font-size: 15px;
  line-height: 1.45;
  display: flex;
  align-items: baseline;
  gap: 12px;
  animation: sol-caption-in 0.22s ease both;
  transition: right 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.sol-caption[hidden] { display: none; }
body.sol-panel-open .sol-caption { right: 380px; }
@keyframes sol-caption-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.sol-caption-name {
  font-family: var(--display);
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  flex: none;
  text-shadow: 0 0 14px color-mix(in srgb, var(--accent) 55%, transparent);
}
.sol-caption-text { flex: 1; min-width: 0; }
/* Already-spoken words stay bright; the rest sits back until it's said. */
.sol-caption-text .said { color: var(--ink); }
.sol-caption-text .unsaid { color: var(--muted); }
body.sol-captioning #main { padding-bottom: 76px; }
@media (max-width: 760px) {
  body.sol-panel-open .sol-caption { right: 0; }
}

/* ================= assistant (docked panel) ================= */
/* Hidden while the hub is open (its own full-screen radial nav — the whole
   .app, topbar included, is already display:none then) and during a focus
   session (practice test, flashcard review, match game) where a panel would
   just be clutter over something meant to hold full attention. */
body.hub-open #chat-root { display: none; }

/* The topbar entry point — a small "sun": the same lit-from-within violet
   radial gradient as the Hub's central orb (see .orb), at icon scale. Not
   the actual orb (that still closes the Hub — repurposing it would break
   existing navigation), but the same visual language, so the assistant
   reads as GCUHQ's own rather than a generic bolted-on chat icon. */
.topbar-chat {
  appearance: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  position: relative;
  background:
    radial-gradient(circle at 50% 42%, rgba(226, 214, 255, 0.95), rgba(168, 128, 246, 0.85) 30%, rgba(126, 86, 214, 0.65) 55%, rgba(58, 34, 104, 0.55) 100%);
  box-shadow: 0 0 10px rgba(176, 130, 255, 0.5), 0 0 22px rgba(139, 147, 248, 0.28);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.topbar-chat:hover { transform: scale(1.1); box-shadow: 0 0 14px rgba(176, 130, 255, 0.65), 0 0 30px rgba(139, 147, 248, 0.4); }
.topbar-chat.open { box-shadow: 0 0 0 2px var(--accent), 0 0 14px rgba(176, 130, 255, 0.65); }

/* ---- Sol's status light ----
   The orb is the only part of Sol visible from every screen with the panel
   closed, so it says what Sol is doing without being opened. State lives on
   pseudo-elements rather than box-shadow, because .open already owns the
   box-shadow and the two would fight. Each state is a different KIND of
   motion, not just a different colour — glanceable from across the room and
   still distinguishable if you don't see colour well. */
.topbar-chat::before,
.topbar-chat::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
}
/* THINKING — a ring pulsing outward, same language as the transcript's
   thinking indicator so the two read as one system. */
.topbar-chat.is-thinking::after {
  border: 1.5px solid color-mix(in srgb, var(--accent) 75%, transparent);
  opacity: 1;
  animation: sol-orb-sonar 1.5s ease-out infinite;
}
/* LISTENING — two rings, faster and offset, so it reads as actively taking
   something in rather than working on something. */
.topbar-chat.is-listening::after,
.topbar-chat.is-listening::before {
  border: 1.5px solid color-mix(in srgb, var(--good) 80%, transparent);
  opacity: 1;
  animation: sol-orb-sonar 1.1s ease-out infinite;
}
.topbar-chat.is-listening::before { animation-delay: 0.55s; }
.topbar-chat.is-listening { box-shadow: 0 0 12px color-mix(in srgb, var(--good) 60%, transparent); }
/* SPEAKING — no rings; the orb itself swells and brightens, so "Sol is
   talking" looks like output where listening looks like intake. */
.topbar-chat.is-speaking { animation: sol-orb-speak 1.05s ease-in-out infinite; }

@keyframes sol-orb-sonar {
  0%   { transform: scale(0.85); opacity: 0.85; }
  100% { transform: scale(2.1);  opacity: 0; }
}
@keyframes sol-orb-speak {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 10px rgba(176, 130, 255, 0.5), 0 0 22px rgba(139, 147, 248, 0.28); }
  50%      { transform: scale(1.16); box-shadow: 0 0 18px rgba(206, 170, 255, 0.9), 0 0 40px rgba(139, 147, 248, 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .topbar-chat.is-thinking::after,
  .topbar-chat.is-listening::after,
  .topbar-chat.is-listening::before { animation: none; opacity: 0.5; }
  .topbar-chat.is-speaking { animation: none; box-shadow: 0 0 18px rgba(206, 170, 255, 0.9); }
}

.chat-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 380px;
  max-width: 92vw;
  background: var(--sheet-raised);
  backdrop-filter: blur(24px);
  border-left: 1px solid var(--rule-strong);
  box-shadow: -8px 0 40px rgba(0, 0, 0, 0.35);
  z-index: 40;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Grows out of the topbar orb instead of sliding in from the screen edge,
     so the panel reads as spawned by the button that was just clicked. The
     origin comes from JS (--sol-ox/--sol-oy, measured off the orb) and lands
     outside the panel's own box, since the orb is at the far left and the
     panel is on the right — scaling about that far point is what produces
     the unfolding-from-the-button feel. Falls back to the top-right corner
     if JS hasn't measured yet.

     visibility is transitioned with a delay rather than left visible: the
     panel now occupies its real position even when closed (it is only
     transparent and scaled down), so without this a shut panel would still
     take focus and keyboard navigation. */
  transform-origin: var(--sol-ox, 100%) var(--sol-oy, 0px);
  transform: scale(0.86);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.18s ease,
              visibility 0s linear 0.26s;
}
.chat-panel.open {
  transform: scale(1);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.16s ease,
              visibility 0s linear 0s;
}
@media (prefers-reduced-motion: reduce) {
  .chat-panel { transition: opacity 0.12s linear, visibility 0s linear 0.12s; transform: none; }
  .chat-panel.open { transform: none; transition: opacity 0.12s linear, visibility 0s linear 0s; }
}
/* A quiet echo of the sun's corona at the top of the panel, not a light show
   — just enough to feel like it belongs to the same app as the Hub. */
.chat-panel::before {
  content: "";
  position: absolute;
  top: -120px;
  right: -80px;
  width: 260px;
  height: 260px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(176, 130, 255, 0.16), rgba(159, 110, 240, 0.05) 45%, transparent 70%);
  pointer-events: none;
}
.chat-panel-head {
  position: relative;
  padding: 16px 16px 12px;
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
}
.chat-panel-title {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.chat-panel-title > :first-child {
  font-family: var(--display);
  font-size: 17px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink);
}
.chat-sol-status {
  font-size: 11px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  color: var(--accent);
}
.chat-panel-actions { display: flex; align-items: center; gap: 10px; }
.chat-panel-close {
  appearance: none;
  background: none;
  border: none;
  color: var(--ink-2);
  font-size: 16px;
  cursor: pointer;
  line-height: 1;
  padding: 2px;
}
.chat-panel-close:hover { color: var(--ink); }
.chat-msgs {
  flex: 1;
  overflow-y: auto;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.chat-msg {
  max-width: 86%;
  padding: 8px 11px;
  border-radius: 12px;
  font-size: 13px;
  line-height: 1.4;
  white-space: pre-wrap;
}
.chat-msg.user {
  align-self: flex-end;
  background: var(--accent-grad);
  color: #fff;
  border-bottom-right-radius: 4px;
}
.chat-msg.assistant {
  align-self: flex-start;
  background: var(--sheet);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-bottom-left-radius: 4px;
}
/* Entrance for genuinely new rows only — renderChatWidget() rebuilds the
   whole list constantly (every dictation partial), so this is gated on
   .is-new from JS rather than applied to every .chat-msg. Matches the
   view-in motion the rest of the app uses, at a shorter distance because a
   chat row is small. */
@keyframes chat-msg-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.chat-msg.is-new { animation: chat-msg-in 0.28s cubic-bezier(0.16, 1, 0.3, 1) both; }

/* "Thinking" — a miniature of the hub's sun rather than grey italic text. */
.chat-msg.thinking {
  display: flex;
  align-items: center;
  gap: 10px;
  background: none;
  border: none;
  padding-left: 2px;
}
.sol-think-orb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex: none;
  position: relative;
  background: radial-gradient(circle at 34% 30%, #d8d2ff 0%, var(--accent) 45%, #5b3fc7 100%);
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 70%, transparent);
  animation: sol-think-breathe 1.5s ease-in-out infinite;
}
/* The sonar ring is what actually reads as "working" — the breathing alone
   is too quiet to notice, and a motion nobody notices is the same as none. */
.sol-think-orb::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
  animation: sol-think-sonar 1.5s ease-out infinite;
}
.sol-think-label {
  font-family: var(--display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  animation: sol-think-fade 1.5s ease-in-out infinite;
}
@keyframes sol-think-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.14); } }
@keyframes sol-think-sonar {
  0%   { transform: scale(0.7); opacity: 0.75; }
  100% { transform: scale(1.75); opacity: 0; }
}
@keyframes sol-think-fade { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .chat-msg.is-new { animation: none; }
  .sol-think-orb, .sol-think-orb::after, .sol-think-label { animation: none; }
  .sol-think-orb::after { opacity: 0.4; }
}
/* A change Sol made to real data. Deliberately styled as a receipt rather
   than a chat bubble — it is a record of something that happened, not
   something said, and it must never be possible to miss. */
.chat-msg.action {
  align-self: stretch;
  background: color-mix(in srgb, var(--good) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--good) 32%, transparent);
  color: var(--ink);
  font-size: 12px;
  border-radius: 8px;
  padding: 6px 10px;
  display: flex;
  align-items: baseline;
  gap: 7px;
}
.chat-msg.action::before {
  content: "✓";
  color: var(--good-text);
  font-weight: 700;
  flex: none;
}
/* The tick lands a beat after its row arrives, so a receipt reads as
   something completing rather than a row that was always there. Runs once
   per new row — .is-new is only ever set on rows that just appeared. */
.chat-msg.action.is-new::before {
  animation: sol-tick-in 0.34s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: inherit;
}
@keyframes sol-tick-in {
  0%   { opacity: 0; transform: scale(0.3) rotate(-25deg); }
  60%  { opacity: 1; transform: scale(1.25) rotate(0deg); }
  100% { opacity: 1; transform: scale(1); }
}
/* The row itself gets a brief green bloom as it settles — draws the eye to
   the fact that real data just changed, then gets out of the way. */
.chat-msg.action.is-new {
  animation: chat-msg-in 0.28s cubic-bezier(0.16, 1, 0.3, 1) both,
             sol-action-bloom 0.9s ease-out both;
}
@keyframes sol-action-bloom {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 45%, transparent); }
  100% { box-shadow: 0 0 0 6px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-msg.action.is-new,
  .chat-msg.action.is-new::before { animation: none; }
}
.chat-empty {
  color: var(--ink-2);
  font-size: 12.5px;
  padding: 8px 2px;
}
.chat-input-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid var(--rule);
}
.chat-input-row textarea {
  flex: 1;
  resize: none;
  min-height: 36px;
  max-height: 160px;
  overflow-y: auto;
  border-radius: 10px;
  border: 1px solid var(--rule-strong);
  background: var(--paper-deep);
  color: var(--ink);
  padding: 8px 10px;
  font-size: 13px;
  font-family: inherit;
  line-height: 1.4;
}
.chat-input-row button {
  align-self: flex-end;
}
.chat-mic {
  flex: none;
  align-self: flex-end;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--rule-strong);
  background: none;
  color: var(--ink-2);
  font-size: 15px;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.chat-mic:hover { border-color: var(--accent); color: var(--ink); }
.chat-mic.active {
  background: var(--critical);
  border-color: var(--critical);
  color: #fff;
  animation: chat-mic-pulse 1.2s ease-in-out infinite;
}
@keyframes chat-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(229, 99, 108, 0.5); }
  50% { box-shadow: 0 0 0 7px rgba(229, 99, 108, 0); }
}

/* ================= GCU email ================= */
.email-head-actions,
.email-reader-actions,
.email-compose-foot,
.email-list-foot {
  display: flex;
  align-items: center;
  gap: 9px;
}
.email-sync-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 7px;
  border-radius: 50%;
  background: var(--ok);
  vertical-align: 1px;
}
.email-sync-dot.active { animation: email-pulse 1.2s ease-in-out infinite; }
@keyframes email-pulse { 50% { opacity: 0.25; transform: scale(0.75); } }

.email-inbox { padding: 0; overflow: hidden; }
.email-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--sheet-raised) 72%, transparent);
}
.email-toolbar input[type="search"] { flex: 1 1 240px; min-width: 180px; }
.email-toolbar label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--ink-2);
  font-size: 12px;
  white-space: nowrap;
}
.email-toolbar label input { width: 14px; height: 14px; }
.email-toolbar select { font-size: 12px; }
.email-list { display: flex; flex-direction: column; }
.email-row {
  display: grid;
  grid-template-columns: 8px minmax(0, 1fr) auto auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 13px 15px;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.email-row:hover { background: var(--accent-soft); }
.email-row-actions { display: inline-flex; align-items: center; gap: 4px; }
.email-attach { white-space: nowrap; }
.email-row.unread { background: color-mix(in srgb, var(--accent-soft) 45%, transparent); }
.email-unread-dot { width: 7px; height: 7px; border-radius: 50%; background: transparent; }
.email-row.unread .email-unread-dot { background: var(--accent); }
.email-row-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.email-row-top { min-width: 0; display: flex; justify-content: space-between; gap: 16px; }
.email-sender,
.email-subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.email-sender { font-size: 13px; color: var(--ink-2); }
.email-row.unread .email-sender,
.email-row.unread .email-subject { color: var(--ink); font-weight: 700; }
.email-subject { font-size: 14px; }
.email-date { flex: none; color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.email-badges { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-size: 11px; }
.email-triage { display:inline-flex;align-items:center;padding:3px 7px;border:1px solid currentColor;border-radius:999px;font-size:9px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap; }
.email-triage-look-now { color:#ff7a86;background:rgba(255,82,101,.12); }
.email-triage-read { color:#9d8cff;background:rgba(137,113,255,.12); }
.email-triage-archive { color:#3ecf8e;background:rgba(62,207,142,.12); }
.email-triage-delete { color:#9290a5;background:rgba(146,144,165,.1); }
.email-triage-card { margin:12px 0;padding:14px;border:1px solid var(--rule-strong);border-radius:12px;background:#171525;display:grid;gap:9px; }
.email-triage-card-head,.email-triage-summary,.email-triage-reason { display:flex;gap:10px;align-items:flex-start; }
.email-triage-card-head { align-items:center;color:var(--muted);font-size:11px; }
.email-triage-summary strong,.email-triage-reason strong { width:64px;flex:none;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em; }
.email-triage-card p { margin:0;color:var(--muted);font-size:11px; }
.email-empty,
.email-error { padding: 28px 18px; color: var(--muted); text-align: center; }
.email-error { color: var(--critical-text); }
.email-list-foot { justify-content: space-between; padding: 10px 14px; color: var(--muted); font-size: 11.5px; }

.email-reader {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-radius: 16px;
  box-shadow: var(--shadow);
  padding: 20px 22px;
}
.email-reader-actions { justify-content: space-between; margin-bottom: 18px; }
.email-reader-subject {
  margin: 0 0 10px;
  font-family: var(--serif);
  font-size: clamp(23px, 3vw, 34px);
  line-height: 1.15;
}
.email-reader-meta { display: flex; justify-content: space-between; gap: 16px; color: var(--ink-2); font-size: 12.5px; }
.email-reader-meta span { text-align: right; }
.email-body {
  margin: 22px 0 0;
  padding: 20px 0 4px;
  border-top: 1px solid var(--rule);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: inherit;
  font-size: 14px;
  line-height: 1.65;
  color: var(--ink);
}
.email-compose { padding: 18px 20px; }
.email-account-chip { margin-left: auto; color: var(--muted); font-size: 11.5px; }
.email-compose-grid { display: grid; gap: 11px; }
.email-compose-grid input,
.email-compose-grid textarea { width: 100%; box-sizing: border-box; }
.email-compose-grid textarea { resize: vertical; line-height: 1.5; }
.email-compose-foot { justify-content: space-between; margin-top: 14px; color: var(--muted); font-size: 11.5px; }
.email-organize summary { cursor: pointer; font-family: var(--display); font-weight: 650; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-2); }
.email-organize[open] summary { margin-bottom: 10px; }
.email-confirm-summary { display: grid; gap: 6px; color: var(--ink-2); font-size: 13px; }
.email-confirm-summary strong { display: inline-block; min-width: 58px; color: var(--ink); }
.email-confirm-body {
  max-height: 240px;
  overflow: auto;
  margin: 14px 0 8px;
  padding: 12px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--paper-deep);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: inherit;
  font-size: 12.5px;
  line-height: 1.45;
}
@media (max-width: 700px) {
  .email-toolbar { align-items: stretch; }
  .email-toolbar input[type="search"] { flex-basis: 100%; }
  .email-row { grid-template-columns: 8px minmax(0, 1fr); }
  .email-badges { grid-column: 2; }
  .email-row-actions { grid-column: 2; justify-content: flex-start; }
  .email-row-top,
  .email-reader-meta,
  .email-compose-foot { align-items: flex-start; flex-direction: column; gap: 4px; }
  .email-reader-meta span { text-align: left; }
}

/* ================= live mic meter =================
   Sits where the caption sits (the caption is hidden while listening — it
   only shows what Sol SAYS), and slides aside for the panel the same way.
   Driven by real audio levels, so a flat meter means a dead mic. */
.sol-meter {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 44;
  padding: 12px 22px 14px;
  background: linear-gradient(to top, rgba(10, 8, 20, 0.97) 60%, rgba(10, 8, 20, 0));
  border-top: 1px solid color-mix(in srgb, var(--good) 26%, transparent);
  display: flex;
  align-items: center;
  gap: 14px;
  animation: sol-caption-in 0.22s ease both;
  transition: right 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.sol-meter[hidden] { display: none; }
body.sol-panel-open .sol-meter { right: 380px; }
.sol-meter-label {
  font-family: var(--display);
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--good-text);
  flex: none;
  text-shadow: 0 0 14px color-mix(in srgb, var(--good) 55%, transparent);
}
.sol-meter-bars {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 26px;
}
.sol-meter-bars i {
  display: block;
  width: 4px;
  height: 26px;
  border-radius: 2px;
  background: linear-gradient(to top, color-mix(in srgb, var(--good) 55%, transparent), var(--good));
  transform: scaleY(0.12);
  transform-origin: center;
  /* Short transition smooths between the ~16/s updates without adding lag
     you can feel. */
  transition: transform 0.07s linear;
}
body.sol-metering #main { padding-bottom: 74px; }
@media (max-width: 760px) {
  body.sol-panel-open .sol-meter { right: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .sol-meter { animation: none; }
  .sol-meter-bars i { transition: none; }
}

/* ================= full-screen voice mode =================
   Voice is a different interaction from typing, so it stops being a panel in
   the corner. The app sits behind a scrim, and one centred orb carries the
   whole state: it swells with the live mic level while listening and breathes
   while Sol talks. */
.sol-stage {
  position: fixed;
  inset: 0;
  z-index: 46;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 30px;
  padding: 40px 6vw;
  text-align: center;
  background:
    radial-gradient(ellipse at 50% 42%, rgba(46, 32, 92, 0.55), transparent 62%),
    rgba(8, 6, 16, 0.9);
  backdrop-filter: blur(3px);
  animation: sol-stage-in 0.32s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.sol-stage[hidden] { display: none; }
@keyframes sol-stage-in { from { opacity: 0; } to { opacity: 1; } }

/* The panel, bottom caption and meter are all redundant here — the stage
   shows the same things, larger and centred. */
body.sol-stage-on .chat-panel,
body.sol-stage-on .sol-caption,
body.sol-stage-on .sol-meter { display: none; }

.sol-stage-orb {
  width: 132px;
  height: 132px;
  border-radius: 50%;
  flex: none;
  position: relative;
  background: radial-gradient(circle at 38% 32%, #efe9ff 0%, #b9a5ff 26%, var(--accent) 52%, #4a2fae 100%);
  box-shadow: 0 0 46px color-mix(in srgb, var(--accent) 60%, transparent),
              0 0 120px color-mix(in srgb, var(--accent) 30%, transparent);
  /* --swell is the live mic level while listening; 1 otherwise. Transition
     is short so it tracks the voice without feeling laggy. */
  transform: scale(var(--swell, 1));
  transition: transform 0.08s linear, box-shadow 0.3s ease;
}
.sol-stage-orb.listening {
  box-shadow: 0 0 46px color-mix(in srgb, var(--good) 55%, transparent),
              0 0 120px color-mix(in srgb, var(--good) 28%, transparent);
}
.sol-stage-orb.listening::after {
  content: "";
  position: absolute;
  inset: -14px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--good) 55%, transparent);
  animation: sol-orb-sonar 1.6s ease-out infinite;
}
.sol-stage-orb.speaking { animation: sol-stage-breathe 1.1s ease-in-out infinite; }
@keyframes sol-stage-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.07); }
}

.sol-stage-text {
  max-width: 30ch;
  font-family: var(--display);
  font-size: clamp(22px, 3.4vw, 40px);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.01em;
  min-height: 1.25em;
}
.sol-stage-text .said { color: var(--ink); }
.sol-stage-text .unsaid { color: rgba(255, 255, 255, 0.26); }
.sol-stage-hint {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  min-height: 1em;
}
@media (prefers-reduced-motion: reduce) {
  .sol-stage { animation: none; }
  .sol-stage-orb, .sol-stage-orb.speaking { animation: none; transition: none; }
  .sol-stage-orb.listening::after { animation: none; opacity: 0.4; }
}

/* Piper install progress. Same shape as the AI spend bar so the two read as
   one system rather than two people's work. */
.piper-setup { margin-top: 4px; }
.piper-bar {
  margin-top: 10px;
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}
.piper-bar > i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent-grad);
  transition: width 0.25s var(--ease-out);
}

/* Spend against the monthly limit. A number alone doesn't say "nearly out";
   a bar does it before you've read the number. */
.ai-cap-bar {
  margin-top: 10px;
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  overflow: hidden;
}
.ai-cap-bar > i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent-grad);
  transition: width var(--dur-small) var(--ease-out);
}
.ai-cap-bar.is-over > i { background: var(--critical); }

/* ============ AI tab ============
   Sol's own routed view — deliberately its own element/classes rather than
   a reuse of #sol-stage: that element's FLIP-docking state machine belongs
   to hands-free voice sessions specifically, and this tab is never one.

   No caption text on the orb by design — idle / listening / thinking /
   speaking are each their own motion, so the animation itself is the
   feedback. The conversation still exists (chatHistory) behind the
   .ai-transcript-toggle, collapsed by default. */
@property --ai-spin { syntax: '<angle>'; inherits: false; initial-value: 0deg; }

/* One spacing scale down the stack instead of a big void under the orb and
   everything else crowded at the bottom. */
/* Centred in the window rather than stacked under the topbar: this view is
   one object and one question, so it should sit in the middle of the space
   it has instead of leaving a screen of dead room beneath it. The subtraction
   covers the topbar (60px) plus #view-root's own vertical padding (90px). */
.ai-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 150px);
  gap: 0;
  padding: 4px 0 8px;
}

/* ---- solo mode ----
   The topbar goes away entirely while this view is open. A nav strip, a
   crumb, a search glyph and a Record button sitting above a single centred
   question is the whole reason the screen read as "a page that happens to
   contain an assistant" — with them gone it reads as the assistant. The one
   control left is the X, which is also what Esc has always done. */
body.ai-solo .topbar { display: none; }
/* The topbar is gone, so its 60px is no longer being subtracted out here. */
body.ai-solo .ai-stage { min-height: calc(100vh - 90px); }

.ai-close {
  position: absolute;
  top: -14px;
  right: 4px;
  z-index: 4;
  appearance: none;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  cursor: pointer;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: rgba(12, 10, 26, 0.5);
  color: var(--muted);
  backdrop-filter: blur(12px);
  transition: color var(--dur-press) var(--ease-out), border-color var(--dur-press) var(--ease-out),
              background var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
}
/* Pinned to the window, not to the stage, once the topbar is out of the way —
   otherwise it tracks the centred content down the page instead of sitting
   in the corner where a close button belongs. */
body.ai-solo .ai-close { position: fixed; top: 14px; right: 18px; }
.ai-close svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
}
.ai-close:hover {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 16%, rgba(12, 10, 26, 0.6));
  transform: scale(1.08);
}
.ai-close:active { transform: scale(0.96); }
.ai-stage .ai-hello { margin-top: -14px; }
.ai-stage .ai-pill { margin-top: 26px; }
/* Only the transcript toggle carries the chevron; the type toggle is a plain label. */
#ai-type-toggle::after { display: none; }
/* A touch more floor light under the orb than the page carries by itself.
   Kept faint and very wide: at full strength it read as a smear across the
   bottom of the window rather than as light. */
.ai-stage::after {
  content: "";
  position: fixed;
  z-index: -1;
  left: 50%;
  bottom: -52vh;
  width: 170vw;
  height: 86vh;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 13%, transparent),
    color-mix(in srgb, var(--accent) 4%, transparent) 60%, transparent 80%);
  pointer-events: none;
}
/* No perspective/transform here in the WebGL path: either one creates a
   stacking context, which isolates the canvas's mix-blend-mode and turns
   the orb back into a black square. Both are scoped to .no-gl below, where
   the CSS bands actually need them. */
.ai-orb-wrap {
  position: relative;
  width: 360px;
  height: 360px;
  max-width: 74vw;
  aspect-ratio: 1;
}
/* A wide, very soft ambient wash. The shader now emits its own halo past
   the silhouette, so this only has to seat the orb in the page — kept large
   and faint deliberately, since a tighter one drew a second visible disc
   just outside the first. */
.ai-orb-wrap::before {
  content: "";
  position: absolute;
  inset: -22%;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 16%, transparent),
    color-mix(in srgb, var(--accent) 6%, transparent) 55%, transparent 80%);
  filter: blur(38px);
  pointer-events: none;
}
/* screen: the canvas is opaque black outside the glow, and screen leaves the
   backdrop untouched wherever the source is black — so the orb adds light
   without laying a dark square (or a greyed disk) over the starfield. */
.ai-orb-canvas { width: 100%; height: 100%; display: block; mix-blend-mode: screen; }
/* With the shader running, the canvas is the entire orb — the CSS sphere
   and its bands are the no-WebGL fallback and stay out of its way. */
.ai-orb:not(.no-gl) {
  background: none !important;
  box-shadow: none !important;
  animation: none !important;
  transform: none !important;
  transform-style: flat;
}
.ai-orb.no-gl { perspective: 900px; }
.ai-orb:not(.no-gl) .ai-orb-band,
.ai-orb:not(.no-gl) .ai-orb-ring,
.ai-orb:not(.no-gl)::before { display: none; }
.ai-orb.no-gl .ai-orb-canvas { display: none; }
.ai-orb {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  flex: none;
  position: relative;
  transform-style: preserve-3d;
  /* Lower alpha in the fill (vs. a fully opaque sphere) plus a tight bright
     rim is what reads as "glass" rather than "plastic ball" — the backdrop
     shows through the middle while the edge still catches the light. */
  background: radial-gradient(circle at 32% 26%,
    rgba(246, 242, 255, 0.95) 0%, rgba(202, 186, 255, 0.75) 16%,
    color-mix(in srgb, var(--accent) 78%, transparent) 45%,
    color-mix(in srgb, #2c1b74 80%, transparent) 100%);
  box-shadow:
    0 0 60px color-mix(in srgb, var(--accent) 50%, transparent),
    0 0 150px color-mix(in srgb, var(--accent) 22%, transparent),
    inset 0 -18px 34px rgba(10, 6, 30, 0.4),
    inset 0 14px 26px rgba(255, 255, 255, 0.25);
  transform: scale(var(--swell, 1));
  transition: transform 0.08s linear, box-shadow 0.5s ease;
  /* Same pass as the shader's MODES: the fallback orb was drifting slowly
     enough to look static too, so every duration here is roughly halved. */
  animation: ai-orb-idle 3.4s ease-in-out infinite;
}
/* A soft rotating aura behind the sphere — the one thing that makes it read
   as "alive" even at rest, without needing a word of text. */
.ai-orb::before {
  content: "";
  position: absolute;
  inset: -38px;
  z-index: -1;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    color-mix(in srgb, var(--accent) 55%, transparent),
    transparent 28%,
    color-mix(in srgb, #cabaff 50%, transparent) 55%,
    transparent 82%,
    color-mix(in srgb, var(--accent) 55%, transparent));
  filter: blur(20px);
  opacity: 0.6;
  animation: ai-orb-spin 5.5s linear infinite;
}
/* Three glowing bands "orbiting" the sphere on different tilted planes —
   the closest a flat-CSS sphere gets to the twisting ribbon-of-light look
   this was modeled on, without a real 3D renderer. Each is a thin ring
   (a conic-gradient masked down to just its rim) tilted in 3D and spun
   continuously, at a different axis/speed/opacity so they read as layered
   depth rather than three flat circles. */
.ai-orb-band {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  --ai-spin: 0deg;
  transform: rotateX(var(--ai-tiltx, 70deg)) rotateY(var(--ai-tilty, 0deg)) rotateZ(var(--ai-spin));
  background: conic-gradient(from 0deg,
    transparent 0%, transparent 4%,
    color-mix(in srgb, #f2ecff 92%, transparent) 10%,
    color-mix(in srgb, var(--accent) 85%, transparent) 20%,
    transparent 34%, transparent 100%);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(50% - 4px), #000 calc(50% - 3px), #000 50%, transparent calc(50% + 1px));
          mask: radial-gradient(farthest-side, transparent calc(50% - 4px), #000 calc(50% - 3px), #000 50%, transparent calc(50% + 1px));
  animation: ai-band-spin 3.8s linear infinite;
  pointer-events: none;
}
.ai-orb-band.b1 { --ai-tiltx: 72deg; --ai-tilty: 0deg; }
.ai-orb-band.b2 { --ai-tiltx: 68deg; --ai-tilty: 58deg; opacity: 0.7; animation-duration: 5.2s; animation-direction: reverse; }
.ai-orb-band.b3 { --ai-tiltx: 76deg; --ai-tilty: -46deg; opacity: 0.5; animation-duration: 6.6s; }
@keyframes ai-band-spin { to { --ai-spin: 360deg; } }

.ai-orb-ring {
  position: absolute;
  inset: -16px;
  border-radius: 50%;
  border: 1px solid transparent;
  opacity: 0;
  pointer-events: none;
}
@keyframes ai-orb-idle {
  0%, 100% { transform: scale(var(--swell, 1)); }
  50%      { transform: scale(calc(var(--swell, 1) * 1.02)); }
}
@keyframes ai-orb-spin { to { transform: rotate(360deg); } }

.ai-orb.listening {
  animation: none;   /* the live mic swell owns the motion instead */
  box-shadow:
    0 0 60px color-mix(in srgb, var(--good) 55%, transparent),
    0 0 150px color-mix(in srgb, var(--good) 25%, transparent),
    inset 0 -18px 34px rgba(10, 6, 30, 0.4),
    inset 0 14px 26px rgba(255, 255, 255, 0.25);
}
.ai-orb.listening::before { animation-duration: 2.6s; }
.ai-orb.listening .ai-orb-band { animation-duration: 1.9s; }
.ai-orb.listening .ai-orb-ring {
  border-color: color-mix(in srgb, var(--good) 55%, transparent);
  animation: ai-orb-sonar 1.2s ease-out infinite;
}
.ai-orb.listening .ai-orb-ring.r2 { animation-delay: 0.6s; }
@keyframes ai-orb-sonar {
  from { transform: scale(1);    opacity: 0.65; }
  to   { transform: scale(1.55); opacity: 0; }
}

.ai-orb.thinking { animation: ai-orb-think 1s ease-in-out infinite; }
.ai-orb.thinking::before { animation-duration: 1.3s; }
.ai-orb.thinking .ai-orb-band { animation-duration: 2.2s; }
@keyframes ai-orb-think {
  0%, 100% { transform: scale(var(--swell, 1)); }
  50%      { transform: scale(calc(var(--swell, 1) * 0.94)); }
}

.ai-orb.speaking { animation: ai-orb-breathe 0.62s ease-in-out infinite; }
.ai-orb.speaking::before { animation-duration: 1.7s; }
.ai-orb.speaking .ai-orb-band { animation-duration: 1.4s; }
@keyframes ai-orb-breathe {
  0%, 100% { transform: scale(var(--swell, 1));         filter: brightness(1); }
  50%      { transform: scale(calc(var(--swell, 1) * 1.09)); filter: brightness(1.15); }
}

/* The whole header. This screen used to carry a 46px condensed question
   under the greeting as well — "How can I help you today?", then "How are
   you today?", then a second variant once a conversation existed — and all
   of them were restating what an orb above a button reading "Talk to Sol"
   already says. Cut entirely 2026-08-31. The italic serif line is the
   design.md treatment it always had; it just no longer introduces anything.
   Nothing else on the page is display type now, which is the point: the orb
   is the thing you look at. */
.ai-hello {
  color: var(--ink-2);
  font-family: var(--serif);
  font-style: italic;
  font-size: 15px;
  letter-spacing: 0.005em;
  opacity: 0.85;
}
/* ---- talk vs type ----
   One slot, two controls. Voice is the default because this tab is an orb
   you talk to; the text box only appears when asked for, which is what makes
   the "Type instead" toggle below mean something. */
.ai-stage .ai-pill { display: none; }
.ai-stage.typing .ai-pill { display: flex; }
.ai-stage.typing .ai-talk { display: none; }

.ai-talk {
  appearance: none;
  cursor: pointer;
  margin-top: 26px;
  display: inline-flex;
  align-items: center;
  gap: 11px;
  padding: 12px 26px 12px 20px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--rule-strong));
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 13%, rgba(12, 10, 26, 0.55));
  box-shadow: 0 10px 30px rgba(6, 4, 18, 0.4), inset 0 1px rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(14px);
  color: var(--ink);
  font-family: var(--ui-sans);
  font-size: 14.5px;
  font-weight: 550;
  transition: border-color var(--dur-small) var(--ease-out), background var(--dur-small) var(--ease-out),
              transform var(--dur-press) var(--ease-out), box-shadow var(--dur-small) var(--ease-out);
}
.ai-talk:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--accent) 66%, transparent);
  background: color-mix(in srgb, var(--accent) 19%, rgba(12, 10, 26, 0.6));
}
.ai-talk:active { transform: translateY(0) scale(0.985); }
.ai-talk-icon { display: flex; width: 17px; height: 17px; }
.ai-talk-icon svg { width: 100%; height: 100%; }
/* The waveform replaces the mic glyph while listening — same signal as the
   orb and the meter, so the three can never disagree. */
.ai-talk-bars { display: none; align-items: flex-end; gap: 2.5px; height: 17px; }
.ai-talk-bars i {
  width: 2.5px;
  height: 100%;
  border-radius: 2px;
  background: var(--good);
  transform: scaleY(0.12);
  transform-origin: 50% 100%;
  transition: transform 0.07s linear;
}
.ai-talk.is-listening .ai-talk-icon { display: none; }
.ai-talk.is-listening .ai-talk-bars { display: flex; }
.ai-talk.is-listening { border-color: color-mix(in srgb, var(--good) 58%, transparent); }
.ai-talk.is-speaking { border-color: color-mix(in srgb, var(--accent) 72%, transparent); }
.ai-talk.is-thinking { border-color: color-mix(in srgb, var(--warning) 50%, transparent); }
/* Live sessions get a slow ring so the button reads as "on" from the corner
   of the eye, without competing with the orb it sits under. */
.ai-talk.is-live { box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent), 0 10px 30px rgba(6, 4, 18, 0.4); }

/* The two quiet controls under the slot, on one line. */
.ai-secondary { display: flex; align-items: center; gap: 4px; margin-top: 6px; }

/* A secondary control, styled and placed like one: below the input rather
   than between the greeting and it, and quiet enough not to compete. */
.ai-transcript-toggle {
  appearance: none;
  background: none;
  border: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 4px;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-family: var(--ui-sans);
  font-size: 12px;
  font-weight: 500;
  transition: color var(--dur-press) var(--ease-out), background var(--dur-press) var(--ease-out);
}
.ai-transcript-toggle::after {
  content: "";
  width: 5px; height: 5px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-1px) rotate(45deg);
  transition: transform var(--dur-small) var(--ease-out);
}
.ai-transcript-toggle.is-open::after { transform: translateY(1px) rotate(-135deg); }
.ai-transcript-toggle:hover {
  color: var(--ink-2);
  background: rgba(255, 255, 255, 0.05);
  text-decoration: none;
}
.ai-transcript {
  display: none;
  width: 100%;
  max-width: 460px;
  max-height: 34vh;
  overflow-y: auto;
  flex-direction: column;
  gap: 10px;
  padding: 8px 2px 0;
}
.ai-transcript.is-open { display: flex; }

/* One rounded glass pill: type in it, or tap the waveform at its right to
   talk. No separate Send button — Enter sends, as in the reference. */
.ai-pill {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 440px;
  padding: 5px 6px 5px 20px;
  border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--rule-strong));
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 11%, rgba(12, 10, 26, 0.55));
  box-shadow: 0 10px 30px rgba(6, 4, 18, 0.4), inset 0 1px rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(14px);
  transition: border-color var(--dur-small) var(--ease-out), box-shadow var(--dur-small) var(--ease-out);
}
.ai-pill:focus-within { border-color: color-mix(in srgb, var(--accent) 62%, transparent); }
.ai-pill.is-listening { border-color: color-mix(in srgb, var(--good) 55%, transparent); }
.ai-pill .chat-draft-input {
  flex: 1;
  min-height: 34px;
  max-height: 120px;
  padding: 7px 0;
  border: none;
  background: none;
  box-shadow: none;
  /* Without this the textarea shows a resize grabber in its corner, which
     is the single clearest "unfinished" tell on the screen. */
  resize: none;
  overflow-y: auto;
  font-family: var(--ui-sans);
  font-size: 14.5px;
  line-height: 1.35;
}
.ai-pill .chat-draft-input:focus { outline: none; }
.ai-pill .chat-draft-input::placeholder { color: var(--muted); }

/* "Searching…" replaces the field while a turn is in flight. */
.ai-pill-busy {
  display: none;
  align-items: center;
  gap: 9px;
  flex: 1;
  padding: 7px 0;
  color: var(--ink-2);
  font-family: var(--ui-sans);
  font-size: 14.5px;
}
.ai-pill.is-busy .chat-draft-input { display: none; }
.ai-pill.is-busy .ai-pill-busy { display: flex; }
.ai-dots { display: inline-flex; gap: 4px; }
.ai-dots i {
  width: 5px; height: 5px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 80%, white);
  animation: ai-dot 1.1s ease-in-out infinite;
}
.ai-dots i:nth-child(2) { animation-delay: 0.16s; }
.ai-dots i:nth-child(3) { animation-delay: 0.32s; }
@keyframes ai-dot {
  0%, 100% { opacity: 0.25; transform: translateY(0); }
  50%      { opacity: 1;    transform: translateY(-3px); }
}

/* The mic button is also the live waveform: idle it's a mic glyph, while
   listening it becomes bars driven by the real mic level (same signal and
   the same centre-weighted shaping as the floating #sol-meter, so the two
   can never disagree about what your voice is doing). */
.ai-pill-mic {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-press) var(--ease-out), color var(--dur-press) var(--ease-out);
}
.ai-pill-mic:hover { background: rgba(255, 255, 255, 0.07); color: var(--ink); }
.ai-pill-mic > span { grid-area: 1 / 1; }
.ai-mic-bars {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 2.5px;
  height: 19px;
}
.ai-pill.is-listening .ai-mic-icon { display: none; }
.ai-pill.is-listening .ai-mic-bars { display: flex; }
.ai-mic-bars i {
  width: 2.5px;
  height: 100%;
  border-radius: 2px;
  background: var(--good);
  transform: scaleY(0.12);
  transition: transform 0.08s linear;
}

@media (prefers-reduced-motion: reduce) {
  .ai-orb, .ai-orb.speaking, .ai-orb.thinking { animation: none; transition: none; }
  .ai-orb::before, .ai-orb-band, .ai-dots i { animation: none; }
  .ai-orb .ai-orb-ring { animation: none; opacity: 0.35; }
}

/* A live, still-arriving answer. Same bubble as a finished one so text does
   not shift when it settles — only a soft caret marks it as in progress. */
.chat-msg.streaming::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 3px;
  vertical-align: -0.15em;
  border-radius: 1px;
  background: var(--accent);
  animation: sol-caret 1s steps(2, start) infinite;
}
@keyframes sol-caret { 0%, 100% { opacity: 1; } 50% { opacity: 0.15; } }
@media (prefers-reduced-motion: reduce) {
  .chat-msg.streaming::after { animation: none; opacity: 0.7; }
}

/* ---- docked voice mode ----
   Sol steps into the corner once it has taken you somewhere, so the screen
   it just navigated to is actually visible. The stage keeps its full-screen
   box (so the FLIP measurement stays simple) but stops painting a scrim and
   stops taking clicks — the app underneath is the point now. */
.sol-stage.docked {
  background: none;
  backdrop-filter: none;
  justify-content: flex-end;
  align-items: flex-end;
  padding: 0 22px 22px 0;
  gap: 12px;
  text-align: right;
  pointer-events: none;
}
.sol-stage.docked .sol-stage-orb {
  width: 54px;
  height: 54px;
  box-shadow: 0 0 22px color-mix(in srgb, var(--accent) 55%, transparent),
              0 0 54px color-mix(in srgb, var(--accent) 22%, transparent);
}
.sol-stage.docked .sol-stage-orb.listening {
  box-shadow: 0 0 22px color-mix(in srgb, var(--good) 55%, transparent),
              0 0 54px color-mix(in srgb, var(--good) 22%, transparent);
}
/* Two lines, then fade. Docked, the SCREEN is the content and Sol's words
   are the footnote — a four-line paragraph in the corner is a wall. The
   full text is still spoken, and expands again the moment Sol answers
   something that isn't a navigation. */
.sol-stage.docked .sol-stage-text {
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 450;
  line-height: 1.4;
  letter-spacing: 0;
  max-width: 340px;
  /* Exactly two lines tall, scrolled by JS to follow the spoken word.
     -webkit-line-clamp was tried first and is wrong here: it pins the FIRST
     two lines, so anything Sol said past them vanished mid-sentence. A fixed
     height plus a scroll that chases the said/unsaid boundary keeps the line
     being spoken always visible. */
  /* Two lines. The JS window keeps the spoken word inside them. */
  height: calc(2 * 1.4em);
  min-height: 0;
  overflow: hidden;
  background: rgba(10, 8, 20, 0.86);
  backdrop-filter: blur(12px);
  border: 1px solid var(--rule);
  border-radius: 12px;
  padding: 9px 13px;
  box-shadow: var(--shadow);
}
.sol-stage.docked .sol-stage-text .unsaid { color: rgba(255, 255, 255, 0.32); }
/* The hint is guidance for a takeover screen; in the corner it is clutter. */
.sol-stage.docked .sol-stage-hint { display: none; }


/* ---- departure times on the schedule ----
   When to stand up, shown under the room. Quiet until it matters, then it
   takes the warning colour and the row grows a "Leave now" tag — the point
   is to catch his eye while he is packing a bag, not to be read carefully. */
.sr-leave {
  display: block;
  font-size: 11.5px;
  color: var(--ink-2);
  margin-top: 2px;
  letter-spacing: 0.01em;
}
.sr-leave.now {
  color: var(--warning);
  font-weight: 600;
}
.sr-now.leave {
  background: color-mix(in srgb, var(--warning) 22%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 55%, transparent);
}

/* ---- "Missed that" ---- */
.rec-miss-btn {
  appearance: none;
  border: 1px solid color-mix(in srgb, var(--warning) 55%, transparent);
  background: color-mix(in srgb, var(--warning) 16%, transparent);
  color: var(--warning);
  font-family: var(--display);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: 999px;
  padding: 5px 11px;
  margin-left: 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.rec-miss-btn:hover { background: color-mix(in srgb, var(--warning) 28%, transparent); }
.rec-miss-n {
  background: var(--warning);
  color: #1a1206;
  border-radius: 999px;
  min-width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  font-size: 10.5px;
}
/* The flagged passages, above the full transcript — it is why he opened it. */
.lec-missed {
  margin-top: 14px;
  padding: 12px 14px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--warning) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 26%, transparent);
}
.lec-missed h5 {
  margin: 0 0 8px;
  font-family: var(--display);
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--warning);
}
.missed-row { align-items: flex-start; }
.missed-row .grow { font-size: 13.5px; line-height: 1.45; }

/* ---- class mode ----
   While a lecture is actually running the app stops being a planner. Pinned
   above everything, carries the class colour, and offers only what is useful
   in the room: record, flag a moment, and this class's open work. */
.class-mode {
  border: 1px solid color-mix(in srgb, var(--c, var(--accent)) 45%, transparent);
  background:
    linear-gradient(160deg, color-mix(in srgb, var(--c, var(--accent)) 16%, transparent), transparent 60%),
    var(--sheet);
  border-radius: 16px;
  padding: 16px 18px 18px;
  box-shadow: 0 0 34px color-mix(in srgb, var(--c, var(--accent)) 16%, transparent), var(--shadow);
}
.cm-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cm-live {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--display); font-size: 11.5px; font-weight: 800;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--c, var(--accent));
}
.cm-left { font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.cm-title {
  appearance: none; background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--display); font-size: 30px; font-weight: 700;
  letter-spacing: 0.01em; color: var(--ink); display: block; text-align: left;
}
.cm-title:hover { color: var(--c, var(--accent)); }
.cm-room { font-size: 13px; color: var(--muted); margin-top: 2px; }
.cm-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.cm-work { margin-top: 16px; border-top: 1px solid var(--rule); padding-top: 12px; }
.cm-work-h {
  font-family: var(--display); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px;
}
.cm-work-row {
  appearance: none; width: 100%; background: none; border: none; cursor: pointer;
  display: flex; align-items: baseline; gap: 10px; padding: 6px 0;
  color: var(--ink); font-size: 13.5px; text-align: left; border-radius: 8px;
}
.cm-work-row:hover { color: var(--c, var(--accent)); }

/* ---- ask your lectures ---- */
.lask h3 { margin-bottom: 2px; }
.lask-answer {
  margin-top: 12px;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--sheet-raised);
  border: 1px solid var(--rule);
  font-size: 14px;
  line-height: 1.55;
  white-space: pre-wrap;
}
.lask-answer.busy { color: var(--ink-2); font-style: italic; }
.lask-answer.err { color: var(--ink-2); border-color: color-mix(in srgb, var(--warning) 35%, transparent); }
.lask-src-h {
  margin: 14px 0 4px;
  font-family: var(--display);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The citation number, matching the [n] markers in the answer. */
.lask-n {
  flex: none;
  width: 20px; height: 20px;
  border-radius: 6px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 700;
  background: var(--accent-soft);
  color: var(--accent);
  align-self: flex-start;
  margin-top: 2px;
}

/* ===== Exam signals (class → Lectures) ===== */
.sig .sig-note { color: var(--ink-2); font-size: 13px; margin: 0 0 12px; }
.sig-row { align-items: flex-start; }
.sig-quote {
  font-family: var(--serif); font-style: italic; font-size: 15px;
  line-height: 1.45; color: var(--ink); margin-bottom: 4px;
}
.sig-quote.hot { color: var(--ink); border-left: 2px solid var(--accent); padding-left: 10px; }
.sig-emph-h {
  margin: 18px 0 8px; font-family: var(--display); text-transform: uppercase;
  letter-spacing: .08em; font-size: 11px; color: var(--muted);
}
.sig-emph { display: flex; flex-wrap: wrap; gap: 6px; }
.sig-term {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 4px 10px; border-radius: 999px;
  background: var(--sheet-raised); border: 1px solid rgba(255,255,255,.06);
  font-size: 13px; color: var(--ink-2);
}
.sig-term-n { font-size: 11px; color: var(--muted); }

/* ===== Exam autopsy ===== */
#autopsy-in { width: 100%; resize: vertical; }
.autopsy-out { margin-top: 16px; border-top: 1px solid rgba(255,255,255,.06); padding-top: 14px; }
.autopsy-topic { margin-bottom: 16px; }
.autopsy-topic-h {
  font-family: var(--display); text-transform: uppercase; letter-spacing: .06em;
  font-size: 12px; color: var(--accent); margin-bottom: 6px;
}
.autopsy-hit { margin-bottom: 10px; padding-left: 10px; border-left: 2px solid rgba(255,255,255,.08); }
.autopsy-quote {
  font-family: var(--serif); font-style: italic; font-size: 14px;
  line-height: 1.45; color: var(--ink); margin-bottom: 3px;
}

/* ===== Attendance ===== */
.att-ask .rows li { gap: 6px; }
.att-rate { font-family: var(--display); font-size: 38px; line-height: 1; color: var(--ink); }
.att-rate.warn { color: var(--warning); }

/* ===== Appearance controls (Settings) ===== */
.ui-group { margin-bottom: var(--space-4); }
.ui-group:last-of-type { margin-bottom: 0; }
.ui-group-h {
  font-family: var(--display); text-transform: uppercase; letter-spacing: .08em;
  font-size: var(--text-xs); color: var(--muted); margin-bottom: var(--space-2);
}
.ui-row { display: flex; align-items: center; gap: 10px; padding: 5px 0; }
.ui-row select { min-width: 168px; }
.ghost.small.on { color: var(--ink); border-color: var(--accent); background: var(--accent-soft); }
.hub-vis-row { display: flex; flex-wrap: wrap; gap: 6px; }
.home-order-row.is-off .grow { color: var(--muted); text-decoration: line-through; }
.hub-slot.hub-slot-off { display: none; }

/* System health folds away: green almost every day, and it was half the
   height of the old Settings column. */
.health-fold > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px;
  padding: var(--space-2) 0; color: var(--ink-2); font-size: var(--text-sm);
}
.health-fold > summary::-webkit-details-marker { display: none; }
.health-fold > summary::after {
  content: "Details"; margin-left: auto; color: var(--muted); font-size: var(--text-xs);
}
.health-fold[open] > summary::after { content: "Hide"; }
.health-fold > summary:hover { color: var(--ink); }

/* Appearance groups pair up — they are all short label/control rows, and
   stacked they were the only Settings tab still asking for a scroll. */
.ui-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4) var(--space-5); }
@media (max-width: 900px) { .ui-grid { grid-template-columns: 1fr; } }


/* ============ Fitting a smaller window ============
   Before this the app had four width-based media queries and three of them
   were cosmetic offsets — Email was the only view ever made narrow-aware.
   Everything else laid out once and overflowed, and since body is
   overflow:hidden, anything escaping #main was simply unreachable.
   Two breakpoints, applied to the shared layout primitives. */
@media (max-width: 900px) {
  #view-root { padding: 24px 20px 48px; }
  .workspace-view { padding:24px 22px 52px!important; }.workspace-view .view-head h2{font-size:clamp(44px,8vw,62px)}.workspace-class .detail-head{min-height:0}.workspace-calendar .calendar-day{min-height:116px}
  .study-workspace { padding:22px 22px 52px!important; }.study-mode-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.study-dashboard{flex-direction:column}.study-library,.study-focus{width:100%}.study-focus{position:static}
  .grid-2 { grid-template-columns: 1fr; }
  /* Four floated buttons in one panel heading stack and break the header. */
  .panel h3 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
  .panel h3 .h3-action { float: none; margin-left: 0; }
  .panel h3 .h3-action:first-of-type { margin-left: auto; }
  /* The time column is flex:none at 148px, which is most of a narrow row. */
  .sched-row { gap: 12px; }
  .sr-when { min-width: 96px; }
  .syl-row { flex-wrap: wrap; }
  .review-score { width: 84px; height: 84px; }
  /* Calendar never got a pass in the note above this block — it kept a
     flat 620px min-width and just forced horizontal scroll on anything
     narrower, instead of shrinking like everything else here. */
  .calendar-grid { grid-template-columns: repeat(7, minmax(72px, 1fr)); min-width: 500px; }
  .calendar-day { min-height: 96px; }
}
@media (max-width: 700px) {
  #view-root { padding: 18px 14px 40px; }
  .workspace-view{padding:20px 16px 44px!important}.workspace-view .view-head{margin-top:2px;margin-bottom:22px}.workspace-view .view-head h2{font-size:clamp(42px,14vw,60px)}.workspace-view .calendar-head,.workspace-view .calendar-head .calendar-actions{align-items:flex-start}.workspace-view .calendar-head .calendar-actions{align-self:auto}.workspace-hero{border-radius:18px}.workspace-classes .class-card{min-height:132px}.workspace-class .detail-head{padding:18px 16px}.workspace-class .detail-head .actions{margin-left:0;flex-wrap:wrap}.workspace-review .review-score-panel{align-items:flex-start;gap:14px}.workspace-review .review-score{width:92px;height:92px}.workspace-review .review-score-num{font-size:34px}.workspace-email .email-toolbar{padding:12px}
  .study-workspace{padding:20px 16px 42px!important}.study-stage-head,.study-now,.study-section-head{align-items:flex-start;flex-direction:column}.study-stage-head{margin-bottom:22px}.study-stage-head h2{font-size:clamp(42px,15vw,62px)}.study-stage-actions,.study-now-action{justify-content:flex-start}.study-now{gap:16px;padding:20px}.study-now:after{display:none}.study-section-head p{text-align:left}.study-mode-grid{grid-template-columns:1fr}.study-mode-card{min-height:174px}.study-mode-card .mode-glyph{margin-top:10px}.study-library{padding:16px}.study-library-head{align-items:flex-start}
  .sched-row { flex-direction: column; gap: 4px; }
  .sr-when { min-width: 0; }
  .timer-face { font-size: 48px; }
  .cm-stage { height: calc(100vh - 150px); }
  .hours-input { width: 100%; }
  .calendar-grid { grid-template-columns: repeat(7, minmax(56px, 1fr)); min-width: 392px; }
  .calendar-day { min-height: 78px; padding: 5px; }
  .calendar-date { width: 20px; height: 20px; font-size: 11px; }
  .calendar-item { font-size: 9.5px; padding: 2px 4px; margin: 1px 0; }
  .calendar-weekday { padding: 6px 4px; font-size: 10px; }
  /* No width constraint on the countdown's own text, so at a narrow window
     it wrapped onto two lines and collided with the Record button. Drop
     the descriptive half and keep just the number rather than truncating
     text that could still overflow. */
  .topbar .countdown.top .sub { display: none; }
}

/* ===== Charts (shared) =====
   One place that knows what a chart looks like. Series colour comes from the
   accent token, so the Appearance setting reaches these too. */
.c-wrap { margin: var(--space-3) 0 0; }
.c-svg { width: 100%; height: auto; display: block; overflow: visible; }
.c-line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.c-fill { fill: var(--accent-soft); stroke: none; }
.c-dot { fill: var(--accent); }
.c-bar { fill: var(--accent); opacity: .85; }
.c-bar.warn { fill: var(--warning); }
.c-bar.bad { fill: var(--critical); }
.c-bar:hover { opacity: 1; }
.c-tick { fill: var(--muted); font-size: 9px; font-family: var(--sans); }
.c-cap { margin-top: 4px; font-size: var(--text-xs); color: var(--ink-2); }
.c-cap strong { color: var(--ink); font-family: var(--display); font-size: var(--text-sm); }

/* ===== Form controls =====
   The base rule set colours and a border but never `appearance: none`, so
   macOS painted its own control on top and every select in the app read as a
   system widget dropped onto a dark panel. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 26px;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%23a9adc4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.75 6 6.25l5-4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 10px 7px;
}
select:hover { border-color: var(--muted); }
input[type="range"] {
  appearance: none; -webkit-appearance: none;
  background: none; border: none; padding: 0; height: 18px; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 4px; border-radius: var(--radius-pill); background: var(--rule-strong);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 14px; height: 14px; margin-top: -5px;
  border-radius: 50%; background: var(--accent); border: none;
}

/* ===== The app's own dropdown =====
   Same shape as the closed control above, but the open list is ours — the
   native popup is drawn by macOS and CSS cannot reach inside it. */
.dd { position: relative; display: inline-block; }
.dd-btn {
  appearance: none; display: flex; align-items: center; gap: 10px;
  min-width: 168px; width: 100%; text-align: left;
  background: var(--sheet-raised); color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: 6px;
  padding: calc(6px * var(--dens)) 10px; font: inherit; cursor: pointer;
  transition: border-color var(--dur-press) var(--ease-out);
}
.dd-btn:hover { border-color: var(--muted); }
.dd.open .dd-btn { border-color: var(--accent); }
.dd-cur { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-chev {
  width: 10px; height: 7px; flex: none; opacity: .75;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round' d='M1 1.75 6 6.25l5-4.5'/%3E%3C/svg%3E") no-repeat center / contain;
  transition: transform var(--motion-fast) ease-out;
}
.dd.open .dd-chev { transform: rotate(180deg); }
.dd-menu {
  position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; right: 0;
  min-width: 100%; max-height: 260px; overflow-y: auto;
  background: var(--sheet-raised);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--rule-strong); border-radius: var(--radius-sm);
  box-shadow: var(--shadow); padding: 4px;
  /* Re-rendered fresh into the DOM each time it opens (see renderSettings()),
     so a plain entrance animation replays correctly with no JS hook needed. */
  animation: dd-menu-in var(--dur-small) var(--ease-out) both;
  transform-origin: top center;
}
@keyframes dd-menu-in {
  from { opacity: 0; transform: translateY(calc(var(--dist-sm) * -1)) scale(0.98); }
  to { opacity: 1; transform: none; }
}
.dd-opt {
  appearance: none; display: block; width: 100%; text-align: left;
  background: none; border: none; color: var(--ink-2);
  padding: 7px 10px; border-radius: 5px; font: inherit; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background var(--dur-press) var(--ease-out), color var(--dur-press) var(--ease-out);
}
.dd-opt:hover { background: var(--accent-soft); color: var(--ink); }
.dd-opt.sel { color: var(--ink); background: var(--accent-soft); }
.dd-opt.sel::after { content: " ✓"; color: var(--accent); }
.ui-row .dd { min-width: 168px; }

/* ============ Doodle notes — "sketchnote paper" theme ============
   Deliberately breaks from the app's own dark-violet chrome: this is meant
   to read as an actual sheet of paper sitting inside the app, not another
   panel of it — the same reason a real notebook page doesn't try to match
   the color of the backpack it's carried in. Exactly three colors: the
   warm off-white paper, a dark ink for structure/text, and one accent
   (the course's own color when a class is known, `--doodle-accent`,
   falling back to --s1) — never more, per spec. Rendered purely in HTML/
   CSS/SVG (DOODLE_ICONS in app.js) so it needs zero network image calls
   and reuses exactly for PDF export what's already on screen. */
.doodle-page {
  --doodle-paper: #f6f0e2;
  --doodle-ink: #2b2620;
  background: var(--doodle-paper);
  color: var(--doodle-ink);
  border: 2.5px solid var(--doodle-ink);
  border-radius: 14px;
  padding: 36px 40px 34px;
  width: 100%;
  aspect-ratio: 8.5 / 11;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  font-family: var(--sans);
  background-image: radial-gradient(circle, rgba(43, 38, 32, 0.16) 1px, transparent 1px);
  background-size: 22px 22px;
  box-shadow: 4px 4px 0 var(--doodle-ink), 4px 4px 0 1px rgba(0, 0, 0, 0.08);
}
.doodle-page-head { margin-bottom: 26px; }
/* Noteworthy ships on every Mac — free, no download, no network fetch,
   and it's the one built-in font that actually reads as a marker-written
   headline instead of a formal typeface. Falls back gracefully on a
   machine without it (still bold/condensed, just not handwritten). */
.doodle-hand-font { font-family: "Noteworthy", "Chalkboard SE", "Marker Felt", var(--display), sans-serif; }
.doodle-title {
  font-family: "Noteworthy", "Chalkboard SE", "Marker Felt", var(--display), sans-serif;
  font-size: 30px;
  font-weight: 800;
  letter-spacing: 0.01em;
  line-height: 1.15;
  display: inline;
}
.doodle-title-underline { display: block; width: 100%; max-width: 200px; height: 8px; margin-top: 2px; }
.doodle-subtitle { margin-top: 8px; font-size: 15px; font-style: italic; opacity: 0.72; }
/* flex:1 so the block area always grows to fill whatever room is left in
   the page — a page with only 2-3 blocks stretches those blocks larger
   instead of leaving the bottom of the sheet empty. */
.doodle-body { display: flex; flex-direction: column; gap: 16px; flex: 1; }
.doodle-body.doodle-layout-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 16px;
}
.doodle-body.doodle-layout-comparison { display: flex; align-items: stretch; gap: 20px; }
.doodle-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 16px; }
.doodle-divider {
  flex: none;
  width: 2px;
  background: repeating-linear-gradient(180deg, var(--doodle-ink) 0 6px, transparent 6px 12px);
}
.doodle-body.doodle-layout-hub { display: flex; flex-direction: column; gap: 16px; align-items: stretch; }
.doodle-hub-center .doodle-block { border-width: 3px; }
.doodle-hub-ring { display: flex; flex-wrap: wrap; gap: 16px; flex: 1; }
.doodle-hub-ring .doodle-block { flex: 1 1 220px; min-width: 0; }
.doodle-body:not(.doodle-layout-grid):not(.doodle-layout-comparison) .doodle-block,
.doodle-col .doodle-block { flex: 1; }
.doodle-block {
  border: 2.5px solid var(--doodle-ink);
  /* slightly asymmetric corners read as hand-drawn, not a machined box */
  border-radius: 14px 20px 16px 22px / 20px 14px 22px 16px;
  padding: 16px 18px;
  display: flex;
  gap: 12px;
  align-items: flex-start;
  background: rgba(255, 255, 255, 0.4);
}
.doodle-body.doodle-layout-grid .doodle-block:nth-child(odd) { transform: rotate(-0.35deg); }
.doodle-body.doodle-layout-grid .doodle-block:nth-child(even) { transform: rotate(0.35deg); }
.doodle-block-icon { width: 44px; height: 44px; flex: none; color: var(--doodle-accent, var(--s1)); }
.doodle-block-icon svg { width: 100%; height: 100%; display: block; }
.doodle-block-body { min-width: 0; align-self: center; }
.doodle-block-heading {
  font-family: "Noteworthy", "Chalkboard SE", "Marker Felt", var(--display), sans-serif;
  font-weight: 700; font-size: 17px; margin-bottom: 4px; letter-spacing: 0.005em;
}
/* overflow-wrap:anywhere lives ONLY here, never on the heading — a short
   heading should wrap at a space or not at all; only the free-form
   AI-written sentence needs a safety net against one unbreakably long
   token forcing the box wider than the page. */
.doodle-block-text { font-size: 15px; line-height: 1.45; overflow-wrap: anywhere; }
.doodle-connector { display: flex; justify-content: center; padding: 2px 0; color: var(--doodle-accent, var(--s1)); flex: none; }
.doodle-connector svg { width: 26px; height: 26px; transform: rotate(90deg); display: block; }
.doodle-callout {
  margin-top: 18px;
  border: 2.5px dashed var(--doodle-accent, var(--s1));
  border-radius: 14px;
  padding: 13px 16px;
  display: flex;
  gap: 12px;
  align-items: flex-start;
  background: rgba(255, 255, 255, 0.4);
  flex: none;
}
.doodle-callout-icon { width: 32px; height: 32px; flex: none; color: var(--doodle-accent, var(--s1)); }
.doodle-callout-icon svg { width: 100%; height: 100%; display: block; }
.doodle-callout-text {
  font-family: "Noteworthy", "Chalkboard SE", "Marker Felt", var(--display), sans-serif;
  font-size: 15px; font-weight: 700; overflow-wrap: anywhere; align-self: center;
}

.doodle-viewer { padding: 4px 0 0; }
.doodle-nav { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 10px; }
.doodle-page-count { font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* Course and study navigation: larger destinations, secondary tools on demand. */
:is(.workspace-class, .workspace-classes, .workspace-lectures, .study-workspace, .lecture-detail) :is(.btn, .ghost, .link-btn, .rec-btn, .class-tab) {
  min-height: 44px; padding: 10px 14px; font-size: 14px;
}
.workspace-class .course-destinations { display:flex; flex-wrap:wrap; gap:6px; overflow:visible; }
.workspace-class .course-destinations .class-tab { flex:1 1 110px; justify-content:center; border-radius:10px; }
.workspace-class .course-destinations .class-tab.active { background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--accent); }
.nav-fold { border:1px solid var(--rule); border-radius:12px; padding:0 14px; }
.nav-fold > summary { min-height:48px; padding:13px 0; color:var(--ink); font-size:14px; font-weight:600; cursor:pointer; }
.nav-fold > summary:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:6px; }
.nav-fold[open] > summary { margin-bottom:10px; border-bottom:1px solid var(--rule); }
.nav-fold[open] { padding-bottom:14px; }
.row-tools { flex:none; align-self:flex-start; }
.row-tools > summary { min-height:44px; padding:11px 0; }
.row-tools[open] { max-width:220px; }
.row-tools[open] > button { display:block; width:100%; margin-top:6px; }
.course-management { align-self:flex-start; }
.workspace-class .course-management .actions { flex-wrap:wrap; }
.lecture-detail-nav { position:sticky; top:-20px; z-index:2; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; padding:12px 0; margin-bottom:18px; background:var(--sheet); border-bottom:1px solid var(--rule); }
.lecture-detail-nav > span { color:var(--ink-2); font-size:13px; }
.lec-primary-actions { display:flex; gap:10px; flex-wrap:wrap; margin:14px 0; }
.lecture-detail .lec-tools-section { margin:18px 0; padding-top:14px; border-top:1px solid var(--rule); }
.lecture-detail .lec-tools-section h5 { margin:0 0 10px; }
.lecture-detail .lec-action-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr)); gap:10px; }
.lecture-detail .lec-action-grid button { text-align:left; white-space:normal; }

.lecture-detail .nav-fold { margin:16px 0; }
.lecture-detail .lec-jump .rows { max-height:360px; overflow:auto; }
.lecture-library-ask, .study-more-modes { margin-top:22px; }
.study-workspace .study-library .lib-section.nav-fold { margin-top:14px; padding:0 14px; }
.study-workspace .study-library .lib-section.nav-fold[open] { padding-bottom:14px; }
.study-workspace .study-library .rows > li,
.workspace-lectures .rows > li { gap:12px; flex-wrap:wrap; }
:is(.workspace-lectures, .study-workspace) .rows > li > .grow { flex-basis:200px; }
.study-workspace .study-mode-card { min-height:170px; }

/* ============ 2026-09-06 usability pass ============
   Shared rules for the pass: every new control is a real <button>, sits at
   44px or more wherever the surrounding layout allows it, states its purpose
   in text, and takes the same focus ring as the rest of the app. */

/* Large-option choosers (Import assignments, Add study material). */
.choice-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap:10px; margin-top:12px; }
.choice { appearance:none; display:flex; flex-direction:column; gap:4px; min-height:64px; padding:14px 16px; border:1px solid var(--rule); border-radius:12px; background:transparent; color:var(--ink); font:inherit; text-align:left; cursor:pointer; transition:border-color var(--dur-small) var(--ease-out), background var(--dur-small) var(--ease-out); }
.choice:hover { border-color:var(--accent); background:var(--accent-soft); }
.choice:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
.choice-title { font-weight:700; font-size:15px; }
.choice-sub { color:var(--ink-2); font-size:12.5px; line-height:1.35; }

/* Month grid: "+N more" is a button now, and grouped campus events use the
   same chip style as a single one. */
button.calendar-more { display:block; width:100%; margin-top:2px; padding:4px 5px; border:1px dashed var(--rule); border-radius:4px; background:transparent; color:var(--accent); font:inherit; font-size:10.5px; font-weight:700; text-align:left; cursor:pointer; }
button.calendar-more:hover { background:var(--accent-soft); border-color:var(--accent); }
button.calendar-more:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* Day view: the grid's own item buttons, sized to be read and clicked. */
.day-view { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
.day-view .calendar-item { box-sizing:border-box; font-size:13.5px; line-height:1.3; padding:12px 12px; min-height:46px; white-space:normal; border-left-width:3px; }
.day-view .calendar-item span { font-size:10.5px; }
.day-view .calendar-item:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.day-view-nav { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px; color:var(--ink-2); font-size:13px; }
.day-view-campus { margin-top:14px; }

/* Week view: a day's campus events folded under one row. */
.week-campus-li { display:block; padding:6px 0; }
.week-campus > summary { font-size:13px; font-weight:600; }
.week-campus .rows { margin-top:4px; }

/* Email rows: the sender/subject block is the one real click target. */
.email-row { cursor:default; }
.email-row-open { appearance:none; display:flex; flex-direction:column; gap:3px; min-width:0; min-height:44px; padding:6px 8px; margin:-6px -8px; border:0; border-radius:8px; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
.email-row-open:hover { background:var(--accent-soft); }
.email-row-open:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.email-row-tools { flex:none; }
.email-row-tools > summary { min-height:44px; padding:11px 4px; font-size:12.5px; }
.email-row-tools[open] > button { display:block; width:100%; margin-top:6px; }

/* Today: the checklist link inside the countdown poster. */
.cp-checklist { align-self:flex-start; margin-top:10px; min-height:44px; }
.count-poster.checklist-only { padding:16px 20px; }

/* Settings → Connections: the second-level tab row. */
.class-tabs.sub-tabs { margin-top:0; margin-bottom:14px; }
.class-tabs.sub-tabs .class-tab { min-height:44px; }

/* Appearance: Shown/Hidden rows. 36px is the practical floor in a dense
   settings list; the state is now stated in text, not only colour. */
.home-order-row .ghost.small { min-height:36px; }
.home-order-row.is-off .grow { color:var(--muted); }
/* Work page header: the page's one primary action and its import sibling
   are full-height targets. Scoped to the two acts so the month calendar's
   prev/next/today strip, which shares .calendar-actions, stays compact. */
.calendar-actions [data-act="add-assign-open"],
.calendar-actions [data-act="import-assignments-open"] { min-height:44px; padding-left:18px; padding-right:18px; }

/* Hub stage headline: one element that has to hold "HQ", "9", "MAT-144" or a
   whole assignment title. At the single 104px size a real title filled the
   hub over two lines and was still truncated mid-word. It now steps down by
   length, wraps on words, and is capped at three lines so the counts below
   it never get pushed off the stage. */
.stage-big { max-width: 92%; overflow-wrap: break-word; text-wrap: balance; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
.stage-big.len-m  { font-size: clamp(34px, 6vw, 68px); }
.stage-big.len-l  { font-size: clamp(28px, 4.6vw, 50px); letter-spacing: -0.005em; line-height: 1.0; max-width: 22ch; }
.stage-big.len-xl { font-size: clamp(23px, 3.4vw, 38px); letter-spacing: 0; line-height: 1.08; max-width: 24ch; }

/* ================================================================
   PHONE BUILD
   ================================================================
   Everything below is scoped to body.is-phone, a class only app.js sets and
   only when there is no native bridge AND the screen is small (or the page
   is running as a home-screen install). The Mac app always has the bridge,
   so it can never match a single rule in this section — that is the whole
   safety story for shipping a phone layout inside the same stylesheet.

   Written against a 375pt iPhone in portrait with the status bar and home
   indicator overlapping the page (viewport-fit=cover), so every edge that
   holds a control pays for itself with an env(safe-area-inset-*). */

body.is-phone {
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}
/* Kills the 300ms double-tap-to-zoom delay on anything tappable without
   disabling pinch-zoom on the page as a whole (which would fail WCAG). */
body.is-phone button,
body.is-phone .row,
body.is-phone summary,
body.is-phone a { touch-action: manipulation; }
/* iOS zooms the whole page in when a focused field is under 16px. */
body.is-phone input,
body.is-phone select,
body.is-phone textarea { font-size: 16px; }

/* ---------- Topbar ---------- */
/* Down to the three things that earn their width at 375pt: the way back to
   the hub, where you are, and search. Record needs a Mac microphone and disk,
   Sol needs the Claude key Swift holds, and the countdown is already the
   largest thing on the hub one tap away. */
body.is-phone .topbar {
  gap: 10px;
  padding: calc(8px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right))
           8px calc(12px + env(safe-area-inset-left));
  position: sticky;
  top: 0;
  z-index: 30;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  background: rgba(10, 8, 20, 0.82);
}
body.is-phone .topbar-record,
body.is-phone .topbar-chat,
body.is-phone .topbar .countdown.top { display: none; }
body.is-phone .mini-orb { width: 36px; height: 36px; font-size: 12px; flex: none; }
body.is-phone .crumb {
  font-size: 12px;
  letter-spacing: 0.08em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
  min-width: 0;
}
body.is-phone .topbar-search { margin-left: auto; flex: none; }

/* ---------- Bottom tab bar ---------- */
/* The quick-nav strip already exists, already routes, and is already hidden
   under 760px — so the phone's primary navigation is that same markup moved
   to the thumb instead of a second nav with its own handlers to keep in sync.
   "More ▾" still opens the full hub. */
body.is-phone .quick-nav {
  display: flex;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 35;
  gap: 0;
  padding: 6px calc(4px + env(safe-area-inset-left))
           calc(4px + env(safe-area-inset-bottom)) calc(4px + env(safe-area-inset-right));
  background: rgba(10, 8, 20, 0.9);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-top: 1px solid var(--rule);
}
body.is-phone .quick-nav button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 44px;
  padding: 6px 2px;
  font-size: 10px;
  letter-spacing: 0.04em;
  border-radius: 10px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
body.is-phone .quick-nav button.current {
  color: var(--ink);
  background: var(--accent-soft);
}
/* The tab bar is fixed, so the scroll container has to end above it. */
body.is-phone #main { padding-bottom: calc(58px + env(safe-area-inset-bottom)); }
/* …but not while the hub is up: the hub is a separate full-screen layer and
   the app underneath is display:none, so a floating tab bar over it would be
   navigation for a screen that isn't there. */
body.is-phone.hub-open .quick-nav { display: none; }

/* ---------- Views ---------- */
body.is-phone #view-root { padding: 16px 14px 28px; }
body.is-phone .workspace-view .view-head { margin: 2px 0 18px; padding-bottom: 12px; }
body.is-phone .workspace-view .view-head h2 { max-width: none; font-size: clamp(30px, 9vw, 40px); }
body.is-phone .workspace-view .view-head .sub { font-size: 14px; margin-top: 6px; }
body.is-phone .view-head .head-row { gap: 10px; }
body.is-phone .stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Anything that lays out in fixed columns gets one column; anything that is
   genuinely a table (the week and calendar grids) already scrolls sideways in
   its own container, so it is deliberately left alone. */
body.is-phone .ui-grid,
body.is-phone .class-grid,
body.is-phone .lib-grid { grid-template-columns: minmax(0, 1fr); }
body.is-phone .row-tools,
body.is-phone .nav-fold { max-width: 100%; }
/* ul.rows > li is the app's list primitive and is horizontal flex. At 375pt
   an assignment title was being squeezed into a ~104px column and wrapping
   over nine lines while its controls kept full width beside it. Letting the
   row wrap and giving the title a floor pushes the controls onto their own
   line instead. This is the same treatment .workspace-lectures and the study
   library already use for their own narrow columns. */
body.is-phone ul.rows > li { flex-wrap: wrap; row-gap: 8px; }
body.is-phone ul.rows > li > .grow { min-width: 62%; flex: 1 1 auto; }
/* Give every real control a thumb-sized target. */
body.is-phone button.ghost,
body.is-phone ul.rows > li button { min-height: 36px; }
/* Study sets its own padding with !important, so the phone gutter has to
   match that weight to win. */
body.is-phone .study-workspace { padding: 16px 14px 28px !important; }

/* ---------- Controls that need the Mac ---------- */
/* These all reach for something that only exists on his machine: a Finder
   window, a file on the Mac's disk, another app, the microphone. In the
   browser they fall through to the shim's "works in the real app" toast,
   which is fine as a dev-preview affordance but is just a dead button on a
   phone. Attachments are hidden rather than disabled so a class hub doesn't
   advertise files the phone cannot open. */
body.is-phone [data-act="reveal-backups"],
body.is-phone [data-act="reveal-file"],
body.is-phone [data-act="open-file"],
body.is-phone [data-act="open-app"],
body.is-phone [data-act="attach-file"],
body.is-phone [data-act="del-file"] { display: none; }
/* The Lectures header's "Choose class to record" is a plain nav button, so it
   has to be addressed by where it sits rather than by its action. It is the
   last remaining path to a recording control on the phone; the library it
   sits above still works, since transcripts and summaries travel in
   state.json and only the audio stays on the Mac. */
body.is-phone .workspace-lectures .view-head [data-act="nav"][data-view="classes"] { display: none; }
/* System health reports the Mac's microphone, notification and disk
   permissions and the native data path — none of which exist here. With the
   healthCheck bridge call refused it would otherwise sit on "Checking…"
   forever. Matched by its Refresh button because the panel has no class of
   its own; :has() is already used elsewhere in this file. */
body.is-phone .panel:has([data-act="health-refresh"]) { display: none; }

/* ---------- Modals become sheets ---------- */
/* A centred 315px card with the keyboard up leaves almost nothing visible.
   Bottom sheets keep the content anchored where the thumb is and let the
   sheet grow to the full height it needs. */
body.is-phone .modal-veil { align-items: flex-end; }
body.is-phone .modal,
body.is-phone .modal.modal-wide,
body.is-phone .modal.modal-reader {
  width: 100%;
  max-height: 92vh;
  max-height: 92dvh;
  border-radius: 20px 20px 0 0;
  border-bottom: none;
  padding: 18px 16px calc(20px + env(safe-area-inset-bottom));
  animation: sheet-in var(--dur-panel) var(--ease-out) both;
}
body.is-phone .modal-veil.closing .modal { animation: sheet-out var(--dur-small) var(--ease-in) both; }
@keyframes sheet-in { from { opacity: 0; transform: translateY(14%); } to { opacity: 1; transform: translateY(0); } }
@keyframes sheet-out { to { opacity: 0; transform: translateY(14%); } }
body.is-phone .modal .modal-actions { position: sticky; bottom: 0; background: #1e1c30; padding-top: 10px; }
@media (prefers-reduced-motion: reduce) {
  body.is-phone .modal,
  body.is-phone .modal.modal-wide,
  body.is-phone .modal.modal-reader { animation: none; }
}

/* ---------- The hub ---------- */
/* The arc is a desktop shape. positionHubBubbles() bails out in phone mode
   and leaves placement entirely to this grid — see the matching comment
   there before changing either side. */
body.is-phone .hub {
  display: block;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: calc(env(safe-area-inset-top) + 8px) 0 calc(env(safe-area-inset-bottom) + 24px);
}
/* MUST outrank the rule above: `.hub[hidden]` is (0,2,0) and `body.is-phone
   .hub` is (0,2,1), so the phone's display:block was beating the hidden
   attribute and painting the whole hub on top of every view. */
body.is-phone .hub[hidden] { display: none; }
body.is-phone .hub-arc { display: none; }
body.is-phone .hub-horizon { bottom: -120vw; height: 150vw; }
body.is-phone .hub-stage {
  position: static;
  inset: auto;
  min-height: 38vh;
  padding: 20px 20px 26px;
}
body.is-phone .stage-big { font-size: clamp(30px, 11vw, 46px); max-width: 100%; }
body.is-phone .stage-big.len-m { font-size: clamp(27px, 9vw, 38px); }
body.is-phone .stage-big.len-l { font-size: clamp(24px, 7.6vw, 32px); max-width: 100%; }
body.is-phone .stage-big.len-xl { font-size: clamp(20px, 6vw, 26px); max-width: 100%; }
body.is-phone .hub-dock {
  position: static;
  height: auto;
  bottom: auto;
  pointer-events: auto;
}
body.is-phone .hub-dock-inner {
  width: auto;
  height: auto;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px 8px;
  padding: 6px 14px 10px;
}
body.is-phone .hub-slot {
  position: static;
  left: auto;
  bottom: auto;
  transform: scale(0.6);
  opacity: 0;
  transition: transform 0.36s cubic-bezier(0.22, 1.4, 0.36, 1), opacity 0.24s ease;
}
body.is-phone .hub.open .hub-slot { transform: scale(1); opacity: 1; }
/* Touch has no hover; the desktop lift/magnify would stick after a tap. */
body.is-phone .hub.open .hub-slot:hover { transform: scale(1); }
body.is-phone .hub.open .hub-slot:hover .hub-bubble { transform: none; }
body.is-phone .hub-slot:has(.hub-bubble:active) { transform: scale(0.94); }
body.is-phone .hub-bubble { width: 62px; height: 62px; }
body.is-phone .hub-dock-label { font-size: 10px; letter-spacing: 0.06em; }
/* The stat caption is the first thing to go: three words under a 62px circle
   in a three-up grid wraps into the row below it. The badge and the glow
   still carry urgency. */
body.is-phone .hub-dock-sub { display: none; }
@media (prefers-reduced-motion: reduce) {
  body.is-phone .hub-slot { transform: scale(1); opacity: 1; transition: none; }
}

/* ---------- Sideways scrolling ---------- */
/* First thing he hit on a real phone. Three containers were left free to
   scroll horizontally because on a desktop they are wide enough never to do
   it; at 390pt every one becomes a sideways drag, and the page rubber-bands
   along with them. Each offender is made to FIT rather than scroll, and the
   document is pinned as a backstop so nothing added later can reintroduce it. */
html:has(body.is-phone) { overflow-x: hidden; }
body.is-phone { overflow-x: hidden; max-width: 100%; }

/* The month grid asked for 620px of width inside an overflow-x:auto parent.
   Seven columns fit at 390pt if the cells stop reserving room for text. */
body.is-phone .calendar-scroll,
body.is-phone .workspace-calendar .calendar-scroll.workspace-hero { overflow-x: visible; }
body.is-phone .calendar-grid {
  min-width: 0;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
body.is-phone .calendar-weekday {
  padding: 6px 1px;
  font-size: 9px;
  letter-spacing: 0.01em;
  text-align: center;
}
body.is-phone .calendar-day { min-height: 60px; padding: 3px 2px; }
body.is-phone .calendar-date { width: 20px; height: 20px; margin: 0 auto 2px; font-size: 11px; }
/* Event chips become colour bars. Their labels cannot be read in a 53px
   column anyway, and the day cell is itself a button (data-act
   "calendar-day"), so the tap target for "what is on this day" is the whole
   cell rather than the chip. */
body.is-phone .calendar-item {
  display: block;
  width: 100%;
  height: 5px;
  margin-top: 2px;
  padding: 0;
  border: none;
  border-radius: 3px;
  background: var(--c);
  color: transparent;
  font-size: 0;
  line-height: 0;
  overflow: hidden;
}
body.is-phone .calendar-item span { display: none; }
body.is-phone .calendar-item.is-done { text-decoration: none; }
body.is-phone button.calendar-more {
  margin-top: 2px;
  padding: 1px;
  border: none;
  font-size: 8.5px;
  text-align: center;
}

/* Tab strips wrap onto a second line instead of hiding their last tabs off
   the right edge — "Data & system" was unreachable without a sideways drag. */
body.is-phone .class-tabs {
  flex-wrap: wrap;
  overflow-x: visible;
  gap: 4px;
  border-bottom: none;
}
body.is-phone .workspace-view .class-tabs { padding: 5px; }
body.is-phone .exam-strip { flex-wrap: wrap; overflow-x: visible; }

/* ---------- Tighter ---------- */
/* "Super loose" — the desktop spacing scale reads as vast on a handset, and
   it pushed the actual content below the fold on every screen. */
body.is-phone #view-root { padding: 12px 12px 24px; }
body.is-phone .panel { padding: 14px 13px; }
body.is-phone .workspace-view .view-head { margin: 0 0 14px; padding-bottom: 10px; }
body.is-phone .workspace-view .view-head h2 { font-size: clamp(26px, 7.6vw, 34px); }
body.is-phone .workspace-view .view-head .sub { margin-top: 5px; font-size: 13px; }
body.is-phone .hub-stage { min-height: 28vh; padding: 10px 18px 16px; }
body.is-phone .hub-dock-inner { gap: 14px 6px; padding: 4px 10px 8px; }
body.is-phone .hub-bubble { width: 56px; height: 56px; }
body.is-phone .hub-dock-label { font-size: 9.5px; }
