/* ============================================================================
   ALUMA BETA - design system layer
   ----------------------------------------------------------------------------
   Loaded LAST, on purpose. styles.css / dashboard.css / animations.css grew as
   a stack of successive redesigns and still hold every component rule the app
   depends on. Rewriting 30k lines in place would have meant touching behaviour;
   instead this file restates the *system* - tokens, type, shape, depth, motion,
   shell geometry, loading visuals - and lets the existing components inherit it.

   Nothing here changes what a control does. Every rule is presentation only.
   Legacy token names (--bg-elev-*, --text-muted, --accent-rgb, ...) are kept and
   re-pointed at the new semantic scale so components written against them pick
   up the new look without being rewritten.
   ============================================================================ */

/* ══════════════════════════ 1 · TYPEFACES ══════════════════════════════════
   Geist Sans for the product, Geist Mono for versions, command hints, usage,
   exam timers and technical statuses. Both are self-hosted variable builds -
   school networks routinely block font CDNs, and the packaged desktop build is
   offline, so nothing may be fetched at runtime. */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/static/vendor/fonts/Geist-Variable.woff2) format('woff2-variations'),
       url(/static/vendor/fonts/Geist-Variable.woff2) format('woff2');
}
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/static/vendor/fonts/GeistMono-Variable.woff2) format('woff2-variations'),
       url(/static/vendor/fonts/GeistMono-Variable.woff2) format('woff2');
}

/* ══════════════════════════ 2 · TOKENS ═════════════════════════════════════
   :root carries the dark scale because the app's original sheet is authored
   dark-first; [data-theme="light"] overrides it and is the shipped default. */
:root,
html[data-theme="dark"],
body[data-theme="dark"] {
  /* semantic surface + ink - near-black with a quiet green undertone (study desk, not HUD) */
  --bg:             #070A08;
  --bg-subtle:      #0A100D;
  --surface:        rgba(12, 20, 16, 0.76);
  --surface-strong: #101814;
  --surface-muted:  #0C1410;
  /* Glass: the bar is a lit pane over the page, not a painted strip. The fill
     stays dim and lets the blur carry the colour; the rim and the specular
     line are what actually read as glass at a glance. */
  --glass-fill:   rgba(18, 28, 23, 0.62);
  --glass-edge:   rgba(255, 255, 255, 0.10);
  --glass-lens:   rgba(255, 255, 255, 0.13);
  --glass-spec:   rgba(255, 255, 255, 0.22);
  --glass-shadow: 0 10px 30px rgb(0 0 0 / 0.45), 0 2px 8px rgb(0 0 0 / 0.3);
  /* On a dark ground the lens reads from its own brightness; a drop shadow
     under it would only muddy the pane behind. */
  --glass-lens-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  --text:           #F4F8F5;
  --text-secondary: #A3B0A8;
  --text-tertiary:  #8D9890;   /* AA 4.5:1 on every dark surface (worst 4.59 on --bg-elev-4) */
  /* The dropdown chevron, drawn in --text-tertiary. A data URI cannot read
     var(), so the colour is written in and the token is minted per theme. */
  --field-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%238D9890' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
  --border:         rgba(255, 255, 255, 0.10);
  --border-strong:  rgba(255, 255, 255, 0.17);
  /* logo greens: seafoam primary, citrus lime hover / secondary */
  --accent:         #78D8C0;
  --accent-hover:   #A4DF5C;
  --accent-soft:    rgba(120, 216, 192, 0.16);
  /* Text ON an accent fill. Near-black on seafoam/lime: white would be
     ~1.6:1 here. 10.7:1 on --accent, 11.5:1 on --accent-hover. */
  --accent-ink:     #051916;
  /* Text ON a danger fill. White on #FF7B89 is 2.49:1; this is 7.3:1. */
  --danger-ink:     #051916;
  /* Text ON a success fill. White on #74C69A is 2.04:1; this is 8.9:1. */
  --success-ink:    #051916;
  --success:        #74C69A;
  --warning:        #F3C969;
  --danger:         #FF7B89;

  /* Assignment-kind chips. These three were the only kinds written as raw
     hex - every other kind reads a token - so they rendered their dark-theme
     pastel in BOTH themes, and light is the shipped default. Measured on the
     light page: essay 1.59:1, lab 1.42:1, project 1.81:1. The label was
     effectively invisible on the theme most students see. */
  --kind-essay:     #FFB066;
  --kind-lab:       #9BD3FF;
  --kind-project:   #C6A4FF;
  --kind-essay-rgb:    255 176 102;
  --kind-lab-rgb:      155 211 255;
  --kind-project-rgb:  198 164 255;

  /* rgb triplets - components compose alpha as rgb(var(--x-rgb) / a) */
  --accent-rgb:       120 216 192;   /* #78d8c0 seafoam */
  --accent-hover-rgb: 164 223 92;    /* #a4df5c lime */
  --accent-blue-rgb:  37 140 122;    /* #258c7a deep teal (legacy name; green depth) */
  --accent-2-rgb:     164 223 92;    /* #a4df5c lime */
  --accent-teal-rgb:  120 216 192;   /* seafoam (no aqua/cyan blue) */
  --success-rgb:      116 198 154;
  --warning-rgb:      243 201 105;
  --danger-rgb:       255 123 137;

  /* legacy aliases - keep the old names alive, pointed at the new scale */
  --bg-sidebar:  var(--bg-subtle);
  --bg-elev-1:   var(--surface-strong);
  --bg-elev-2:   #141C18;
  --bg-elev-3:   #1A2420;
  --bg-elev-4:   #24302A;
  --glass-bg:    var(--surface);
  --glass-bord:  var(--border);
  --glass-workbench: var(--surface-strong);
  --glass-panel:     var(--surface-strong);
  --glass-control:   var(--bg-elev-2);
  --glass-highlight: rgba(255, 255, 255, 0.07);
  --text-muted:  var(--text-secondary);
  --text-dim:    var(--text-tertiary);
  --text-faint:  #55665A33;
  --border-focus: var(--accent);
  --accent-blue: #258C7A;
  --accent-2:    rgb(var(--accent-2-rgb));
  --accent-3:    #78D8C0;
  --accent-glow: var(--accent-soft);
  --info:        var(--accent);
  --hud-accent:  var(--accent);
  --hud-track:   var(--accent-soft);
  --hud-bg-edge: transparent;
  --grad:        var(--accent);
  --grad-warm:   var(--accent-hover);
  --grad-cool:   var(--accent);
  --grad-soft:   var(--accent-soft);
  --grad-text:   var(--text);

  /* elevation */
  --card-shadow:  0 18px 48px rgba(0, 0, 0, 0.22);
  --shadow-sm:    0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow-md:    0 8px 24px rgba(0, 0, 0, 0.28);
  --shadow-lg:    var(--card-shadow);
  --shadow-xl:    0 28px 72px rgba(0, 0, 0, 0.34);
  --shadow-accent: var(--shadow-md);
  --glow-accent:  0 0 0 1px var(--border-strong);
  /* The aluma.fun surfaces. A filled button sits on a darker ledge it presses
     into; a panel is one of the logo's own gradients. pop.css spends these
     and mints none. On the dark theme the panels are glows on the night
     ground rather than pastel sheets, so light text stays readable on them. */
  --lip-color:  #3A9A84;
  --lip-quiet:  rgba(0, 0, 0, 0.45);
  --panel-hero: radial-gradient(70% 60% at 100% 0%, rgba(120, 216, 192, 0.20), transparent 70%),
                radial-gradient(60% 50% at 0% 100%, rgba(164, 223, 92, 0.12), transparent 70%),
                linear-gradient(150deg, #14241E 0%, #0F1C17 100%);
  --panel-mint: linear-gradient(160deg, rgba(120, 216, 192, 0.16) 0%, rgba(120, 216, 192, 0.07) 100%);
  --panel-aqua: linear-gradient(160deg, rgba(98, 210, 210, 0.16) 0%, rgba(98, 210, 210, 0.07) 100%);
  --panel-lime: linear-gradient(160deg, rgba(164, 223, 92, 0.15) 0%, rgba(164, 223, 92, 0.06) 100%);
  --panel-ink:  var(--text-secondary);
  --fill-bar:   linear-gradient(90deg, #4FB58E 0%, #78D8C0 100%);
}

html[data-theme="light"],
body[data-theme="light"] {
  /* paper-green study surfaces (matches dashboard academic canvas) */
  --bg:             #F6F8F2;
  --bg-subtle:      #EFF4EE;
  --surface:        rgba(255, 255, 255, 0.76);
  --surface-strong: #FFFFFF;
  --surface-muted:  #FBFCF7;
  /* On paper-green the glass has to stay light or the bar reads as a grey
     slab. The lens is near-white rather than tinted: it is a lens over the
     page, and the accent lives in the icon it sits behind. */
  /* The fill is deliberately thinner than the lens. On a light ground a
     near-white pane and a near-white lens sit at the same value and the
     "you are here" pill disappears; the gap between these two numbers IS the
     affordance. Measured on the Home dashboard at 390px before changing it. */
  --glass-fill:   rgba(255, 255, 255, 0.55);
  --glass-edge:   rgba(255, 255, 255, 0.72);
  --glass-lens:   rgba(255, 255, 255, 1);
  --glass-spec:   rgba(255, 255, 255, 0.95);
  --glass-shadow: 0 10px 28px rgb(20 40 32 / 0.16), 0 2px 6px rgb(20 40 32 / 0.10);
  --glass-lens-shadow: 0 2px 6px rgb(20 40 32 / 0.14), 0 0 0 0.5px rgb(20 40 32 / 0.06);
  --text:           #13201B;
  --text-secondary: #536159;
  /* Was #5E6761, checked against the neutral surfaces. On a tinted chip -
     a count inside a Missing pill, the sub-line under an overdue row - the
     backdrop is darker than any of them and it fell to 4.30:1. */
  --text-tertiary:  #5B635D;
  --field-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%235B635D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
  --border:         rgba(19, 32, 27, 0.10);
  --border-strong:  rgba(19, 32, 27, 0.17);
  /* AA deep teal on white + lime secondary */
  /* Ink, not material. rgb(var(--accent-rgb)) stays #1F7A68 and is what the
     chips, fills and borders are made of; this is what gets WRITTEN on them,
     and it is a shade deeper so it can be read there. */
  --accent:         #1C6A5B;
  --accent-hover:   #176357;
  --accent-soft:    rgba(31, 122, 104, 0.12);
  /* Deep teal accent in light theme, so white is the readable ink:
     5.2:1 on --accent, 7.1:1 on --accent-hover. */
  --accent-ink:     #FFFFFF;
  /* White on the light theme's deeper #C54255 is 4.9:1 - already AA. */
  --danger-ink:     #FFFFFF;
  /* White on the light theme's #1F8060 is 4.51:1 - AA. */
  --success-ink:    #FFFFFF;
  /* These three were tuned against WHITE, and they cleared it. Almost nowhere
     do they sit on white: a status word lives on a 10-16% chip of its own
     hue, which is meaningfully darker than the page. Measured in a browser,
     "Overdue" on its own chip was 3.29:1, "1 day late" 3.30:1, "Today" 3.63:1
     and "Homework" 4.27:1 - the ink was verified against the wrong backdrop.
     Re-solved against the DARKEST chip each one is drawn on, hue and
     saturation held, so nothing about the palette changes but its depth. The
     tints themselves are untouched: they read --success-rgb / --warning-rgb /
     --danger-rgb, which these do not. */
  --success:        #18664D;   /* was #1F8060 - 3.72:1 on its own chip */
  --warning:        #885710;   /* was #9C6412 - 3.63:1 on its own chip */
  --danger:         #A43243;   /* was #C54255 - 3.29:1 on its own chip */

  /* Same hues, darkened until they clear AA against their own 10%-alpha chip
     over the light page: essay 4.66:1, lab 5.11:1, project 5.43:1. */
  --kind-essay:     #9A5410;
  --kind-lab:       #18658F;
  --kind-project:   #6B42B8;
  --kind-essay-rgb:    154  84  16;
  --kind-lab-rgb:       24 101 143;
  --kind-project-rgb:  107  66 184;

  --accent-rgb:       31 122 104;    /* #1f7a68 deep teal */
  --accent-hover-rgb: 23 99 87;      /* #176357 */
  --accent-blue-rgb:  23 99 87;      /* deep teal for primary buttons */
  --accent-2-rgb:     71 112 22;     /* #477016 - the lime, darkened for light.
                                        The dark value (#a4df5c) was used in BOTH
                                        themes, alone among the colour tokens, and
                                        measured 1.47:1 on the light page - a label
                                        nobody could read on the shipped default.
                                        styles.css:3225 already carried a darkened
                                        light value; this file loads later and had
                                        been quietly overwriting it. 5.41:1 now. */
  --accent-teal-rgb:  37 140 122;    /* #258c7a */
  --success-rgb:      31 128 96;
  --warning-rgb:      156 100 18;
  --danger-rgb:       197 66 85;

  --bg-sidebar:  var(--bg-subtle);
  --bg-elev-1:   var(--surface-strong);
  --bg-elev-2:   var(--surface-muted);
  --bg-elev-3:   #E5EEE7;
  --bg-elev-4:   #D6E3DA;
  --glass-bg:    var(--surface);
  --glass-bord:  var(--border);
  --glass-workbench: var(--surface-strong);
  --glass-panel:     var(--surface-strong);
  --glass-control:   var(--surface-muted);
  --glass-highlight: rgba(31, 41, 36, 0.05);
  --text-muted:  var(--text-secondary);
  --text-dim:    var(--text-tertiary);
  --text-faint:  #9AA89F;
  --border-focus: var(--accent);
  --accent-blue: #176357;
  --accent-2:    rgb(var(--accent-2-rgb));
  --accent-3:    #217E6E;   /* was #258C7A - 4.11:1 on white, under AA */
  --accent-glow: var(--accent-soft);
  --info:        var(--accent);
  --hud-accent:  var(--accent);
  --hud-track:   var(--accent-soft);
  --hud-bg-edge: transparent;
  --grad:        var(--accent);
  --grad-warm:   rgb(var(--accent-2-rgb));
  --grad-cool:   var(--accent);
  --grad-soft:   var(--accent-soft);
  --grad-text:   var(--text);

  /* Depth as aluma.fun draws it: diffuse, green-tinted, pulled under the
     card rather than spread around it. */
  --card-shadow:  0 22px 44px -22px rgba(19, 32, 27, 0.24), 0 1px 2px rgba(19, 32, 27, 0.04);
  --shadow-sm:    0 1px 2px rgba(19, 32, 27, 0.06);
  --shadow-md:    0 10px 24px -10px rgba(19, 32, 27, 0.16);
  --shadow-lg:    var(--card-shadow);
  --shadow-xl:    0 48px 90px -36px rgba(19, 32, 27, 0.42), 0 6px 16px rgba(19, 32, 27, 0.06);
  --shadow-accent: var(--shadow-md);
  --glow-accent:  0 0 0 1px var(--border-strong);
  /* The aluma.fun surfaces, as the site paints them: the brand-deep ledge under
     every filled button, and the lime / mint / aqua panels of the logo.
     --panel-ink is the quiet line of text on a panel; the site's --text-secondary
     falls to 4.1:1 on the aqua end of the hero, this clears 7:1 on all of it. */
  --lip-color:  #0E4238;
  --lip-quiet:  rgba(19, 32, 27, 0.10);
  --panel-hero: radial-gradient(60% 70% at 88% 12%, rgba(255, 255, 255, 0.5), transparent 70%),
                radial-gradient(55% 60% at 8% 100%, rgba(98, 210, 210, 0.45), transparent 70%),
                linear-gradient(150deg, #CDEDA6 0%, #A8E6C1 38%, #8FDCC6 70%, #86D7D3 100%);
  --panel-mint: linear-gradient(160deg, #CFF2DC 0%, #B4E8C9 100%);
  --panel-aqua: linear-gradient(160deg, #C8EFEE 0%, #A6E2E2 100%);
  --panel-lime: linear-gradient(160deg, #E3F5C6 0%, #CDEBAA 100%);
  --panel-ink:  #2B3A33;
  --fill-bar:   linear-gradient(90deg, #78D8A4 0%, #1F8A70 100%);
}

:root {
  /* ── type ─────────────────────────────────────────────────────────────── */
  --font-sans: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --font-mono: 'Geist Mono', 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  /* Geist for headlines too — one product face, not a bolted-on editorial serif */
  --font-display: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;

  /* Headings are drawn the way aluma.fun draws them: heavy and tight. */
  --t-page-size: 2.25rem;      --t-page-lh: 1.15;  --t-page-w: 800;  --t-page-tr: -0.045em;
  --t-section-size: 1.375rem;  --t-section-lh: 1.25; --t-section-w: 760; --t-section-tr: -0.03em;
  --t-card-size: 1rem;         --t-card-lh: 1.4;   --t-card-w: 680;
  --t-body-size: 0.9375rem;    --t-body-lh: 1.55;  --t-body-w: 430;
  --t-small-size: 0.8125rem;   --t-small-lh: 1.45; --t-small-w: 450;
  --t-label-size: 0.75rem;     --t-label-lh: 1.35; --t-label-w: 650;

  /* ── shell geometry ───────────────────────────────────────────────────── */
  --sidebar-w: 264px;
  --sidebar-rail-w: 72px;
  --topbar-h: 56px;
  /* How long a surface takes to stop. Where one shell surface meets the next -
     a sticky sidebar cap over the nav scrolling behind it, the top bar against
     the page canvas - the boundary is spent over this distance instead of on
     one hard pixel. Short enough that the cap still reads as one surface, long
     enough that no single row of pixels carries the transition. Spent in
     blend.css; minted here, with the rest of the shell's geometry. */
  --seam: 18px;
  --content-max: 1440px;
  --reading-max: 920px;
  /* How wide a whole page of content may get. --reading-max caps a COLUMN OF
     PROSE, where a long line is genuinely hard to follow; this caps a page of
     cards, which can be much wider before it stops being useful. The dashboard
     used to stop dead at 1120px, so a 2560px monitor showed it in a 1120px
     strip with 583px of empty margin on each side. It now takes the room it is
     given, up to a ceiling that keeps a to-do row from stretching to absurdity. */
  --shell-max: 1680px;
  --gutter: 32px;
  --page-pad-y: 28px;
  --grid-gap: 20px;
  --card-pad: 20px;
  --row-pad: 12px 14px;

  /* ── the ink guard ──────────────────────────────────────────────────────
     Geist's content area - ascender to descender, the box the browser
     actually paints - is 1.31em. Any line box tighter than that clips its own
     glyphs the moment the element also carries `overflow: hidden`, which
     every single-line truncating label in this app does, because that is what
     `text-overflow: ellipsis` requires.

     `--leading-snug` was 1.25 and `--leading-tight` 1.2, so the two steps the
     titles and headings used were BOTH under the floor. They now clear it,
     with the ladder's shape intact: tight is still tighter than snug, snug
     than normal. fit.css spends the two guard values on the elements that
     truncate; it mints nothing. */
  --ink-guard: 0.22em;   /* slack inside a single-line clip box, in ems   */
  --ink-floor: 1.34em;   /* the smallest box Geist's glyphs fit inside    */

  /* ── the soft shell ─────────────────────────────────────────────────────
     The sidebar and Settings are rounded panels on the canvas rather than
     columns welded to it by a 1px rule. soft.css spends these; like the
     guard above, the values live here so there is one ladder and not two. */
  --soft-panel:   var(--r-card);   /* a grouped run of rows        */
  --soft-row:     22px;            /* one row inside a run         */
  --soft-control: 999px;           /* anything a student taps      */
  --soft-press:   0.975;
  --soft-rail-w:  248px;

  /* ── shape ────────────────────────────────────────────────────────────── */
  /* aluma.fun's ladder: tile 14, tip 16, field 18, row 22, card 28, frame 32,
     panel 40. Every step is rounder than the one it replaced, so no corner in
     the app got sharper when the product took the site's shape. */
  --r-card: 28px;
  --r-dialog: 32px;
  --r-input: 18px;
  --r-btn: 22px;
  --r-pill: 999px;
  --r-xs: 14px; --r-sm: 16px;  --r-md: 22px;
  --r-lg: 28px; --r-xl: 32px;  --r-2xl: 40px; --r-full: 999px;
  /* Measured before this line changed: light drew --r-md at 18px and
     --r-xl at 30px, dark drew them at 16px and 28px. The app had two
     different corner radii and nothing said so.

     The split came from body[data-theme="light"] in dashboard.css, which is
     (0,1,1) and beat this :root, re-pointing the app-wide ladder at the
     dashboard's own --dash-radius-* scale - which THIS file then re-points
     again at --r-input / --r-card / --r-dialog a couple of thousand lines
     down. A three-hop alias chain across two files, resolving to a
     different number depending on the theme.

     One value each, and in both cases the larger of the two, so nothing in
     the app got sharper: --r-md 20px (was 18/16), --r-xl 30px (was 30/28,
     and 30 is --r-dialog, which is what light was already resolving to). */
  /* Shape aliases, one name per job. rounded.css spends these; it defines no
     tokens of its own so the design system stays the single source of shape. */
  /* Above Geist's 1.31em floor - see the ink guard above. */
  --leading-tight: 1.32;
  --leading-snug:  1.38;

  --r-touch: 999px;    /* anything a student taps: buttons, chips, pills */
  --r-field: 18px;     /* what they type into */
  --r-media: 22px;     /* images, video, code, tables */
  --r-surface: 28px;   /* cards, panels, menus */

  /* ── press ──────────────────────────────────────────────────────────────
     How a control answers a finger. feel.css spends these; it mints none.
     The asymmetry is the point: down is immediate and mechanical, up is slow
     and elastic. Equal timings in both directions read as a web page. */
  /* The ledge a filled button sits on (aluma.fun's .btn-3d). Its colour is
     per theme: --lip-color / --lip-quiet. */
  --lip: 4px;
  --lip-sm: 3px;
  --press-scale: 0.97;      /* large controls: cards, rows, wide buttons */
  --press-scale-sm: 0.94;   /* small round controls: icons, send, chips */
  --press-in: 90ms;         /* finger lands - already there */
  --press-out: 380ms;       /* finger lifts - settles back */
  --ease-press: cubic-bezier(0.3, 0, 0.1, 1);
  --ease-settle: cubic-bezier(0.22, 1.2, 0.36, 1);
  /* Less motion is spent here rather than argued for in selectors. Every
     press in the app - feel.css's two rules and soft.css's three - reads
     these three tokens, so zeroing them is the whole answer, and it needs no
     specificity. The rules that used to answer those two by selector both
     lost, quietly, for as long as they existed. A scale of 1 is no scale and
     0s is no travel; the press still confirms itself, by dimming. */
  --blur-nav: 20px;
  --blur-dialog: 24px;

  /* ── the bottom bar's glass ─────────────────────────────────────────────
     Geometry lives here rather than in the nav's own rule because four other
     sheets have to clear the bar, and a hardcoded number in each is how a
     phone ends up with its last row of work tucked underneath it. */
  --mnav-h: 60px;            /* 6px pad + 48px tap target + 6px pad */
  --mnav-gap: 10px;          /* the float: gap between the bar and the edge */
  /* What every scrolling surface must leave free at the bottom. The safe-area
     inset belongs IN here: on a phone with a home indicator the bar sits
     higher than its own height, and a flat 96px left the last row under it. */
  --mnav-clear: calc(var(--mnav-h) + var(--mnav-gap) + 26px + env(safe-area-inset-bottom, 0px));
  /* The lens travels on this. Apple's own tab curve: leaves fast, arrives
     slow, no overshoot - overshoot on a control you are still touching reads
     as the interface arguing with your finger. */
  --ease-lens: cubic-bezier(0.32, 0.72, 0, 1);

  /* ── controls ─────────────────────────────────────────────────────────── */
  --ctl-h: 44px;
  --ctl-h-compact: 36px;
  --ctl-icon: 40px;
  --tap-min: 44px;
  /* How far a filter or toolbar row sits above the first card of the list it
     filters. It was 0 to 42px depending on the view (Mastery's row sat on its
     first topic above 1450px wide); one step of the --s-* scale now, 12px on
     a phone (section 23). */
  --filter-gap: 16px;

  /* ── motion ───────────────────────────────────────────────────────────── */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out: var(--ease);
  --ease-in-out: var(--ease);
  --ease-spring: var(--ease);
  --d-micro: 140ms;
  --d-standard: 190ms;
  --d-panel: 240ms;
  --d-fast: var(--d-micro);
  --d-base: var(--d-standard);
  --d-slow: var(--d-panel);
  --skel-dur: 1400ms;
}

/* ══════════════════════════ 3 · BASE TYPE ══════════════════════════════════ */
html, body {
  font-family: var(--font-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  font-weight: var(--t-body-w);
  font-feature-settings: normal;
  letter-spacing: -0.005em;
}
body { background: var(--bg); color: var(--text); }

.page-title,
.view > .topbar .page-title,
.admin-hero,
.voice-hero,
.dash-headline,
.auth-title {
  font-family: var(--font-sans);
  font-size: var(--t-page-size);
  line-height: var(--t-page-lh);
  font-weight: var(--t-page-w);
  letter-spacing: var(--t-page-tr);
  color: var(--text);
  text-transform: none;
}
.dash-section-header h2,
.set-section-title,
.section-title,
.tp-section-title,
.conn-section-title,
.admin-section-title,
.recents-label,
.project-title {
  font-family: var(--font-sans);
  font-size: var(--t-section-size);
  line-height: var(--t-section-lh);
  font-weight: var(--t-section-w);
  letter-spacing: var(--t-section-tr);
  color: var(--text);
  text-transform: none;
}
.dash-section-kicker,
.terminal-kicker,
.sidebar-header,
.settings-tabs-divider,
.field-label,
.li-label {
  font-size: var(--t-label-size);
  line-height: var(--t-label-lh);
  font-weight: var(--t-label-w);
  letter-spacing: 0.02em;
  color: var(--text-tertiary);
  text-transform: none;
}
.set-label,
.card-title,
.conn-tile-name,
.dash-card-title { font-size: var(--t-card-size); line-height: var(--t-card-lh); font-weight: var(--t-card-w); }
.set-sub, .muted, .modal-sub, .set-hint,
.conn-tile-blurb, .dash-summary {
  font-size: var(--t-small-size);
  line-height: var(--t-small-lh);
  font-weight: var(--t-small-w);
  color: var(--text-secondary);
}

/* Nav is sentence case, never uppercase. */
.nav-item, .nav-item > span, .settings-tab, .project-tab, .tp-tab,
.mobile-bottom-nav button span { text-transform: none; letter-spacing: 0; }

/* Mono is reserved for what a student would type or read as code: command
   hints, key caps, model ids and the exam timer. A value in a sentence - a
   usage figure, a date, an email, the user id - is set in the sans with
   tabular figures (soft.css section 7), so a label and its answer are one
   typeface. */
.credit-ver, .nav-kbd, kbd, .mono,
.tp-exam-timer,
.model-short, .composer-model { font-family: var(--font-mono); font-feature-settings: 'tnum' 1; }

/* The brand wordmark stops being gradient text - solid ink reads at every size. */
.brand-name {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: var(--text);
  -webkit-text-fill-color: var(--text);
  font-weight: 680;
  font-size: 15px;
  letter-spacing: -0.01em;
}
.brand-sub { color: var(--text-tertiary); font-size: 11.5px; letter-spacing: 0; }
.project-item.active .name {
  background: none; -webkit-background-clip: border-box; background-clip: border-box;
  color: var(--text); -webkit-text-fill-color: var(--text);
}

/* ══════════════════════════ 4 · AMBIENT FX ═════════════════════════════════
   The scanline / grid / vignette layers belong to the old HUD era and fight the
   calm surface treatment. Kept in the DOM, painted out. */
.fx-grid, .fx-scan, .fx-vignette { display: none !important; }
body[data-theme="light"] .app,
body[data-theme="dark"] .app,
.app { background: var(--bg); }

/* ══════════════════════════ 5 · APP SHELL ══════════════════════════════════ */
.app { grid-template-columns: var(--sidebar-w) 1fr; }

.sidebar {
  background: var(--surface);
  -webkit-backdrop-filter: blur(var(--blur-nav));
  backdrop-filter: blur(var(--blur-nav));
  border-right: 1px solid var(--border);
  /* Beat late styles.css !important spacing archaeology (20px gap / 12px brand pad). */
  padding: 14px 12px 12px !important;
  gap: 8px !important;
}
.brand { padding: 2px 6px 4px; gap: 10px; }
.sidebar .brand {
  padding: 2px 6px 4px !important;
  gap: 10px !important;
}
.brand-logo {
  width: 42px;
  height: 42px;
  border-radius: 16px;
  background: none;
  overflow: visible;
}
.rail-toggle {
  width: 32px; height: 32px; border-radius: var(--r-sm);
  color: var(--text-tertiary);
}
.rail-toggle:hover { background: var(--surface-muted); color: var(--text); }

/* Collapsed rail - 72px, icons only, tooltips on hover/focus.

   The two rules that resize the SHELL are scoped to the docked breakpoint,
   because below it there is no sidebar column to collapse. At 768-900px the
   sidebar is an off-canvas drawer (styles.css, MOBILE DRAWER) and at 767px and
   under phone.css deletes it outright - so `.app` is a single track, and a
   rail width there reserves 72px for an element that is not in the grid.
   `.main` lands in that track and the whole app paints as a 72px strip down
   the left edge, which is the phone Home screen collapsed to the width of its
   own title. One stored preference from a laptop, or one Calendar tap through
   autoHideSidebar, was enough to put every later phone load into it - with the
   toggle that would undo it hidden by phone.css.

   app.js holds the same line on the class itself (SIDEBAR_DOCK_MQ), so the
   cosmetic rail rules below never reach the drawer either. This is the guard
   for the frame before that runs, and for the case where it does not. */
@media (min-width: 901px) {
  body.rail { --sidebar-w: var(--sidebar-rail-w); }
  body.rail .app { grid-template-columns: var(--sidebar-rail-w) 1fr !important; }
}
body.rail .sidebar { padding-left: 10px !important; padding-right: 10px !important; }
body.rail .nav-section .nav-item { height: 42px; border-radius: var(--r-btn); }
body.rail .nav-group-label, body.rail .nav-more-toggle span { display: none !important; }
body.rail .nav-group-label { padding: 0; height: 1px; background: var(--border); margin: 8px 6px; overflow: hidden; text-indent: -999em; }

/* Icon-only tooltip. Uses the item's own label text so it can never drift. */
body.rail .nav-section .nav-item { position: relative; }
body.rail .nav-section .nav-item::after {
  content: attr(data-tip);
  position: absolute; left: calc(100% + 10px); top: 50%;
  transform: translateY(-50%) translateX(-4px);
  background: var(--surface-strong); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  padding: 6px 10px; font-size: var(--t-small-size); font-weight: 500;
  white-space: nowrap; pointer-events: none; opacity: 0; z-index: 90;
  box-shadow: var(--card-shadow);
  transition: opacity var(--d-micro) var(--ease), transform var(--d-micro) var(--ease);
}
body.rail .nav-section .nav-item:hover::after,
body.rail .nav-section .nav-item:focus-visible::after { opacity: 1; transform: translateY(-50%) translateX(0); }

/* Account + settings stay anchored at the bottom of the rail. */
.sidebar-footer { margin-top: auto; gap: 8px; padding-top: 12px; }

.nav-section { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  border-radius: var(--r-btn);
  min-height: 38px;
  font-size: var(--t-small-size);
  font-weight: 500;
  color: var(--text-secondary);
  transition: background var(--d-micro) var(--ease), color var(--d-micro) var(--ease);
}
.nav-item:hover { background: var(--surface-muted); color: var(--text); }
.nav-item.active {
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 560;
}
.nav-item.active svg { color: var(--accent); }

/* Grouped navigation: New chat + Home above the rule, then destinations,
   then Tools, then More. */
.nav-group-label {
  font-size: var(--t-label-size);
  line-height: var(--t-label-lh);
  font-weight: var(--t-label-w);
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
  padding: 14px 8px 6px;
  text-transform: none;
}
.nav-count {
  font-family: var(--font-mono);
  font-size: 11px;
  border-radius: var(--r-pill);
  background: var(--surface-muted);
  color: var(--text-secondary);
  padding: 1px 7px;
}
.nav-item.active .nav-count { background: var(--surface-strong); color: var(--accent); }

.topbar {
  min-height: var(--topbar-h);
  padding: 8px var(--gutter);
  background: var(--surface);
  -webkit-backdrop-filter: blur(var(--blur-nav));
  backdrop-filter: blur(var(--blur-nav));
  border-bottom: 1px solid var(--border);
  gap: 16px;
  overflow: visible;
}
.topbar-left { min-width: 0; display: flex; align-items: center; gap: 12px; overflow: visible; }
/* A page title and its count ("Tutor  5 sessions") read as one line, so they
   share a baseline - the same answer the phone header already gives
   (phone.css). Centred, the 12px count floated in the middle of a 34px
   title's line box, visibly above the line the title sits on. `:has` keeps
   this to headers that lead with a title; Home, Classes and the chat header
   lay out other things in this slot. */
.view > .topbar > .topbar-left:has(> .page-title) { align-items: baseline; }
/* A status pill is a box, not a line of text: its baseline is its dot's, so it
   is centred on the title instead. Plain text in the same slot keeps the
   baseline above. */
.view > .topbar > .topbar-left > #tasks-platform-label:has(.conn-pill) { align-self: center; }
/* Page titles use a large display size with descenders (p, g, y). Keep enough
   vertical room so the topbar border never slices through the glyph. */
.view > .topbar .page-title {
  line-height: 1.3;
  padding-bottom: 0.12em;
}
/* ...but "centred on the title" means centred on its box, and that box was
   lopsided: fit.css's ink guard pads 0.22em above and below and takes it back
   with a -0.22em margin, then the 0.12em above replaced only the bottom half.
   The title's layout box lost 0.1em off its foot, its middle rode 0.05em above
   the middle of the line, and the pill rode up with it. The same total padding
   split evenly keeps the header the height it was and puts the middle of the
   box on the middle of the line - which in Geist is the middle of the caps,
   give or take how the platform rounds the font's metrics. Where the browser
   can trim the box to the capitals themselves, it does, and the padding hands
   the rest of the line back on both sides equally: same height again, centred
   on the caps exactly. On a phone the pill is the taller of the two, so the
   title is centred in the row as well rather than sitting at its top. */
.view > .topbar > .topbar-left:has(> #tasks-platform-label .conn-pill) > .page-title {
  align-self: center;
  padding-block: calc((var(--ink-guard) + 0.12em) / 2);
}
@supports (text-box: trim-both cap alphabetic) {
  .view > .topbar > .topbar-left:has(> #tasks-platform-label .conn-pill) > .page-title {
    text-box: trim-both cap alphabetic;
    padding-block: calc((1lh - 1cap) / 2 + (var(--ink-guard) + 0.12em) / 2);
  }
}

/* Content column: capped, gutter-aware, generous vertical padding. */
.school-shell, .learn-shell, .projects-page, .tasks-body, .tasks-filters,
.hist-list, .hist-bar, .disc-body, .mem-feed, .mem-feed-bar,
.conn-grid, .conn-toolbar, .ap-dash, #tp-root, .admin-page, .voice-page {
  max-width: var(--content-max);
  /* width:100% matters - `margin:auto` on a grid item cancels stretch and the
     column would collapse to shrink-to-fit. */
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}
.view > .topbar { padding-left: var(--gutter); padding-right: var(--gutter); }

/* ══════════════════════════ 6 · SURFACES ═══════════════════════════════════
   Reading surfaces stay solid. Blur is reserved for sidebar, top bar, dialogs
   and the composer - anything you read through it loses legibility. */
.dash-module, .learn-topic, .conn-tile, .project-card, .skill-card,
.aside-card, .set-row, .set-callout, .hub-assignment, .task-card,
.tp-mode-card, .tp-custom-card, .ap-card, .plan-card, .pw-card,
.hist-row, .mem-atom, .disc-card {
  background: var(--surface-strong);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  box-shadow: var(--card-shadow);
}
.dash-module, .conn-tile, .plan-card, .pw-card, .disc-card, .aside-card { padding: var(--card-pad); }
.hub-assignment, .hist-row, .mem-atom, .set-row { padding: var(--row-pad); }
.set-row { border-radius: var(--r-card); }

.modal, .palette, .auth-card, .onboard-card, .tutorial-card, .paywall-wrap,
.conn-browser-modal, .notif-panel, .account-popover, .model-dropdown,
.composer-model-menu, .tp-dd-menu {
  border-radius: var(--r-dialog);
  background: var(--surface);
  -webkit-backdrop-filter: blur(var(--blur-dialog));
  backdrop-filter: blur(var(--blur-dialog));
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-xl);
}
.modal-backdrop, .palette-backdrop { background: rgba(7, 17, 13, 0.42); }

/* ══════════════════════════ 7 · CONTROLS ═══════════════════════════════════ */
.primary-btn, .danger-btn {
  min-height: var(--ctl-h);
  padding: 0 18px;
  border-radius: var(--r-btn);
  font-size: var(--t-small-size);
  font-weight: 600;
  letter-spacing: 0;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  transition: background var(--d-micro) var(--ease), transform var(--d-micro) var(--ease),
              box-shadow var(--d-standard) var(--ease);
}
/* Ink must follow the theme: white reads on light theme's deep teal but
   measured 1.70:1 on dark theme's seafoam (and 1.58:1 on the lime hover). */
.primary-btn { background: var(--accent); color: var(--accent-ink); border: 1px solid transparent; }
.primary-btn:hover:not(:disabled) { background: var(--accent-hover); transform: translateY(-1px); }
.primary-btn:active:not(:disabled) { transform: translateY(0); }
.danger-btn { background: var(--danger); color: var(--danger-ink); border: 1px solid transparent; }

.ghost-btn {
  min-height: var(--ctl-h-compact);
  padding: 0 14px;
  border-radius: var(--r-btn);
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--t-small-size);
  font-weight: 500;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
}
.ghost-btn:hover { background: var(--surface-muted); color: var(--text); }

.set-input, .set-textarea, .hist-search, .hist-filter,
.mem-feed-search, #task-title, #task-subject,
#task-due, #task-link, #np-name, #school-url, #school-user, #school-pass,
.auth-field input, .modal input[type="text"], .modal input[type="url"],
.modal input[type="date"], .modal input[type="password"], .modal input[type="email"],
.modal select, .modal textarea {
  min-height: var(--ctl-h);
  border-radius: var(--r-input);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--t-body-size);
  padding: 10px 14px;
}
/* Nested search fields keep chrome on the outer wrapper only. */
.settings-search input,
.conn-search input {
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.set-textarea, .modal textarea { min-height: 96px; }
.set-input:focus, .set-textarea:focus, .modal input:focus, .modal textarea:focus,
.modal select:focus { border-color: var(--accent); }

/* ── One dropdown ────────────────────────────────────────────────────────
   Every single-line <select> draws the same chevron: --field-chevron, 12px,
   its box 14px inside the padding edge, so with the 1px border and the 1px
   the stroke sits inside its own box the ink ends 16px from the outer edge,
   centred. 38px of padding keeps the longest option 13px clear of it.
   Three kinds of arrow shipped before this: the browser's own, 4px from a
   rounded edge (Mastery, Assignments, every modal, the birthday pickers);
   a drawn one 20px in (Tutor); and none, where a later `background`
   shorthand had erased the image and left an empty gap (History, Settings).

   Longhands only, so each control keeps its own fill, border and radius.
   (1,2,2) with the bang because the shorthands that erased the old chevrons
   reach (0,2,2) !important (soft.css); `body:not(#_)` is the idiom clean.css
   and soft.css already use. [multiple] and a [size] over 1 are list boxes,
   not dropdowns: they have nothing to open and get no arrow (size="1", or
   "0", is still a dropdown and keeps it). */
body:not(#_) select:not([multiple]):is(:not([size]), [size="0"], [size="1"]) {
  -webkit-appearance: none !important;
  appearance: none !important;
  background-image: var(--field-chevron) !important;
  background-repeat: no-repeat !important;
  background-size: 12px 12px !important;
  background-position: right 14px center !important;
  padding-right: 38px !important;
  text-overflow: ellipsis;
}
/* A dropdown takes a pointer, not the text caret styles.css gives every
   select; a disabled one keeps not-allowed. */
body:not(#_) select:not([multiple]):is(:not([size]), [size="0"], [size="1"]):enabled { cursor: pointer; }
/* Four component rules switched this ring off for their own 0.08-0.18 alpha
   shadow, which a keyboard user could barely find (the birthday pickers,
   Settings, the reminder hour, the memory import). The same ring as every
   other control. */
body:not(#_) select:not([multiple]):is(:not([size]), [size="0"], [size="1"]):focus-visible {
  outline: 2px solid var(--accent) !important;
  outline-offset: 2px !important;
}
/* The native pickers follow the theme: with no color-scheme the dark theme
   drew a black calendar glyph on a near-black field, and the option list
   opened white. */
body[data-theme="dark"] :is(select, input[type="date"], input[type="time"], input[type="datetime-local"]) {
  color-scheme: dark;
}
/* The calendar / clock glyph sat 18.5px in; 2px back puts it on the same
   16px line as the chevron, in the muted ink rather than full black. */
::-webkit-calendar-picker-indicator {
  margin-inline-end: -2px;
  opacity: 0.62;
  cursor: pointer;
}
::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* ── The filter row and the first card ──────────────────────────────────
   Every view with a filter or toolbar row over a list now leaves the same
   --filter-gap under it. It was 0 to 42px: Mastery's row sat ON its first
   topic above 1450px (styles.css only spaced it below that, and above it
   relied on the .learn-shell grid gap section 45 turns off), Classes left
   38-42px, Discover 32, Connectors 26, Assignments 24-28, the Test Prep tabs
   20, the calendar 18. The row owns the gap where the row is always there;
   where the list's top padding carried it, the list keeps carrying it. */
.learn-toolbar, .tp-tabs, .disc-filters,
.view-autopilot .tt-bar { margin-bottom: var(--filter-gap); }
.view-history .hist-bar + .hist-list,
.view-memories .mem-feed-bar + .mem-feed { padding-top: var(--filter-gap); }
/* The filter bar is hidden while there are no assignments, and the empty
   state under it keeps the page's own top padding. */
.view-tasks .tasks-filterbar:not([style*="none"]) + .tasks-body { padding-top: var(--filter-gap); }
/* The segmented row paid for the gap; the grid's own top padding was a
   second one on top of it. */
.view-projects .projects-mode ~ .projects-grid { padding-top: 0; }

.icon-btn {
  width: var(--ctl-icon); height: var(--ctl-icon);
  border-radius: var(--r-btn);
  color: var(--text-secondary);
}
.icon-btn:hover { background: var(--surface-muted); color: var(--text); }
.sidebar-header .icon-btn { width: 28px; height: 28px; }

.badge, .pill, .seg-btn, .conn-filter-chip, .tasks-filter-pill {
  border-radius: var(--r-pill);
  font-size: var(--t-small-size);
  font-weight: 500;
}

/* Disabled controls must stay readable and say why. */
button:disabled, [aria-disabled="true"] {
  opacity: 0.72;
  cursor: not-allowed;
  color: var(--text-tertiary);
}
button:disabled { background-image: none; }

/* ══════════════════════════ 8 · FOCUS + SKIP LINK ══════════════════════════ */
*:focus { outline: none; }
/* No border-radius here: outline already follows the element's own, so
   setting one reshaped the ELEMENT instead - this rule pulled every button
   and link to --r-btn on focus, turning the Lumi orb from a circle into a
   rounded rectangle while the student looked at it. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, [tabindex]:focus-visible, [role="button"]:focus-visible,
.nav-item:focus-visible, .seg-btn:focus-visible, .conn-tile:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.skip-link {
  position: fixed;
  top: 8px; left: 8px;
  z-index: 9999;
  transform: translateY(-160%);
  background: var(--surface-strong);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-btn);
  padding: 10px 16px;
  font-size: var(--t-small-size);
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--card-shadow);
  transition: transform var(--d-standard) var(--ease);
}
.skip-link:focus { transform: translateY(0); }

/* ══════════════════════════ 9 · MOTION ═════════════════════════════════════ */
.view { animation: alumaViewIn var(--d-panel) var(--ease) both; }
@keyframes alumaViewIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

.modal-backdrop:not(.hidden) > .modal,
.modal-backdrop:not(.hidden) > .import-modal,
.palette-backdrop:not(.hidden) > * {
  animation: alumaPanelIn var(--d-panel) var(--ease) both !important;
}
@keyframes alumaPanelIn { from { opacity: 0; transform: translateY(10px) scale(0.985); } to { opacity: 1; transform: none; } }

.dash-module:hover, .conn-tile:hover, .task-card:hover, .project-card:hover,
.disc-card:hover, .learn-topic:hover { transform: translateY(-2px); }
.dash-module, .conn-tile, .task-card, .project-card, .disc-card, .learn-topic {
  transition: transform var(--d-standard) var(--ease), box-shadow var(--d-standard) var(--ease),
              border-color var(--d-standard) var(--ease);
}

/* No looping decoration anywhere in the product chrome. */
.brand-icon::after, .orb::after, .nav-pulse { animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  :root {
    --press-scale: 1;
    --press-scale-sm: 1;
    --press-in: 0s;
    --press-out: 0s;
  }

  /* `*` is specificity (0,0,0). Roughly a hundred rules in this app set a
     transition or an animation with `!important` - feel.css presses every
     control from a repeated `:is()` chain, animations.css lights #send-btn,
     styles.css spins the logo - and `!important` on both sides means the more
     SPECIFIC selector wins. So this block read as full coverage and lost to
     every one of them. `:not(#\9)` matches everything and contributes an id
     each time, which lifts this to (3,0,0) without changing what it matches.
     A student who asks the OS for less motion now actually gets it. */
  *:not(#\9):not(#\9):not(#\9),
  *:not(#\9):not(#\9):not(#\9)::before,
  *:not(#\9):not(#\9):not(#\9)::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    /* The delays too. Zeroing only the duration leaves a staggered entrance
       WAITING: these animations carry `both` or `backwards`, so the element
       holds its `from` state - opacity 0, for almost all of them - for the
       length of the delay and then appears instantly. A student who asked
       the OS for less motion got no motion and, in exchange, content that
       was invisible for up to 450ms with nothing happening on screen to
       explain it. Assignments cascade eight cards deep, the suggestion row
       three, the sign-in switch 400ms. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}

/* Reduced transparency: drop every blur to a solid surface. */
@media (prefers-reduced-transparency: reduce) {
  .sidebar, .topbar, .composer, .modal, .palette, .auth-card, .onboard-card,
  .tutorial-card, .paywall-wrap, .conn-browser-modal, .notif-panel,
  .account-popover, .model-dropdown, .composer-model-menu, .tp-dd-menu,
  .mobile-bottom-nav {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    background: var(--surface-strong) !important;
  }
}

/* ══════════════════════ 10 · ONE SKELETON SYSTEM ═══════════════════════════
   The app already decides *when* to show a skeleton and what shape it takes -
   none of that changes. What changes is that four competing shimmer treatments
   (styles.css, animations.css, the "maxout" block, and the per-view rules) now
   resolve to one primitive: same gradient, same 1400ms sweep, same radius
   language, and a single reduced-motion opt-out. */
.skeleton, .skel-card, .skel-line,
.view-tasks .tasks-skel .sk,
.ap-skel-card::before,
.import-loading::before,
.mmd-skel-sheen,
.hub-skel-greet, .hub-skel-headline, .hub-skel-sub, .hub-skel-date, .hub-skel-cta,
.hub-skel-kicker, .hub-skel-title, .hub-skel-input, .hub-skel-chip,
.hub-skel-mod-link, .hub-skel-rank, .hub-skel-abody .l1, .hub-skel-abody .l2,
.hub-skel-abody .l3, .hub-skel-badge, .hub-skel-btn, .hub-skel-stat, .hub-skel-signal,
.hub-skel-ico, .hub-skel-row-body .l1, .hub-skel-row-body .l2, .hub-skel-class,
.hub-skel-ptime, .hub-skel-planner .l1, .hub-skel-planner .l2, .hub-skel-source .pill,
.hub-hero.is-skel #dash-greeting::after, .hub-hero.is-skel #hub-headline::after,
.hub-hero.is-skel #hub-subline::after, .hub-hero.is-skel #dash-date::after,
.hub-hero.is-skel #hub-next-action {
  animation-duration: var(--skel-dur) !important;
  animation-timing-function: ease-in-out !important;
  animation-iteration-count: infinite !important;
}
.skeleton, .skel-card, .skel-line, .view-tasks .tasks-skel .sk,
.hub-skel-greet, .hub-skel-headline, .hub-skel-sub, .hub-skel-date, .hub-skel-cta,
.hub-skel-kicker, .hub-skel-title, .hub-skel-input, .hub-skel-chip,
.hub-skel-mod-link, .hub-skel-rank, .hub-skel-abody .l1, .hub-skel-abody .l2,
.hub-skel-abody .l3, .hub-skel-badge, .hub-skel-btn, .hub-skel-stat, .hub-skel-signal,
.hub-skel-ico, .hub-skel-row-body .l1, .hub-skel-row-body .l2, .hub-skel-class,
.hub-skel-ptime, .hub-skel-planner .l1, .hub-skel-planner .l2, .hub-skel-source .pill,
.hub-hero.is-skel #dash-greeting::after, .hub-hero.is-skel #hub-headline::after,
.hub-hero.is-skel #hub-subline::after, .hub-hero.is-skel #dash-date::after,
.hub-hero.is-skel #hub-next-action {
  background-image: linear-gradient(
    90deg,
    var(--surface-muted) 0%,
    var(--bg-elev-3) 42%,
    var(--surface-muted) 84%
  ) !important;
  background-color: var(--surface-muted);
  background-size: 220% 100% !important;
  background-repeat: no-repeat;
  animation-name: alumaSkelSweep !important;
  border-radius: var(--r-input);
}
@keyframes alumaSkelSweep {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}
.skel-card, .skel-cardish { border-radius: var(--r-card); }

/* Skeletons mirror the geometry of what replaces them, so nothing jumps. */
.view-skel {
  padding: var(--page-pad-y) var(--gutter);
  display: grid;
  gap: var(--grid-gap);
  max-width: var(--content-max);
  margin: 0 auto;
  width: 100%;
}
.view-skel .skel-line { height: 14px; }
.view-skel .skel-line.w40 { max-width: 320px; }
.view-skel .skel-line.w55 { max-width: 460px; }
.view-skel .skel-row { display: grid; grid-template-columns: 2fr 1fr; gap: var(--grid-gap); }
.view-skel .skel-card { height: 168px; border-radius: var(--r-card); margin: 0; }
/* Compact rows for list-shaped routes (Assignments, History). */
.view-skel[data-skel="rows"] .skel-row { grid-template-columns: 1fr; }
.view-skel[data-skel="rows"] .skel-card { height: 62px; }

@media (prefers-reduced-motion: reduce) {
  .skeleton, .skel-card, .skel-line, .view-tasks .tasks-skel .sk,
  .ap-skel-card::before, .import-loading::before, .mmd-skel-sheen,
  .view-history .hist-skel-row, .view-memories .mem-skel-wrap,
  .hub-skel-greet, .hub-skel-headline, .hub-skel-sub, .hub-skel-date, .hub-skel-cta,
  .hub-skel-kicker, .hub-skel-title, .hub-skel-input, .hub-skel-chip,
  .hub-skel-mod-link, .hub-skel-rank, .hub-skel-abody .l1, .hub-skel-abody .l2,
  .hub-skel-abody .l3, .hub-skel-badge, .hub-skel-btn, .hub-skel-stat, .hub-skel-signal,
  .hub-skel-ico, .hub-skel-row-body .l1, .hub-skel-row-body .l2, .hub-skel-class,
  .hub-skel-ptime, .hub-skel-planner .l1, .hub-skel-planner .l2, .hub-skel-source .pill,
  .hub-hero.is-skel #dash-greeting::after, .hub-hero.is-skel #hub-headline::after,
  .hub-hero.is-skel #hub-subline::after, .hub-hero.is-skel #dash-date::after,
  .hub-hero.is-skel #hub-next-action {
    animation: none !important;
    background-image: none !important;
    background-color: var(--surface-muted) !important;
  }
}
/* Never a spinner and a skeleton for the same content. */
.tasks-body.loading .tasks-spinner,
.ap-dash .ap-skel + .ap-spinner { display: none !important; }

/* ══════════════════════ 11 · HOME (Hub) ════════════════════════════════════ */
.school-shell { padding: var(--page-pad-y) var(--gutter); }
.dash-greeting { color: var(--text-secondary); font-size: var(--t-small-size); font-weight: 500; }
.dash-headline { margin-top: 4px; }
.dash-summary { max-width: 62ch; margin-top: 8px; }
.dash-date { font-family: var(--font-sans); font-size: var(--t-label-size); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-tertiary); }

.dash-primary-action, .hub-next-action {
  min-height: var(--ctl-h);
  border-radius: var(--r-btn);
  background: var(--accent);
  color: var(--accent-ink);   /* white measured 1.70:1 on the dark accent */
  padding: 10px 18px;
  box-shadow: none;
}
.dash-primary-action:hover, .hub-next-action:hover { background: var(--accent-hover); transform: translateY(-1px); }

.dash-grid { gap: var(--grid-gap); }
.dash-section-header { align-items: baseline; margin-bottom: 14px; }
.dash-section-header > button {
  min-height: var(--ctl-h-compact);
  padding: 0 12px;
  border-radius: var(--r-btn);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: var(--t-small-size);
}
.dash-section-header > button:hover { background: var(--surface-muted); color: var(--text); }

/* A first-run account gets one calm panel, never a wall of zero cards. */


/* Home makes the first question reachable before optional school setup. */
.hub-load-status {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 20px;
  padding: 16px;
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--warning);
  border-radius: var(--r-input);
  background: var(--surface-strong);
}
.hub-load-status p { margin: 0; flex: 1; color: var(--text-secondary); line-height: 1.5; }
.hub-load-status button { flex-shrink: 0; }
.hub-load-status.hidden { display: none; }
/* `.school-shell.empty-school .dash-tutor-suggestions { display: flex }` used
   to sit here, under a selector dashboard.css:657 already uses to HIDE the
   suggestions in the empty-school state. Identical selector, and this file
   loads later, so the hide never fired. The wrapping this rule wanted is
   already in the base `.dash-tutor-suggestions` rule, so it said nothing else. */
.dash-tutor-suggestions .dash-tutor-chip { white-space: normal; text-align: left; }
.view-school .dash-class-card { min-width: 0; }
.view-school .dash-class-card strong,
.view-school .dash-continue-body strong { overflow-wrap: anywhere; }
@media (max-width: 600px) {
  .hub-load-status { align-items: stretch; flex-direction: column; gap: 12px; }
}

/* First-value checklist - disappears for good once a milestone lands. */

/* ══════════════════════ 12 · TUTOR + CHAT ══════════════════════════════════ */
.chat { scroll-padding-bottom: 24px; }
.msg.assistant .msg-body { max-width: 72ch; }
.chat-split { min-height: 0; }

.composer {
  background: var(--surface);
  -webkit-backdrop-filter: blur(var(--blur-nav));
  backdrop-filter: blur(var(--blur-nav));
  border-top: 1px solid var(--border);
  padding: 12px var(--gutter) 16px;
}
.composer-inner {
  max-width: 840px;
  min-height: 56px;
  border-radius: var(--r-card);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  padding: 8px 8px 8px 16px;
  gap: 8px;
  align-items: flex-end;
}
.composer-inner:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
#input { max-height: 160px; font-size: var(--t-body-size); line-height: var(--t-body-lh); padding: 10px 2px; }
.composer-actions { align-self: flex-end; padding-bottom: 2px; }
.composer-hint {
  max-width: 840px;
  font-size: var(--t-label-size);
  color: var(--text-tertiary);
  justify-content: flex-start;
}
.send-btn {
  width: var(--ctl-icon); height: var(--ctl-icon);
  border-radius: var(--r-btn);
  background: var(--accent);
  box-shadow: none;
}
.send-btn:hover:not(:disabled) { background: var(--accent-hover); transform: none; }

/* Guided-session structure: each beat of the loop is its own readable block
   instead of one continuous wall of chat. */
.tutor-beat {
  border-left: 2px solid var(--border-strong);
  padding: 2px 0 2px 14px;
  margin: 14px 0;
}
.tutor-beat > .tb-label {
  display: block;
  font-size: var(--t-label-size);
  font-weight: var(--t-label-w);
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
  margin-bottom: 4px;
}
.tutor-beat[data-beat="ask"]      { border-left-color: var(--border-strong); }
.tutor-beat[data-beat="step"]     { border-left-color: var(--accent); }
.tutor-beat[data-beat="attempt"]  { border-left-color: var(--text-tertiary); }
.tutor-beat[data-beat="hint"]     { border-left-color: var(--warning); }
.tutor-beat[data-beat="check"]    { border-left-color: var(--accent); }
.tutor-beat[data-beat="evidence"] { border-left-color: var(--success); }
.tutor-beat[data-beat="ask"] .tb-label::before      { content: "Assignment asks · "; }
.tutor-beat[data-beat="step"] .tb-label::before     { content: "Current step · "; }
.tutor-beat[data-beat="attempt"] .tb-label::before  { content: "Your attempt · "; }
.tutor-beat[data-beat="hint"] .tb-label::before     { content: "Hint · "; }
.tutor-beat[data-beat="check"] .tb-label::before    { content: "Check · "; }
.tutor-beat[data-beat="evidence"] .tb-label::before { content: "Mastery evidence · "; }

/* Tutor header: subject, assignment, mode, progress.
   Honour [hidden] - a bare display:flex here used to beat the attribute and
   leave an empty strip that stole the chat view's 1fr row (huge new-chat gap). */
.tutor-head[hidden] { display: none !important; }
.tutor-head:not([hidden]) {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  padding: 10px var(--gutter);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--t-small-size);
  color: var(--text-secondary);
}
/* The topbar already names the class in full, so this copy is a breadcrumb,
   not a second heading: label-sized and tertiary, with the assignment title
   left as the one thing the eye lands on. */
.tutor-head .th-class {
  font-size: var(--t-label-size);
  color: var(--text-tertiary);
}
.tutor-head .th-title { color: var(--text); font-weight: 600; }
.tutor-head .th-mode {
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: 3px 10px;
  font-size: var(--t-label-size);
}
.tutor-head .th-progress { margin-left: auto; font-family: var(--font-mono); font-size: var(--t-label-size); }

/* Finishing the assignment is the point of the session, so the button that
   says so sits in the header - not only on a result card further up. It takes
   the auto margin when there is no progress counter to hold it. */
.tutor-head .th-done[hidden] { display: none !important; }
.tutor-head .th-done {
  margin-left: auto;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: 4px 12px;
  background: transparent;
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--t-label-size);
  cursor: pointer;
}
.tutor-head .th-progress + .th-done { margin-left: 0; }
.tutor-head .th-done:hover:not(:disabled) {
  border-color: rgb(var(--accent-rgb));
  color: rgb(var(--accent-rgb));
}
.tutor-head .th-done.is-done {
  border-color: var(--success);
  color: var(--success);
}
.tutor-head .th-done:disabled { cursor: default; opacity: 0.85; }

/* Model names are a secondary detail, not the decision - but still bold + tinted. */
.tutor-mode-btn .tm-model,
.model-dropdown .md-secondary,
.composer-model-menu .md-secondary {
  font-size: var(--t-label-size);
  font-weight: 700;
}
.tutor-mode-btn .tm-model { color: var(--text-tertiary); }

/* ══════════════════════ 13 · ASSIGNMENTS ═══════════════════════════════════ */
.tasks-filters { padding: var(--page-pad-y) var(--gutter) 0; gap: 8px; }
.tasks-body { padding: 16px var(--gutter) var(--page-pad-y); }
.task-card { padding: var(--row-pad); border-radius: var(--r-card); }
.task-card .tc-title { font-size: var(--t-card-size); font-weight: var(--t-card-w); }
.task-card .tc-meta { font-size: var(--t-small-size); color: var(--text-secondary); }
/* Calm status colour - urgency reads from weight and label, not a red wall. */
.task-card .tc-due.urgent { color: var(--danger); font-weight: 600; }
.task-card .tc-due.soon   { color: var(--warning); font-weight: 600; }
.task-card .tc-due.later  { color: var(--text-secondary); font-weight: 500; }
.task-card.overdue { border-color: rgb(var(--danger-rgb) / 0.35); background: var(--surface-strong); }
.task-card.overdue::before { content: none; }

/* ══════════════════════ 14 · MASTERY ═══════════════════════════════════════ */
.learn-shell { padding: var(--page-pad-y) var(--gutter); gap: var(--grid-gap); }
.learn-summary {
  background: var(--surface-strong);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  box-shadow: var(--card-shadow);
}
.learn-overall strong { font-family: var(--font-mono); letter-spacing: -0.02em; }
.learn-chip { color: var(--text-tertiary); }
.learn-chip strong { font-family: var(--font-mono); }

/* Status never rides on colour alone - each carries a glyph and a word. */
.mastery-status { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-label-size); font-weight: 600; border-radius: var(--r-pill); padding: 3px 10px; border: 1px solid var(--border-strong); }
.mastery-status::before { font-family: var(--font-mono); }
.mastery-status[data-status="starting"]  { color: var(--text-secondary); }
.mastery-status[data-status="starting"]::before  { content: "○"; }
.mastery-status[data-status="needs-work"] { color: var(--warning); border-color: rgb(var(--warning-rgb) / 0.4); }
.mastery-status[data-status="needs-work"]::before { content: "!"; }
.mastery-status[data-status="learning"]  { color: var(--accent); border-color: rgb(var(--accent-rgb) / 0.4); }
.mastery-status[data-status="learning"]::before  { content: "◐"; }
.mastery-status[data-status="verified"]  { color: var(--success); border-color: rgb(var(--success-rgb) / 0.4); }
.mastery-status[data-status="verified"]::before  { content: "✓"; }

.mastery-evidence { display: grid; gap: 6px; margin-top: 10px; }
.mastery-evidence li { list-style: none; display: flex; gap: 8px; align-items: flex-start; font-size: var(--t-small-size); color: var(--text-secondary); }
.mastery-evidence li .me-mark { font-family: var(--font-mono); flex: none; color: var(--text-tertiary); }
.mastery-evidence li.met .me-mark { color: var(--success); }

/* ══════════════════════ 15 · TEST PREP ═════════════════════════════════════ */
#tp-root { padding: var(--page-pad-y) var(--gutter); }
.tp-exam-timer { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 1.125rem; font-weight: 600; }
/* The timed runner must not shift while answering. */
.tp-exam-header { min-height: var(--topbar-h); }
.tp-exam-actions { min-width: 340px; justify-content: flex-end; }
.tp-exam-nav { min-height: 44px; }

/* Extra topbar room so "Test Prep" descenders never meet the hairline. */
.view-test-prep > .topbar {
  padding-top: 14px;
  padding-bottom: 16px;
  min-height: calc(var(--topbar-h) + 8px);
  align-items: center;
}
.view-test-prep > .topbar .page-title {
  line-height: 1.35;
  padding-bottom: 0.18em;
  overflow: visible;
}

/* Progression: colorless horizontal timeline (Choose → … → Master). */
.tp-progression {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0;
  font-size: var(--t-small-size);
  color: var(--text-tertiary);
  margin: 0 0 var(--grid-gap);
  padding: 16px var(--gutter) 18px;
  border-bottom: 1px solid var(--border);
  background: transparent;
}
/* syncTpProgression stands the rail down on the Test Hub with .hidden, and no
   global .hidden rule exists, so display:flex above kept it drawn there with
   "Choose" lit. [hidden] is what _syncRouteChrome sets off the route. */
.tp-progression.hidden,
.tp-progression[hidden] { display: none; }
/* Same gap: "← All tests" on the All-tests list itself, re-rendering it. */
#tp-back-to-picker.hidden { display: none; }
.tp-progression span {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 0 4px;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--text-tertiary);
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
  min-width: 0;
  transition: color var(--d-standard) var(--ease);
}
/* Node */
.tp-progression span::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong, var(--border));
  background: var(--surface-strong, var(--bg));
  box-sizing: border-box;
  z-index: 1;
  flex-shrink: 0;
  transition: background var(--d-standard) var(--ease),
              border-color var(--d-standard) var(--ease);
}
/* Connector to the next stage */
.tp-progression span::after {
  content: "";
  position: absolute;
  top: 4px;
  left: calc(50% + 7px);
  width: calc(100% - 14px);
  height: 1.5px;
  background: var(--border);
  pointer-events: none;
}
.tp-progression span:last-child::after { content: none; }
.tp-progression [data-tp-stage].done {
  color: var(--text-secondary);
}
.tp-progression [data-tp-stage].done::before {
  background: var(--text-secondary);
  border-color: var(--text-secondary);
}
.tp-progression [data-tp-stage].done::after {
  background: var(--text-secondary);
}
.tp-progression [data-tp-stage].on {
  color: var(--text);
  font-weight: 700;
}
.tp-progression [data-tp-stage].on::before {
  background: var(--text);
  border-color: var(--text);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--text) 12%, transparent);
}
body[data-theme="light"] .tp-progression {
  background: transparent;
}
@media (max-width: 560px) {
  .tp-progression {
    padding-left: 12px;
    padding-right: 12px;
  }
  .tp-progression span {
    font-size: 11px;
    gap: 6px;
  }
}

/* ══════════════════════ 16 · CLASSES ═══════════════════════════════════════ */
.project-page { padding: var(--page-pad-y) var(--gutter); }
.project-tabs { gap: 4px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
/* Nine tabs on a Canvas class do not fit the Overview column beside the
   rail: they wrap to a second row rather than running under the rail or
   off the page, and no label breaks across two lines. */
.project-tab { white-space: nowrap; }
.project-tab, .tp-tab, .settings-tab {
  min-height: var(--ctl-h-compact);
  border-radius: var(--r-btn) var(--r-btn) 0 0;
  font-size: var(--t-small-size);
  font-weight: 500;
  color: var(--text-secondary);
}
.project-tab.active, .tp-tab.active { color: var(--text); font-weight: 600; }

/* ══════════════════════ 17 · CONNECTORS ════════════════════════════════════ */
.conn-privacy-note { display: none !important; }
.conn-toolbar {
  padding-left: var(--gutter); padding-right: var(--gutter);
  justify-content: center;
}
.conn-filters { justify-content: center; }
/* The search box and the chips are one row: it was 41px beside 34.9px
   chips on a desktop, and 64px on a phone, where 9px of padding wrapped
   the input's 44px tap floor. One compact height and the chips' type;
   phone.css still lifts both to 44px. */
.conn-search { min-height: var(--ctl-h-compact); padding-block: 0; }
.conn-search input { font-size: var(--t-small-size); }
.conn-filter-chip { min-height: var(--ctl-h-compact); }
.conn-filter-chip {
  color: var(--text) !important;
  font-weight: 600 !important;
  border-color: var(--border-strong) !important;
  background: var(--surface) !important;
}
.conn-filter-chip:hover {
  color: var(--text) !important;
  background: var(--surface-muted) !important;
}
.conn-filter-chip.active {
  color: var(--text) !important;
  background: rgb(var(--accent-rgb) / 0.14) !important;
  border-color: rgb(var(--accent-rgb) / 0.45) !important;
  font-weight: 650 !important;
}
.conn-section-title {
  padding: var(--page-pad-y) var(--gutter) 10px;
  font-size: 0.9375rem !important;
  line-height: 1.3 !important;
  font-weight: 650 !important;
  letter-spacing: -0.01em !important;
  color: var(--text) !important;
  opacity: 1 !important;
  text-transform: none;
  font-family: var(--font-sans);
  border-left: 0 !important;
  padding-left: var(--gutter) !important;
  padding-right: var(--gutter) !important;
  text-align: center !important;
}
.conn-section-title::before { display: none !important; }
.conn-section-sub {
  margin: -4px var(--gutter) 12px;
  padding: 0;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--text-dim, #5a6f68);
  text-align: center;
}
/* Clear break between School cards and Productivity */
.conn-grid:not(.conn-section-hidden) + .conn-section-title {
  position: relative;
  margin-top: 28px !important;
  padding-top: 40px !important;
  border-top: none !important;
}
.conn-grid:not(.conn-section-hidden) + .conn-section-title::after {
  content: '';
  position: absolute;
  top: 0;
  left: var(--gutter, 36px);
  right: var(--gutter, 36px);
  height: 1px;
  background: color-mix(in srgb, var(--text) 28%, transparent);
}
.conn-grid {
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  gap: var(--grid-gap);
  align-items: stretch;
}
.conn-tile {
  height: 100%;
}
.conn-tile-blurb {
  flex: 1 1 auto;
}
.conn-tile-status {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--t-label-size); font-weight: 600;
}
.conn-tile-status .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
/* Status carries a glyph as well as a colour. */
.conn-tile-status::after { font-family: var(--font-sans); font-size: 11px; color: currentColor; }
.conn-tile-status.connected     { color: var(--success); }
.conn-tile-status.connected .dot     { background: var(--success); }
.conn-tile-status.syncing       { color: var(--accent); }
.conn-tile-status.syncing .dot       { background: var(--accent); }
.conn-tile-status.error,
.conn-tile-status.needs-login   { color: var(--warning); }
.conn-tile-status.error .dot,
.conn-tile-status.needs-login .dot   { background: var(--warning); }
.conn-tile-status.disconnected,
.conn-tile-status.coming-soon   { color: var(--text-tertiary); }
.conn-tile-status.disconnected .dot,
.conn-tile-status.coming-soon .dot   { background: var(--text-tertiary); }

/* ══════════════════════ 18 · SETTINGS ══════════════════════════════════════ */
.settings-shell { gap: var(--grid-gap); }
.settings-tabs { padding: 16px 12px; gap: 2px; }
.settings-tabs-divider {
  padding: 16px 10px 6px;
  font-size: var(--t-label-size);
  font-weight: var(--t-label-w);
  color: var(--text-tertiary);
  text-transform: none;
  letter-spacing: 0.04em;
}
.settings-tab { border-radius: var(--r-btn); padding: 0 12px; text-align: left; justify-content: flex-start; }
.settings-tab.active { background: var(--accent-soft); color: var(--text); font-weight: 600; }
.settings-pane { padding: var(--page-pad-y) var(--gutter); max-width: var(--reading-max); }
.set-row { border: 1px solid var(--border); margin-bottom: 10px; padding: 16px 18px; }
.set-section-title { margin: 28px 0 12px; }

/* Data controls live apart from ordinary saves and read as destructive. */
.set-data-controls {
  margin-top: 36px;
  border: 1px solid rgb(var(--danger-rgb) / 0.3);
  border-radius: var(--r-card);
  padding: 18px;
  background: rgb(var(--danger-rgb) / 0.04);
}
.set-data-controls .set-section-title { margin-top: 0; color: var(--danger); }
.set-data-controls .set-row { border-color: rgb(var(--danger-rgb) / 0.24); background: var(--surface-strong); }
.set-row.danger { border-color: rgb(var(--danger-rgb) / 0.3); }

/* API key setup card - only rendered when the runtime genuinely needs a key. */
.set-ai-connect {
  border: 1px solid var(--accent);
  border-radius: var(--r-card);
  background: var(--accent-soft);
  padding: 20px;
}
.set-ai-connect .set-hint { color: var(--text-secondary); }
#ai-key-input { font-family: var(--font-mono); }

/* ══════════════════════ 19 · AUTH + ONBOARDING ═════════════════════════════ */
.auth-overlay { background: var(--bg); }
.auth-card {
  width: 420px;
  max-width: calc(100vw - 32px);
  padding: 32px 28px 26px;
  border-radius: var(--r-dialog);
  gap: 12px;
}
.auth-wordmark { letter-spacing: -0.02em; font-weight: 600; color: var(--accent); font-family: var(--font-display); }
.auth-wordmark span { color: inherit; -webkit-text-fill-color: inherit; background: none; }
.auth-sub { color: var(--text-secondary); font-size: var(--t-body-size); text-align: center; }
.beta-disclosure {
  display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface-muted);
  padding: 6px 14px;
  font-size: var(--t-label-size);
  color: var(--text-tertiary);
  text-align: center;
}
.beta-disclosure strong { color: var(--text-secondary); font-weight: 600; }
.auth-field input { min-height: var(--ctl-h); }
.auth-submit { width: 100%; }
.auth-consent { font-size: var(--t-small-size); color: var(--text-secondary); align-items: flex-start; gap: 10px; }
.auth-consent-group {
  border: 1px solid var(--border);
  border-radius: var(--r-input);
  padding: 12px 14px;
  display: grid;
  gap: 10px;
  background: var(--surface-muted);
}
.auth-consent-group .acg-label {
  font-size: var(--t-label-size); font-weight: var(--t-label-w); color: var(--text-tertiary);
}
.oauth-btn { min-height: var(--ctl-h); border-radius: var(--r-btn); }

/* The overlay these sit in pays `padding: 24px`, so a card capped at
   100vw - 32px is 16px wider than the box it sits in: measured
   scrollWidth 406 against clientWidth 390 on the sign-in, signup and
   verify gates, and the onboarding card landed at x=24 with a 0px right
   gutter, its corner cut by the screen edge. 48px is 24px a side. */
.onboard-card, .tutorial-card { width: 520px; max-width: calc(100vw - 48px); border-radius: var(--r-dialog); }
.onboard-progress {
  display: flex; align-items: center; gap: 10px;
  font-size: var(--t-label-size); color: var(--text-tertiary);
  margin-bottom: 6px;
}
.onboard-progress .op-track { flex: 1; height: 4px; border-radius: var(--r-pill); background: var(--surface-muted); overflow: hidden; }
.onboard-progress .op-fill { height: 100%; background: var(--accent); border-radius: inherit; transition: width var(--d-panel) var(--ease); }

/* ══════════════════════ 20 · STATE BLOCKS ══════════════════════════════════ */
.state-block {
  display: grid;
  gap: 10px;
  justify-items: start;
  padding: 28px var(--card-pad);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  background: var(--surface-strong);
  color: var(--text-secondary);
  max-width: 68ch;
}
.state-block h3 { font-size: var(--t-card-size); font-weight: var(--t-card-w); color: var(--text); }
.state-block p { font-size: var(--t-small-size); }
.state-block .sb-safe { color: var(--text-tertiary); font-size: var(--t-small-size); }
.state-block .sb-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; }
.state-block[data-tone="error"]   { border-color: rgb(var(--danger-rgb) / 0.32); }
.state-block[data-tone="warning"] { border-color: rgb(var(--warning-rgb) / 0.32); }
.state-block[data-tone="offline"] { border-color: var(--border-strong); }

/* ══════════════════════ 21 · FEEDBACK ══════════════════════════════════════ */
.feedback-cats { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0 12px; }
.feedback-cats button {
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: 7px 14px;
  font-size: var(--t-small-size);
  color: var(--text-secondary);
  min-height: var(--ctl-h-compact);
}
.feedback-cats button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--text); }
.feedback-route {
  font-family: var(--font-sans);
  font-size: var(--t-label-size);
  color: var(--text-tertiary);
  border: 1px solid var(--border);
  border-radius: var(--r-input);
  padding: 8px 12px;
  margin-bottom: 10px;
  word-break: break-all;
}
.feedback-consent { display: flex; gap: 10px; align-items: flex-start; font-size: var(--t-small-size); color: var(--text-secondary); margin-bottom: 8px; }

/* Site-wide feedback survey (centered modal) */
.feedback-survey-modal {
  position: relative;
  max-width: 460px;
  gap: 14px;
}
.feedback-survey-modal .modal-title {
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  padding-right: 28px;
}
.feedback-survey-modal .modal-sub {
  margin: 0;
  color: var(--text-secondary);
  line-height: 1.5;
}
.feedback-survey-x {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text-tertiary);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.feedback-survey-x:hover { color: var(--text); background: var(--bg-elev-2); }
.feedback-survey-field { display: block; margin: 0; }
.feedback-survey-field textarea {
  width: 100%;
  min-height: 88px;
  resize: vertical;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 12px 14px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.feedback-survey-field textarea::placeholder {
  color: var(--text-tertiary);
  opacity: 1;
}
.feedback-survey-field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  outline: none;
}
#feedback-survey-error {
  color: var(--danger);
  margin: 0;
}
.feedback-survey-modal .modal-actions {
  margin-top: 4px;
}
#feedback-survey-backdrop:not(.hidden) .feedback-survey-modal {
  animation: feedbackSurveyIn 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes feedbackSurveyIn {
  from { transform: translateY(18px) scale(0.97); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* ══════════════════════ 22 · MOBILE BOTTOM NAV ═════════════════════════════
   A pane of glass floating over the page, with a lens that slides to whatever
   you are looking at. You can tap a tab, or put a thumb down anywhere on the
   bar and drag: the lens follows, each tab it crosses ticks, and letting go
   goes there. Both gestures end in the same place, which is the point - a
   phone control that only answers one of them feels broken on the other.

   The float (inset from all three edges rather than welded to the bottom) is
   what makes the glass read as glass: a strip flush to the screen edge has no
   page visible underneath it to refract. */
.mobile-bottom-nav {
  left: max(var(--mnav-gap), env(safe-area-inset-left, 0px));
  right: max(var(--mnav-gap), env(safe-area-inset-right, 0px));
  bottom: calc(var(--mnav-gap) + env(safe-area-inset-bottom, 0px));
  height: var(--mnav-h);
  align-items: stretch;
  padding: 6px;
  gap: 0;
  border: 1px solid var(--glass-edge);
  border-top: 1px solid var(--glass-edge);
  border-radius: var(--r-pill);
  background: var(--glass-fill);
  /* Saturation is half the trick. Blur alone gives frosted plastic; pushing
     saturation is what makes the colour behind the pane bleed through as
     light rather than grey. */
  -webkit-backdrop-filter: blur(30px) saturate(180%);
  backdrop-filter: blur(30px) saturate(180%);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-spec);
  /* A vertical swipe that starts on the bar still scrolls the page; the
     browser hands us the horizontal axis and cancels us on the vertical one. */
  touch-action: pan-y;
  /* The lens is a child at z-index 0; this keeps it out of the page's stack. */
  isolation: isolate;
}

/* The lens. Position and width are written by the script from the real
   geometry of the active tab, because five equal columns stops being true the
   moment a label wraps or a font loads late. */
.mnav-lens {
  position: absolute;
  top: 6px;
  bottom: 6px;
  left: 0;
  width: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: var(--r-pill);
  background: var(--glass-lens);
  box-shadow: inset 0 1px 0 var(--glass-spec), var(--glass-lens-shadow);
  transform: translate3d(0, 0, 0);
  /* `opacity` belongs in this list. `.is-lit` turns the lens on, and with
     the property left out the pill appeared at full strength in one frame
     while its position was still gliding under it - the one piece of
     motion in the bar that did not move. */
  transition: transform 420ms var(--ease-lens), width 420ms var(--ease-lens),
              opacity 420ms var(--ease-lens);
  opacity: 0;
}
.mobile-bottom-nav.is-lit .mnav-lens { opacity: 1; }
/* Under a finger the lens tracks rather than glides - 420ms of easing behind
   a thumb that is still moving reads as lag, not polish. */
.mobile-bottom-nav.is-dragging .mnav-lens {
  transition: transform 140ms var(--ease-press), width 140ms var(--ease-press);
}

.mobile-bottom-nav button {
  position: relative;
  z-index: 1;
  align-content: center;
  min-height: 0;
  padding: 0 2px;
  border-radius: var(--r-pill);
  background: none;
  color: var(--text-tertiary);
  font-size: 10.5px;
  font-weight: 550;
  gap: 3px;
  transition: color 220ms var(--ease-press);
}
.mobile-bottom-nav button svg { width: 21px; height: 21px; }
/* The lens supplies the fill, so the button must not paint its own - two
   stacked pills was the bug the lens replaces. */
.mobile-bottom-nav button.active,
.mobile-bottom-nav button[aria-current="page"],
.mobile-bottom-nav button.is-previewing {
  color: var(--accent);
  background: none;
}
/* Mid-drag the destination lights up before you commit to it, so you can see
   where letting go would take you. */
.mobile-bottom-nav.is-dragging button.active:not(.is-previewing) { color: var(--text-tertiary); }

@media (prefers-reduced-motion: reduce) {
  .mnav-lens { transition: none; }
}
/* No backdrop-filter (older Android WebView, Firefox with it disabled): the
   bar must still be opaque enough to read text against a scrolling page. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .mobile-bottom-nav { background: var(--surface-strong); }
}

/* ══════════════════════ 23 · RESPONSIVE ════════════════════════════════════
   Verified layouts at 320 / 390 / 768 / 900 / 1024 / 1180 / 1440 / 1728.
   Compact chrome (drawer + bottom nav) is 900px - phones and tablet portrait. */
@media (min-width: 1729px) {
  :root { --gutter: 40px; }
}
@media (max-width: 1180px) {
  /* Tutor context rail folds away before it can squeeze the reading column. */
  .artifact-panel:not(.hidden) { position: fixed; inset: auto 0 0 0; height: 62vh; z-index: 220; border-radius: var(--r-dialog) var(--r-dialog) 0 0; }
  .learn-inspector { display: none; }
}
@media (max-width: 1024px) {
  :root { --gutter: 24px; --grid-gap: 16px; }
  .dash-layout .dash-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .settings-shell { grid-template-columns: 1fr; }
  .settings-tabs { flex-direction: row; flex-wrap: wrap; overflow-x: auto; }
  /* Compact chrome: the docked sidebar becomes a drawer (see FIT ALL DEVICES). */
  .app { grid-template-columns: 1fr !important; }
}
@media (max-width: 768px) {
  :root { --gutter: 16px; --page-pad-y: 20px; --card-pad: 16px; --filter-gap: 12px; }
  .school-shell, .learn-shell, #tp-root, .project-page, .settings-pane { padding-left: var(--gutter); padding-right: var(--gutter); }
  .composer { padding-left: var(--gutter); padding-right: var(--gutter); }
  .view-skel .skel-row { grid-template-columns: 1fr; }
  .topbar { flex-wrap: wrap; row-gap: 8px; }
  .topbar-right { flex-wrap: wrap; }
}
@media (max-width: 420px) {
  :root { --t-page-size: 1.625rem; }
  .beta-disclosure { border-radius: var(--r-input); }
  .auth-card { padding: 24px 18px 20px; }
  .topbar-right .ghost-btn { padding: 0 10px; }
}
/* 320px: nothing may push the page into a horizontal scroll. */
@media (max-width: 340px) {
  :root { --gutter: 12px; }
  .dash-tutor-suggestions { display: none; }
}
html, body { overflow-x: hidden; }
.view, .main, .school-shell, .dash-grid { min-width: 0; }

/* Zoom to 200% must not trap content: allow the shell to scroll rather than clip. */
@media (max-height: 460px) {
  html, body { overflow-y: auto; height: auto; max-height: none; }
  .app, .view { height: auto; min-height: 100vh; min-height: 100dvh; max-height: none; }
}

/* ══════════════════════ 24 · SCREEN READER HELPERS ═════════════════════════ */
.sr-only:not(:focus):not(:active), .visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
[aria-busy="true"] { cursor: progress; }

/* ══════════════════════ 25 · CLASS TABS (added surfaces) ═══════════════════
   Two class tabs that previously had no panel of their own - Mastery and
   Settings - reuse the same right-rail card mechanics as Files and Memory.
   Grades and Feedback (Canvas only) are full-width the same way. */
.project-page[data-active-ptab="mastery"],
.project-page[data-active-ptab="grades"],
.project-page[data-active-ptab="feedback"],
.project-page[data-active-ptab="settings"] { grid-template-columns: minmax(0, 1fr); }
.project-page[data-active-ptab="mastery"] .project-main > *:not(.project-header):not(.project-subinfo):not(.project-composer):not(.project-tabs),
.project-page[data-active-ptab="grades"] .project-main > *:not(.project-header):not(.project-subinfo):not(.project-composer):not(.project-tabs),
.project-page[data-active-ptab="feedback"] .project-main > *:not(.project-header):not(.project-subinfo):not(.project-composer):not(.project-tabs),
.project-page[data-active-ptab="settings"] .project-main > *:not(.project-header):not(.project-subinfo):not(.project-composer):not(.project-tabs) { display: none; }
.project-page[data-active-ptab="mastery"] [data-ptab-show~="mastery"],
.project-page[data-active-ptab="grades"] [data-ptab-show~="grades"],
.project-page[data-active-ptab="feedback"] [data-ptab-show~="feedback"],
.project-page[data-active-ptab="settings"] [data-ptab-show~="settings"] { display: block; }

.pm-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--t-small-size);
}
.pm-row:last-child { border-bottom: none; }
.pm-row strong { flex: 1; font-weight: 550; color: var(--text); }
.pm-row .pm-score { font-family: var(--font-sans); font-weight: 650; font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.ps-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }

/* ══════════════════════ 26 · SIDEBAR MORE PANEL ═══════════════════════════ */
/* Compose now lives inside .nav-section (see index.html), so it rides the
   nav's 2px row gap. It spans the full sidebar content box - same left/right
   extent as Home - instead of sitting in an inset well. */
/* margin-bottom separates New chat from Home so the two accent pills don't
   visually merge; they stay a pair above the rule with breathing room. */
.sidebar-compose { order: -3; display: block; width: 100%; padding: 0; margin-bottom: 6px; }
.nav-compose {
  width: 100%;
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 560;
  min-height: 40px;
}
.nav-compose svg { color: var(--accent); }
.nav-compose:hover { background: var(--accent-soft); filter: brightness(1.04); }
.nav-item.nav-home { margin-top: 0; }
/* In the 68px rail the button goes icon-only and square, like every nav row. */
body.rail .sidebar-compose { padding: 0; }
body.rail .nav-compose { justify-content: center; gap: 0; padding-left: 0; padding-right: 0; }

.nav-more-open { width: 100%; }
/* Rows in the panel are tap targets; they must never absorb the shrink. */

/* ── Sidebar flex contract ─────────────────────────────────────────────────
   .sidebar is a flex column. Nothing in it declared flex-shrink, so opening
   either "More" made the browser distribute the overflow across EVERY region:
   nav rows, the compose button and the footer all compressed at once and the
   choices became unreadable instead of the sidebar simply scrolling.
   Fix the contract: the fixed chrome never shrinks, and Pinned/Recents is the
   single elastic region that yields space. */
.sidebar > .brand,
.sidebar > .nav-section,
.sidebar > .nav-more-rule,
.sidebar > .trial-banner,
.sidebar > .sidebar-footer { flex-shrink: 0; }

.sidebar > .sidebar-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-top: 0 !important;
  gap: 6px !important;
}

/* Nav rows keep their designed 30px height (styles.css pins it !important)
   instead of being squeezed to slivers - flex-shrink is what was missing, not
   a taller row, so don't restate min-height here and fight that rule. */
.nav-section > .nav-item,
.nav-section > .sidebar-compose,
.nav-section > .nav-more-toggle { flex-shrink: 0; }
/* Customize sidebar now lives in the Quick settings panel - it is a preference
   about the sidebar, not a place to go, and the nav holds destinations only. */

/* While the footer More panel is open, Pinned/Recents steps aside entirely
   rather than fighting the panel for the same pixels. */
/* No :has() (older Safari/Firefox ESR): the panel still scrolls internally,
   it just shares the column. Cap it harder so it can't crowd the nav out. */
@supports not selector(:has(*)) {
}

/* Expanding the nav's own "More" peek must push the sidebar into scroll,
   never compress the rows it just revealed. */
.sidebar { overflow-y: auto; overscroll-behavior: contain; }
body.rail .credit,
body.rail .upgrade-cta { display: none !important; }
body.rail .footer-search span, body.rail .footer-search .nav-kbd { display: none !important; }
body.rail .account-card .ac-info, body.rail .account-card .ac-chev { display: none !important; }

/* ══════════════════════ 27 · SKILLS ═══════════════════════════════════════ */
.skills-page { padding: var(--page-pad-y) var(--gutter); max-width: var(--content-max); width: 100%; margin: 0 auto; display: grid; gap: var(--grid-gap); align-content: start; }
.skills-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--grid-gap); }
.skill-card { display: grid; gap: 12px; align-content: start; padding: var(--card-pad); }
.skill-card-head { display: flex; gap: 12px; align-items: flex-start; }
.skill-ico { font-size: 20px; line-height: 1; }
.skill-name { font-size: var(--t-card-size); font-weight: var(--t-card-w); }
.skill-desc { font-size: var(--t-small-size); color: var(--text-secondary); margin-top: 2px; }
.skill-state {
  margin-left: auto; flex: none;
  font-size: var(--t-label-size); font-weight: 600;
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  padding: 3px 10px; color: var(--text-tertiary);
}
.skill-state[data-on="1"] { color: var(--success); border-color: rgb(var(--success-rgb) / 0.4); }
.skill-preview summary { cursor: pointer; font-size: var(--t-small-size); color: var(--text-secondary); }
.skill-preview pre {
  margin-top: 8px; padding: 12px;
  background: var(--surface-muted); border: 1px solid var(--border);
  border-radius: var(--r-input);
  font-family: var(--font-mono); font-size: var(--t-label-size);
  white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow: auto;
}
.skill-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.skill-actions .danger { color: var(--danger); border-color: rgb(var(--danger-rgb) / 0.35); }
.skill-skel { pointer-events: none; }

/* ══════════════════════ 28 · MASTERY EVIDENCE + ASSIGNMENT META ═══════════ */
.lt-evidence-why { margin-top: 12px; display: grid; gap: 8px; }
.mastery-evidence-log li .me-mark { color: var(--text-tertiary); }
.learn-empty-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 12px; }
.learn-lane[data-status="needs-work"] .lane-prompt { color: var(--warning); }
.learn-lane[data-status="learning"] .lane-prompt { color: var(--accent); }
.learn-lane[data-status="verified"] .lane-prompt { color: var(--success); }

.task-provenance {
  display: flex; flex-wrap: wrap; gap: 6px 12px;
  margin: 8px 0 2px;
  font-size: var(--t-label-size);
  color: var(--text-tertiary);
}
.task-provenance .tp-effort, .task-provenance .tp-sync { font-family: var(--font-sans); font-variant-numeric: tabular-nums; }
/* One assignment set in several classes collapses to a single card; this
   names the classes it covers so the merge never reads as lost work. */
.task-provenance .tp-also {
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.filter-row.secondary { margin-top: 6px; opacity: 0.92; }

/* ══════════════════════ 29 · AVAILABILITY PILL ════════════════════════════ */
.avail-pill {
  display: inline-flex; align-items: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: 2px 9px;
  margin-left: 8px;
  font-size: var(--t-label-size);
  font-weight: 600;
  color: var(--text-tertiary);
  vertical-align: middle;
}
.avail-pill[data-avail="available"] { color: var(--success); border-color: rgb(var(--success-rgb) / 0.4); }
.avail-pill[data-avail="limited"]   { color: var(--warning); border-color: rgb(var(--warning-rgb) / 0.4); }
.avail-pill[data-avail="desktop"]   { color: var(--text-secondary); }

/* ══════════════════════ 30 · MOBILE MORE SHEET ════════════════════════════ */
.mobile-more-sheet {
  position: fixed; inset: 0; z-index: 460;
  background: rgba(7, 17, 13, 0.42);
  display: flex; align-items: flex-end;
}
.mobile-more-sheet.hidden { display: none; }
.mms-panel {
  width: 100%;
  max-height: 82vh;
  overflow-y: auto;
  background: var(--surface-strong);
  border-radius: var(--r-dialog) var(--r-dialog) 0 0;
  border-top: 1px solid var(--border-strong);
  padding: 8px 12px calc(16px + env(safe-area-inset-bottom, 0));
  box-shadow: var(--shadow-xl);
  animation: alumaSheetIn var(--d-panel) var(--ease) both;
}
@keyframes alumaSheetIn { from { transform: translateY(16px); opacity: 0; } to { transform: none; opacity: 1; } }
.mms-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 6px 6px;
  position: sticky; top: 0; background: var(--surface-strong);
}
.mms-head strong { font-size: var(--t-section-size); font-weight: var(--t-section-w); letter-spacing: var(--t-section-tr); }
.mms-group-label {
  font-size: var(--t-label-size); font-weight: var(--t-label-w);
  color: var(--text-tertiary); padding: 14px 8px 4px;
}
.mms-item {
  display: flex; align-items: center;
  width: 100%;
  min-height: var(--tap-min);
  padding: 12px 10px;
  border-radius: var(--r-btn);
  color: var(--text);
  font-size: var(--t-body-size);
  font-weight: 500;
  text-align: left;
  text-decoration: none;
}
.mms-item:hover, .mms-item:focus-visible { background: var(--surface-muted); }
/* The sheet is a mobile affordance only - the sidebar owns this on desktop. */
@media (min-width: 901px) { .mobile-more-sheet { display: none !important; } }

/* ══════════════════════ 31 · SIGN-IN STAGE ════════════════════════════════
   styles.css locks the sign-in screen to a green glass treatment with
   !important, from the era when the mark was green. Restated here in the new
   palette so the first screen a student sees matches the rest of the product.
   Structure, fields and behaviour are untouched - this is colour and metrics. */
.auth-overlay {
  /* Soft green study wash - keep this stronger than a flat paper fill so
     sign-in reads educational, not cool blue-gray. */
  background:
    radial-gradient(42% 36% at 50% 46%, rgba(120, 216, 192, 0.28), transparent 72%),
    radial-gradient(38% 32% at 28% 40%, rgba(31, 122, 104, 0.16), transparent 70%),
    radial-gradient(36% 30% at 74% 52%, rgba(164, 223, 92, 0.18), transparent 70%),
    radial-gradient(130% 100% at 50% -10%, #e8f5ec 0%, #f0f7f2 55%, #f4f7f5 100%) !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  padding: 24px;
  overflow-y: auto;
}
.auth-card {
  width: 420px !important;
  max-width: calc(100vw - 48px) !important;   /* the overlay pays 24px a side */
  padding: 32px 28px 26px !important;
  background: var(--surface-strong) !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--r-dialog) !important;
  box-shadow: var(--card-shadow) !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  text-align: center;
}
.auth-card::before, .auth-card::after { content: none !important; }
/* A card taller than the screen (a sign-up under 13, Google on, a short
   laptop) starts at the top and scrolls; auto margins still centre a short
   one. `center` pushed a tall card's top out of reach above the overlay, and
   styles.css's `overflow: hidden` let the grid row stop at the screen's
   height, so a tall card ended flush on the bottom edge, padding lost. */
.auth-overlay > .auth-card { align-self: start; margin-block: auto; overflow: visible; }
/* The supplied artwork carries its own transparent margin - the solid mark is
   only ~50% of the file's width and ~45% of its height. A 72px box therefore
   drew a ~36x32px mark floating in a pool of dead space. The box is opened up
   so the mark reads at roughly 60px, and the negative margins claw back the
   artwork's built-in padding so the optical gaps match the rest of the card.
   The image itself is untouched: sized proportionally, object-fit: contain. */
.auth-card .auth-logo {
  width: 140px !important;
  height: 140px !important;
  margin: -24px auto -20px !important;
  display: block !important;
  object-fit: contain !important;
  filter: none !important;
  border: none !important;
  border-radius: 0 !important;
  background: none !important;
}
/* Geist wordmark - natural "Aluma" spacing, brand green (not Auron blue).
   var(--accent) is #1F7A68 in light and #78D8C0 in dark, so the wordmark keeps
   its contrast on both themes instead of staying a fixed deep teal. */
.auth-wordmark {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(34px, 7vw, 42px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--accent);
  margin: 0 0 12px;
  gap: 0;
}
.auth-wordmark > span { display: inline; letter-spacing: inherit; }
.auth-card .auth-sub, #auth-sub.auth-sub {
  color: var(--text-secondary) !important;
  font-size: var(--t-body-size) !important;
  font-weight: 500 !important;
  margin: 0 0 4px !important;
}
.auth-card .beta-disclosure {
  flex-direction: row !important;
  align-items: center; justify-content: center;
  width: auto; margin: 0 auto 16px !important;
  padding: 5px 12px !important;
  border-radius: var(--r-pill) !important;
  border: 1px solid var(--border) !important;
  background: var(--surface-muted) !important;
  color: var(--text-tertiary) !important;
  font-size: var(--t-label-size) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
.auth-card .beta-disclosure strong {
  color: var(--text-secondary) !important;
  font-size: var(--t-label-size) !important;
  letter-spacing: 0 !important;
  text-transform: none !important;
}
.auth-card .beta-disclosure strong::after { content: " ·"; }
.auth-card .auth-field input {
  min-height: var(--ctl-h);
  padding: 11px 16px 11px 44px !important;
  background: var(--surface-strong) !important;
  border: 1px solid var(--border-strong) !important;
  border-radius: var(--r-input) !important;
  color: var(--text) !important;
  font-family: var(--font-sans);
  font-size: var(--t-body-size) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
.auth-card .auth-field input::placeholder { color: var(--text-tertiary) !important; }
.auth-card .auth-field input:focus {
  border-color: var(--accent) !important;
  background: var(--surface-strong) !important;
  box-shadow: 0 0 0 3px var(--accent-soft) !important;
}
.auth-card .auth-field .auth-ico { color: var(--text-tertiary); }
.auth-card .pw-toggle { color: var(--text-tertiary); }
.auth-card .pw-toggle:hover { color: var(--text); background: var(--surface-muted) !important; }
.auth-card .auth-submit, .auth-card .auth-submit.btn-hero {
  min-height: var(--ctl-h);
  padding: 12px !important;
  border-radius: var(--r-btn) !important;
  font-family: var(--font-sans);
  font-size: var(--t-body-size) !important;
  font-weight: 600 !important;
  background: var(--accent) !important;
  /* This !important is what actually paints the Sign In button, so it is the
     rule that has to be theme-aware: white here measured 1.70:1 on the dark
     theme's seafoam accent (1.58:1 on the lime hover) - the worst contrast in
     the app, on its most important control. */
  color: var(--accent-ink) !important;
}
.auth-card .auth-submit:hover { background: var(--accent-hover) !important; filter: none !important; }
.auth-card .auth-forgot a,
.auth-card .auth-switch a,
.auth-card .auth-linkbtn { color: var(--accent) !important; }

/* "Forgot password?", "Create an account", "Resend code" and "Use a different
   email" were <a> elements with no href. An anchor without an href is not
   focusable, so all four were absent from the tab cycle: measured by real Tab
   presses, the sign-in gate went username -> password -> show-password ->
   submit -> the support mailto and wrapped, and signup had no keyboard route
   back to sign-in at all. A keyboard-only student could not recover a password
   or create an account. They are <button type="button"> now, which the click
   handlers bind to identically; this rule keeps them drawn as links, and the
   ::after grows the hit area to 44px without moving the text. */
.auth-card .auth-linkbtn {
  position: relative;
  display: inline-block;
  background: none !important;
  border: 0 !important;
  padding: 6px 2px;
  font: inherit;
  line-height: inherit;
  cursor: pointer;
  text-align: inherit;
}
.auth-card .auth-linkbtn::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 100%; min-width: 44px; height: 44px;
}
.auth-card .auth-linkbtn:hover { text-decoration: underline; }
.auth-card .auth-linkbtn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.auth-card .auth-switch { color: var(--text-secondary) !important; font-size: var(--t-small-size) !important; }
.auth-divider { color: var(--text-tertiary); }
.auth-divider::before, .auth-divider::after { background: var(--border); }
.auth-card .oauth-btn {
  border: 1px solid var(--border-strong) !important;
  background: var(--surface-strong) !important;
  color: var(--text) !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
.auth-card .oauth-apple { background: #0A0F0D !important; color: #fff !important; }
.auth-card .auth-consent { color: var(--text-secondary) !important; font-size: var(--t-small-size) !important; }
.auth-card .auth-error {
  color: var(--danger) !important;
  background: rgb(var(--danger-rgb) / 0.08) !important;
  border: 1px solid rgb(var(--danger-rgb) / 0.28);
  border-radius: var(--r-input);
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
.brand-beta {
  border: 1px solid var(--border-strong);
  color: var(--text-tertiary);
  font-size: 9px;
  letter-spacing: 0.06em;
}

/* Onboarding shares the sign-in stage. Three visible steps, one progress bar. */
.onboard-card, .tutorial-card, .paywall-wrap {
  background: var(--surface-strong) !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--r-dialog) !important;
  box-shadow: var(--card-shadow) !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
.onboard-dots span, .tut-dots span { background: var(--border-strong); }
.onboard-dots span.on, .tut-dots span.on, .onboard-dots span.active, .tut-dots span.active { background: var(--accent); }

/* ══════════════════════ 32 · ONBOARDING (three steps) ═════════════════════ */
.onboard-progress { display: flex; align-items: center; gap: 12px; margin: 2px 0 14px; }
.onboard-progress .op-count { font-size: var(--t-label-size); color: var(--text-tertiary); font-weight: var(--t-label-w); white-space: nowrap; }
.onboard-progress .op-track { flex: 1; height: 4px; border-radius: var(--r-pill); background: var(--surface-muted); overflow: hidden; display: block; }
.onboard-progress .op-fill { display: block; height: 100%; background: var(--accent); border-radius: inherit; transition: width var(--d-panel) var(--ease); }
.onboard-dots { display: flex; gap: 5px; margin-bottom: 14px; }
.onboard-dots .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--border-strong); }
.onboard-dots .dot.on { background: var(--accent); width: 16px; border-radius: var(--r-pill); }

.onboard-card { text-align: left; padding: 28px 26px 22px !important; }
.onboard-title { font-size: var(--t-section-size); line-height: var(--t-section-lh); font-weight: var(--t-section-w); letter-spacing: var(--t-section-tr); color: var(--text); }
.onboard-sub { font-size: var(--t-small-size); line-height: var(--t-small-lh); color: var(--text-secondary); margin: 6px 0 16px; max-width: 60ch; }
.onboard-group { display: grid; gap: 16px; max-height: 46vh; overflow-y: auto; padding-right: 4px; }
.onboard-field-row { display: grid; gap: 7px; }
.onboard-field-label { font-size: var(--t-label-size); font-weight: var(--t-label-w); color: var(--text-tertiary); }
.onboard-input {
  width: 100%;
  min-height: var(--ctl-h);
  padding: 10px 14px;
  border-radius: var(--r-input);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--t-body-size);
}
.onboard-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.onboard-choices { display: flex; flex-wrap: wrap; gap: 8px; }
.onboard-chip {
  min-height: var(--ctl-h-compact);
  padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text-secondary);
  font-size: var(--t-small-size);
}
.onboard-chip:hover { background: var(--surface-muted); color: var(--text); }
/* Solid accent fill so the selection reads clearly. Light theme forces white
   on-accent text via styles.css; dark keeps near-black on seafoam. */
.onboard-chip.sel {
  background: var(--accent);
  border-color: var(--accent);
  color: #051916;
  font-weight: 600;
}

.onboard-actions-grid { display: grid; gap: 10px; }
.onboard-action {
  display: grid; gap: 3px; text-align: left;
  padding: 16px 18px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  background: var(--surface-strong);
  min-height: 64px;
  transition: border-color var(--d-standard) var(--ease), transform var(--d-standard) var(--ease);
}
.onboard-action strong { font-size: var(--t-card-size); font-weight: var(--t-card-w); color: var(--text); }
.onboard-action span { font-size: var(--t-small-size); color: var(--text-secondary); }
.onboard-action:hover { border-color: var(--accent); transform: translateY(-2px); }
.onboard-nav { display: flex; align-items: center; gap: 10px; margin-top: 18px; }
.onboard-skip { position: absolute; top: 18px; right: 18px; }
/* Leave room so a larger "Just take me in" doesn't collide with the progress label */
.onboard-progress { padding-right: 148px; }
@media (max-width: 420px) {
  .onboard-progress { padding-right: 0; margin-top: 40px; }
  .onboard-skip.onboard-skip-enter { top: 12px; right: 12px; left: 12px; width: auto; text-align: center; }
}
.shake { animation: alumaShake 0.42s var(--ease); }
@keyframes alumaShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

/* ══════════════════════ 33 · DASHBOARD TOKEN BRIDGE ═══════════════════════
   dashboard.css carries its own --dash-* palette. Re-point them at the semantic
   academic-green scale so Home cannot drift away from the rest of the product. */
:root,
html[data-theme="light"], body[data-theme="light"],
html[data-theme="dark"], body[data-theme="dark"] {
  --dash-canvas:        var(--bg);
  --dash-surface:       var(--surface-strong);
  --dash-surface-warm:  var(--bg-subtle);
  --dash-text:          var(--text);
  --dash-text-muted:    var(--text-secondary);
  --dash-text-dim:      var(--text-tertiary);
  --dash-border:        var(--border);
  --dash-border-strong: var(--border-strong);
  --dash-accent:        var(--accent);
  --dash-accent-rgb:    var(--accent-rgb);
  --dash-accent-soft:   var(--accent-soft);
  --dash-info:          var(--accent);
  --dash-info-rgb:      var(--accent-rgb);
  --dash-attention:     var(--warning);
  --dash-attention-rgb: var(--warning-rgb);
  --dash-lavender:      var(--accent);
  --dash-lavender-rgb:  var(--accent-rgb);
  --dash-danger:        var(--danger);
  --dash-danger-rgb:    var(--danger-rgb);
  --dash-radius-sm:     var(--r-input);
  --dash-radius-md:     var(--r-card);
  --dash-radius-lg:     var(--r-dialog);
  --dash-shadow:        var(--card-shadow);
  --dash-shadow-hover:  var(--card-shadow);
  --dash-focus-ring:    0 0 0 3px var(--accent-soft);
}

/* ══════════════════════ 34 · NAV ORDER ════════════════════════════════════
   New chat (sidebar-compose order -3) + Home, then a rule, then the
   learning-loop destinations, then Tools (Grades first) + customize / more.
   The rule sits between Home and Tutor so the two top actions are not
   nestled into the destination list. */
.nav-item.nav-home                        { order: -2 !important; }
.nav-home-rule                            { order: 0 !important; }
.nav-item[data-nav-key="autopilot"]       { order: 1 !important; }
.nav-item[data-nav-key="tasks"]           { order: 2 !important; }
.nav-item[data-nav-key="learn"]           { order: 3 !important; }
.nav-item[data-nav-key="test-prep"]       { order: 4 !important; }
.nav-item[data-nav-key="projects"]        { order: 5 !important; }
.nav-item[data-nav-key="calendar"]        { order: 6 !important; }
.nav-section .nav-group-label             { order: 7; }
.nav-item[data-nav-key="grades"]          { order: 8 !important; }
.nav-item[data-nav-key="history"]         { order: 9 !important; }
.nav-item[data-nav-key="discover"]        { order: 10 !important; }
.nav-item[data-nav-key="connectors"]      { order: 11 !important; }
.nav-item[data-nav-key="admin"]           { order: 12 !important; }
#sidebar-customize-btn, #nav-more-toggle  { order: 20 !important; }
.nav-home-rule { height: 1px; background: var(--border-strong); margin: 6px 6px 8px !important; }

/* The More panel must honour [hidden] - a display value here would beat it. */

/* The footer is Search + the account card now that the More drawer is gone. */
.sidebar-footer > .footer-search,
.sidebar-footer > .account-card { display: flex !important; }

/* ══════════════════════ 35 · SMALL-SCREEN CHROME ══════════════════════════ */
@media (max-width: 900px) {
  /* ── One row: menu, title, action ────────────────────────────────────────
     The bar used to wrap. The title sat left behind a 64px indent for the
     floating hamburger, and the action dropped onto a second line under it -
     three things on three different baselines, and a header twice as tall as
     it needed to be.

     Three grid columns instead, with the outer two locked to the same width:
     `1fr auto 1fr` makes the title DEAD centre in the bar, not centre of
     whatever is left after the action. The first column is empty on purpose -
     the hamburger is position:fixed and floats over it, so this is the space
     it is reserving. */
  .view > .topbar {
    display: grid;
    grid-template-columns: minmax(48px, 1fr) auto minmax(48px, 1fr);
    align-items: center;
    padding-left: var(--gutter);
    /* Grid children never wrap onto a new row; undo the flex wrap below. */
    row-gap: 0;
  }
  .view > .topbar > .topbar-left {
    grid-column: 2;
    justify-content: center;
    justify-self: center;
  }
  .view > .topbar > .topbar-right { grid-column: 3; justify-self: end; }

  /* Three bars opt back out, and the reason is arithmetic rather than taste:
     Projects, Calendar and Memories each carry three or four actions (plus a
     day/week switcher on Calendar). A centred title needs equal space either
     side of it, and at 390px there is not enough left over - forcing the
     grid made these bars TALLER than the flex row they replaced, measured:
     Projects 101 -> 142, Calendar 171 -> 233, Memories 186 -> 189. A bar with
     a toolbar in it should wrap; that is what wrapping is for. */
  .view-projects > .topbar,
  .view-calendar > .topbar,
  .view-memories > .topbar {
    display: flex;
    align-items: flex-start;
    padding-left: 64px;
  }
  /* A wrapped title would grow the bar and break the shared baseline. */
  .view > .topbar .page-title {
    font-size: 1.5rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* The hamburger is fixed to the viewport, so it has to be centred against
     the bar by arithmetic rather than by alignment. 42px button in a
     --topbar-h bar, under whatever notch the device has. */
  .mobile-menu-btn {
    top: calc(env(safe-area-inset-top, 0px) + (var(--topbar-h) - 42px) / 2);
    left: var(--gutter);
  }
  /* Every scrolling surface clears the bottom bar. */
  /* Every scrolling surface clears the bottom bar.
     Two things were wrong with this list and both were silent.

     `.disc-body` and `.ap-dash` are WRAPPERS. The elements that actually
     scroll are `.disc-scroll` and `.ap-dash-scroll`, so the rule had never
     applied to the two views it named. Measured in Chromium at 390x844:
     Discover's last row ended 70px below the top of the bar.

     And the ones it did name were losing anyway. Several of these elements
     carry a legacy `padding` SHORTHAND with !important in styles.css, which a
     plain `padding-bottom` cannot beat at any specificity - so Settings,
     Skills and Autopilot kept 60px / 80px / 40px and put their last row under
     the bar. Matching !important is the fix; the alternative is unpicking
     four hardcoded shorthands across two sheets, which is a bigger change
     than this one and risks every desktop width to fix a phone. */
  /* The list is repeated three times on purpose. Plain `.settings-pane` and
     `.composer` both LOST here, and measuring is the only way that shows up:
       body[data-view="settings"] .settings-pane  (0,0,2,1) !important
       body[data-view="chat"] .view-chat .composer (0,0,3,1)
     `:is(LIST):is(LIST):is(LIST)` reads as (0,0,3,0), which clears the first
     on the class column, and !important clears the second outright. Same
     technique feel.css uses, for the same reason: these legacy rules are load
     bearing at other widths and rewriting them to fix a phone would risk
     every desktop layout in the app. */
  :is(.school-shell, .learn-shell, .tasks-body, .hist-list, .mem-feed, .conn-grid,
      .projects-page, .project-page, .settings-pane, .skills-page,
      .admin-page, .voice-page, .disc-scroll, .ap-dash-scroll, .tt-browse, .composer):is(.school-shell,
      .learn-shell, .tasks-body, .hist-list, .mem-feed, .conn-grid,
      .projects-page, .project-page, .settings-pane, .skills-page, .admin-page,
      .voice-page, .disc-scroll, .ap-dash-scroll, .tt-browse, .composer):is(.school-shell,
      .learn-shell, .tasks-body, .hist-list, .mem-feed, .conn-grid,
      .projects-page, .project-page, .settings-pane, .skills-page, .admin-page,
      .voice-page, .disc-scroll, .ap-dash-scroll, .tt-browse, .composer) {
    padding-bottom: var(--mnav-clear) !important;
  }
  /* #tp-root is deliberately NOT in the lists above. `:is()` takes the
     specificity of its most specific argument, so one id in there made each
     :is() worth an ID and the whole selector (0,3,0,0) instead of (0,3,0) -
     nine times heavier than intended, and heavy enough that a later, more
     targeted rule could not refine it. An id needs no help beating the legacy
     shorthands anyway, so it gets its own line. */
  #tp-root { padding-bottom: var(--mnav-clear) !important; }
  .app { padding-bottom: 0; }
}
/* The bar's fill used to be forced here with !important, to beat the legacy
   sheet's hardcoded cream. It is gone on purpose. §22 already wins that fight
   on load order alone (this sheet is after dashboard.css), and an !important
   this late in the file OUTRANKED the reduced-transparency fallback in §9 -
   so a reader who had asked their OS for less transparency was handed
   translucent glass anyway. An accessibility opt-out that a cosmetic rule can
   overrule is not an opt-out. */
.mobile-menu-btn { background: var(--surface-strong); border-color: var(--border-strong); box-shadow: var(--card-shadow); }
.mobile-menu-btn:hover { border-color: var(--accent); box-shadow: var(--card-shadow); }

/* ══════════════════════ 36 · LABEL SPACING FIXES ══════════════════════════
   A few legacy eyebrow treatments still carry terminal-era tracking. */
.conn-section-title, .settings-tab, .settings-tabs-divider,
.dash-section-kicker, .terminal-kicker,
.sidebar-header, .recents-label, .tp-section-title {
  letter-spacing: normal !important;
  font-family: var(--font-sans) !important;
  text-transform: none !important;
}

/* ══════════════════════ 37 · COMPOSER SHORTCUT HINT ═══════════════════════
   Always-visible shortcut bar under the composer - not a toggle button. */
.composer-help { display: none !important; }
.composer-help-btn { display: none !important; }
.composer-hint {
  position: static;
  max-width: 840px;
  margin: 10px auto 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 10px;
  padding: 0;
  border: none;
  background: transparent;
  box-shadow: none;
  color: var(--text-secondary);
  z-index: auto;
}

/* ══════════════════════ 38 · TEST PREP PICKER ═════════════════════════════ */
.ap-group-title {
  font-size: var(--t-card-size);
  font-weight: var(--t-card-w);
  color: var(--text);
  margin-bottom: 10px;
  letter-spacing: normal;
  text-transform: none;
}
details.ap-group { border-top: 1px solid var(--border); padding: 12px 0; }
details.ap-group > summary {
  cursor: pointer;
  list-style: none;
  display: flex; align-items: center; gap: 10px;
  min-height: var(--ctl-h-compact);
  margin-bottom: 0;
}
details.ap-group > summary::-webkit-details-marker { display: none; }
details.ap-group > summary::before {
  content: "›";
  display: inline-block;
  font-family: var(--font-mono);
  color: var(--text-tertiary);
  transition: transform var(--d-standard) var(--ease);
}
details.ap-group[open] > summary::before { transform: rotate(90deg); }
.ap-group-count {
  margin-left: auto;
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-label-size);
  font-weight: 700;
  line-height: 1.2;
  min-width: 1.6em;
  text-align: center;
  color: var(--text);
  background: var(--surface-muted);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  padding: 3px 9px;
}
/* Count chips pick up a light wash of the group accent — keep it quiet. */
.ap-group[data-ap-tone] .ap-group-count {
  color: var(--text-secondary);
  background: rgb(var(--ap-tone) / 0.14);
  border-color: rgb(var(--ap-tone) / 0.32);
}
body[data-theme="dark"] .ap-group[data-ap-tone] .ap-group-count,
html[data-theme="dark"] .ap-group[data-ap-tone] .ap-group-count {
  color: var(--text-secondary);
  background: rgb(var(--ap-tone) / 0.18);
  border-color: rgb(var(--ap-tone) / 0.38);
}
body[data-theme="light"] .ap-group-count {
  color: var(--text-secondary);
  background: #eef1ef;
  border-color: rgba(15, 47, 40, 0.14);
}
body[data-theme="light"] .ap-group[data-ap-tone] .ap-group-count {
  color: #243532;
  background: rgb(var(--ap-tone) / 0.14);
  border-color: rgb(var(--ap-tone) / 0.30);
}
/* A closed <details> hides its own content - unless an author rule says
   otherwise, and styles.css:6298 does: `.tp-pills { display: flex }`. Author
   styles beat the user-agent stylesheet whatever the specificity, so that one
   declaration has been defeating the collapse since the groups shipped. The
   triangle rotated, the group measured 61px tall, and all of its pills went
   on rendering outside it.

   Which is why Test Prep reads as a wall: 111 course pills painted on arrival
   with every group shut. renderTpPicker's comment says "every course stays
   present and reachable - collapsed, not cut"; this is the line that makes
   the collapsed half true. */
details.ap-group:not([open]) > .tp-pills { display: none; }
details.ap-group[open] > .tp-pills { margin-top: 12px; }
.tp-pill {
  min-height: var(--ctl-h);
  border-radius: var(--r-btn);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text);
  font-size: var(--t-small-size);
  font-weight: 550;
  padding: 0 16px;
  /* Never squeeze a pill to fit: the row wraps to a second line instead. */
  flex: 0 0 auto;
  white-space: nowrap;
  transition: border-color var(--d-standard) var(--ease),
              background var(--d-standard) var(--ease),
              transform var(--d-standard) var(--ease),
              color var(--d-standard) var(--ease);
}
.tp-pill:hover { border-color: var(--accent); background: var(--accent-soft); }
/* #tp-pills is itself the flat row of exam pills - it wraps, it never scrolls. */
#tp-pills { display: flex; flex-wrap: wrap; gap: 10px; }
#tp-pills > .tp-empty-note { flex: 1 1 100%; }

/* Category-coded tones: exam family or AP subject group — muted, few hues. */
.tp-pill[data-tp-tone] {
  --tp-tone: 70 105 100;
  border-color: rgb(var(--tp-tone) / 0.32);
  background: rgb(var(--tp-tone) / 0.10);
  color: var(--text);
  font-weight: 600;
  box-shadow: inset 2px 0 0 rgb(var(--tp-tone) / 0.70);
  padding-left: 16px;
}
.tp-pill[data-tp-tone]:hover {
  border-color: rgb(var(--tp-tone) / 0.55);
  background: rgb(var(--tp-tone) / 0.18);
  transform: translateY(-1px);
}
/* Standardized: college battery vs entrance exams. */
.tp-pill[data-tp-tone="college"]   { --tp-tone: 28 98 90; }     /* deep teal */
.tp-pill[data-tp-tone="entrance"]  { --tp-tone: 102 112 128; }  /* cool slate (no green) */
.tp-pill[data-tp-tone="custom"]    { --tp-tone: 70 105 100; }
/* AP: one muted hue per subject family (pills match the group). */
.tp-pill[data-tp-tone="stem"]      { --tp-tone: 40 112 106; }
.tp-pill[data-tp-tone="history"]   { --tp-tone: 138 112 78; }
.tp-pill[data-tp-tone="english"]   { --tp-tone: 126 98 102; }
.tp-pill[data-tp-tone="arts"]      { --tp-tone: 128 108 90; }
.tp-pill[data-tp-tone="language"]  { --tp-tone: 72 114 110; }
.tp-pill[data-tp-tone="portfolio"] { --tp-tone: 104 110 106; }
body[data-theme="light"] .tp-pill[data-tp-tone] {
  background: rgb(var(--tp-tone) / 0.15);
  border-color: rgb(var(--tp-tone) / 0.34);
  color: #1a2e2a;
  box-shadow: inset 2px 0 0 rgb(var(--tp-tone) / 0.80);
}
body[data-theme="light"] .tp-pill[data-tp-tone]:hover {
  background: rgb(var(--tp-tone) / 0.22);
  border-color: rgb(var(--tp-tone) / 0.50);
}

/* AP group titles: thin left accent matching the subject family. */
.ap-group[data-ap-tone] > summary.ap-group-title {
  border-left: 2px solid rgb(var(--ap-tone) / 0.65);
  padding-left: 10px;
}
.ap-group[data-ap-tone="stem"]      { --ap-tone: 45 115 110; }
.ap-group[data-ap-tone="history"]   { --ap-tone: 130 108 78; }
.ap-group[data-ap-tone="english"]   { --ap-tone: 120 95 100; }
.ap-group[data-ap-tone="arts"]      { --ap-tone: 125 105 88; }
.ap-group[data-ap-tone="language"]  { --ap-tone: 70 112 108; }
.ap-group[data-ap-tone="portfolio"] { --ap-tone: 100 108 105; }

/* ══════════════════════ 39 · ASSIGNMENT DETAIL ════════════════════════════ */
.task-detail {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  /* `100%`, not `100vw`: a fixed element is laid out against the
     viewport minus a classic scrollbar, and `100vw` counts the
     scrollbar in - which pushed the whole sheet off the left edge by
     the scrollbar's width on every desktop browser that draws one.
     Identical on iOS, where the scrollbar is an overlay. */
  width: min(460px, 100%);
  z-index: 320;
  display: flex;
  flex-direction: column;
  gap: 18px;
  overflow-y: auto;
  padding: var(--page-pad-y) var(--gutter);
  background: var(--surface-strong);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  animation: alumaPanelSlide var(--d-panel) var(--ease) both;
}
.task-detail.hidden { display: none; }
@keyframes alumaPanelSlide { from { transform: translateX(16px); opacity: 0; } to { transform: none; opacity: 1; } }
.td-head { display: flex; align-items: flex-start; gap: 12px; }
.td-course { font-size: var(--t-label-size); font-weight: var(--t-label-w); color: var(--text-tertiary); }
.td-title {
  font-size: var(--t-section-size); line-height: var(--t-section-lh);
  font-weight: var(--t-section-w); letter-spacing: var(--t-section-tr);
  margin-top: 4px;
  /* A title is whatever the school typed, run-on URLs included. */
  overflow-wrap: anywhere;
}
.td-head > div { min-width: 0; }
.td-course { overflow-wrap: anywhere; }
.td-head .icon-btn { margin-left: auto; flex: none; font-size: 20px; }
.td-facts { display: grid; gap: 8px; }
/* The label is a fixed column and the value is whatever the platform sent, so
   the value gets the floor-free half of the row: it shrinks, it wraps inside
   itself, and when even 180px is not there it takes the next line rather than
   running off the side of the phone. */
.td-facts > div { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: var(--t-small-size); }
.td-facts dt { color: var(--text-tertiary); min-width: 120px; flex: none; }
.td-facts dd {
  color: var(--text); margin: 0;
  flex: 1 1 180px; min-width: 0; overflow-wrap: anywhere;
}
.td-section h3 { font-size: var(--t-card-size); font-weight: var(--t-card-w); margin-bottom: 8px; }
.td-source {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: var(--t-small-size);
  line-height: 1.6;
  color: var(--text-secondary);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-input);
  padding: 14px;
  max-height: 40vh;
  overflow-y: auto;
}
.td-source.is-empty { color: var(--text-tertiary); font-style: italic; }
.td-readonly { margin-top: 8px; font-size: var(--t-label-size); color: var(--text-tertiary); }
.td-file {
  font-size: var(--t-small-size);
  border: 1px solid var(--border);
  border-radius: var(--r-input);
  padding: 8px 12px;
  margin-bottom: 6px;
}
.td-actions { margin-top: auto; display: flex; flex-wrap: wrap; gap: 10px; padding-top: 8px; }
/* `flex: 1` is `flex-basis: 0` - the primary button asked for no width, so
   three actions stayed on one line however narrow it got and compressed each
   other instead of wrapping. A basis of 9rem is the width at which two still
   sit side by side; below that the row wraps, which is what `flex-wrap` was
   put here for. The floor under each button is in fit.css section 3b. */
.td-actions .primary-btn, .td-actions .ghost-btn { flex: 1 1 9rem; }
/* `.ghost-btn { display: inline-flex }` outranks the [hidden] attribute's UA
   `display: none`, so "Open on the platform" stayed on screen for work with no
   link and opened Aluma itself in a new tab. openTaskDetail sets [hidden];
   this makes it mean what it says. */
.td-actions [hidden] { display: none !important; }
/* The Test Prep slot is a plain <span> the app fills with a button. `contents`
   hands that button straight to the row, so it wraps on the same terms as the
   other two rather than riding along inside a box that cannot. */
#td-stp { display: contents; }
#td-stp > * { flex: 1 1 9rem; }
/* "Your doc": Open doc reuses the drawer's material row; the create / link
   buttons wrap on the same 9rem basis as the actions below. */
.td-doc { display: flex; flex-direction: column; gap: 8px; }
.td-doc-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.td-doc-actions .ghost-btn { flex: 1 1 9rem; }
.td-doc-link { display: flex; flex-wrap: wrap; gap: 8px; }
.td-doc-link[hidden] { display: none; }
.td-doc-link input {
  flex: 1 1 12rem; min-width: 0;
  min-height: var(--ctl-h);
  border-radius: var(--r-input);
  border: 1px solid var(--border-strong);
  background: var(--surface-strong);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--t-body-size);
  padding: 10px 14px;
}
.td-doc-foot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.td-doc-note { margin: 0; font-size: var(--t-label-size); color: var(--text-tertiary); }
.td-doc-foot .td-doc-note { flex: 1 1 12rem; }
.td-doc-unlink {
  background: none; border: 0; padding: 4px 0; cursor: pointer;
  font-size: var(--t-label-size); color: var(--text-secondary); text-decoration: underline;
}
.td-doc-unlink:hover { color: var(--text); }
.td-doc-error { margin: 0; font-size: var(--t-label-size); color: var(--danger, #d0413b); }
.td-doc-error[hidden] { display: none; }
@media (max-width: 768px) {
  .task-detail { width: 100%; padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)); }
}

/* ══════════════════════ 40 · MEMORY META ══════════════════════════════════ */
.mem-atom-meta { display: flex; flex-wrap: wrap; gap: 6px 12px; font-size: var(--t-label-size); color: var(--text-tertiary); }
.mem-atom-source { color: var(--text-secondary); font-weight: 550; }

/* ══════════════════ 41 · OFFICIAL BRAND MARK (authoritative) ═══════════════
   Every place the Aluma logo appears renders the one supplied asset,
   /static/brand/logo.png, untouched.

   The rules being overridden here predate the real logo: back when the mark
   was an abstract orb, styles.css spun it (orbSpin, 10s linear infinite),
   glowed it with accent drop-shadows, pop-scaled it on mount and pulsed it on
   hover. Applied to actual brand artwork those read as rotation, recolouring
   and added effects, so they are cancelled sitewide rather than per-screen -
   this sheet loads last, and !important is needed because several of the
   originals are themselves !important.

   Sizing stays with the surrounding container; the mark itself is only ever
   scaled proportionally (object-fit: contain). */
.auth-logo,
.welcome-mark,
.brand-logo img,
.topnav img {
  animation: none !important;
  filter: none !important;
  -webkit-filter: none !important;
  transform: none !important;
  opacity: 1 !important;
  object-fit: contain !important;
  border: none !important;
  border-radius: 0 !important;
  background: none !important;
  box-shadow: none !important;
}
/* The decorative orb ring that used to sit behind the mark. */
.auth-logo::before, .auth-logo::after,
.welcome-mark::before, .welcome-mark::after { content: none !important; }
/* Hover must not re-introduce a glow or a scale. */
.welcome-mark:hover, .auth-logo:hover, .brand-logo img:hover {
  animation: none !important;
  filter: none !important;
  transform: none !important;
}

/* The mark's transparent margin means a box of size N draws only ~N/2 of
   artwork. These slots keep a stable layout footprint - the extra size is
   absorbed by negative margins - so the visible mark reads at roughly the
   height of the brand lockup beside it. Overflow is transparent, so nothing
   collides. */
.brand-logo img {
  width: 90px !important;
  height: 90px !important;
  margin: -24px !important;
}
.welcome-mark {
  width: 112px !important;
  height: 112px !important;
  margin: -22px auto 0 !important;
}
/* Marketing/legal topnav lockup - same transparent-margin compensation. */
.topnav img {
  width: 56px !important;
  height: 56px !important;
  margin: -10px -8px -10px -10px !important;
}

/* ── storage notice ───────────────────────────────────────────────────────
   Informational bar (see index.html for why this is not a consent gate).
   Fixed to the bottom, above nothing else that is interactive, and dismissed
   permanently via localStorage. Colours come from the tokens so it tracks
   both themes; the copy is small but sits on --text-secondary, which clears
   AA on every surface it can land on. */
/* `display: flex` below outranks the [hidden] attribute's UA `display: none`,
   so without this rule the notice ignores both its initial hidden state and
   the dismiss button. */
.storage-notice[hidden] { display: none; }

.storage-notice {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: max(16px, env(safe-area-inset-bottom, 0px));
  z-index: 9000;
  width: min(680px, calc(100vw - 32px));
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px 12px 18px;
  background: var(--surface-strong);
  border: 1px solid var(--border-strong);
  border-radius: 18px;
  box-shadow: var(--shadow-lg);
}
.storage-notice p {
  margin: 0;
  flex: 1;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-secondary);
}
.storage-notice a { color: var(--accent); text-decoration: underline; }
.storage-notice-ok {
  flex: none;
  /* 44px min target - a dismiss control has to be tappable on a phone. */
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 16px;
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.storage-notice-ok:hover { background: var(--accent-hover); }
.storage-notice-ok:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}
@media (max-width: 560px) {
  /* Stack so the copy is not squeezed to two words a line on a phone. */
  .storage-notice { flex-direction: column; align-items: stretch; gap: 10px; padding: 14px; }
  .storage-notice-ok { width: 100%; }
}
/* Never cover the docked sidebar or an open drawer - those are how you
   reach Search and the account card on a short screen. */
@media (min-width: 901px) {
  .storage-notice {
    left: calc(var(--sidebar-w) + (100vw - var(--sidebar-w)) / 2);
    width: min(680px, calc(100vw - var(--sidebar-w) - 32px));
  }
}
@media (max-width: 900px) {
  .storage-notice {
    bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }
  /* One thing at a time. The notice lands on the same corner as the Lumi
     button and wins on z-index, so a tap aimed at the assistant dismissed the
     notice instead. The button waits for the one tap this takes - measured by
     hit-testing every control at 390px, where #lumi-fab came back unreachable
     despite being 56x56. */
  html.has-storage-notice .lumi-fab { display: none; }
  body.nav-open .storage-notice { visibility: hidden; pointer-events: none; }
}

/* ══════════════════════ 42 · FIT ALL DEVICES ════════════════════════════════
   The docked sidebar is taller than an iPad / short laptop once Safari chrome
   and the home indicator are counted. styles.css locks the shell to 100vh and
   html/body to overflow:hidden, so the last nav rows (Search, account) clip
   with no way to scroll them into view.

   Contract:
   - The shell tracks the *visible* viewport (100dvh, 100vh fallback).
   - .sidebar is a bounded column that scrolls as one unit.
   - Brand sticks to the top; Search + account stick to the bottom.
   - Compact chrome (drawer + hamburger + bottom nav) is 900px.
   - Nothing may push the page into a horizontal scroll. */
:root { --bp-compact: 900px; --shell-h: 100vh; }
@supports (height: 100dvh) {
  :root { --shell-h: 100dvh; }
}

html, body {
  height: var(--shell-h);
  max-height: var(--shell-h);
  overflow-x: hidden;
  overflow-y: hidden;
}

.app {
  height: var(--shell-h);
  max-height: var(--shell-h);
  min-height: 0;
  width: 100%;
  max-width: 100%;
  overflow: hidden;
}

.main, .sidebar {
  min-width: 0;
  min-height: 0;
  max-height: 100%;
}

.view {
  height: var(--shell-h);
  max-height: var(--shell-h);
  min-height: 0;
  min-width: 0;
  overflow-x: hidden;
}

/* Sidebar: one scrollport. Nav never shrinks itself into unreadability;
   the column scrolls instead, and the footer stays on screen. */
.sidebar {
  height: 100%;
  max-height: 100%;
  min-height: 0;
  overflow-x: hidden !important;
  overflow-y: auto !important;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding-bottom: max(12px, env(safe-area-inset-bottom, 0px)) !important;
}

.sidebar > .brand {
  position: sticky;
  top: 0;
  z-index: 4;
  flex-shrink: 0;
  background: var(--surface);
}

.sidebar > .nav-section,
.sidebar > .nav-more-rule,
.sidebar > .trial-banner { flex-shrink: 0; }

.sidebar > .sidebar-scroll {
  flex: 1 1 auto;
  min-height: 0;
}

.sidebar > .sidebar-footer {
  position: sticky;
  bottom: 0;
  z-index: 4;
  flex-shrink: 0;
  margin-top: auto;
  background: var(--surface);
  padding-top: 10px;
  box-shadow: 0 -10px 16px -12px rgb(15 47 40 / 0.18);
}

.account-popover {
  max-height: min(70vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Short laptop / iPad landscape: tighten the nav so more of it fits before
   the column has to scroll, without shrinking tap targets below 32px. */
@media (max-height: 800px) {
  .sidebar { gap: 6px !important; padding-top: 10px !important; }
  .sidebar .brand { padding-bottom: 2px !important; }
  .sidebar .nav-item {
    min-height: 34px;
    padding-top: 6px !important;
    padding-bottom: 6px !important;
  }
  .sidebar .credit { display: none; }
}

/* Tablet landscape (sidebar still docked): kill the leftover hamburger
   indent from the 761-1024 "sig-responsive" pass, wrap crowded toolbars,
   and keep cards from overflowing the content column. */
@media (max-width: 1180px) and (min-width: 901px) {
  :root { --sidebar-w: 240px; --gutter: 20px; }
  .app { grid-template-columns: var(--sidebar-w) minmax(0, 1fr); }
  .view > .topbar,
  .topbar { padding-left: var(--gutter); }
  .topbar, .topbar-right, .tasks-filters, .filter-row, .tasks-bulkbar {
    flex-wrap: wrap;
    min-width: 0;
  }
  .topbar-right { gap: 8px; }
  .page-title { min-width: 0; overflow-wrap: anywhere; }
  .task-card, .dash-assignment-card, .hub-assignment, .project-card, .conn-tile {
    min-width: 0;
    max-width: 100%;
  }
}

/* Phones + tablet portrait: drawer already off-canvas; keep the full
   sidebar reachable and clear the home indicator / bottom nav. */
@media (max-width: 900px) {
  .sidebar {
    height: auto;
    max-height: none;
    padding-top: max(14px, env(safe-area-inset-top, 0px)) !important;
  }
  .view > .topbar { padding-right: max(14px, env(safe-area-inset-right, 0px)); }
  .topbar, .topbar-right, .tasks-filters, .filter-row, .tasks-bulkbar,
  .mem-feed-bar, .composer-actions { flex-wrap: wrap; min-width: 0; }
  .task-card .task-actions,
  .dash-assignment-actions { flex-wrap: wrap; }
  .onboard-card, .modal, .palette {
    max-width: calc(100vw - 24px);
    max-height: calc(var(--shell-h) - 24px);
  }
  /* The setup card sits in .auth-overlay, which pads 24px a side (the same
     allowance .auth-card takes). At 24px it ran 24px off a 390px screen and
     clipped its own "Skip this" pill. */
  .onboard-card { max-width: calc(100vw - 48px); }
}

/* Very narrow phones: no horizontal bleed from chips, selects, or titles. */
@media (max-width: 420px) {
  .topbar-right .primary-btn,
  .topbar-right .ghost-btn,
  .topbar-right select { flex: 1 1 auto; min-width: 0; }
  .page-title { font-size: 1.375rem; }
}

/* Shared overflow guards - every surface that can grow wider than its column. */
.topbar, .topbar-left, .topbar-right,
.tasks-filters, .task-card, .task-grid,
.dash-assignment-card, .hub-assignment,
.project-card, .conn-tile, .ap-card,
.skills-grid, .projects-grid { min-width: 0; }

img, video, canvas { max-width: 100%; }

/* Zoom / landscape phone: let the document scroll rather than clip. */
@media (max-height: 460px) {
  html, body { overflow-y: auto; height: auto; max-height: none; }
  .app, .view { height: auto; min-height: var(--shell-h); max-height: none; }
}

/* Connector card actions: equal-size Sync / Assignments / Disconnect.
   Loaded last so it wins over the older flex + shrink-wrapped danger button.
   One or three actions stack full-width so labels never clip; two sit in
   equal columns. */
.view-connectors .conn-tile-actions {
  display: grid !important;
  grid-template-columns: 1fr !important;
  align-items: stretch !important;
  align-self: stretch !important;
  width: 100% !important;
  gap: 8px !important;
  flex-shrink: 0;
}
.view-connectors .conn-tile-actions:has(> :nth-child(2)):not(:has(> :nth-child(3))) {
  grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}
.view-connectors .conn-tile-actions button,
.view-connectors .conn-tile-actions a {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  flex: 1 1 auto !important;
  width: 100% !important;
  min-width: 0 !important;
  min-height: 42px !important;
  height: auto !important;
  box-sizing: border-box !important;
  padding: 8px 12px !important;
  white-space: normal !important;
  overflow: visible !important;
  text-align: center !important;
  line-height: 1.25 !important;
}
.view-connectors .conn-tile-actions button.danger {
  flex: 1 1 auto !important;
}

/* Featured connectors: up to 3 equal cards on a wide pane; auto-fit so
   iPad/phone get fewer, wider cards instead of clipped columns. */
.view-connectors .conn-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)) !important;
}

/* ── Room between the two halves of this page ──────────────────────────────
   Education and Productivity render as two sibling .conn-grid blocks with
   NOTHING between them - no heading, no rule, and `margin: 0` on both. The
   measured gap at iPad width was 0px: the "View all education connectors"
   row ran straight into the Notion and Slack cards underneath it.

   Both stylesheets carry a `.conn-grid + .conn-section-title` separator with
   a 28px margin, a 40px pad and a hairline. It has never fired here, because
   this page renders no .conn-section-title at all - the rule was written for
   a structure that is not on screen. Rather than reinstate headings nobody
   asked for, the separation is put on the thing that IS there: one grid
   following another. */
.view-connectors .conn-grid + .conn-grid { margin-top: 44px; }

/* Only the LAST block on a page has to clear the bottom bar. Connectors has
   two grids and the bar-clearance rule was padding BOTH, so on a phone the
   education grid carried 96px of dead space in the middle of the page before
   the productivity cards even started. Same specificity as that rule
   (0,0,3,0) and later in the file, so this wins the tie. */
.view-connectors .conn-grid:has(+ .conn-grid) { padding-bottom: 0 !important; }

/* And room at the sides. A 20px gutter is right for a phone and mean on a
   tablet, where this page is mostly read - the cards sat 20px off both
   window edges with a sidebar already crowding the left. Scoped to this view
   so the shared --gutter keeps its meaning everywhere else. */
@media (min-width: 901px) {
  .view-connectors .conn-grid,
  .view-connectors .conn-toolbar {
    padding-left: clamp(var(--gutter), 3vw, 44px);
    padding-right: clamp(var(--gutter), 3vw, 44px);
  }
}

/* overflow:hidden on .conn-tile was cutting button labels ("Disconnect"),
   blurbs, and "See all connectors" on desktop. Cards grow with their copy. */
.view-connectors .conn-tile {
  overflow: visible !important;
  min-height: min-content !important;
  height: auto !important;
}
.view-connectors .conn-tile-head,
.view-connectors .conn-tile-info,
.view-connectors .conn-tile-meta,
.view-connectors .conn-tile-actions,
.view-connectors .conn-more-inner {
  overflow: visible !important;
  min-width: 0;
  max-width: 100%;
}
.view-connectors .conn-tile-name,
.view-connectors .conn-tile-blurb,
.view-connectors .conn-tile-meta,
.view-connectors .conn-section-title,
.view-connectors .conn-section-sub,
.view-connectors .conn-more-title,
.view-connectors .conn-more-sub {
  overflow: visible !important;
  overflow-wrap: break-word;
  white-space: normal !important;
  text-overflow: unset !important;
}
.view-connectors .conn-tile-actions button,
.view-connectors .conn-tile-actions a {
  white-space: normal !important;
  overflow: visible !important;
  text-overflow: unset !important;
}

/* ── Settings-row switches ──────────────────────────────────────────────────
   Speak responses, Let Aluma drive a browser and Safe sites only were switches
   in the sidebar's More drawer, where .toggle-btn drew each one as a full-width
   labelled box. Inside a Settings row the label and the explanation are already
   on the left, so the switch is just the switch: no box, no border, no fill.
   The .on / .loading states and the ids are untouched, so the handlers that
   drive these three are the same ones that always drove them. */
.set-row > .toggle-btn.set-inline-toggle {
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
}
.set-row > .toggle-btn.set-inline-toggle:hover,
.set-row > .toggle-btn.set-inline-toggle:hover:not(:disabled) {
  background: none;
  border: 0;
  box-shadow: none;
}
/* A switch at Settings scale - the sidebar drew these at 28x16. */
.set-row > .set-inline-toggle .toggle-switch { width: 40px; height: 24px; }
.set-row > .set-inline-toggle .toggle-knob { width: 18px; height: 18px; top: 3px; left: 3px; }
.set-row > .set-inline-toggle.on .toggle-knob { transform: translateX(16px); }

/* ══════════════════════ 36 · REDUCTION ════════════════════════════════════
   Taking things away, on purpose.

   The sidebar carried four horizontal rules inside one 264px column - under
   the brand, under Home, above "Show all", and over the footer - plus a
   heading for a Recents list that had nothing in it. Four lines to separate
   five things a student can already tell apart by shape and position.

   Two rules survive, and each answers a question:
     .nav-home-rule     - Home is the hub, the rows under it are destinations
     .sidebar-footer    - the scrolling middle ends, you + settings begin

   The brand does not need underlining to read as the brand, and "Show all"
   does not need a line above it to read as a separate control from the row
   above.
   ────────────────────────────────────────────────────────────────────────── */
.sidebar .brand { border-bottom: 0; }
.sidebar .nav-more-rule { display: none; }

/* A card inside a card does not need its own hairline. The parent already said
   where the group starts, so an inner border only draws a box inside a box -
   and Home draws a bordered card holding four more bordered cards holding
   bordered buttons. The fill still separates them; only the line goes.

   Hover keeps its border, so the row still confirms it is a target. */
.dash-module .dash-class-card,
.dash-module .dash-continue-item,
.dash-module .dash-planner-item,
.dash-module .hub-source,
.dash-module .hub-signal,
.dash-module .hub-assignment.dash-assignment-card,
.dash-module .hub-empty-mini.dash-empty { border-color: transparent; }

.dash-module .dash-class-card:hover,
.dash-module .dash-continue-item:hover,
.dash-module .dash-planner-item:hover,
.dash-module .hub-source:hover,
.dash-module .hub-assignment.dash-assignment-card:hover { border-color: var(--dash-border); }

/* Room to breathe between the cards, and inside them. Aluma is read by
   someone with a lot on, on a small screen, in a hurry. */
.dash-grid { gap: var(--s-5); }
.dash-module { padding: var(--s-5); }

/* An empty section is not a section. renderRecentChats() sets [hidden] on the
   whole block - heading included - when there is nothing recent yet. */
.sidebar-section[hidden] { display: none !important; }

/* A lone topbar action must not stretch. Under 420px the topbar lets its
   buttons grow so several of them share the row instead of overflowing -
   Calendar still has five. Home now has one, and that rule turned Sync into a
   full-bleed bar across the top of the phone. :only-child keeps the sharing
   behaviour where there is something to share with. */
.topbar-right > .primary-btn:only-child,
.topbar-right > .ghost-btn:only-child { flex: 0 0 auto; }

/* One height for a page's action row. Assignments held four in one line:
   the two selects 35px, Select 36.6, + Add assignment 44, the bell 34 - and
   every other view mixed at least two of them. A phone already lifts the row
   to 44px (phone.css); a desktop now matches it, the height of the primary
   action. The bell takes the row's 13px type as well: its svg and badge
   size themselves, so that changes nothing it draws. Only a row that HAS such
   actions: in Chat the bell sits beside the 34px Mark done and Mastered, and
   a 44px bell there was the one thing out of line. The streak badge is a
   chip, not an action. */
.topbar-right > :is(select, button:not(.badge)) { min-height: var(--ctl-h); }
.topbar-right:has(> :is(select, .primary-btn, .ghost-btn)) .notif-bell-btn {
  min-height: var(--ctl-h); width: var(--ctl-h); height: var(--ctl-h); font-size: 13px;
}

/* The roomier card padding above is a desktop luxury. On a phone the column is
   already narrow, and the extra inset is what pushes the tutor placeholder
   past the bottom of its two-row box. */
@media (max-width: 699px) {
  .dash-module { padding: var(--s-4); }
  .dash-grid { gap: var(--s-4); }

  /* Under 700px the topbar actions wrap onto their own row below the title.
     Left-aligned they sit under the page title looking like part of it; the
     right edge is where an action belongs and where the thumb already is. */
  .topbar-right { justify-content: flex-end; }

  /* The tutor prompt is the first thing a student reads on Home, and at 390px
     its two-row box cut the example off mid-sentence - scrollable, which is
     not the same as readable. Measured: the placeholder needs 84px, the box
     gave it 61. */
  #dash-tutor-input { min-height: 88px; }
}

/* ══════════════════════ 43 · AN EMPTY CHAT IS ITS STARTERS ════════════════
   The welcome block was pinned toward the top with `margin: 2vh auto auto`,
   and the rule says why: "hero sits toward the TOP". That hero - a logo, a
   headline and a tagline above the starter cards - is gone. What is left is
   three cards, and 2vh left them stranded at the top of a 718px column with
   a 340px void underneath: measured 48px above, 340px below.

   Centred instead, which is what `margin: auto` in the base rule always
   intended. The first-run screen (.welcome-first) is still a tall hero with
   its own intro, groups and demos, so it keeps the pin. */
.welcome:not(.welcome-first) { margin: auto; }

/* ══════════════════════ 44 · THE ONE LINE, WHERE THERE IS ROOM ════════════
   An empty chat is its starters. On a screen with vertical room to spare it
   also gets one line that reads the clock - "Late one, Krish?" at 11pm,
   "Morning, Krish." at nine.

   Hidden by default rather than shown by default, and gated on HEIGHT as much
   as width. Width alone would be the wrong test: the complaint this answers
   is "on an iPad it's too crowded and I have to scroll", and scrolling is a
   height problem. An iPad in landscape is wide and short - it clears a width
   test easily and still has no room. Both conditions must hold.

   `display: none` also takes it out of the accessibility tree on the screens
   where it is not shown, so a phone reader is not read a greeting that is
   not on the page. */
.welcome-line { display: none; }

@media (min-width: 1200px) and (min-height: 800px) {
  .welcome:not(.welcome-first) .welcome-line {
    display: block;
    margin: 0 0 18px;
    font-family: var(--font-display, inherit);
    font-size: 1.375rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.25;
    color: var(--text-secondary);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .welcome-line { animation: fadeUp 0.5s var(--ease-out, ease) both; }
}

/* ══════════════════════ 45 · MASTERY ══════════════════════════════════════
   How close you are, and one tap back to the chat. Two shapes on the page:
   one card at the top and a flat list of topics. Every topic is the same
   shape whatever state it is in - a progress bar reads at a glance in a way
   four kanban columns never did. */
.learn-shell { display: block; }
.view-learn .genome-focus, .view-learn .learn-summary,
.view-learn .learn-inspector { display: none; }

.mx-top {
  display: grid;
  gap: 12px;
  padding: 20px;
  margin-bottom: 20px;
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  background: var(--surface-strong);
}
.mx-top:empty { display: none; }
.mx-top-row { display: flex; align-items: flex-start; gap: 16px; }
.mx-top-copy { display: grid; gap: 3px; min-width: 0; }
.mx-count { font-size: 1.125rem; font-weight: 650; color: var(--text); }
.mx-next {
  font-size: 0.8125rem; color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mx-overall {
  margin-left: auto; font-size: 2rem; font-weight: 700; line-height: 1;
  color: var(--accent); font-variant-numeric: tabular-nums;
}

/* One bar, used at both sizes. The number beside it is the label, so the bar
   carries no text of its own. */
.mx-bar {
  height: 8px; border-radius: var(--r-pill);
  background: var(--bg-elev-3); overflow: hidden;
}
.mx-bar > span {
  display: block; height: 100%; border-radius: var(--r-pill);
  background: var(--accent);
  transition: width 420ms var(--ease-settle, ease);
}
.mx-topic.is-known .mx-bar > span { background: var(--success, var(--accent)); }
.mx-topic.is-due .mx-bar > span { background: var(--warning); }

.mx-go {
  justify-self: start; min-height: var(--tap-min);
  padding: 0 20px; border: 0; border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink, #fff);
  font: inherit; font-size: 0.9375rem; font-weight: 650; cursor: pointer;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mx-go:hover { filter: brightness(1.06); }
.mx-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ── The list ─────────────────────────────────────────────────────────────
   Not a grid. A student reads this top to bottom in priority order, and
   columns would put the thing that needs attention beside the thing that
   does not. */
/* One column. The legacy rule is `repeat(3, minmax(205px, 1fr))` at the same
   specificity, and setting only `display` here left those three columns in
   place - which put the topic that needs attention beside the one that does
   not, and broke the priority order this list is sorted into. */
.learn-board { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
/* `display: grid` on .learn-toolbar beats the `hidden` attribute, so the
   search row for one topic kept rendering after being hidden. */
.learn-toolbar[hidden] { display: none !important; }
.mx-topic {
  display: grid; gap: 10px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  background: var(--surface-strong);
  cursor: pointer;
}
.mx-topic:hover { border-color: var(--border-strong); }
.mx-topic-acts { cursor: default; }
.mx-topic.is-due { border-color: rgb(var(--warning-rgb, 220 150 60) / 0.5); }
.mx-topic-head { display: flex; align-items: flex-start; gap: 12px; }
.mx-topic-name { min-width: 0; display: grid; gap: 2px; }
.mx-topic-name h3 {
  margin: 0; font-size: 0.9375rem; font-weight: 600; line-height: 1.3; color: var(--text);
  overflow-wrap: anywhere;
}
/* Subject and status share one line. Two separate inline elements let the
   pill wrap under a dangling separator. */
.mx-topic-sub {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 0.75rem; color: var(--text-tertiary);
}
.mx-topic-pct {
  margin-left: auto; font-size: 1.125rem; font-weight: 700;
  color: var(--text-secondary); font-variant-numeric: tabular-nums;
}
.mx-topic.is-known .mx-topic-pct { color: var(--accent); }
.mx-pill {
  display: inline-block; padding: 1px 8px; border-radius: var(--r-pill);
  font-size: 0.6875rem; font-weight: 650;
}
.mx-pill.is-due { background: rgb(var(--warning-rgb, 220 150 60) / 0.16); color: var(--warning); }
.mx-pill.is-known { background: rgb(var(--accent-rgb) / 0.14); color: var(--accent); }

.mx-topic-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mx-act {
  min-height: 36px; padding: 0 14px;
  border: 1px solid var(--border-strong); border-radius: var(--r-pill);
  background: none; color: var(--text); font: inherit; font-size: 0.8125rem;
  font-weight: 600; cursor: pointer;
}
.mx-act:hover { border-color: var(--accent); color: var(--accent); }
.mx-act.is-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink, #fff); }
.mx-act.is-primary:hover { filter: brightness(1.06); color: var(--accent-ink, #fff); }
.mx-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The quiet one. Marking something known is a real control, not a headline. */
.mx-got {
  margin-left: auto; width: 36px; height: 36px; flex: 0 0 auto;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: none; color: var(--text-tertiary);
  font-size: 14px; line-height: 1; cursor: pointer;
}
.mx-got:hover { border-color: var(--accent); color: var(--accent); }
.mx-got.is-on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink, #fff); }
.mx-got:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (hover: none) and (pointer: coarse) {
  .mx-act, .mx-got { min-height: var(--tap-min); }
  .mx-got { width: var(--tap-min); height: var(--tap-min); }
}
@media (prefers-reduced-motion: reduce) { .mx-bar > span { transition: none; } }

/* ── Skill progress ──────────────────────────────────────────────────────────
   Board bars carry their colour inline (skillFill in app.js): red at 0, amber
   through the middle, green at 100, so the colour is the number. JS moves the
   width frame by frame when a bar climbs, so the width transition is off. */
.mx-bar.is-skill > span { transition: none; }
/* Known is done: the card steps back - grey, quiet - and sits at the bottom.
   The ✓ keeps its colour, because it is still the control that undoes it. */
.mx-topic { transition: background-color 420ms var(--ease-out, ease), border-color 420ms var(--ease-out, ease); }
.mx-topic.is-known { background: var(--surface-muted); border-color: var(--border); box-shadow: none; }
.mx-topic.is-known :is(.mx-topic-name h3, .mx-topic-pct) {
  color: var(--text-tertiary) !important; transition: color 420ms var(--ease-out, ease);
}
.mx-topic.is-known .mx-bar > span { background: var(--text-tertiary) !important; opacity: 0.5; }
.mx-topic.is-known .mx-pill.is-known { background: var(--bg-elev-3); color: var(--text-tertiary); }
/* Practice is still there for a known topic, but it stops shouting. pop.css
   gives every primary button a brand ledge at (0,5,1) !important, so this
   matches that shape one step up rather than hoping load order wins. */
body[data-theme] .mx-topic.is-known .mx-act.is-primary:not(:disabled) {
  background: none; border-color: var(--border-strong); color: var(--text-secondary);
  box-shadow: 0 2px 0 0 var(--lip-quiet) !important;
}

/* The popup: top centre of the page column, over nothing that matters. */
.mpop {
  position: fixed; z-index: 9990;
  top: calc(env(safe-area-inset-top, 0px) + var(--topbar-h, 56px) + 12px);
  left: 50%;
  width: min(380px, calc(100vw - 32px));
  display: grid; gap: 14px;
  padding: 14px 18px 16px;
  border: 1px solid var(--border-strong); border-radius: var(--r-card, 22px);
  background: var(--surface-strong);
  box-shadow: var(--shadow-lg);
  opacity: 0; transform: translate(-50%, -10px) scale(0.98);
  transition: opacity 260ms var(--ease-out, ease), transform 260ms var(--ease-out, ease);
  cursor: pointer;
}
@media (min-width: 901px) {
  .mpop { left: calc(var(--sidebar-w, 264px) + (100vw - var(--sidebar-w, 264px)) / 2); }
}
.mpop.show { opacity: 1; transform: translate(-50%, 0) scale(1); }
.mpop-skill { display: grid; gap: 8px; }
.mpop-top { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
.mpop-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 0.9375rem; font-weight: 650; color: var(--text);
}
.mpop-pct {
  margin-left: auto; flex: 0 0 auto;
  font-size: 0.9375rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}
.mpop-track { height: 10px; border-radius: 999px; background: var(--bg-elev-3); overflow: hidden; }
.mpop-fill { display: block; height: 100%; width: 0; border-radius: 999px; }
.mpop-skill.is-mastered .mpop-pct { animation: mpop-pop 360ms var(--ease-spring, cubic-bezier(.34,1.56,.64,1)); }
@keyframes mpop-pop { 0% { transform: scale(0.85); } 60% { transform: scale(1.08); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
  .mpop { transition: opacity 120ms linear; transform: translate(-50%, 0); }
  .mpop-skill.is-mastered .mpop-pct { animation: none; }
}

/* An empty tutor session: one question and the student's own starts. */
.tutor-welcome { display: grid; justify-items: center; gap: 10px; text-align: center; }
.tutor-welcome .tw-title { margin: 0; font-size: 1.5rem; font-weight: 650; color: var(--text); }
.tutor-welcome .tw-sub { margin: 0; max-width: 460px; font-size: 0.9375rem; color: var(--text-secondary); }
.tw-chips {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  max-width: 640px; margin-top: 8px;
}
.tw-chip {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 40px; padding: 0 16px;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--surface-strong); color: var(--text);
  font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer;
  max-width: 100%;
}
.tw-chip > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-chip:hover { border-color: var(--accent); color: var(--accent); }
.tw-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tw-chip-ico { display: inline-grid; width: 16px; height: 16px; flex: 0 0 auto; }
.tw-chip-ico svg { width: 16px; height: 16px; }
.tw-chip-skel { display: inline-block; width: 150px; height: 40px; border-radius: 999px; }

/* "I've got this" under the newest answer: the student's own call, in green. */
.msg-suggest-btn.msg-suggest-gotit { border-color: rgb(var(--accent-rgb) / 0.45); color: var(--accent); }

/* The class line over the assignment drawer's title is the way back out to
   that class page - when there is a class to go back out to. */
.td-course.is-link { cursor: pointer; }
.td-course.is-link:hover { color: var(--accent); text-decoration: underline; }
.td-course.is-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ══════════════════ CONNECTORS · TIERS AND BREATHING ROOM ══════════════════
   Two things this page now says out loud:

   1. Three connectors are highlighted and the rest are plain. Canvas wears
      the gold Recommended ribbon; Google Classroom and Blackbaud wear the
      accent Special one. Marking every connector that had an API key put ten
      gold cards on one screen, which is the same as marking none. A platform
      with no student API is still a read-only card that explains itself
      rather than a Connect button with nothing behind it.
   2. Room. The tier ribbon is pinned above each card's top edge, and the
      grid used to start 4px under the filter chips - so every ribbon in the
      first row landed ON the toolbar (and the outer cards sat flush against
      the page edge). The gutter below is deliberate, not decorative. */

/* ── Room between the toolbar, the page edges and the cards ────────────────
   Every card ribbon sits at top:-9px, so a grid that starts flush under the
   chips overlaps them. A section heading always comes first (a section's
   heading and grid hide together), and the 18px under each heading is what
   gives the ribbon somewhere to live; the toolbar itself leaves the same
   --filter-gap as every other filter row. It was 26px plus 6px of padding,
   32px of air, the widest gap in the app. */
.view-connectors .conn-toolbar {
  padding-top: 18px;
  padding-bottom: 0;
  margin-bottom: var(--filter-gap);
}
.view-connectors .conn-section-title {
  padding-top: 8px !important;
  padding-bottom: 18px !important;
  margin-top: 0 !important;
}
/* The first heading follows the toolbar, which already paid for the gap.
   Under the Productivity chip the School heading and grid are hidden, and
   the Productivity heading is the first one. */
.view-connectors .conn-toolbar + .conn-section-title,
.view-connectors .conn-grid.conn-section-hidden + .conn-section-title { padding-top: 0 !important; }
/* The second heading separates two grids - that one keeps its rule + space,
   but only when the grid above it is showing (it put 56px between the
   toolbar and the only heading on the page). */
.view-connectors .conn-grid:not(.conn-section-hidden) + .conn-section-title {
  margin-top: 40px !important;
  padding-top: 36px !important;
}
.view-connectors .conn-grid + .conn-section-title::after {
  left: clamp(var(--gutter, 24px), 3vw, 44px) !important;
  right: clamp(var(--gutter, 24px), 3vw, 44px) !important;
}
/* Side gutters wide enough that a ribbon on an edge card never touches the
   pane border, at every width - the media query below only widens them. */
.view-connectors .conn-grid,
.view-connectors .conn-toolbar {
  padding-left: clamp(var(--gutter, 24px), 3vw, 44px);
  padding-right: clamp(var(--gutter, 24px), 3vw, 44px);
}
/* Enough row gap that a ribbon on row 2 clears the card above it. */
.view-connectors .conn-grid { row-gap: 26px; }
/* Two grids on this page; only the last one clears the bottom bar. */
.view-connectors .conn-grid:has(+ .conn-section-title) { padding-bottom: 0 !important; }

/* ── Tier ribbons ──────────────────────────────────────────────────────────
   Left-aligned and inset so a long label ("Recommended · API key") can't run
   off the right edge of a narrow card the way a right-pinned ribbon did. */
.conn-tile-badge {
  top: -10px; right: auto; left: 16px;
  max-width: calc(100% - 32px);
  overflow: hidden; text-overflow: ellipsis;
}
.conn-tile-badge-special {
  background: var(--surface, var(--bg-elev-2));
  color: var(--accent);
  border: 1px solid rgb(var(--accent-rgb) / 0.45);
}

/* ── No student API: readable, not broken ─────────────────────────────────
   These cards are honest dead ends - Aluma has nothing to call. They stay in
   the grid (a missing card reads as a bug) but never wear a tier ribbon and
   never pretend a Connect button would do something. */
.conn-tile-no-api {
  opacity: 0.72;
  border-style: dashed;
}
.conn-tile-no-api:hover { opacity: 1; border-color: var(--border-strong); }
.conn-tile-no-api .conn-tile-blurb { color: var(--text-dim, #5a6f68); }
.conn-tile-no-api .conn-tile-actions button { font-weight: 600; }

/* ── Method chooser: the two paths, colored like their tier ───────────────── */
.authmethod-opt.am-recommended { border-color: rgba(250, 204, 21, 0.5); }
.authmethod-opt.am-recommended:hover:not(.disabled) { border-color: rgba(250, 204, 21, 0.85); }
.authmethod-opt.am-special { border-color: rgb(var(--accent-rgb) / 0.45); }
.authmethod-opt.am-special:hover:not(.disabled) { border-color: rgb(var(--accent-rgb) / 0.75); }
/* The reader card carries no tier: it is not a way of talking to the vendor's
   API, so a Recommended / Special ribbon would be claiming something about it
   that the tiers do not mean. A neutral border keeps it legible as the third
   option without competing with the two that are ranked. */
.authmethod-opt.am-lumi { border-color: var(--border, rgba(0, 0, 0, 0.14)); }
.authmethod-opt.am-lumi:hover:not(.disabled) { border-color: rgb(var(--accent-rgb) / 0.6); }

/* ── "See all" rows ────────────────────────────────────────────────────────
   No tier badges here. Someone opening this list wants to connect something,
   and every row already says whether it can be: a Connect button, or a reason
   it has none. An "OAUTH" chip in between is a detail about plumbing.

   Dimming follows THAT - whether the row is connectable - not whether it
   carries a ribbon. Keyed off the tier, it faded Schoology, a perfectly
   connectable API-key platform that just isn't on the three-connector
   shortlist, so its Connect button read as disabled next to Classroom's. */
.conn-row[data-connectable="0"] { opacity: 0.66; }
.conn-row[data-connectable="0"]:hover { opacity: 1; }


/* The chooser's tier badge wears its tier's color - it was inheriting the
   accent pill, so "Recommended · API key" came out the same green as the
   OAuth option it is supposed to be distinguished from. */
.authmethod-opt.am-recommended .am-badge.rec {
  color: #78350f;
  border-color: rgba(250, 204, 21, 0.65);
  background: linear-gradient(135deg, #fef9c3, #facc15);
}

/* An unconnectable row's description IS the answer to "why can't I connect
   this?", so it wraps instead of being clamped to one truncated line like the
   one-sentence pitches on the connectable rows. */
.conn-row[data-connectable="0"] .conn-row-desc {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  display: block;
  line-height: 1.45;
}

/* The featured row holds four cards (CONN_FEATURED_LIMIT). The auto-fit
   300px track resolved to three columns on a normal laptop, so the fourth
   card always dropped alone onto a second row next to a half-page of empty
   grid. Wide panes get an explicit four-up row instead; narrower ones keep
   auto-fit, which is what makes this page work on an iPad and a phone. The
   "View all" control still spans the row (grid-column: 1 / -1). */
@media (min-width: 1320px) {
  .view-connectors .conn-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  }
}


/* ══════════════════════ 26 · CLASS GRADEBOOK (Grades tab) ══════════════════
   Overall grade, graded / late / on-time / missing counts, category
   weights, every assignment with its score, and outcome (P/D) feedback. */
/* The rail cards cap their body at 300px and keep pre-wrap whitespace for
   the Instructions text; the gradebook is the whole tab and is markup. */
#project-grades-body, #project-feedback-body { max-height: none; overflow: visible; white-space: normal; }
/* ...and the card around it. Rail cards are capped at the viewport height with
   overflow hidden, which cut the gradebook off below the fold with no way to
   reach the rest. The Grades card grows to its full length and the class page
   itself (.project-page, overflow: auto) scrolls. */
body[data-view="project"] .aside-card.gb-card { max-height: none !important; overflow: visible !important; }
.gb-overall { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; padding: 4px 0 14px; }
.gb-overall-label { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); width: 100%; }
.gb-overall-val { display: flex; align-items: baseline; gap: 10px; }
.gb-overall-val strong { font-size: 34px; line-height: 1; font-variant-numeric: tabular-nums; }
.gb-overall-letter { font-size: 18px; padding: 2px 10px; border-radius: 14px; background: rgb(var(--accent-rgb) / 0.12); }
.gb-overall.grade-a .gb-overall-letter { background: rgb(34 160 108 / 0.14); color: #146b46; }
.gb-overall.grade-c .gb-overall-letter { background: rgb(214 150 30 / 0.16); color: #9a6a0a; }
.gb-overall.grade-low .gb-overall-letter { background: rgb(220 60 60 / 0.14); color: var(--danger, #c53030); }
.gb-overall.no-grade strong { color: var(--text-muted); }
.gb-overall-note { width: 100%; font-size: 12px; color: var(--text-muted); }
.gb-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; margin-bottom: 8px; }
.gb-stat { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--text); cursor: pointer; font: inherit; text-align: left; }
.gb-stat:hover { border-color: rgb(var(--accent-rgb) / 0.5); }
.gb-stat strong { font-size: 20px; font-variant-numeric: tabular-nums; }
.gb-stat span { font-size: 12px; color: var(--text-muted); }
.gb-s-late strong, .gb-s-missing strong { color: var(--danger, #c53030); }
.gb-s-ontime strong { color: #1d8a5c; }
.gb-sub { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 16px; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; }
.gb-section { margin-top: 18px; }
.gb-section-h { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: 8px; }
.gb-groups { display: flex; flex-direction: column; }
.gb-group { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
.gb-gn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-gw { font-size: 12px; color: var(--text-muted); }
.gb-gp { min-width: 56px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.gb-filters { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.gb-filter { padding: 4px 11px; border-radius: 999px; border: 1px solid var(--border); background: transparent; color: var(--text-muted); font: inherit; font-size: 13px; cursor: pointer; }
.gb-filter.active { background: rgb(var(--accent-rgb) / 0.14); border-color: rgb(var(--accent-rgb) / 0.45); color: var(--text); }
.gb-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 6px 12px; padding: 10px 4px; border-bottom: 1px solid var(--border); cursor: pointer; border-radius: 6px; }
.gb-row:hover, .gb-row:focus-visible { background: rgb(var(--accent-rgb) / 0.05); outline: none; }
.gb-row-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-row-meta { font-size: 12px; color: var(--text-muted); }
.gb-row-pills { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.gb-row-score { min-width: 88px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 14px; }
.gb-row-score.grade-a { color: #1d8a5c; }
.gb-row-score.grade-c { color: #9a6a0a; }
.gb-row-score.grade-low { color: var(--danger, #c53030); }
.gb-dash { color: var(--text-muted); font-weight: 400; }
.gb-row-detail { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 4px 16px; font-size: 13px; padding: 4px 0 2px; }
.gb-row-detail[hidden] { display: none; }
.gb-row-detail span { display: block; font-size: 11px; color: var(--text-muted); }
.gb-open { align-self: end; font-size: 13px; }
.gb-pill, .gb-tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: rgb(var(--accent-rgb) / 0.10); color: var(--text); white-space: nowrap; }
.gb-tag { margin-left: 6px; }
.gb-graded { background: rgb(34 160 108 / 0.14); color: #146b46; }
.gb-missing, .gb-late { background: rgb(220 60 60 / 0.13); color: var(--danger, #c53030); }
.gb-ontime { background: rgb(34 160 108 / 0.10); color: #1d8a5c; }
.gb-excused { background: rgb(120 120 140 / 0.14); color: var(--text-muted); }
.gb-submitted { background: rgb(var(--accent-rgb) / 0.14); }
.gb-tag.gb-score { background: rgb(var(--accent-rgb) / 0.10); }
.gb-tag.gb-score.grade-a { background: rgb(34 160 108 / 0.14); color: #146b46; }
.gb-tag.gb-score.grade-c { background: rgb(214 150 30 / 0.16); color: #9a6a0a; }
.gb-tag.gb-score.grade-low { background: rgb(220 60 60 / 0.13); color: var(--danger, #c53030); }
.gb-okey { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; }
.gb-orow { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(0, 1.3fr) auto; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border); }
.gb-otitle { font-size: 14px; }
.gb-ochips { display: flex; flex-wrap: wrap; gap: 4px; }
.gb-ochip { display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 24px; padding: 0 6px; border-radius: 6px; font-weight: 700; font-size: 13px; background: rgb(var(--accent-rgb) / 0.14); color: var(--text); }
.gb-ochip.gb-o-ok { background: #2f9e44; color: #fff; }
.gb-ochip.gb-o-mid { background: #f2b705; color: #fff; }
.gb-ochip.gb-o-low { background: #e03131; color: #fff; }
.gb-onow { font-size: 12px; color: var(--text-muted); }
@media (max-width: 640px) {
  .gb-row { grid-template-columns: minmax(0, 1fr) auto; }
  .gb-row-pills { grid-column: 1 / -1; justify-content: flex-start; grid-row: 2; }
  .gb-orow { grid-template-columns: minmax(0, 1fr); gap: 4px; }
}

/* ══════════════════════ 27 · CLASSES → GRADES (every class at once) ═════════ */
.projects-mode { display: inline-flex; gap: 4px; padding: 3px; border: 1px solid var(--border); border-radius: 999px; margin: 0 0 var(--filter-gap); }
.projects-mode-btn { border: 0; background: transparent; color: var(--text-muted); font: inherit; font-size: 13px; font-weight: 600; padding: 6px 16px; border-radius: 999px; cursor: pointer; }
.projects-mode-btn.active { background: rgb(var(--accent-rgb) / 0.14); color: var(--text); }
.all-grades[hidden], .all-feedback[hidden], .projects-grid[hidden], .projects-mode[hidden], .projects-mode-btn[hidden] { display: none; }
/* Select picks classes to delete, so it leaves with the grid - .ghost-btn's
   display would otherwise keep it on screen over Grades and Feedback. */
#projects-select-btn[hidden] { display: none !important; }
.ag-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 16px; }
.ag-tile { display: flex; flex-direction: column; gap: 2px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; }
.ag-tile strong { font-size: 26px; line-height: 1.1; font-variant-numeric: tabular-nums; }
.ag-tile span { font-size: 12px; color: var(--text-muted); }
.ag-tile.grade-a strong { color: #1d8a5c; }
.ag-tile.grade-c strong { color: #9a6a0a; }
.ag-tile.grade-low strong, .ag-tile.ag-bad strong { color: var(--danger, #c53030); }
.ag-table { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.ag-row { display: grid; grid-template-columns: minmax(0, 2.2fr) minmax(110px, 1fr) repeat(5, minmax(56px, .6fr)); align-items: center; gap: 8px; padding: 12px 16px; border: 0; border-bottom: 1px solid var(--border); border-left: 4px solid transparent; background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
.ag-row:last-child { border-bottom: 0; }
.ag-row:not(.ag-headrow):hover, .ag-row:focus-visible { background: rgb(var(--accent-rgb) / 0.06); outline: none; }
.ag-headrow { cursor: default; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding-top: 10px; padding-bottom: 10px; }
.ag-name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ag-name small { display: block; font-weight: 400; font-size: 12px; color: var(--text-muted); }
.ag-grade { display: inline-flex; align-items: baseline; gap: 6px; font-variant-numeric: tabular-nums; }
.ag-grade strong { font-size: 16px; }
.ag-grade b { font-size: 12px; padding: 1px 7px; border-radius: 10px; background: rgb(var(--accent-rgb) / 0.12); }
.ag-grade.grade-a b { background: rgb(34 160 108 / 0.14); color: #146b46; }
.ag-grade.grade-c b { background: rgb(214 150 30 / 0.16); color: #9a6a0a; }
.ag-grade.grade-low b { background: rgb(220 60 60 / 0.14); color: var(--danger, #c53030); }
.ag-grade i { font-style: normal; font-size: 12px; color: var(--text-muted); }
.ag-grade.no-grade { color: var(--text-muted); }
.ag-n { font-variant-numeric: tabular-nums; }
.ag-n.ag-bad { color: var(--danger, #c53030); font-weight: 700; }
.ag-foot { font-size: 12px; color: var(--text-muted); margin-top: 10px; }
.ag-empty { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 24px; border: 1px dashed var(--border); border-radius: 12px; color: var(--text-muted); }
.ag-empty strong { color: var(--text); font-size: 16px; }
@media (max-width: 760px) {
  .ag-headrow { display: none; }
  .ag-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 6px; }
  .ag-n { grid-column: auto; font-size: 12px; color: var(--text-muted); }
  .ag-n::before { content: attr(data-label) " "; }
  .ag-row .ag-n { display: inline; }
}

/* ══════════════════════ 28 · TEACHER FEEDBACK (Canvas) ═════════════════════
   The comment thread and rubric scores on each piece of work: the class's
   Feedback tab, and every class at once on Classes → Feedback and on the
   Grades view. Comments keep the teacher's own line breaks. */
body[data-view="grades"] .view-grades { display: grid; grid-template-rows: auto 1fr; }
.fb-list { display: flex; flex-direction: column; gap: 12px; }
.fb-item { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; }
.fb-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.fb-head-main { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.fb-class { border: 0; padding: 2px 9px; margin-bottom: 4px; border-radius: 999px; background: rgb(var(--accent-rgb) / 0.12); color: var(--text); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fb-class:hover, .fb-class:focus-visible { background: rgb(var(--accent-rgb) / 0.22); outline: none; }
.fb-title { font-weight: 600; font-size: 15px; overflow-wrap: anywhere; }
.fb-meta { font-size: 12px; color: var(--text-muted); }
.fb-score { flex: none; font-variant-numeric: tabular-nums; font-weight: 600; font-size: 14px; white-space: nowrap; }
.fb-score.grade-a { color: #1d8a5c; }
.fb-score.grade-c { color: #9a6a0a; }
.fb-score.grade-low { color: var(--danger, #c53030); }
.fb-thread { display: flex; flex-direction: column; gap: 8px; }
.fb-comment { padding: 10px 12px; border-radius: 10px; background: rgb(var(--accent-rgb) / 0.06); border-left: 3px solid rgb(var(--accent-rgb) / 0.55); }
.fb-comment.mine { background: transparent; border-left-color: var(--border); }
.fb-who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13px; margin-bottom: 3px; }
.fb-who span { font-size: 12px; color: var(--text-muted); }
.fb-text { font-size: 14px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.fb-attach { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.fb-rubric { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.fb-rubric-h { display: flex; justify-content: space-between; gap: 10px; padding: 8px 12px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); border-bottom: 1px solid var(--border); }
.fb-rubric-total { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); letter-spacing: 0; }
.fb-crit { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(48px, auto); align-items: baseline; gap: 4px 12px; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 14px; }
.fb-crit:last-child { border-bottom: 0; }
.fb-crit-name { min-width: 0; overflow-wrap: anywhere; }
.fb-crit-rating { font-size: 12px; color: var(--text-muted); }
.fb-crit-pts { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.fb-crit-comment { grid-column: 1 / -1; font-size: 13px; color: var(--text-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
.fb-open { align-self: flex-start; }
.fb-n { font-variant-numeric: tabular-nums; color: var(--text-muted); font-size: 12px; }
.fb-filters { margin: 6px 0 12px; }
@media (max-width: 640px) {
  .fb-head { flex-direction: column; gap: 4px; }
  .fb-crit { grid-template-columns: minmax(0, 1fr) auto; }
  .fb-crit-rating { grid-column: 1 / -1; grid-row: 2; }
}
