/* Satoshi (Fontshare) and Source Serif 4 (Google Fonts) — the exact pair
   named in the Planit brand style guide — are loaded via <link
   rel="stylesheet"> tags in each page's <head> (plus preconnect hints),
   not an @import here — an @import at the top of a stylesheet is a known
   load-time trap: it forces the browser to fetch and parse it serially
   before the rest of this file can apply, where a <link> lets the browser
   discover and fetch it in parallel with this file from the very first
   pass over the HTML. */

/* Native cross-document view transitions — every full-page navigation
   between Planit's static pages (index.html -> tasks.html, etc.) gets this
   handled entirely by the browser, replacing the old hand-rolled
   body-opacity fade. No-ops safely in browsers that don't support it yet
   (Chromium 111+ currently) — those just navigate normally.

   The keyframes below replace the browser's flat default crossfade with a
   small "settle into place" motion (fade + gentle scale/vertical drift)
   using the same --ease-spring curve as the rest of the app's hover/glide
   motion, so navigating between pages feels like part of the same alive
   system rather than a generic fade. */
@view-transition {
    navigation: auto;
}

::view-transition-old(root){
    animation: page-transition-out .35s var(--ease-spring) both;
}

::view-transition-new(root){
    animation: page-transition-in .45s var(--ease-spring) both;
}

@keyframes page-transition-out{
    to{ opacity:0; transform:scale(.98) translateY(-10px); }
}

@keyframes page-transition-in{
    from{ opacity:0; transform:scale(1.02) translateY(10px); }
}

:root {

    /* Colors */

    /* =========================
    Brand — exact values from the Planit brand style guide (logo + palette
    + type reference), superseding "Nightfall"'s approximated near-black +
    gold with the five hex values the guide specifies verbatim: 0D0E11,
    1B1C21, 2A2B31, E7E4DB, C8A46E. Everything else below (surface-light,
    hover states, text-secondary/muted, borders) is interpolated between
    those five and re-verified for contrast (contrast.mjs), not eyeballed.
    Names kept stable (hundreds of usages across this file reference them
    by name) even where a name no longer literally matches its color
    (accent-pink is warm terracotta, not pink; accent-cyan is steel-blue,
    not cyan; accent-lime is verdigris, not lime) — repainting via token
    values only, not a rename, keeps this a low-risk, high-leverage
    change. See DESIGN.md for the full direction contract.
    ========================= */

    --primary: #c8a46e;
    --primary-hover: #d1b385;
    --primary-light: #dac19c;
    --accent-pink: #b98868;
    --accent-cyan: #7a93a8;
    --accent-lime: #7fa889;

    /* =========================
    Surfaces — background and the deepest card step (0D0E11, 1B1C21) are
    the guide's exact values; surface-light and surface-hover are
    interpolated one and two steps further up the same ramp. Flat steps,
    not a gradient family — this direction doesn't lean on tonal
    gradients to create depth (see Components).
    ========================= */

    --background: #0d0e11;
    --surface: #1b1c21;
    --surface-light: #222328;
    --surface-hover: #2a2b31;

    /* =========================
    Text — E7E4DB is the guide's exact value. Cool-neutral grays below it,
    each re-derived from the new surfaces rather than reused from
    "Nightfall" — both re-measured against every surface step
    (contrast.mjs, not eyeballed) and clear WCAG's 4.5:1 body-text floor
    against --surface-hover, the most demanding of the four.
    ========================= */

    --text: #e7e4db;
    --text-secondary: #b3b1ab;
    --text-muted: #94938e;

    /* =========================
    Borders — one step above surface-hover, still subtle; no bronze warmth
    to double as an "engraved line" anymore (see Components — the bevel
    itself is retired, not just recolored).
    ========================= */

    --border: #23242a;
    --border-light: #3b3c41;

    /* =========================
    Status Colors — danger unmistakable next to the gold primary; warning
    still shares the primary gold on purpose (a dial doesn't invent a
    second amber for "warning") — unchanged reasoning from the previous
    iteration, just recalculated for the cooler ground.
    ========================= */

    --success: #6fae7f;
    --warning: #c8a46e;
    --danger: #e0685a;
    --info: #7f97a3;
    /* Friends' real-time "in a group focus session" status (2026-08-12) —
       a fixed literal, not the user's own selectable --primary accent
       (:root[data-accent="purple"] already claims that swap), so this
       status still reads as its own distinct thing even for someone whose
       chosen theme accent happens to be purple. */
    --status-session: #9b87c4;

    /* =========================
    Course identity colors — a separate vocabulary from the status colors
    above on purpose. class-dot-warning/-danger used to double as both "this
    is Organic Chemistry's color" and the Legend's fixed "Assignment
    Due"/"High Priority" meanings, so a student whose course happened to get
    warning or danger would see their ordinary class dots read as alerts
    everywhere they appeared. These two exist so course identity never has
    to borrow status meaning, and give the five-token course picker (which
    collided by the 5th course in the app's own seed data) two more
    perceptually distinct, comfortably-spaced hues: teal sits in the gap
    between verdigris and steel, olive in the gap between brass and
    verdigris.
    ========================= */
    --course-teal: #4f8985;
    --course-olive: #83884f;


    /* Shadows — neutral near-black now, not warm-tinted; this direction's
       cards are flat (see Components), so these mostly serve floating
       chrome (the launcher FAB, modals, dropdowns), not the card family. */

    --shadow-small:
        0 4px 10px rgba(0,0,0,.4);

    --shadow-medium:
        0 14px 34px rgba(0,0,0,.5);

    --shadow-large:
    0 28px 64px rgba(0,0,0,.6);

    --shadow-focus:
        0 0 0 4px rgba(200, 164, 110,.32);

    /* Retired along with the engraved-plate card treatment it was built
       for (see Components) — cards now use a single hairline border
       instead. Kept defined at 0 rather than deleted so any selector
       still listing it in a box-shadow value degrades to a no-op instead
       of an invalid empty declaration. */
    --shadow-inset: 0 0 0 transparent;


    /* Border Radius */

    --radius-small: 8px;
    --radius-medium: 12px;
    --radius-large: 16px;

    /* Spacing */

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --space-12: 48px;

    /* Two sidebar-specific values that never matched an existing --space-N
       step exactly (10px, 6px — see .nav-btn/.sidebar-top's own uses of
       these). Originally added 2026-08-16 as a compact-vs-comfortable
       pair when Settings still had a Density toggle; that toggle (and
       the compact values these switched to) is gone — see the removed
       :root[data-density="compact"] block's own comment, a few lines
       below where it used to be. Left as named tokens rather than
       inlined back into .nav-btn/.sidebar-top directly since they're
       still genuinely shared between the two rules. */
    --nav-btn-padding-y: 10px;
    --sidebar-nav-gap: 6px;


    /* Fonts — the exact pair from the Planit brand style guide. Big
       Shoulders (condensed grotesk) and later Inter/Fraunces are both
       retired; Satoshi's own weight axis carries every --font-display role
       (page titles, card titles, numerals, chip values — see the h1-h4
       selector list a few hundred lines down) the same way Inter's did:
       hierarchy from weight/size/tracking, not a second typeface, on every
       one of those roles except the one true "greeting" moment per page
       (see --font-serif below and .hero-title). */
    --font-display: "Satoshi", -apple-system, "Segoe UI", sans-serif;
    --font-body: "Satoshi", -apple-system, "Segoe UI", sans-serif;

    /* The one deliberately rare serif moment — the dashboard's "Good
       evening" greeting and the quote banner beneath it, nowhere else.
       Same logic as the Rarity Rule for gold: a second typeface that
       showed up everywhere would just be a second default, not a
       moment. */
    --font-serif: "Source Serif 4", Georgia, "Iowan Old Style", serif;

    /* Font Sizes — the four here were previously bare numbers (22/24/28/46px)
       repeated across the file, each independently drifting a little from
       whichever near neighbor it was meant to match. Naming them closes the
       one real gap in the old six-step scale (nothing between --text-lg's
       20px and --text-xl's 32px, where three separate real roles already
       lived) and gives --text-2xl — defined below since the start but never
       once referenced — an actual job: the display headline, nudged 46→48
       to land on it instead of sitting 2px off the scale it was already next
       to. */
    /* --text-2xs: added later than the rest — 7 separate micro-label spots
       (nav section dividers, chat timestamps, chart axis labels, badge
       counts...) had all independently landed on exactly 11px with no name
       for it. Same situation the other four were in, one size down. */
    --text-2xs: 11px;      /* micro labels/captions — nav dividers, timestamps, badge counts */
    --text-xs: 13px;
    --text-sm: 15px;
    --text-base: 16px;
    --text-lg: 20px;
    --text-stat: 22px;     /* page-header metric numerals (hero-metric strong) */
    --text-title: 24px;    /* compact display titles — brand wordmark, focus-session heading */
    --text-section: 28px;  /* in-page section titles, and the dashboard hero's larger metric numerals */
    --text-xl: 32px;
    --text-2xl: 48px;      /* the one display headline big enough to earn it */


    /* Font Weights */

    --weight-regular: 400;
    --weight-medium: 500;
    --weight-semibold: 600;
    --weight-bold: 700;
    --weight-extrabold: 800;


    /* Glow */

    --primary-glow: rgba(200, 164, 110, 0.18);


    /* Animation */

    --transition-fast:.15s ease;
    --transition:.25s ease;
    --transition-slow:.4s ease;

    /* Precision-settle easing — the "alive" motion layer, used on top of
       the plain transitions above for anything that should feel present
       rather than clinical (buttons, cards, checkboxes, modals, toasts).
       Was an actual elastic overshoot (cubic-bezier's y >1 = past the
       target, then back — a rubber-ball bounce); tightened to exponential
       deceleration with no overshoot, like an instrument needle settling
       on a reading rather than a toy bouncing. Names kept stable (used by
       name throughout this file) even though neither curve literally
       "bounces" or "springs" anymore. */
    --ease-bounce: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-spring: cubic-bezier(0.19, 1, 0.22, 1);

    /* Duration-only (no baked-in timing function) — pair these with
       --ease-spring/--ease-bounce explicitly. --transition/--transition-slow
       above already bundle "ease", so combining them with a second easing
       function (e.g. "var(--transition-slow) var(--ease-spring)") produces
       two timing-functions in one transition entry, which is invalid CSS
       and silently drops the whole transition list — the exact bug behind
       hover motion that looked instant instead of eased. */
    --dur:.25s;
    --dur-slow:.4s;
    --dur-nav:.6s;


    /* Z-index Scale */

    --z-base:1;
    --z-dropdown:100;
    --z-sticky:300;
    --z-page-overlay:400; /* full-page takeovers like the Study session — above ordinary content, below --z-modal so a real modal (e.g. "End this session?") opened from inside one still wins */
    --z-modal:500;
    --z-toast:700;
    --z-tooltip:900;


    /* Icon Sizes */

    --icon-xs:14px;
    --icon-sm:16px;
    --icon-md:20px;
    --icon-lg:24px;
    --icon-xl:32px;


    /* Widths */

    --sidebar-width:196px;
    --header-height:70px;
    --content-max-width:1700px;

}


/* =========================
   Light Theme
   (dark values above are the default; this block is the only place
   a "Light" appearance choice has to override anything)
========================= */

:root[data-theme="light"]{

    --background:#f5f5f3;
    --surface:#ffffff;
    --surface-light:#fafaf9;
    --surface-hover:#ececea;

    --text:#19191a;
    --text-secondary:#55555a;
    /* Lightest gray that still clears 4.5:1 against --surface-hover (the
       most demanding of the four surface steps) — computed, not eyeballed;
       see the dark theme's --text-muted comment above for the same fix on
       the other side of the palette. */
    --text-muted:#696a6c;

    --border:#e2e1de;
    --border-light:#d0cfcb;

    /* The dark-tuned gold (#c8a46e) drops to ~2:1 against these light
       surfaces — nowhere near readable as foreground text/icons, only
       ever measured against the dark palette it was tuned for. Darkened
       here, same hue family, until it clears 4.5:1 again. */
    --primary:#866533;
    --primary-hover:#a47b3e;
    --primary-light:#bf9556;

}

:root[data-theme="light"] body{

    background:
        radial-gradient(
            circle at top,
            #eae9e5 0%,
            #f5f5f3 40%,
            #fbfbfa 100%
        );

}

:root[data-theme="light"] .header{

    background:rgba(255,255,255,.82);

}

:root[data-theme="light"] .widget,
:root[data-theme="light"] .card,
:root[data-theme="light"] .note-card{

    background:rgba(255,255,255,.9);

}

:root[data-theme="light"] .nova-chat-header,
:root[data-theme="light"] .nova-input-bar{

    background:rgba(255,255,255,.75);

}


/* =========================
   Accent color — Settings > Appearance. Re-tints just the brand color
   (--primary and its two derived shades), independent of the light/dark
   theme above, which never touches these three tokens. Brass is now the
   default (base :root values), so — unlike the old purple-default setup —
   'purple' needs its own explicit override below: it used to be silently
   free (whatever the default was), but a swatch promising purple that
   quietly did nothing once the default changed to brass would be a real
   bug, not a simplification.
========================= */

:root[data-accent="purple"]{
    --primary:#a855f7;
    --primary-hover:#c084fc;
    --primary-light:#d8b4fe;
}

:root[data-accent="blue"]{
    --primary:#3b82f6;
    --primary-hover:#60a5fa;
    --primary-light:#93c5fd;
}

:root[data-accent="green"]{
    --primary:#22c55e;
    --primary-hover:#4ade80;
    --primary-light:#86efac;
}

:root[data-accent="pink"]{
    --primary:#ec4899;
    --primary-hover:#f472b6;
    --primary-light:#f9a8d4;
}

:root[data-accent="orange"]{
    --primary:#f97316;
    --primary-hover:#fb923c;
    --primary-light:#fdba74;
}

:root[data-accent="red"]{
    --primary:#ef4444;
    --primary-hover:#f87171;
    --primary-light:#fca5a5;
}

:root[data-accent="teal"]{
    --primary:#14b8a6;
    --primary-hover:#2dd4bf;
    --primary-light:#5eead4;
}

:root[data-accent="yellow"]{
    --primary:#eab308;
    --primary-hover:#facc15;
    --primary-light:#fde047;
}

:root[data-accent="indigo"]{
    --primary:#6366f1;
    --primary-hover:#818cf8;
    --primary-light:#a5b4fc;
}

/* A fully custom accent — Common.applyAccentColor sets --custom-accent
   inline on <html> from settings.customAccentColor, so this is the only
   accent option without hardcoded hex values in this file. One color for
   all three shades (no hover/light math) is a deliberate simplification. */
:root[data-accent="custom"]{
    --primary:var(--custom-accent, #c8a46e);
    --primary-hover:var(--custom-accent, #c8a46e);
    --primary-light:var(--custom-accent, #c8a46e);
}


/* =========================
   Appearance extras — Settings > Appearance's other creative-control
   knobs. Each is one data-attribute on <html> (see
   Common.applyAppearanceExtras) so picking a combination is just a handful
   of independent CSS overrides, the same pattern as data-theme/data-accent.
========================= */

/* ---- Background style ---- */

/* Same twinkle technique as .hero::before, anchored to .container instead
   of one card — NOT body::before/::after, since those two pseudo-elements
   already belong to the page-loading veil (see that section further down);
   reusing them here would silently fight the veil's own z-index/opacity
   the moment this option is on. .container is already position:relative
   with its own stacking context (z-index:1) and clips to its own bounds
   (overflow:hidden), so z-index:-1 here is scoped to it, not the page. */
:root[data-bg-style="starfield"] .container::before{

    content:"";
    position:absolute;
    inset:0;

    z-index:-1;
    pointer-events:none;

    background-image:
        radial-gradient(1.5px 1.5px at 8% 15%, rgba(255,255,255,.7), transparent),
        radial-gradient(1.5px 1.5px at 22% 70%, rgba(255,255,255,.5), transparent),
        radial-gradient(1px 1px at 38% 30%, rgba(255,255,255,.6), transparent),
        radial-gradient(1.5px 1.5px at 52% 85%, rgba(255,255,255,.4), transparent),
        radial-gradient(1px 1px at 67% 42%, rgba(255,255,255,.65), transparent),
        radial-gradient(1.5px 1.5px at 81% 65%, rgba(255,255,255,.5), transparent),
        radial-gradient(1px 1px at 91% 20%, rgba(255,255,255,.55), transparent),
        radial-gradient(1.5px 1.5px at 12% 92%, rgba(255,255,255,.45), transparent),
        radial-gradient(1px 1px at 45% 55%, rgba(255,255,255,.5), transparent),
        radial-gradient(1.5px 1.5px at 74% 10%, rgba(255,255,255,.6), transparent);

    animation:hero-twinkle 5s ease-in-out infinite;

}

/* Retint only — the option is still called "aurora" in settings/JS, but
   the glow itself is now brass dial-light and blued steel instead of
   purple/cyan/pink, so it reads as instrument light bleeding through a
   panel rather than a nightclub gradient. */
:root[data-bg-style="aurora"] body{

    background-image:
        radial-gradient(ellipse 80% 50% at 20% -10%, rgba(200, 164, 110,.22), transparent),
        radial-gradient(ellipse 70% 50% at 90% 10%, rgba(122, 147, 168,.16), transparent),
        radial-gradient(ellipse 60% 40% at 50% 110%, rgba(127, 168, 137,.12), transparent);
    background-attachment:fixed;

    animation:aurora-shift 18s ease-in-out infinite;

}

@keyframes aurora-shift{
    0%, 100%{ background-position:0% 0%, 0% 0%, 0% 0%; }
    50%{ background-position:5% 8%, -5% -4%, 4% -6%; }
}

/* ---- Card style ---- */

:root[data-card-style="sharp"]{
    --radius-small:2px;
    --radius-medium:4px;
    --radius-large:6px;
}

/* ---- Density: removed, 2026-08-16 direct request — "make the
   comfortable density setting the default. Then, get rid of changing
   that setting entirely. There will only be one density for now." This
   block used to hold :root[data-density="compact"], a scaled-down
   --space-N set (and the two sidebar-only tokens right below) that
   Settings > Appearance's now-removed Density selector switched to.
   Comfortable was already just :root's own base --space-N values with
   nothing layered on top — so removing the compact override and the
   UI that switched to it is the entire change; the base tokens above
   didn't need to move at all. */

.accent-swatches{
    display:flex;
    gap:var(--space-3);
    margin-top:var(--space-4);
}

.accent-swatch{
    position:relative;

    width:36px;
    height:36px;

    border-radius:50%;
    border:2px solid transparent;

    cursor:pointer;

    transition:transform var(--dur) var(--ease-spring), border-color var(--transition);
}

.accent-swatch:hover{
    transform:scale(1.12);
}

.accent-swatch.active{
    border-color:var(--text);
}

.accent-swatch.active::after{
    content:"\2713";

    position:absolute;
    inset:0;

    display:flex;
    align-items:center;
    justify-content:center;

    color:#fff;
    font-size:14px;
    font-weight:var(--weight-bold);
}

/* The custom-color swatch is a <label> wrapping a real <input type="color">
   — same circle footprint as the preset swatches, but painted with a
   conic rainbow gradient by default (or the picked color, once one's
   chosen — see settings.js#initAccentSwatches setting its background
   inline) so it reads as "pick your own" rather than a locked-in preset. */
.accent-swatch-custom{
    display:flex;
    align-items:center;
    justify-content:center;

    background:conic-gradient(from 0deg, #f87171, #facc15, #4ade80, #22d3ee, #818cf8, #f472b6, #f87171);
    overflow:hidden;
}

/* ---- Email/phone verification controls (Settings) ---- */

/* :not([hidden]) — an unconditional display here would defeat the phone
   control's hidden toggle (see settings.js#renderPhoneVerifyControl) the
   same way .nova-launcher-panel's own :not([hidden]) guard exists for:
   author CSS always overrides the UA [hidden]{display:none} default, so a
   bare .verify-control{display:flex} would leave it visible (and its
   button clickable) even while marked hidden. */
.verify-control:not([hidden]){
    display:flex;
    align-items:center;
    gap:var(--space-3);
    flex-shrink:0;
}

.field .verify-control:not([hidden]){
    margin-top:var(--space-2);
}

.accent-swatch-custom input[type="color"]{
    width:150%;
    height:150%;

    border:none;
    padding:0;

    cursor:pointer;

    background:none;
}

/* =========================
   Reduced motion — Settings > Appearance. Shrinks every animation and
   transition in the app to effectively instant (rather than disabling
   them individually per rule) and caps looping decorative animations
   (breathing avatars, pulses) to a single, near-instant pass so they
   settle into their resting frame instead of flickering in a fast loop.
========================= */

body.reduce-motion *,
body.reduce-motion *::before,
body.reduce-motion *::after{
    animation-duration:.001s !important;
    animation-iteration-count:1 !important;
    transition-duration:.001s !important;
    scroll-behavior:auto !important;
}


/* =========================
   Global Reset
========================= */

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

/* --font-display and --font-body currently resolve to the same family
   (Satoshi — see the definitions above), so this rule's job is just
   weight, not a separate typeface: every heading/title/big-number surface
   gets --weight-semibold explicitly rather than inheriting body weight.
   Element selectors cover any un-classed heading; the class list covers
   every named title/number surface across the app. */
h1, h2, h3, h4,
.hero-title,
.page-toolbar-title,
.auth-heading,
.card-title h3,
.hero-metric strong,
.xp-chip-value,
.streak-chip-value,
.settings-section-header h3 {

    font-family: var(--font-display);
    font-weight: var(--weight-semibold);

}


/* =========================
   Page
========================= */

body {
    font-family: var(--font-body);

    background:
        radial-gradient(
            circle at top,
            #1c1d21 0%,
            #111215 38%,
            var(--background) 100%
        );

    color: var(--text);

    height: 100vh;
    overflow: hidden;
}

/* Base link color — there was never a global `a` rule in this file, so any
   bare <a> in template-generated copy (e.g. dashboard.js's "add your first
   class") fell back to the browser default blue. Invisible against the old
   purple-heavy palette; a hard miss against this one. */
a {
    color: var(--primary-light);
}

a:hover {
    color: var(--primary-hover);
}

/* One global focus ring, applied everywhere a more specific component rule
   doesn't already define its own (a handful of inputs did — .search-bar,
   .hero-command-bar, .nova-input, .settings-avatar-clickable, .text-input,
   .select-input). Everything else — every nav link, icon button, and chip
   — was silently falling back to the browser default outline, measured at
   ~1.1-1.2:1 contrast against this app's dark backgrounds (WCAG requires
   3:1): technically present, not actually visible to a keyboard user.
   Reuses --shadow-focus so it matches the ring the few already-covered
   elements use. box-shadow follows whatever border-radius the element
   already has on its own, so this doesn't need to redeclare one. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
    outline: none;
    box-shadow: var(--shadow-focus);
}


/* =========================
   Page loading veil — the app shell itself stays visible underneath (just
   blurred + dimmed) rather than hidden outright, and the veil fades in and
   out via opacity/filter transitions instead of snapping on/off with the
   .app-ready class — so a page load reads as one continuous soft
   transition, not an instant swap. Real data still replaces "Loading…"
   placeholders before .app-ready lands (see js/app.js), the blur+dim just
   means whatever's under it during that window is illegible anyway rather
   than a harsh flash of raw placeholder text. login.html has no .container
   and adds .app-ready to <body> synchronously (it never waits on Store),
   so it's never meaningfully gated by this even without special-casing it
   below.

   Redesigned 2026-08-14, direct request ("too much... more minimal,
   sleek, smooth") — replaces a heavier combination (a full-viewport dark
   scrim + a centered spinning ring, stacked on top of the content's own
   blur+dim) with a single slim gold bar sweeping across the very top
   edge, the same restrained pattern Linear/GitHub/YouTube use for their
   own page transitions. One moving element instead of three simultaneous
   ones (scrim fade, spinner fade, spinner spin) is also a real, not just
   perceived, saving during the exact moment the page is already busiest
   parsing/executing scripts and waiting on auth+Store — one fewer
   continuously-animating layer for the compositor to keep up during that
   window. The scrim's old job (blocking clicks on stale content while
   loading) didn't need its own element: .container below already goes
   pointer-events:none for the same duration.

   Tightened once already, 2026-08-11 (.4s → .2s, 8px blur → 5px) — this
   pass goes further still (.2s → .15s, 5px → 3px blur, brightness .55 →
   .82) now that there's no separate dark scrim underneath doing a second
   round of dimming on top of the content's own filter; on the fast path
   (Store.init()'s own 45s freshness window short-circuits most in-app
   navigation to an effectively-instant resolve — see store.js) this veil
   is barely on screen long enough to be seen at all, so shaving its own
   cost matters more than shaving its duration further. */

/* .header no longer needs its own selector here, 2026-08-14 — it's a
   descendant of .container now (inside .content-column), not a sibling
   bar above it, so blurring .container already blurs .header along
   with everything else in one paint instead of two. */
.container{

    transition:filter .15s ease;

}

/* No blur/dim here any more, 2026-08-16 — direct request: sidebar and
   header should stay fully visible and normal during a page load,
   never blurred, only the actual page content should be hidden-then-
   revealed. .container still gates interaction for the whole page
   while data isn't ready yet (pointer-events:none/user-select:none) —
   that's still correct, a click landing mid-load on the sidebar or
   header is exactly as premature as one landing on page content — it
   just no longer visually dims those two areas along with it. */
body:not(.app-ready) .container{

    pointer-events:none;
    user-select:none;

}

/* The actual "blank until ready" treatment, scoped to .content only —
   .content's own direct children (each page's top-level sections: the
   toolbar, each workspace section, Nova's own chat column) start fully
   invisible and pop straight to visible the instant .app-ready lands,
   no transition of their own here — Common.revealData()'s existing
   fine-grained cascade (.reveal-in, staggered fade+rise on every real
   data element within) is what the student actually sees animate in;
   this is just the outer switch that keeps all of it hidden until that
   cascade is ready to begin, rather than sitting fully rendered (just
   blurred) underneath the veil the whole time. */
body:not(.app-ready) .content > *{

    opacity:0;

}

body.app-ready .content > *{

    opacity:1;

}

/* The slim top-edge sweep — a soft gold bar, one third of the viewport
   wide, gliding left to right and fading at both ends rather than a
   hard-edged block. Unconditional (like the old scrim/spinner) so the
   fade itself is transitioned, not snapped. */
body::before{

    content:"";
    position:fixed;
    top:0;
    left:0;

    z-index:9000;

    width:100%;
    height:3px;

    background:linear-gradient(90deg, transparent, var(--primary), transparent);
    background-size:34% 100%;
    background-repeat:no-repeat;
    background-position:-34% 0;

    opacity:0;
    pointer-events:none;

    transition:opacity .15s ease;

}

body:not(.app-ready)::before{

    opacity:1;

    animation:app-veil-sweep 1.1s ease-in-out infinite;

}

@keyframes app-veil-sweep{

    0%{ background-position:-34% 0; }
    100%{ background-position:134% 0; }

}

/* =========================
   Data reveal — 2026-08-16 direct request: the veil above already
   handles the page-level transition ("there is a minimal animation/
   transition. Good.") but everything *inside* it — stat numbers, cards,
   the sidebar streak/profile, list rows — was appearing all at once the
   instant the veil cleared, one flat unanimated swap. js/modules/
   common.js#revealData applies this class, once per page load, to a
   deliberately broad set of real data containers (see that function's
   own comment for the exact list) with a small incremental delay per
   element so they settle into place in a soft cascade instead of
   popping in together. Same motion language everywhere: a short rise +
   fade, nothing bouncier — "meaningful and minimalistic," not a flourish.
========================= */

@keyframes data-reveal{

    from{ opacity:0; transform:translateY(8px); }
    to{ opacity:1; transform:translateY(0); }

}

.reveal-in{

    /* .4s -> .56s (~40% slower), 2026-08-16 direct request — the page
       now starts genuinely blank (see body:not(.app-ready) .content > *
       above) rather than blurred-but-visible, so the cascade itself is
       what the student watches happen; a little more time to actually
       see it land, per the request. */
    animation:data-reveal .56s cubic-bezier(.16,.8,.3,1) both;

}

@media (prefers-reduced-motion: reduce){

    .reveal-in{

        animation:none;

    }

}


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

.container {
    position: relative;
    z-index: 1;

    display: flex;
    /* No longer minus --header-height, 2026-08-14 — the header isn't a
       page-wide bar above this row anymore (see .content-column below),
       so .sidebar now stretches the full viewport height, direct
       request ("the sidebar should reach all the way to the top of the
       screen"). */
    height: 100vh;

    overflow: hidden;
}

/* New wrapper, 2026-08-14, alongside the header's move out of a
   page-wide bar and into this column: .header now sits above .content
   but only spans the content column's own width, to the right of the
   full-height sidebar, rather than the whole page. */
.content-column{

    display:flex;
    flex-direction:column;

    flex:1;
    min-width:0;
    min-height:0;
    height:100%;

}

.content {
    flex: 1;

    width: 100%;
    max-width: var(--content-max-width);
    margin: 0 auto;

    min-width: 0;

    padding: var(--space-10);

    overflow-y: auto;
}


/* =========================
   Header
========================= */

/* No longer a page-wide bar, 2026-08-14 direct request ("get rid of the
   top bar itself") — this now sits inside .content-column, to the right
   of the full-height sidebar, so it only spans the content area's own
   width. The brand lockup that used to be its first child moved to
   .sidebar-logo below; with two children left (search, header-right),
   the same justify-content:space-between now puts search at this bar's
   own left edge and the profile/notifications cluster at its own right
   edge — still "search bar, user details in top right," just scoped to
   the content column instead of the full page. No longer sticky —
   nothing above it scrolls (.content-column itself doesn't scroll, only
   .content below does), so position:sticky had nothing to stick against
   even before this change. */
.header {

    /* position:relative + z-index, 2026-08-16 — real bug: the
       notifications dropdown (.dropdown-panel, z-index:var(--z-dropdown))
       is a child of .header-icon-btn, and that button gets a hover
       transform (translateY(-2px), below) — a CSS transform always
       creates a new stacking context regardless of z-index, which traps
       the dropdown's own z-index *inside* that small context rather than
       letting it compete directly against page content. .header itself
       had no position/z-index of its own, so with no explicit stacking
       context to anchor to, the whole header (dropdown included) fell
       back to plain DOM-order stacking against its siblings — and lost
       to .dashboard-rail, which at the time was position:sticky (later
       in the DOM) and rendered on top of it. Confirmed via screenshot,
       not assumed: clicking the bell (which leaves the cursor hovering
       it, triggering the transform) showed the dropdown's own bottom
       half hidden behind the Nova rail card. Elevating .header itself to
       the dropdown tier fixes it regardless of whether that inner
       transform is active. Still needed after .dashboard-rail's own
       position:sticky was removed the same day (see that rule's own
       comment) — the transform-stacking-context problem this also fixes
       is unrelated to sticky and applies on every page, not just this
       one competing element on the dashboard. */
    position: relative;
    z-index: var(--z-dropdown);

    display: flex;
    justify-content: space-between;
    align-items: center;

    height: var(--header-height);
    padding: 0 24px;

    background: rgba(43,45,49,.82);

    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);

    border-bottom: 1px solid rgba(255,255,255,.05);

}

/* The brand lockup, blended into the sidebar's own top rather than
   boxed in a separate header bar, 2026-08-14 direct request, replacing
   the old header's .logo/.logo-text (both fully removed — no page's
   markup references either anymore). Same .logo-mark asset the old
   header used, just no tagline: "Focus · Plan · Achieve" is dropped
   entirely per the same request, not hidden responsively like the old
   .logo-text p used to be. Sized down from the old header's 48px mark /
   24px wordmark to fit the sidebar's own 196px width without crowding —
   this is the one place in the sidebar that isn't reusing an existing
   pattern verbatim, because nothing else in the sidebar previously
   needed to fit a two-part brand lockup into this narrow a column. The
   hover flourish (halo + mark lift, both quiet until touched per the
   Rarity Rule) is carried over from the old .logo/.logo:hover .logo-mark
   pair, just rescaled for this smaller mark. */
.sidebar-logo{

    position:relative;
    z-index:0;

    display:flex;
    align-items:center;
    gap:10px;

    margin-bottom:var(--space-2);

    color:inherit;
    text-decoration:none;

    transition:transform var(--transition);

}

/* A tight, true circle centered on just the mark, 2026-08-15 — the
   glow used to be `inset` stretched across .sidebar-logo's whole row
   (icon + "Planit" wordmark), and a fully-rounded border-radius on a
   wide/short box like that renders as a stretched pill, not a glow —
   it read as smeared and off-center rather than a deliberate accent.
   Sized and positioned off the icon alone (34px mark, this circle 18px
   larger all round) so the halo reads as one focused light source
   behind the mark, not a vague haze behind mixed content. */
.sidebar-logo::before{

    content:"";

    position:absolute;
    left:-9px;
    top:50%;

    width:52px;
    height:52px;

    transform:translateY(-50%);

    z-index:-1;

    border-radius:50%;

    background:radial-gradient(circle, rgba(200, 164, 110,.32), transparent 72%);

    opacity:0;

    transition:opacity var(--transition);

}

.sidebar-logo:hover::before{

    opacity:1;

}

.sidebar-logo:hover{

    transform:scale(1.02);

}

.sidebar-logo .logo-mark{

    width:34px;
    height:34px;

}

.sidebar-logo:hover .logo-mark{

    filter:drop-shadow(0 0 10px rgba(200, 164, 110,.35));

}

.sidebar-logo-text{

    color:var(--text);

    font-family:var(--font-serif);
    font-size:var(--text-lg);
    font-weight:500;
    letter-spacing:-.01em;

}

/* The brand mark — the product owner's own rendered artwork
   (assets/logo.png: a "P" crossed by a rotated orbital ellipse with a
   filled planet dot), used directly as of 2026-08-12 rather than the
   inline-SVG recreation this rule used to describe. That recreation
   (geometry solved to match the reference, a themed color-mix to
   reproduce the reference's own two-tone rendering) is retired — see
   Logo & Nova mark in DESIGN.md for why the raster source was preferred
   over keeping the vector one current. object-fit:contain since the
   source file isn't a tight crop (real padding around the mark itself);
   sized a touch larger than the old SVG's 44px to compensate, so the ink
   reads at a comparable visual weight instead of looking smaller for
   having more breathing room baked into the file. */
.logo-mark {

    width: 48px;
    height: 48px;
    flex-shrink: 0;

    object-fit: contain;
    display: block;

    transition:
        transform var(--transition),
        filter var(--transition);

}

.header-nav {

    position: relative;

}

.search-bar {

    width: 360px;

    padding: 12px 18px;

    color: var(--text);

    background: var(--surface-light);

    border: 1px solid transparent;

    border-radius: 999px;

    font-size: var(--text-sm);

    outline: none;

    transition:
        background-color var(--transition),
        border-color var(--transition),
        box-shadow var(--transition),
        width var(--transition-slow);

}

.search-bar:focus {

    width: 420px;

    background: var(--surface);

    border-color: var(--primary);

    box-shadow: var(--shadow-focus);

}

.profile {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

/* Name + role, the same pairing as .sidebar-profile-info's h4/p a few
   hundred lines down — that one already differentiates the two lines
   (semibold display face vs. small muted caption); this header twin was
   missing its own rules entirely and fell back to inheriting one bold
   16px size for both "Guest" and its "Student" caption below it. */
.profile-info strong{
    display: block;

    font-size: var(--text-base);
    font-weight: var(--weight-bold);
}

.profile-info p{
    font-size: var(--text-xs);
}

/* Neutral by default — this is every person's avatar initial in the app
   (the signed-in user's own, plus every friend/leaderboard/suggestion
   avatar via the shared .avatar class), not a signal. Under the Rarity
   Rule, brass marks progress/action/AI/achievement/active-state; "this is
   a person" isn't one of those, so it used to be the single biggest
   source of "gold is everywhere" — every avatar in the app, all the time,
   regardless of who. */
.avatar{

    width:42px;
    height:42px;

    display:flex;

    justify-content:center;
    align-items:center;

    background:var(--surface-hover);

    border-radius:50%;
    overflow:hidden;

    font-weight:700;

    color:var(--text-secondary);

}

.header-right{

    display:flex;

    align-items:center;

    gap:var(--space-4);

}

.header-icon-btn{

    position:relative;

    width:42px;
    height:42px;

    display:flex;

    justify-content:center;
    align-items:center;

    font-size:var(--icon-md);

    background:var(--surface);

    color:var(--text);

    border:1px solid var(--border);

    border-radius:50%;

    cursor:pointer;

    transition:
        background-color var(--transition),
        transform var(--transition),
        border-color var(--transition);

}

.header-icon-btn:hover{

    background:var(--surface-hover);

    border-color:var(--primary);

    transform:translateY(-2px);

}

.xp-chip{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    padding:4px var(--space-3) 4px 4px;

    background:var(--surface);

    border:1px solid var(--border);

    border-radius:999px;

    color:var(--text);
    cursor:pointer;

    transition:
        background-color var(--transition),
        transform var(--transition),
        border-color var(--transition);

}

.xp-chip:hover{

    background:var(--surface-hover);

    border-color:var(--primary);

    transform:translateY(-1px);

}

/* Injected next to .xp-chip by js/modules/common.js#ensureStreakChip — see
   its comment for why this is JS-injected rather than hand-added to every
   header block. Color tiers match friends.js's streakTier() exactly. */
.streak-chip{

    display:flex;
    align-items:center;
    gap:6px;

    padding:6px 12px;

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:999px;

    color:var(--text-secondary);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    cursor:pointer;

    transition:background-color var(--transition), border-color var(--transition), color var(--transition);

}

.streak-chip:hover{

    background:var(--surface-hover);

}

.streak-chip .icon{

    font-size:var(--icon-sm);

}

/* Matches .xp-chip-value's size exactly — these two chips sit side by side
   in the header as the same "stat chip" role, so their numbers read as one
   family. Without this the value here fell through to .streak-chip's own
   15px base font-size instead of the chip-value role's 13px, a size
   mismatch invisible until you look for it. */
.streak-chip-value{

    font-size:var(--text-xs);

}

.streak-chip.streak-warm{

    color:var(--warning);
    border-color:rgba(200, 164, 110,.4);

}

.streak-chip.streak-hot{

    color:var(--danger);
    border-color:rgba(224, 104, 90,.4);

}

/* Shared bump used by both .xp-chip and .streak-chip — see
   gamification.js#renderHeaderChip / common.js#renderStreakChip, which
   only add this when the value actually increased this page visit. */
@keyframes chip-bump{
    0%{ transform:scale(1); }
    40%{ transform:scale(1.1); }
    100%{ transform:scale(1); }
}

.chip-bump{
    animation: chip-bump .5s var(--ease-bounce);
}

.xp-chip-ring{

    position:relative;

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:28px;
    height:28px;

    border-radius:50%;

    background:conic-gradient(var(--primary) calc(var(--progress) * 1%), var(--surface-hover) 0);

}

.xp-chip-ring::before{

    content:"";

    position:absolute;
    inset:3px;

    border-radius:50%;

    background:var(--surface);

}

.xp-chip-level{

    position:relative;
    z-index:1;

    font-size:var(--text-xs);
    font-weight:var(--weight-bold);

}

.xp-chip-value{

    font-size:var(--text-xs);
    white-space:nowrap;

}

.notification-dot{

    position:absolute;

    top:8px;
    right:8px;

    width:8px;
    height:8px;

    border-radius:50%;

    background:var(--danger);

}

.notification-dot::after{

    content:"";

    position:absolute;
    inset:0;

    border-radius:50%;

    background:var(--danger);

    animation:notificationPing 2s ease-out infinite;

}


/* =========================
   Sidebar
========================= */

.sidebar {
    display: flex;
    flex-direction: column;

    width: var(--sidebar-width);
    padding: var(--space-5);
    /* var(--space-3), 2026-08-16 — was a hardcoded 12px (same value as
       --space-3's own comfortable default), so density had no effect on
       the one gap between the nav list and the streak/profile footer.
       Direct feedback: "compact... makes the sidebar less compact." */
    gap: var(--space-3);

    background-color: var(--surface-light);
    /* First layer (painted on top of the rest) is a darkening toward the
       bottom — deepened and started earlier 2026-08-15 (was transparent
       through 55% to rgba(0,0,0,.22); now 40% to .34) so the "this is
       night sky, not just a dark panel" read is clearer, direct request
       ("make the minimalism space theme more apparent, especially closer
       to the bottom"). Star dots brightened the same day (most were
       .07-.12, barely visible against --surface-light) and extended
       further down the sidebar — the old set stopped at y:530, leaving
       the stretch between the nav list and the streak/profile footer
       starless on anything but a short viewport; a few more now reach
       past y:700 so the sky reads as continuing all the way down, not
       thinning out before the horizon glow actually starts. */
    /* Opacity bumped a further notch, 2026-08-16 direct request
       ("increase the presence slightly") — .12-.18 to .17-.24, still the
       same faint dots, just a little more readable against
       --surface-light than the previous pass left them. */
    /* Base darkening deepened again, and a second, faint warm wash added
       near the *top* of the sidebar (behind the logo), same 2026-08-16
       request ("make the space gradient and ambient coloring... more
       apparent and aesthetic"). Previously all the atmosphere lived at
       the bottom (this gradient's own dark end, plus .sidebar-glow's
       warm wash below) — the top of the sidebar was flat --surface-light
       with nothing but stars. A quiet gold radial behind the logo (same
       --primary token as every other warm accent in this file, no new
       hue introduced) bookends it, so the whole panel reads as one
       continuous atmosphere top to bottom rather than dark-at-the-
       bottom-only. */
    /* Fixed px size, not a percentage, 2026-08-16 real bug caught from a
       live screenshot — a percentage-sized radial-gradient here scales
       against .sidebar's own full box, which spans the entire viewport
       height edge to edge, not just the logo area. On a genuinely tall
       window (a common real one, not an edge case) 40% of that full
       height reaches hundreds of pixels down, well into the middle of
       the nav list — exactly the "glow in random places" being
       reported, a diagonal-looking wash crossing behind nav items far
       from the logo it was meant to sit behind. A fixed 220x160px stays
       genuinely small and anchored near the top corner regardless of
       how tall the sidebar itself ends up being. */
    background-image:
        radial-gradient(220px 160px at 15% 0%, rgba(200,164,110,.12), transparent 70%),
        linear-gradient(to bottom, transparent 32%, rgba(0,0,0,.4) 100%),
        radial-gradient(1px 1px at 20px 40px, rgba(255,255,255,.24), transparent),
        radial-gradient(1px 1px at 80px 120px, rgba(255,255,255,.2), transparent),
        radial-gradient(1.5px 1.5px at 140px 60px, rgba(255,255,255,.22), transparent),
        radial-gradient(1px 1px at 40px 200px, rgba(255,255,255,.18), transparent),
        radial-gradient(1.5px 1.5px at 180px 260px, rgba(255,255,255,.22), transparent),
        radial-gradient(1px 1px at 60px 340px, rgba(255,255,255,.2), transparent),
        radial-gradient(1px 1px at 155px 390px, rgba(255,255,255,.18), transparent),
        radial-gradient(1.5px 1.5px at 25px 470px, rgba(255,255,255,.2), transparent),
        radial-gradient(1px 1px at 170px 530px, rgba(255,255,255,.17), transparent),
        radial-gradient(1px 1px at 100px 590px, rgba(255,255,255,.19), transparent),
        radial-gradient(1.5px 1.5px at 45px 650px, rgba(255,255,255,.22), transparent),
        radial-gradient(1px 1px at 165px 700px, rgba(255,255,255,.18), transparent),
        radial-gradient(1px 1px at 90px 750px, rgba(255,255,255,.17), transparent);
    background-repeat:no-repeat;

    overflow-y: auto;
    overflow-x: hidden;
}

.sidebar-top {
    display: flex;
    flex-direction: column;
    /* Tightened from --space-3 (12px) 2026-08-12, direct request — the nav
       list read as too airy at the narrower sidebar width below.
       var(--sidebar-nav-gap), not hardcoded, 2026-08-16 — same density
       fix as .nav-btn's own padding above; see that token's comment at
       :root. */
    gap: var(--sidebar-nav-gap);

    /* flex-grow:0 (not flex:1 as before), 2026-08-15 — this list now
       takes only its own natural height when there's room, rather than
       stretching its own box to fill the sidebar and leaving a wall of
       empty space below the last item. min-height:0 + overflow-y:auto is
       the real fix for the bug this was found alongside: on a real, not
       especially short, browser window (measured failing at 720px tall
       — a completely ordinary laptop viewport, not an edge case), the
       9 nav items plus the streak/profile section below genuinely don't
       both fit, and the old flex:1-on-top layout had no way to express
       "shrink me, not the footer" — the whole .sidebar just overflowed
       its own bottom edge with no visible scroll affordance, silently
       cutting off Resources and the entire profile/Settings row. Now
       *this list* is what scrolls internally (min-height:0 is required
       for a flex child to shrink below its content size at all) when
       space is truly tight, while .sidebar-bottom below always stays
       fully on-screen — direct request ("stop the buttons closer to the
       bottom from being cut off"), solved structurally rather than by
       trimming pixels until today's specific viewport happens to fit. */
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;

    /* position:relative + z-index:1, 2026-08-16 — real bug, confirmed
       from a live screenshot ("the button underneath Progress gets
       hidden behind another element"): .sidebar-bottom below is
       position:relative;z-index:0 (an explicit stacking value, even
       though it's zero), and .sidebar-glow inside it — the horizon
       photo — extends well upward past .sidebar-bottom's own top edge
       (bottom:-24px on a 340px-tall element). This list had no
       position/z-index of its own, so it painted as plain static
       in-flow content — and per CSS stacking rules, *any* explicitly
       z-indexed positioned element (even z-index:0) always paints above
       ordinary static content, regardless of DOM order. Wherever the
       glow's own geometric reach happened to overlap this list's last
       item or two, the photo painted directly on top of it, hiding
       Resources behind the horizon image rather than the two elements
       ever actually competing on equal terms. Elevating this list above
       .sidebar-bottom's own z-index guarantees nav items are never
       covered, regardless of how far the glow's own bleed reaches. */
    position: relative;
    z-index: 1;

    /* Soft fade at the bottom edge only, 2026-08-16 — on a genuinely
       short window (measured down to ~600px tall, an ordinary 1366x768
       laptop with browser chrome) this list's own internal scroll above
       is what keeps Resources/Settings reachable rather than hidden, but
       a plain overflow:auto with no visual cue reads as a layout bug:
       the last item was getting sliced clean in half at a hard edge,
       immediately adjacent to Focus Streak, with nothing suggesting
       "scroll for more." A static mask (not JS/scroll-driven — this
       list is short enough that a permanent, always-on bottom fade is
       simpler and still correct in both states) turns that hard slice
       into a deliberate-looking gradient: clipped, it reads as "more
       below, scroll"; full height, it's a barely-visible soft edge into
       the gap before Focus Streak rather than a flaw. No top fade to
       match — the list always starts scrolled to its own top, where
       Home is already flush and complete, so fading that edge too would
       introduce a clipped look on the one edge that was never actually
       cut off. */
    mask-image: linear-gradient(to bottom, black calc(100% - 16px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 16px), transparent 100%);
}

.sidebar-bottom{
    margin-top:auto;

    /* Never the side that shrinks — see .sidebar-top's comment above for
       why the nav list is the one that scrolls when space is short,
       never this section. */
    flex-shrink:0;

    position:relative;
    z-index:0;

    display:flex;
    flex-direction:column;
    gap:var(--space-4);
}

/* The reference screenshot's ambient glow behind the streak ring and
   profile row — a hand-authored planet-horizon curve (bottom-left
   weighted rim + scattered stars), on-brand with Nova's own ringed-
   planet motif. First pass (2026-08-12) approximated this with a plain
   CSS radial-gradient dome and the owner correctly called it out as not
   matching the reference — a soft blob read nothing like an asymmetric
   lit horizon line. Rebuilt as a real asset instead: authored in SVG
   (gradients + blurred stroke for the rim's glow, same technique as
   Nova's own art), then rasterized via a headless-browser screenshot at
   2x into `assets/sidebar-horizon.png` — no image-generation tool exists
   in this environment, so this is hand-drawn vector art turned into a
   static PNG, not a photo. Purely decorative: aria-hidden, no layout
   impact (position:absolute, sized off .sidebar-bottom which the
   z-index above turns into its own stacking context so this can't paint
   over the streak ring or profile text below it). */
.sidebar-glow{

    position:absolute;
    /* Negative, not 0 — .sidebar-bottom sits inside .sidebar's own
       padding, so left/right:0 only reached that inset content width.
       Bleeding out by var(--space-5) (the padding itself) reaches the
       sidebar's true outer edges instead, and .sidebar's own
       overflow-x:hidden clips it there cleanly — direct request
       ("fit the entire width of the sidebar"). */
    left:calc(-1 * var(--space-5));
    right:calc(-1 * var(--space-5));
    bottom:-24px;

    overflow:hidden;
    pointer-events:none;

    /* "Space gradient" backdrop, 2026-08-14 direct request, painted
       behind the (now half-opacity, see below) photo so it reads as one
       atmospheric scene rather than a faded photo pasted over a flat
       panel. Built from the app's own darkest tokens — --background is
       a hair cooler/deeper than the sidebar's own --surface-light —
       rather than an off-brand blue/purple "space" color, so the depth
       is real but still on-brand. The warm gold wash echoes the photo's
       own glow and is centered where the photo's brightest corner sits,
       so the two layers read as one continuous source of light instead
       of a color mismatch showing through the faded photo. */
    /* Warm wash strengthened .18 -> .23 and the base gradient's dark end
       pushed to pure #000 at the very bottom edge (was --background,
       already the app's deepest token but still a lit charcoal, not
       black) 2026-08-15 — same "more apparent, especially closer to the
       bottom" request as the star field above. */
    /* Faint stars, 2026-08-16 direct request ("near the planet horizon
       and focus streak") — .sidebar's own star field (above) never
       actually reaches here: this element paints its own opaque
       backdrop (the linear-gradient below has no transparent stops) on
       top of it, so that lower star field is fully hidden behind this
       one the moment it starts, no matter how far down it's extended.
       These are new dots local to this element instead, listed ahead of
       the opaque base gradient so they show through, scattered through
       the upper/left sky away from the photo's own bright corona
       (78% 100%) so they read as background sky, not clutter over the
       lit horizon itself. Same faint opacity range as .sidebar's set. */
    /* Star opacity and warm wash both bumped a further notch, 2026-08-16
       — same "increase the presence slightly... more apparent and
       aesthetic" request as .sidebar's own field and top wash above. */
    background:
        radial-gradient(1px 1px at 24px 18px, rgba(255,255,255,.22), transparent),
        radial-gradient(1px 1px at 68px 52px, rgba(255,255,255,.19), transparent),
        radial-gradient(1.5px 1.5px at 120px 30px, rgba(255,255,255,.21), transparent),
        radial-gradient(1px 1px at 150px 78px, rgba(255,255,255,.18), transparent),
        radial-gradient(1px 1px at 40px 96px, rgba(255,255,255,.2), transparent),
        radial-gradient(1.5px 1.5px at 96px 118px, rgba(255,255,255,.19), transparent),
        radial-gradient(1px 1px at 14px 140px, rgba(255,255,255,.18), transparent),
        radial-gradient(1px 1px at 165px 150px, rgba(255,255,255,.16), transparent),
        /* A second, tighter "shine" right at the horizon's own bright
           point, 2026-08-16 direct request ("a very very faint gradient
           or shine of gold/yellow near the planet horizon... for light
           effect and slight ambience") — the wash below it is already
           warm but broad/diffuse across the whole lower-right quadrant;
           this is a small, concentrated glint layered on top of it,
           roughly where the photo's own sunrise point sits, so there's a
           real (if subtle) highlight to catch the eye there instead of
           just ambient warmth spread evenly. Deliberately faint (.14) —
           "very very faint" was explicit. */
        radial-gradient(45% 30% at 76% 88%, rgba(255,214,150,.14), transparent 70%),
        radial-gradient(120% 90% at 78% 100%, rgba(200,164,110,.3), transparent 60%),
        /* transparent (was var(--surface-light)) 2026-08-16 — real bug,
           direct report ("a slight dip/change in color between the top
           and bottom half of the sidebar"). .sidebar's own background
           (above) already darkens continuously top to bottom via its own
           linear-gradient — by the point .sidebar-glow begins (partway
           down, wherever the streak/profile footer starts) that gradient
           has already progressed well past its lightest value. Starting
           this gradient over at a hardcoded var(--surface-light) reset
           the color back to its *lightest* shade right at that boundary,
           then immediately began darkening again on its own separate
           curve — a real, visible jump lighter then darker again, not a
           smooth continuation. transparent lets whatever .sidebar's own
           gradient is already showing at that point carry through
           uninterrupted, so this gradient's own darkening (toward
           var(--background), then #000) reads as a continuation of it
           rather than a reset. */
        linear-gradient(to bottom, transparent 0%, var(--background) 65%, #000 100%);
    /* Required for the star dots above — without it each tiny radial-
       gradient tiles across the whole element instead of rendering as
       one dot, the same reason .sidebar's own star field (above) sets
       this. */
    background-repeat:no-repeat;

}

.sidebar-glow img{

    display:block;
    width:100%;
    /* Grown 180px -> 340px, 2026-08-14, matching the owner's reference
       screenshot directly: the reference's sidebar glow reads as a much
       bigger, bolder presence — more of the photo's own bright corona
       visible, not just a thin band at the very bottom. Taller height
       here also zooms the crop in further (object-fit:cover scales to
       the larger of the two fill ratios, and the container's fixed
       narrow width means height is normally the binding one), which is
       what makes the glow read as large and vivid instead of a thin
       distant sliver. */
    height:340px;

    object-fit:cover;
    object-position:center 38%;

    /* Half opacity, 2026-08-14 direct request ("prettify") — lets the
       space-gradient backdrop above show through and blend with the
       photo instead of sitting as an opaque, fairly literal photograph.
       The photo's own black is a true #000; blended at 50% over the
       gradient backdrop it also reads as far less of a hard rectangle,
       so the top-edge mask fade below is now more insurance than the
       load-bearing fix it originally was. */
    /* Reduced to ~75% of that, 2026-08-16 direct request — .5 * .75
       = .375. */
    opacity:.375;

    /* The photo's own black is a true #000; the sidebar's background is
       var(--surface-light), a lighter charcoal — close enough to read as
       "the same dark" everywhere except a thin seam right at the image's
       own top edge. Fades that one edge into transparency so the glow
       reads as emerging from the sidebar itself, not a pasted rectangle. */
    -webkit-mask-image:linear-gradient(to bottom, transparent, black 90px);
    mask-image:linear-gradient(to bottom, transparent, black 90px);

}

/* ---- Sidebar Focus Streak — replaces the header's old .streak-chip
   (still real, still Store.selectStreakDays(), just relocated and made
   the sidebar's one persistent piece of gamification instead of a small
   header chip). The header keeps .xp-chip; that one's wired into
   Gamification's level-up celebration and wasn't worth breaking just to
   match a reference screenshot pixel-for-pixel. ---- */

.sidebar-streak{

    position:relative;
    z-index:1;

    display:flex;
    flex-direction:column;
    align-items:center;

    padding:var(--space-4) var(--space-3);

    border-top:1px solid var(--border);

    text-align:center;

}

.sidebar-streak-label{

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    color:var(--text-muted);
    text-transform:uppercase;
    letter-spacing:.06em;

    margin-bottom:var(--space-3);

}

.study-ring-md{

    width:72px;
    height:72px;

}

.study-ring-md::before{

    inset:8px;

}

.study-ring-md .study-ring-value{

    font-size:var(--text-lg);
    font-weight:700;
    line-height:1.1;

}

.study-ring-md .study-ring-unit{

    display:block;

    font-size:9px;
    font-weight:600;
    color:var(--text-muted);
    text-transform:uppercase;
    letter-spacing:.04em;

}

.sidebar-streak-caption{

    display:flex;
    /* flex-start, not center, 2026-08-16 — real bug from a live
       screenshot: "Keep it going, {name}." is one anonymous flex item
       alongside the flame icon, and at the sidebar's narrow width a
       longer real name routinely wraps it onto 2-3 lines. align-
       items:center centered the icon against that *entire* multi-line
       block's height, so it sat oddly mid-paragraph instead of next to
       the first line — the standard "icon beside wrapping text" look
       needs the icon anchored to the top, matching where its own line
       actually is. */
    align-items:flex-start;
    gap:5px;

    margin-top:var(--space-3);

    font-size:var(--text-2xs);
    color:var(--text-muted);
    line-height:1.4;

}

.sidebar-streak-caption i{

    flex-shrink:0;

    /* Nudged down to sit on the first line's own baseline now that the
       row aligns to flex-start instead of center — a bare icon glyph's
       vertical metrics don't naturally line up with surrounding text at
       the top of its line box without this. */
    margin-top:2px;

    color:var(--primary);
    font-size:12px;

}

.daily-quote{

    padding:var(--space-3) var(--space-2);

    margin-bottom:var(--space-2);

    border-left:2px solid var(--border);

}

.daily-quote p{

    font-size:var(--text-xs);
    font-style:italic;
    line-height:1.5;

    color:var(--text-muted);

}

.daily-quote span{

    display:block;
    margin-top:4px;

    font-size:var(--text-2xs);
    font-weight:var(--weight-medium);

    color:var(--text-secondary);

}

.sidebar.is-collapsed .daily-quote{
    display:none;
}

.action-btn{
    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3);
    color:var(--text);
    background:var(--surface);
    border:1px solid transparent;
    border-radius:var(--radius-medium);
    text-align:left;
    cursor:pointer;
    transition:
        background-color var(--transition),
        transform var(--transition),
        border-color var(--transition),
        box-shadow var(--transition);
}

.action-btn:hover{
    background:var(--surface-hover);
    border-color:var(--border);
    transform:translateX(6px);
    box-shadow:var(--shadow-small);
}

.nav-btn::before{

    content:"";

    position:absolute;

    left:0;
    top:6px;

    width:4px;
    height:calc(100% - 12px);

    background:var(--primary);

    border-radius:999px;

    opacity:0;

    transition:
        opacity var(--transition);

}

.nav-btn.active::before{

    opacity:1;

}

.nav-btn {

    position: relative;

    display:flex;
    align-items:center;
    gap:var(--space-3);

    /* 10px vertical (was 12px), 2026-08-12 — paired with .sidebar-top's
       tightened gap above so the whole nav list reads more compact.
       var(--nav-btn-padding-y)/var(--space-3), not hardcoded px,
       2026-08-16 — the actual density fix; see that token's own comment
       at :root. Horizontal already matched --space-3 (12px) exactly, a
       risk-free swap on its own; vertical needed a new dedicated token
       since 10px never matched an existing --space-N step. */
    padding: var(--nav-btn-padding-y) var(--space-3);

    color: var(--text);

    background: transparent;

    font-size: var(--text-base);

    text-align: left;
    text-decoration: none;

    border: none;

    border-radius: var(--radius-medium);

    cursor: pointer;

    overflow: hidden;

    transition:
        background-color var(--transition),
        color var(--transition),
        transform var(--dur-nav) var(--ease-spring);

}

.icon{

    font-size:var(--icon-md);

    flex-shrink:0;

    display:inline-block;

}

.nav-btn:hover {
    background-color: var(--surface-hover);
    transform: translateX(6px);
}

.nav-btn.active{

    background:rgba(200, 164, 110,.15);

    color:var(--primary);

    font-weight:var(--weight-semibold);

}

.nav-section-label{

    margin:var(--space-2) 0 2px 12px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.sidebar-title {
    margin-top: 8px;
    margin-bottom: 6px;

    color: var(--text-muted);

    font-size: var(--text-xs);
    font-weight: var(--weight-bold);

    text-transform: uppercase;
    letter-spacing: 1px;
}

.sidebar-group-header{

    display:flex;

    justify-content:space-between;

    align-items:center;

}

.collapse-btn{

    display:flex;
    align-items:center;
    justify-content:center;

    width:24px;
    height:24px;

    font-size:var(--icon-xs);

    background:transparent;

    border:none;

    color:var(--text-muted);

    cursor:pointer;

    border-radius:50%;

    transition:
        background-color var(--transition),
        color var(--transition);

}

.collapse-btn:hover{

    background:var(--surface-hover);

    color:var(--text);

}

.sidebar-divider {
    height: 1px;

    margin: 12px 0;

    background-color: var(--border);
}

/* Reused/rebuilt for the reference screenshot's bottom-of-sidebar identity
   card (2026-08-12) — this class existed before but was fully dead (no
   page's HTML referenced it) and assumed different markup (h4/p instead
   of the header's own .avatar/.profile-info pieces, which this now reuses
   directly so the two never drift). Sits above .sidebar-glow via z-index
   so its text stays crisp over the glow. */
.sidebar-profile{

    position:relative;
    z-index:1;

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3);

    border-radius:var(--radius-medium);

    color:var(--text);
    text-decoration:none;

    cursor:pointer;

    transition:background-color var(--transition);

}

.sidebar-profile:hover{

    background:var(--surface-hover);

}

.sidebar-profile.active{

    background:rgba(200, 164, 110,.15);

}

.sidebar-profile .profile-info{

    min-width:0;
    flex:1;

}

/* flex-shrink:0, matching the chevron's own rule below — the avatar has
   no explicit basis otherwise, so under any squeeze it could compete
   with .profile-info's own flex:1 for space instead of staying a fixed
   42px circle, tightening the name's own available width more than it
   needs to for what should be a short, comfortable name like "Ethan". */
.sidebar-profile .avatar{

    flex-shrink:0;

}

.sidebar-profile .profile-info strong{

    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;

}

.sidebar-profile > .icon{

    flex-shrink:0;
    font-size:var(--icon-sm);
    color:var(--text-muted);

}


/* =========================
   Sidebar — Semester & Classes
========================= */

.semester-card{

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

    padding:var(--space-4);

    background:var(--surface);

    border:1px solid var(--border);

    border-radius:var(--radius-medium);

}

.semester-row{

    display:flex;
    justify-content:space-between;
    align-items:center;

}

.semester-name{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.semester-caption{

    color:var(--text-muted);

    font-size:var(--text-xs);

}

.pinned-classes{

    display:flex;
    flex-direction:column;
    gap:var(--space-1);

}

.pinned-class{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-2) var(--space-3);

    background:transparent;
    border:none;

    border-radius:var(--radius-medium);

    color:var(--text);
    text-align:left;
    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        transform var(--transition-fast);

}

.pinned-class:hover{

    background:var(--surface);

    transform:translateX(4px);

}

.pinned-class h4{

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.pinned-class p{

    margin-top:1px;

    font-size:var(--text-xs);
    color:var(--text-muted);

}

.class-dot{

    width:8px;
    height:8px;

    border-radius:50%;

    flex-shrink:0;

}

.class-dot-blurple{ background:var(--primary); }
.class-dot-info{ background:var(--info); }
.class-dot-success{ background:var(--success); }
.class-dot-warning{ background:var(--warning); }
.class-dot-danger{ background:var(--danger); }
.class-dot-teal{ background:var(--course-teal); }
.class-dot-olive{ background:var(--course-olive); }


/* =========================
   .classes-table — Classes' own card grid (2026-08-15, see DESIGN.md)
   replaced the table this styled, but Progress's own grades table
   (progress.html) reuses these same base cell/row rules, so the class
   name and its generic th/td/tr styling stay — only the classes.html-
   specific wrapper/cell classes that had zero remaining references
   (the scrollable card shell, the course-name-plus-dot cell, the actions
   column) were removed alongside that page's own table markup.
========================= */

.classes-table{

    width:100%;

    border-collapse:collapse;

}

.classes-table th{

    padding:14px var(--space-5);

    background:var(--surface-hover);
    border-bottom:1px solid var(--border);

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);

    text-align:left;
    text-transform:uppercase;
    letter-spacing:.5px;
    white-space:nowrap;

}

.classes-table td{

    padding:14px var(--space-5);

    border-bottom:1px solid var(--border);

    color:var(--text);

    font-size:var(--text-sm);
    white-space:nowrap;

    vertical-align:middle;

}

.classes-table tbody tr:last-child td{

    border-bottom:none;

}

.classes-table tbody tr{

    transition:background-color var(--transition-fast);

}

.classes-table tbody tr:hover{

    background:var(--surface-hover);

}


/* =========================
   Classes — card grid, 2026-08-15 visual overhaul against an owner-
   supplied reference (see DESIGN.md's Classes section for the full
   rebuild story: what's newly real data vs. reused, and why).
========================= */

/* A plain, non-interactive label — the reference showed a term-switcher
   dropdown, but this app only ever tracks one current semester's worth
   of courses (no term history to switch between), so this reads as a
   real, honest fact (Store's own semester.termName, the same field
   Calendar/Dashboard already surface) rather than implying a feature
   that isn't there. */
.term-pill{

    display:inline-flex;
    align-items:center;
    gap:8px;

    padding:10px 16px;

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:999px;

    color:var(--text-secondary);
    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.term-pill i{

    color:var(--primary);
    font-size:16px;

}

/* The hero — real per-course stats on the left, the same real horizon
   photo Calendar/Nova/Friends already use behind Nova on the right,
   here with no Nova figure (nothing on this page calls for her
   presence, just the ambient "this app lives in one visual world"
   glow). Bottom-right-anchored + left-edge-masked, same "bleed past the
   card edge, fade the seam" technique those other heroes established. */
.classes-hero{

    position:relative;

    display:flex;
    align-items:center;

    min-height:180px;
    padding:var(--space-6);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

    overflow:hidden;

    margin-bottom:var(--space-6);

}

.classes-hero-glow{

    position:absolute;
    top:0;
    right:0;
    bottom:0;

    width:56%;
    height:100%;

    object-fit:cover;
    object-position:60% 45%;

    /* Fades the photo's own left edge into the card rather than a hard
       seam — the same job .sidebar-glow img's mask does for its own top
       edge. */
    -webkit-mask-image:linear-gradient(to right, transparent, black 35%);
    mask-image:linear-gradient(to right, transparent, black 35%);

    opacity:.9;

    pointer-events:none;

}

.classes-hero-stats{

    position:relative;
    z-index:1;

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

    max-width:60%;

}

.classes-hero-label{

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.classes-hero-tiles{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-3);

}

.classes-hero-tile{

    display:flex;
    align-items:center;
    gap:10px;

    padding:var(--space-3) var(--space-4);

    background:var(--surface-light);
    border:1px solid var(--border);
    border-radius:var(--radius-medium);

}

.classes-hero-tile i{

    color:var(--primary);
    font-size:18px;

}

.classes-hero-tile strong{

    display:block;

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-extrabold);
    letter-spacing:-.3px;

}

.classes-hero-tile span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

/* Filter row — real client-side search/status/day/sort (see
   js/modules/classes.js#applyFilters), not decorative controls. */
.classes-filter-row{

    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-4);

    margin-bottom:var(--space-6);

}

.classes-search{

    position:relative;

    flex:1;
    min-width:220px;
    max-width:340px;

}

.classes-search i{

    position:absolute;
    top:50%;
    left:14px;
    transform:translateY(-50%);

    color:var(--text-muted);
    font-size:16px;

    pointer-events:none;

}

.classes-search input{

    width:100%;

    padding:10px 14px 10px 40px;

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:999px;

    color:var(--text);
    font-size:var(--text-sm);

    outline:none;

    transition:border-color var(--transition-fast);

}

.classes-search input:focus{

    border-color:var(--primary);

}

.classes-filter-controls{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-3);

}

.classes-filter-controls .select-input{

    min-width:140px;

}

/* The grid itself — responsive 3-up, matching the reference's desktop
   density without a hardcoded column count (auto-fit reflows to 2 or 1
   on narrower viewports the same way .supporting-grid already does
   elsewhere in this app). */
.classes-grid{

    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
    gap:var(--space-5);

    margin-bottom:var(--space-6);

}

.class-card{

    display:flex;
    flex-direction:column;

    padding:var(--space-5);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

    transition:border-color var(--transition-fast);

}

.class-card:hover{

    border-color:rgba(200, 164, 110,.3);

}

.class-card-top{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);

    margin-bottom:10px;

}

.class-card-code{

    display:flex;
    align-items:center;
    gap:8px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

/* Computed, never hand-set (see js/modules/classes.js#computeCourseStatus)
   — four real states derived from actual assignment completion + due
   dates, not a status the student has to remember to update. Reuses the
   app's existing --success/--danger tokens (the Rarity Rule's own
   status-color pair) rather than inventing new ones; 'not-started' and
   the "nothing tracked yet" case both stay neutral on purpose — an
   empty course isn't a problem, so it doesn't get an alarm color. */
.class-status-pill{

    flex-shrink:0;

    padding:4px 10px;

    border-radius:999px;

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.02em;

}

.class-status-pill.status-neutral{

    background:var(--surface-hover);
    color:var(--text-secondary);

}

.class-status-pill.status-success{

    background:rgba(111, 174, 127,.14);
    color:var(--success);

}

.class-status-pill.status-danger{

    background:rgba(224, 104, 90,.14);
    color:var(--danger);

}

.class-status-pill.status-info{

    background:rgba(200, 164, 110,.14);
    color:var(--primary-light);

}

.class-card-title{

    margin-bottom:10px;

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-bold);
    letter-spacing:-.2px;
    line-height:1.3;

}

.class-card-meta{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-4);

    margin-bottom:4px;

}

.class-card-meta span{

    display:flex;
    align-items:center;
    gap:6px;

    color:var(--text-secondary);
    font-size:var(--text-xs);

}

.class-card-meta i{

    color:var(--text-muted);
    font-size:14px;

}

.class-card-location{

    display:flex;
    align-items:center;
    gap:6px;

    margin-bottom:var(--space-4);

    color:var(--text-secondary);
    font-size:var(--text-xs);

}

.class-card-location i{

    color:var(--text-muted);
    font-size:14px;

}

.class-card-progress{

    margin-bottom:var(--space-4);

}

.class-card-progress-label{

    display:flex;
    align-items:center;
    justify-content:space-between;

    margin-bottom:8px;

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.class-card-progress-label span:first-child{

    color:var(--text-muted);

}

.class-card-progress-label span:last-child{

    color:var(--text);

}

.class-card-progress .progress-bar{

    margin-bottom:6px;

}

.class-card-progress-caption{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.class-card-next{

    display:flex;
    align-items:center;
    gap:10px;

    padding:10px 12px;
    margin-top:auto;
    margin-bottom:var(--space-4);

    background:var(--surface-light);
    border-radius:var(--radius-medium);

    color:inherit;
    text-decoration:none;

    transition:background-color var(--transition-fast);

}

a.class-card-next:hover{

    background:var(--surface-hover);

}

.class-card-next > i:first-child{

    flex-shrink:0;

    color:var(--primary);
    font-size:16px;

}

.class-card-next-body{

    display:flex;
    flex-direction:column;
    gap:1px;

    min-width:0;
    flex:1;

}

.class-card-next-label{

    color:var(--text-muted);

    font-size:10px;
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.class-card-next-body strong{

    overflow:hidden;

    color:var(--text);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    white-space:nowrap;
    text-overflow:ellipsis;

}

.class-card-next-body .text-muted{

    font-size:var(--text-2xs);

}

.class-card-next > i:last-child{

    flex-shrink:0;

    color:var(--text-muted);
    font-size:16px;

}

.class-card-next-empty{

    color:var(--text-muted);
    font-size:var(--text-xs);

}

.class-card-next-empty i{

    color:var(--success);
    font-size:16px;

}

.class-card-footer{

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding-top:var(--space-3);

    border-top:1px solid var(--border);

}

/* A real toggle on the same course.pinned field Study's own Subjects
   list already reads (see js/modules/classes.js) — not decorative. */
.class-card-pin{

    display:flex;
    align-items:center;
    justify-content:center;

    width:32px;
    height:32px;

    background:transparent;
    border:none;
    border-radius:50%;

    color:var(--text-muted);
    font-size:16px;

    cursor:pointer;

    transition:
        color var(--transition-fast),
        background-color var(--transition-fast);

}

.class-card-pin:hover{

    background:var(--surface-hover);
    color:var(--text-secondary);

}

.class-card-pin.is-pinned{

    color:var(--primary);

}

/* Nova Insight — real, computed from the same per-course assignment
   progress as the cards (js/modules/classes.js#renderInsight), styled
   as its own full-width bar rather than another card in the grid, same
   "the AI voice gets a distinct surface" pattern the dashboard/calendar
   rail's own Nova Insight cards already use. */
.classes-insight{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-5);

    background:rgba(200, 164, 110,.06);
    border:1px solid rgba(200, 164, 110,.2);
    border-radius:var(--radius-large);

}

.classes-insight-icon{

    flex-shrink:0;

    width:40px;
    height:40px;

    border-radius:50%;
    overflow:hidden;

}

.classes-insight-icon img{

    display:block;
    width:100%;
    height:100%;

    object-fit:cover;

}

.classes-insight-body{

    flex:1;
    min-width:0;

}

.classes-insight-label{

    display:flex;
    align-items:center;
    gap:6px;

    margin-bottom:4px;

    color:var(--primary-light);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.classes-insight-body p{

    color:var(--text);
    font-size:var(--text-sm);

}

.classes-insight-link{

    flex-shrink:0;

    display:flex;
    align-items:center;
    gap:6px;

    color:var(--primary-light);

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);
    text-decoration:none;

    transition:gap var(--transition-fast);

}

.classes-insight-link:hover{

    gap:9px;

}

@media (max-width:900px){

    .classes-hero{

        flex-direction:column;
        align-items:stretch;

        min-height:0;

    }

    .classes-hero-glow{

        position:relative;
        width:100%;
        height:120px;

        order:-1;
        margin:calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-5);

        -webkit-mask-image:linear-gradient(to bottom, transparent, black 40%);
        mask-image:linear-gradient(to bottom, transparent, black 40%);

    }

    .classes-hero-stats{

        max-width:100%;

    }

}


/* =========================
   Section Heading
========================= */

.section-heading {

    display: flex;
    justify-content: space-between;
    align-items: center;

    margin-bottom: var(--space-6);

}

.section-heading-actions{

    display:flex;
    align-items:center;
    gap:var(--space-4);

}

.section-heading h2 {
    font-size: var(--text-section);
    font-weight: var(--weight-bold);

    margin-bottom: var(--space-2);
}

.section-heading p {
    color: var(--text-secondary);

    font-size: var(--text-base);
}

.section-heading.muted{
    margin-bottom: var(--space-4);
}

.section-heading.muted h2{
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    color: var(--text-secondary);

    margin-bottom: 2px;
}

.section-heading.muted p{
    font-size: var(--text-sm);
    color: var(--text-muted);
}


/* =========================
   Hero — .hero-eyebrow/.hero-metric were originally shared between a boxed
   .hero panel (index.html) and the plainer .page-toolbar header (every
   other page). The Dashboard rebuild moved the greeting out onto the bare
   page background (see .dash-greeting-row/.dash-eyebrow below), and no
   other page ever used the boxed version, so .hero/.hero-grand/.hero-content
   and their hero-rise entrance animation were retired here rather than
   re-tuned to the new palette — confirmed dead via a grep sweep across
   every .html and .js file before deleting. .hero-eyebrow/.hero-metric
   live on, now solely as .page-toolbar's header styling (Calendar, Classes,
   Friends, Settings, Study — pending their own rebuilds).
========================= */

/* Tuned for .page-toolbar's fixed-dark gradient background (below), always
   dark regardless of the light/dark theme toggle — a deliberate "always
   dramatic" banner, not a themed surface — so this is hardcoded to light
   colors rather than the theme-flipping var(--text)/var(--text-secondary)
   tokens, which would otherwise turn dark-on-dark and unreadable in light
   mode. */
.hero-eyebrow{

    color:rgba(242,236,224,.7);

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:1px;

    margin-bottom:var(--space-2);

}

/* Dashboard's greeting now sits bare on the page background (no boxed
   .hero panel — see .dashboard-layout), which is theme-aware, unlike
   .page-toolbar's fixed-dark banner that .hero-eyebrow's hardcoded light
   color above was tuned for. Same role, theme-aware color instead. */
.dash-eyebrow{

    color:var(--text-muted);

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:1px;

    margin-bottom:var(--space-2);

}

/* The one place --font-serif appears — see its definition in :root. A
   serif at extrabold weight with the old grotesk's -1.5px tracking reads
   clenched, not elegant; regular-to-medium weight and near-zero tracking
   is where a serif like this actually wants to sit. */
.hero-title{

    color:var(--text);

    font-family:var(--font-serif);
    font-size:var(--text-2xl);
    font-weight:500;

    letter-spacing:-.01em;

    margin-bottom:var(--space-3);

}

/* The greeted user's first name, called out in the accent gold per the
   owner-supplied Calendar reference (2026-08-12) — reused on Dashboard's
   hero too, since both greetings share the same "Good afternoon, Name."
   pattern and the Rarity Rule treats a single named person as a legitimate
   emphasis target, not decoration. */
.hero-greeting-name{
    color:var(--primary);
}

/* Not shared with .page-toolbar (that uses plain .text-secondary) — only
   the dashboard hero uses this, and that hero is no longer a boxed
   always-dark panel, so this can safely be theme-aware. */
.hero-subtitle{

    max-width:460px;

    color:var(--text-secondary);

    font-size:17px;
    line-height:1.55;

    margin-bottom:var(--space-4);

}

/* Quiet presence signal — a slight, slow pulse rather than a dramatic
   scale/glow swing. Present, not performing. Now applied to
   .rail-nova-avatar (the Dashboard rail's Nova panel) — this used to be
   the old boxed hero's Nova insight pill (.hero-nova-note/-avatar/
   -message, plus #dashboard-nova-action), all deleted here as confirmed
   dead: the Dashboard rebuild moved Nova's presence into the rail and
   nothing in any .html file has referenced this pill's classes since. */
@keyframes nova-breathe{
    0%, 100%{ transform:scale(1); box-shadow:0 0 0 0 rgba(200, 164, 110,.22); }
    50%{ transform:scale(1.03); box-shadow:0 0 0 3px rgba(200, 164, 110,.08); }
}

/* The command bar — the hero's one interactive surface, replacing the old
   separate "View Today's Plan" button + Nova Capture card with a single
   glowing input. See js/modules/dashboard.js#initNovaCapture. */
.hero-command-bar{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    max-width:560px;
    /* margin-bottom removed, 2026-08-16 — real redundant-spacing bug
       found chasing a direct "less spacing... more compact" request:
       .dashboard-main (this element's parent) already sets its own
       flex `gap` between every direct child, so this margin-bottom was
       stacking an *extra* --space-6 (24px) on top of that gap between
       the search bar and the stat grid below it — a leftover from
       before .dashboard-main had its own gap property, never cleaned
       up once it did. Removing it doesn't lose any spacing, just the
       doubled-up portion of it. */
    padding:6px 6px 6px 8px;

    background:rgba(255,255,255,.06);
    border:1px solid rgba(255,255,255,.14);
    border-radius:999px;

    box-shadow:0 8px 30px rgba(0,0,0,.25);

    transition:border-color var(--transition), box-shadow var(--transition);

}

.hero-command-bar:focus-within{

    border-color:rgba(200, 164, 110,.6);
    box-shadow:0 0 0 3px rgba(200, 164, 110,.18), 0 8px 30px rgba(0,0,0,.25);

}

.hero-command-mic,
.hero-command-send{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:38px;
    height:38px;

    border:none;
    border-radius:50%;

    cursor:pointer;

    transition:transform var(--dur) var(--ease-spring), background-color var(--transition-fast);

}

/* .hero-command-mic's color used to assume the same fixed-dark ground as
   .hero-eyebrow (a leftover from when this bar lived inside the old boxed
   .hero) — but .hero-command-bar itself is a translucent white overlay
   (background/border above, both rgba(255,255,255,..)) sitting directly on
   the page's own theme-flipping background, not a fixed-dark box. A
   hardcoded near-white icon color there was invisible in light theme; the
   translucent white background/border stay as-is since those merely go
   *weaker* in light theme rather than disappearing. */
.hero-command-mic{

    background:rgba(255,255,255,.08);
    color:var(--text-secondary);

}

.hero-command-mic:hover{

    background:rgba(255,255,255,.16);

}

.hero-command-mic.recording{

    color:#fff;
    background:var(--danger);

    animation: chip-bump .5s var(--ease-bounce);

}

.hero-command-send{

    background:linear-gradient(135deg, var(--primary), var(--accent-cyan));
    color:#fff;

    box-shadow:0 0 16px rgba(200, 164, 110,.45);

}

.hero-command-send:hover{

    transform:scale(1.08);

}

.hero-command-input{

    flex:1;
    min-width:0;

    padding:12px 4px;

    background:transparent;
    border:none;

    color:var(--text);

    font-family:inherit;
    font-size:var(--text-base);

    outline:none;

}

.hero-command-input::placeholder{

    color:var(--text-muted);

}

/* .hero-metric (singular, below) is shared with .page-toolbar-stats on
   every other page and is very much alive. .hero-metrics (plural — a
   wrapper + staggered pop-in entrance + stat-pop keyframes + a font-size
   bump, all scoped under it) was the old boxed-hero dashboard's own stat
   row; the Dashboard rebuild (see .stat-grid/.stat-card elsewhere in this
   file) replaced it and nothing in any .html file references the plural
   wrapper anymore — confirmed via a grep sweep before deleting, same as
   the earlier .hero cleanup. */
.hero-metric{

    display:flex;
    flex-direction:column;
    gap:2px;

}

.hero-metric strong{

    color:var(--text);

    font-size:var(--text-stat);
    font-weight:var(--weight-extrabold);
    letter-spacing:-.5px;

}

.hero-metric span{

    color:rgba(242,236,224,.65);

    font-size:var(--text-xs);

}


/* =========================
   Dashboard — right-rail layout. Scoped to index.html only (the class
   only appears there) so no other page's .content is affected. Nova's
   insight moves from the hero pill into a persistent rail panel here;
   the brain-dump capture bar (a real, documented product differentiator
   — PRODUCT.md names it explicitly) stays in the main column rather
   than getting dropped just because the reference screenshot's crop
   didn't happen to show it.
========================= */

.dashboard-layout{

    display:grid;
    grid-template-columns:1fr 360px;
    /* --space-5 (was --space-6), 2026-08-16 direct request — "less
       spacing between elements for a more compact and immediate look." */
    gap:var(--space-5);
    align-items:start;

}

.dashboard-main{

    display:flex;
    flex-direction:column;
    /* --space-5 (was --space-6), same request as .dashboard-layout's own
       gap above — this is the vertical rhythm between the greeting row,
       the brain-dump bar, the stat grid, and Today's Plan. */
    gap:var(--space-5);

    min-width:0;

}

.dash-greeting-row{

    display:flex;
    align-items:flex-start;
    justify-content:space-between;
    gap:var(--space-4);

}

.focus-mode-btn{

    display:inline-flex;
    align-items:center;
    gap:var(--space-2);
    flex-shrink:0;

    padding:9px 18px;
    margin-top:4px;

    background:transparent;
    color:var(--primary);

    border:1px solid rgba(200, 164, 110,.4);
    border-radius:999px;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    cursor:pointer;

    transition:background-color var(--transition-fast), border-color var(--transition-fast);

}

.focus-mode-btn:hover{

    background:rgba(200, 164, 110,.1);
    border-color:rgba(200, 164, 110,.6);

}

/* ---- Stat cards — Focus / Classes / Tasks / Streak. Flat: one hairline
   border, no gradient, no bevel — this direction's cards don't lean on
   tonal depth to read as elevated (see the reference this was built
   from). ---- */

.stat-grid{

    display:grid;
    grid-template-columns:repeat(4, 1fr);
    gap:var(--space-4);

}

.stat-card{

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

    padding:var(--space-5);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.stat-card-head{

    display:flex;
    align-items:center;
    justify-content:space-between;

    font-size:var(--text-sm);
    color:var(--text-secondary);

}

.stat-card-tag{

    font-size:var(--text-2xs);
    color:var(--text-muted);

}

.stat-card-value{

    font-size:28px;
    font-weight:700;
    letter-spacing:-.01em;

    font-variant-numeric:tabular-nums;

}

.stat-card-caption{

    font-size:var(--text-xs);
    color:var(--text-muted);

}

.stat-card-progress{

    display:flex;
    flex-direction:column;
    gap:6px;

    margin-top:2px;

}

.stat-card-progress span{

    font-size:var(--text-2xs);
    color:var(--text-muted);

}

/* Reuses .study-ring's conic-gradient (see Study) at a size that fits a
   stat card instead of the full focus-timer or a leaderboard row. */
.stat-card-ring-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

}

.study-ring-sm{

    width:52px;
    height:52px;

}

.study-ring-sm::before{

    inset:6px;

}

.study-ring-sm .study-ring-value{

    font-size:var(--text-xs);
    font-weight:700;

}

.stat-card-tasks-priority{

    display:inline-flex;
    align-items:center;
    gap:5px;

    color:var(--danger);

}

.stat-card-tasks-priority::before{

    content:"";

    width:6px;
    height:6px;
    border-radius:50%;

    background:currentColor;

}

/* ---- Today's Plan — replaces the old flat "Today's Schedule" timeline
   with a real Day/Week/Agenda switcher. ---- */

.plan-tabs{

    display:flex;
    gap:2px;

    padding:3px;

    background:var(--surface-hover);
    border-radius:999px;

}

.plan-tab{

    padding:6px 14px;

    background:transparent;
    border:none;
    border-radius:999px;

    color:var(--text-secondary);
    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    cursor:pointer;

    transition:background-color var(--transition-fast), color var(--transition-fast);

}

.plan-tab.active{

    background:rgba(200, 164, 110,.16);
    color:var(--primary);

}

.plan-timeline{

    position:relative;

    display:flex;
    flex-direction:column;

}

.plan-row{

    position:relative;

    display:flex;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-3) 0;

}

/* The connecting vertical line running through every dot except the
   last row's — a real timeline, not a bare list. */
.plan-row:not(:last-child)::after{

    content:"";

    position:absolute;
    left:73px;
    top:calc(50% + 10px);
    bottom:calc(-50% + 4px);

    width:1px;

    background:var(--border);

}

.plan-row-time{

    width:58px;
    flex-shrink:0;

    font-size:var(--text-xs);
    color:var(--text-muted);

    font-variant-numeric:tabular-nums;

}

.plan-row-dot{

    width:9px;
    height:9px;
    flex-shrink:0;

    border-radius:50%;

    background:var(--border-light);

    z-index:1;

}

.plan-row.is-focus .plan-row-dot{

    background:var(--primary);
    box-shadow:0 0 0 3px rgba(200, 164, 110,.18);

}

.plan-row-body{

    flex:1;
    min-width:0;

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);

    padding:10px 14px;

    border-radius:var(--radius-medium);

}

.plan-row.is-focus .plan-row-body{

    background:var(--surface-hover);

}

.plan-row-title{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);
    color:var(--text);

}

.plan-row-title p{

    font-size:var(--text-xs);
    font-weight:400;
    color:var(--text-muted);
    margin-top:2px;

}

.plan-row-duration{

    flex-shrink:0;

    font-size:var(--text-xs);
    color:var(--text-muted);

    font-variant-numeric:tabular-nums;

}

/* ---- Quote banner — a CSS-approximated night sky (no photo asset
   available to source honestly), not the same starfield used for the
   optional Settings > Appearance background: darker, horizon-anchored,
   built specifically to sit behind a quote instead of an entire page. ---- */

.quote-banner{

    position:relative;
    overflow:hidden;

    padding:36px 40px;

    border-radius:var(--radius-large);
    border:1px solid var(--border);

    background:
        radial-gradient(1.5px 1.5px at 12% 22%, rgba(255,255,255,.5), transparent),
        radial-gradient(1px 1px at 24% 55%, rgba(255,255,255,.35), transparent),
        radial-gradient(1.5px 1.5px at 38% 15%, rgba(255,255,255,.4), transparent),
        radial-gradient(1px 1px at 52% 40%, rgba(255,255,255,.3), transparent),
        radial-gradient(1.5px 1.5px at 68% 25%, rgba(255,255,255,.45), transparent),
        radial-gradient(1px 1px at 82% 60%, rgba(255,255,255,.3), transparent),
        radial-gradient(1.5px 1.5px at 91% 18%, rgba(255,255,255,.4), transparent),
        radial-gradient(ellipse 70% 45% at 50% 118%, rgba(200, 164, 110,.35), transparent),
        linear-gradient(180deg, #08080a 0%, #0e0e12 55%, #17140f 100%);

}

.quote-text{

    position:relative;
    z-index:1;

    max-width:46ch;

    font-family:var(--font-serif);
    font-weight:400;
    font-size:22px;
    line-height:1.4;
    font-style:italic;
    color:#e7e4db;

}

.quote-text em{

    color:var(--primary-light);
    font-style:italic;

}

.quote-author{

    position:relative;
    z-index:1;

    margin-top:14px;

    font-size:var(--text-xs);
    color:rgba(231,228,219,.6);

}

/* Rail-width variant, 2026-08-16 — moved from the bottom of
   .dashboard-main into .dashboard-rail (direct request: "there is
   negative space that is unused in the bottom right corner... perhaps
   the daily quote can go there" — see .dashboard-rail's own comment
   above for the related sticky-positioning fix that was also part of
   this request). The base .quote-banner rule above was sized for the
   wide ~1fr main column (36px/40px padding, 22px serif text) — applied
   as-is in the ~360px rail it would read cramped and cause awkward
   wrapping, so this tightens both for the narrower context. The
   starfield background/border/radius need no adjustment: the radial
   gradients are percentage-positioned, so they scale to whatever box
   they're in rather than needing new coordinates for a new width. */
.quote-banner.rail-quote{

    padding:var(--space-5);

}

.quote-banner.rail-quote .quote-text{

    max-width:none;
    font-size:17px;

}

/* ---- Right rail ---- */

.dashboard-rail{

    display:flex;
    flex-direction:column;
    gap:var(--space-5);

    /* position:sticky removed, 2026-08-16 direct request — "allow for the
       whole page to scroll down with it, including the right side
       portion, as opposed to just the center portion." Sticky pinned this
       column in the viewport once its own top scrolled past var(--space-6),
       so scrolling the page moved .dashboard-main underneath it while the
       rail itself stayed put — including whatever empty space sat below
       its last panel, which read as the same wasted bottom-right gap on
       screen the whole time no matter how far down the page you scrolled.
       Now it's a plain grid item, same as .dashboard-main, and the two
       columns scroll together as one page. */

}

.rail-panel{

    padding:var(--space-5);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.rail-panel-head{

    display:flex;
    align-items:center;
    justify-content:space-between;

    margin-bottom:var(--space-4);

}

.rail-panel-head h3{

    font-size:var(--text-base);
    font-weight:600;

}

.rail-panel-head a{

    font-size:var(--text-xs);
    color:var(--text-secondary);
    text-decoration:none;

}

.rail-panel-head a:hover{

    color:var(--primary);

}

.chip-ai{

    padding:3px 9px;

    background:rgba(200, 164, 110,.14);
    color:var(--primary);

    border-radius:999px;

    font-size:var(--text-2xs);
    font-weight:700;
    letter-spacing:.04em;

}

.rail-nova-insight{

    display:flex;
    align-items:flex-start;
    gap:var(--space-3);

    margin-bottom:var(--space-4);

}

.rail-nova-avatar{

    width:40px;
    height:40px;
    flex-shrink:0;

    border-radius:50%;
    overflow:hidden;

    box-shadow:0 0 0 1px var(--border);

    /* Nova's one sanctioned idle loop (see DESIGN.md's Motion section) —
       was on the old boxed hero's Nova avatar, orphaned (animation and all)
       when the Dashboard rebuild moved Nova's presence into this rail
       panel and nothing carried it over. Restored here rather than left
       dead: a slight, slow pulse, present not performing. */
    animation: nova-breathe 3.2s ease-in-out infinite;

}

.rail-nova-avatar img{

    width:100%;
    height:100%;
    object-fit:cover;

}

.rail-nova-insight p{

    font-size:var(--text-sm);
    line-height:1.5;
    color:var(--text);

}

.rail-nova-suggestions{

    display:flex;
    flex-direction:column;
    gap:6px;

    margin-bottom:var(--space-4);

}

.rail-suggestion{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:10px 12px;

    background:var(--surface-hover);
    border-radius:var(--radius-medium);

    text-decoration:none;
    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.rail-suggestion:hover{

    background:var(--surface-light);
    border-color:var(--border-light);

}

.rail-suggestion-icon{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:28px;
    height:28px;

    background:var(--surface-light);
    border-radius:8px;

    color:var(--primary);

}

.rail-suggestion-body{

    flex:1;
    min-width:0;

}

.rail-suggestion-body strong{

    display:block;

    font-size:var(--text-xs);
    font-weight:600;
    color:var(--text);

}

.rail-suggestion-body span{

    font-size:var(--text-2xs);
    color:var(--text-muted);

}

.rail-suggestion-chevron{

    flex-shrink:0;

    color:var(--text-muted);

}

.rail-nova-chat-btn{

    display:flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);

    width:100%;
    padding:11px;

    background:var(--surface-hover);
    border-radius:var(--radius-medium);

    color:var(--text);
    text-decoration:none;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    transition:background-color var(--transition-fast);

}

.rail-nova-chat-btn:hover{

    background:var(--surface-light);

}

.upcoming-list{

    display:flex;
    flex-direction:column;
    gap:var(--space-1);

}

.upcoming-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:10px 4px;

    text-decoration:none;

}

.upcoming-row-when{

    width:38px;
    flex-shrink:0;

    text-align:center;

    font-size:var(--text-2xs);
    color:var(--text-muted);
    text-transform:uppercase;

}

.upcoming-row-when strong{

    display:block;

    font-family:var(--font-body);
    font-size:var(--text-lg);
    font-weight:700;
    color:var(--text);

}

.upcoming-row-body{

    flex:1;
    min-width:0;

    font-size:var(--text-xs);
    font-weight:600;
    color:var(--text);

}

.upcoming-row-body span{

    display:block;

    margin-top:1px;

    font-size:var(--text-2xs);
    font-weight:400;
    color:var(--text-muted);

}

.upcoming-row-dot{

    flex-shrink:0;

    width:7px;
    height:7px;

    border-radius:50%;

    background:var(--primary);

}

.upcoming-row-dot.is-urgent{

    background:var(--danger);

}

.rail-more-link{

    display:block;

    margin-top:var(--space-2);

    font-size:var(--text-xs);
    color:var(--text-secondary);
    text-decoration:none;

}

.rail-more-link:hover{

    color:var(--primary);

}

@media (max-width:1400px){

    .dashboard-layout{

        grid-template-columns:1fr;

    }

    .dashboard-rail{

        position:static;

        flex-direction:row;

    }

    /* .quote-banner added to this selector, 2026-08-16 — it moved into
       .dashboard-rail the same day (see its own comment in index.html)
       alongside Nova/Upcoming's existing .rail-panel cards, but isn't
       itself .rail-panel-classed (it keeps its own distinct starfield
       look, see .rail-quote), so without this it wouldn't share this
       row's equal-width split and would sit an inconsistent size next
       to the other two. */
    .rail-panel,
    .quote-banner{

        flex:1;

    }

}

@media (max-width:900px){

    .stat-grid{

        grid-template-columns:repeat(2, 1fr);

    }

    .dashboard-rail{

        flex-direction:column;

    }

    .dash-greeting-row{

        flex-direction:column;

    }

}


/* =========================
   Page Toolbar — compact, non-hero page header
========================= */

.page-toolbar{

    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-6);

    /* var(--space-6) var(--space-8), 2026-08-16 — was hardcoded to the
       exact same values, so every page's own title bar ignored density. */
    padding:var(--space-6) var(--space-8);
    margin-bottom:var(--space-8);

    background:linear-gradient(135deg,#1c1d21 0%,#18191c 55%,var(--background) 100%);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

    box-shadow:var(--shadow-medium);

}

.page-toolbar-title{

    font-size:var(--text-xl);
    font-weight:var(--weight-extrabold);

    letter-spacing:-1px;

    /* Matches every other heading-into-subtitle pairing in the system
       (.hero-title, .section-heading h2, .settings-section-header h3):
       the heading owns the gap, so a subtitle sitting directly under it —
       Friends already has one — reads as a pair instead of two elements
       jammed together. Collapses harmlessly to nothing when a page's
       title has no subtitle below it. */
    margin-bottom:var(--space-1);

}

.page-toolbar-stats{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-6);

}

.page-toolbar-actions{

    display:flex;
    gap:var(--space-3);

}


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

.primary-btn {
    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);

    padding: 12px 20px;

    color: var(--background);
    background: linear-gradient(135deg, var(--primary), var(--primary-hover));

    font-size: var(--text-sm);
    font-weight: 600;

    border: none;
    border-radius: var(--radius-medium);

    cursor: pointer;

    box-shadow: 0 4px 12px rgba(200, 164, 110,.14);

    transition:
        background-color var(--transition),
        transform var(--dur-slow) var(--ease-spring),
        box-shadow var(--transition);
}

/* Solid brass-to-brass, not a two-hue rainbow gradient — the button reads
   as one piece of metal catching light, in keeping with "one precise
   accent" rather than a decorative color mix. Dark text on the brass
   face (not var(--text)'s pale ivory) for real contrast against a
   mid-value amber. */
.primary-btn:hover{

    background: linear-gradient(135deg, var(--primary-hover), var(--primary-light));

    transform:translateY(-1px) scale(1.02);

    box-shadow:0 8px 20px rgba(200, 164, 110,.22);

}

.primary-btn:active {
    transform: scale(0.96);
    transition-duration: var(--transition-fast);
    transition-timing-function: var(--ease-bounce);
}

/* :not([hidden]) — #session-back (js/modules/study.js) is toggled via the
   hidden attribute; an unconditional `display` here would defeat it the
   same way .icon-btn/.verify-control/.study-session already had to guard
   against elsewhere in this file. */
.secondary-btn:not([hidden]) {

    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);

    padding: 12px 20px;

    background: transparent;

    color: var(--text-secondary);

    border: 1px solid var(--border);

    border-radius: var(--radius-medium);

    cursor: pointer;

    font-size: var(--text-sm);

    font-weight: var(--weight-medium);

    transition:
        background-color var(--transition),
        color var(--transition),
        border-color var(--transition),
        transform var(--dur-slow) var(--ease-spring);

}

.secondary-btn:hover{

    background: var(--surface-hover);

    color: var(--text);

    border-color: var(--primary);

    transform: translateY(-1px) scale(1.015);

}

.secondary-btn:active{

    transform: scale(0.96);
    transition-duration: var(--transition-fast);
    transition-timing-function: var(--ease-bounce);

}

.ghost-btn{

    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:var(--space-2);

    padding:12px 20px;

    background:rgba(200, 164, 110,.10);

    color:var(--primary-light);

    border:1px solid rgba(200, 164, 110,.25);
    border-radius:var(--radius-medium);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

    cursor:pointer;

    transition:
        background-color var(--transition),
        border-color var(--transition),
        transform var(--dur-slow) var(--ease-spring);

}

.ghost-btn:hover{

    background:rgba(200, 164, 110,.18);

    border-color:var(--primary);

    transform:translateY(-1px) scale(1.015);

}

.ghost-btn:active{

    transform: scale(0.96);
    transition-duration: var(--transition-fast);
    transition-timing-function: var(--ease-bounce);

}

.text-btn {

    background: transparent;

    border: none;

    color: var(--primary);

    cursor: pointer;

    font-size: var(--text-sm);

    font-weight: var(--weight-semibold);

    transition:
        color var(--transition),
        transform var(--transition);

}

.text-btn:hover {

    color: var(--primary-hover);

    transform: translateX(4px);

}

.text-btn.small{

    font-size:var(--text-xs);

}


/* =========================
   Today's Schedule — the Dashboard's one remaining "workspace" card,
   sitting directly in .content now that Workspace/Supporting are gone.
========================= */

.card-hero-primary{

    margin-bottom:var(--space-12);

}


/* =========================
   Supporting widgets
========================= */

.supporting{

    margin-bottom:var(--space-8);

}

.supporting-grid{

    display:grid;
    grid-template-columns:repeat(3,1fr);
    gap:var(--space-5);

    max-height:2000px;
    opacity:1;

    overflow:hidden;

    transition:max-height var(--transition-slow), opacity var(--transition), margin var(--transition-slow);

}

.supporting-grid.is-collapsed{

    max-height:0;
    opacity:0;
    margin-top:calc(-1 * var(--space-5));

}

.supporting-toggle{

    display:inline-flex;
    align-items:center;
    gap:4px;

}


/* =========================
   Widgets
========================= */

.widget{

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

    padding:var(--space-6);

    background:rgba(27, 28, 33,.75);

    backdrop-filter:blur(18px);
    -webkit-backdrop-filter:blur(18px);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

    box-shadow:var(--shadow-small);

    animation: card-pop-in .35s var(--ease-spring) both;

    transition:
        transform var(--dur-slow) var(--ease-spring),
        box-shadow var(--transition),
        border-color var(--transition);

}

.widget-header{

    display:flex;
    justify-content:space-between;
    align-items:center;

}

.widget:hover{

    transform:translateY(-4px) scale(1.015);

    border-color:rgba(200, 164, 110,.25);

    box-shadow:var(--shadow-medium);

}

.widget-quiet{

    padding:var(--space-5);

    background:var(--surface);

    backdrop-filter:none;
    -webkit-backdrop-filter:none;

    border-radius:var(--radius-medium);

    box-shadow:none;

    gap:var(--space-3);

}

.widget-quiet:hover{

    transform:none;

    border-color:var(--border-light);

    box-shadow:none;

}

.widget-quiet .widget-header h3{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    color:var(--text-secondary);

}

.quiet-list{

    list-style:none;

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

}

.quiet-list li{

    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:var(--space-3);

    font-size:var(--text-sm);
    color:var(--text-secondary);

}

.quiet-list li span:first-child{

    color:var(--text);

    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;

}

.quiet-list li span:last-child{

    flex-shrink:0;

    font-size:var(--text-xs);

}


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

@keyframes card-pop-in{
    from{ opacity:0; transform:translateY(10px) scale(.96); }
    to{ opacity:1; transform:translateY(0) scale(1); }
}

/* Flat surface, one hairline border — the engraved-plate bevel this used
   to be (a two-stop diagonal gradient standing in for the bevel highlight,
   a warm-brown pair left over from the pre-"Nightfall" palette and never
   swept when the actual bevel/--shadow-inset was retired below) is gone;
   see DESIGN.md's "Elevation & Depth — Cards" for why flat-plus-border is
   the target, not a gradient standing in for depth. */
.card {
    min-height: 180px;
    /* var(--space-6), 2026-08-16 — was a hardcoded 24px, so this ignored
       Settings > Appearance's density setting entirely (the same value
       either way). --space-6 already equals 24px comfortable, so this
       changes nothing by default; compact now genuinely tightens every
       card in the app (18px) instead of only the abstract token doing
       so with nothing built on top of it. */
    padding: var(--space-6);

    background:var(--surface);

    backdrop-filter:blur(18px);
    -webkit-backdrop-filter:blur(18px);

    border: 1px solid rgba(200, 164, 110,.16);
    border-radius: var(--radius-large);

    /* One elevation device — the border above, not a shadow. --shadow-inset
       is defined as a no-op (0 0 0 transparent, see :root) specifically so
       this line degrades harmlessly rather than needing removal everywhere
       it's still listed. See DESIGN.md "Elevation & Depth — Cards". */
    box-shadow: var(--shadow-inset);

    transition:
        transform var(--dur-slow) var(--ease-spring),
        box-shadow var(--transition),
        border-color var(--transition);
}

/* :not(.reveal-in) — 2026-08-16. Every .card already played its own
   entrance (card-pop-in, unconditional, no stagger) the instant it was
   inserted; js/modules/common.js#revealData's new .reveal-in class now
   also matches most cards, for the ordered top-left-to-bottom-right
   stagger the previous flat pop-in never had. Both rules set the same
   `animation` shorthand property, and without this guard whichever rule
   sits later in this file — .card, since it's declared after .reveal-in
   — would silently win the cascade and play card-pop-in with no delay
   at all, discarding the whole ordered reveal for every card on the
   page. Scoped to just this one declaration (not the full .card rule)
   so a revealed card still gets .card's own padding/background/border —
   those aren't affected by which entrance animation plays. */
.card:not(.reveal-in){
    animation: card-pop-in .35s var(--ease-spring) both;
}

.card:hover{

    transform:translateY(-2px) scale(1.005);

    border-color:rgba(200, 164, 110,.32);

    /* Hover feedback comes from the lift + brighter border above, not an
       added shadow ring — box-shadow intentionally doesn't change here. */

}

.card-primary{

    padding:28px;

    /* var(--surface) as the middle stop (not a hardcoded rgba literal) so
       this card actually flips light/dark with the theme — a hardcoded
       dark stop here previously made light-mode text nearly unreadable. */
    background:linear-gradient(160deg, rgba(200, 164, 110,.08), var(--surface) 45%, rgba(200, 164, 110,.04) 100%);

    border:1px solid rgba(200, 164, 110,.2);

    box-shadow: var(--shadow-inset);

    /* No animation override here on purpose — inherits .card's card-pop-in
       entrance and nothing else. This class used to also declare an
       infinite "primaryGlow" pulse, but a later, unrelated .card{} rule
       (removed — see the old Animations section) had been silently
       overriding this whole property for months, so it never actually
       ran; keeping the card's rest state genuinely inert (not just
       accidentally so) is the point now, not a bug to route around. */

}

.card-primary:hover{

    border-color:rgba(200, 164, 110,.3);

}


/* =========================
   Auth (login / sign up)
========================= */

/* The sign-up form (name/pronouns/school/dob/etc.) is taller than one
   viewport on most screens. body{height:100vh;overflow:hidden} (the app
   shell's own fixed-sidebar layout) would otherwise clip whatever
   overflows equally off the top and bottom with no scrollbar and no way
   to reach it — this override lets the login page scroll like a normal
   page instead of inheriting that fixed-shell behavior. */
body[data-page="login"]{

    height:auto;
    min-height:100vh;

    overflow-y:auto;

}

.auth-shell{

    position:relative;
    z-index:1;

    display:flex;
    align-items:center;
    justify-content:center;

    min-height:100vh;

    padding:var(--space-6);

    overflow:hidden;

}

/* Faint stars behind sign-in and create-account both — one shared shell,
   one mode toggle within the same card (see login.js), not two separate
   pages, so one rule covers both. Static, not the existing (currently
   inert — .container's own starfield option points at a hero-twinkle
   keyframes that's never actually defined anywhere in this file, a
   pre-existing latent bug, not revived here) twinkle animation: the
   Motion section's own standard is animation-communicates-state, and a
   background star field isn't a state to communicate, so it stays still
   rather than adding a second ambient loop to a screen that doesn't need
   one. Same radial-gradient-dot technique as the Appearance starfield
   option, hand-placed positions, deliberately low opacity (.12-.32) for
   "faint" rather than the decorative option's brighter .4-.7 range —
   this is atmosphere behind a form, not a feature being shown off. */
.auth-shell::before{

    content:"";
    position:absolute;
    inset:0;

    z-index:-1;
    pointer-events:none;

    background-image:
        radial-gradient(1px 1px at 6% 12%, rgba(255,255,255,.28), transparent),
        radial-gradient(1.5px 1.5px at 14% 38%, rgba(255,255,255,.18), transparent),
        radial-gradient(1px 1px at 9% 68%, rgba(255,255,255,.24), transparent),
        radial-gradient(1.5px 1.5px at 20% 88%, rgba(255,255,255,.16), transparent),
        radial-gradient(1px 1px at 28% 22%, rgba(255,255,255,.22), transparent),
        radial-gradient(1.5px 1.5px at 33% 55%, rgba(255,255,255,.14), transparent),
        radial-gradient(1px 1px at 41% 8%, rgba(255,255,255,.26), transparent),
        radial-gradient(1px 1px at 45% 76%, rgba(255,255,255,.18), transparent),
        radial-gradient(1.5px 1.5px at 58% 30%, rgba(255,255,255,.2), transparent),
        radial-gradient(1px 1px at 63% 62%, rgba(255,255,255,.15), transparent),
        radial-gradient(1.5px 1.5px at 69% 90%, rgba(255,255,255,.22), transparent),
        radial-gradient(1px 1px at 74% 18%, rgba(255,255,255,.16), transparent),
        radial-gradient(1.5px 1.5px at 80% 48%, rgba(255,255,255,.28), transparent),
        radial-gradient(1px 1px at 86% 72%, rgba(255,255,255,.18), transparent),
        radial-gradient(1.5px 1.5px at 91% 33%, rgba(255,255,255,.24), transparent),
        radial-gradient(1px 1px at 95% 82%, rgba(255,255,255,.14), transparent),
        radial-gradient(1px 1px at 52% 6%, rgba(255,255,255,.2), transparent),
        radial-gradient(1px 1px at 24% 5%, rgba(255,255,255,.16), transparent);

}

.auth-card{

    width:100%;
    max-width:380px;

    min-height:0;

    display:flex;
    flex-direction:column;
    align-items:center;

    text-align:center;

    transition:
        opacity .18s ease,
        transform .18s ease,
        max-width .25s ease;

}

.auth-card:hover{

    transform:none;

}

/* Sign-in <-> sign-up transition (login.js#switchMode) — fades/settles the
   whole card out, swaps the actual field markup while invisible, then
   fades it back in, rather than the instant hidden-attribute snap this
   used to be. Kept to a plain settle (no bounce) on purpose: .auth-card's
   own --ease-bounce hover-lift is a response to a direct interaction, this
   is a content swap, and the Motion section's own standard is animation-
   communicates-state — "sleek and minimal" here means composed, not
   springy. */
.auth-card.auth-card-switching{

    opacity:0;
    transform:translateY(6px);

}

/* The brand style guide's "dark-on-gold" treatment — one of the four named
   pairings alongside gold-on-dark (.logo-mark in the header) — realized
   here as a solid gold badge (not the old primary->accent-cyan gradient,
   which diluted the brand pairing into a decorative one) with the mark
   itself in ink rather than gold, since gold-on-gold would vanish. */
.auth-badge{

    display:flex;
    align-items:center;
    justify-content:center;

    width:56px;
    height:56px;

    margin-bottom:var(--space-4);

    border-radius:50%;

    background:var(--primary);

    box-shadow:0 0 10px rgba(200, 164, 110,.22);

}

/* The one context the plain <img> logo-mark can't cover: a solid gold
   badge means the raster mark's own gold tones would vanish against it
   (gold-on-gold), the exact problem the old currentColor SVG's dark-on-
   gold inversion existed to solve. Reproduced here without going back to
   an SVG: mask-image turns the same assets/logo.png into a silhouette,
   recolored via a plain background-color instead of the file's own
   pixels — still one source image, just recolored where a flat <img>
   physically can't be. */
.auth-badge .logo-mark{

    width:32px;
    height:32px;

    background-color:var(--background);

    -webkit-mask-image:url('../assets/logo.png');
    mask-image:url('../assets/logo.png');
    -webkit-mask-size:contain;
    mask-size:contain;
    -webkit-mask-repeat:no-repeat;
    mask-repeat:no-repeat;
    -webkit-mask-position:center;
    mask-position:center;

}

/* Solid brass, not gradient-clipped text — a decorative gradient on the
   product name reads as generic AI-UI polish, not as a considered choice;
   the wordmark should look engraved, not painted. */
/* The wordmark, same treatment as .logo-text h2 in the header (see its
   comment) — this is the login page's one standalone appearance of
   "Planit" as a brand moment rather than in-context chrome, so it gets
   the same serif lockup font rather than inheriting the global h1-h4
   Satoshi rule further down. */
.auth-heading{

    margin-bottom:var(--space-1);

    color:var(--primary-light);

    font-family:var(--font-serif);
    font-size:var(--text-xl);
    font-weight:500;
    letter-spacing:-.01em;

}

.auth-tagline{

    margin-bottom:var(--space-6);

    color:var(--text-secondary);
    font-size:var(--text-sm);

}

.auth-form{

    width:100%;

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

    text-align:left;

}

.auth-submit-btn{

    width:100%;

    margin-top:var(--space-2);

}

.auth-error{

    width:100%;
    margin-bottom:var(--space-4);
    padding:var(--space-3);

    background:rgba(224, 104, 90,.12);
    border:1px solid rgba(224, 104, 90,.3);
    border-radius:var(--radius-medium);

    color:var(--danger);
    font-size:var(--text-xs);

}

/* Sign-up's First/Last name row needs a bit more room than sign-in's single
   email/password column to avoid wrapping — widening only while
   #auth-signup-fields is visible (see login.js#applyMode) keeps the plain
   sign-in card from looking oddly wide for just two fields. */
.auth-card.auth-card-wide{

    max-width:440px;

}

.auth-defer-note{

    margin-top:-4px;

    color:var(--text-muted);
    font-size:var(--text-xs);

}

/* Always reachable regardless of how far the (now scrollable, see
   .auth-shell above) sign-up form has been scrolled — the bottom
   "Already have an account?" toggle still works too, this is just the
   version you don't have to scroll to find. Shown only in sign-up mode. */
.auth-back-btn:not([hidden]){

    position:fixed;
    top:var(--space-5);
    left:var(--space-5);

    z-index:var(--z-sticky);

    display:flex;
    align-items:center;
    gap:6px;

    padding:8px 14px;

    background:var(--surface-light);
    border:1px solid var(--border);
    border-radius:999px;

    color:var(--text-secondary);
    font-size:var(--text-xs);
    font-weight:var(--weight-medium);

    cursor:pointer;

    transition:background-color var(--transition-fast), color var(--transition-fast);

}

.auth-back-btn:hover{

    background:var(--surface-hover);
    color:var(--text);

}

.field-label-row{

    display:flex;
    align-items:baseline;
    justify-content:space-between;
    gap:var(--space-2);

}

.auth-forgot-link{

    font-size:var(--text-xs);

}

.auth-field-hint-warning{

    color:var(--warning);

}

.auth-toggle{

    margin-top:var(--space-6);

    color:var(--text-secondary);
    font-size:var(--text-sm);

}

.card-ai{

    background:linear-gradient(160deg, rgba(185, 136, 104,.12), var(--surface) 55%, rgba(122, 147, 168,.08) 100%);

    border:1px solid rgba(185, 136, 104,.22);

    /* No animation override — see .card-primary's comment above; the
       infinite border-color "aiGlow" pulse this used to declare had the
       same dead-code problem and the same fix. */

}

.card-compact{

    min-height:auto;

    padding:20px;

}

.card-header {

    display: flex;
    justify-content: space-between;
    align-items: center;

    margin-bottom: 18px;

}

.card-title{

    display:flex;
    align-items:center;
    gap:var(--space-2);

}

/* The most-repeated heading role in the app (every card, every page) had
   no explicit size — it was riding the browser's default h3 (~1.17em,
   ~18.7px), an arbitrary value that happens to sit between --text-base and
   --text-lg without deliberately being either. Pinning it to --text-lg
   both puts it on the app's own scale and matches
   .settings-section-header h3, the same "title of a boxed section" role
   used elsewhere. */
.card-title h3{

    font-size:var(--text-lg);

}

.card-icon{

    display:flex;
    justify-content:center;
    align-items:center;

    width:32px;
    height:32px;

    border-radius:8px;

    background:var(--surface-hover);

    color:var(--text-secondary);

    font-size:var(--icon-sm);

}

.card-primary .card-icon,
.card-icon-ai{

    width:36px;
    height:36px;

    font-size:var(--icon-lg);

    background:rgba(200, 164, 110,.20);

    color:var(--primary-light);

}

.card-actions {

    display: flex;
    align-items: center;
    gap: var(--space-2);

}

/* :not([hidden]) — an unconditional `display` here would defeat any
   .icon-btn's hidden-attribute toggle (e.g. Settings' #profile-edit-btn)
   the same way — see the longer note on .field further down. */
.icon-btn:not([hidden]) {

    width: 32px;
    height: 32px;

    display: flex;
    justify-content: center;
    align-items: center;

    font-size: var(--icon-sm);

    background: transparent;

    color: var(--text-secondary);

    border: none;

    border-radius: 50%;

    cursor: pointer;

    transition:
        background-color var(--transition-fast),
        color var(--transition-fast),
        transform var(--dur) var(--ease-spring);

}

.icon-btn:hover {

    background: var(--surface-hover);

    color: var(--text);

    transform: scale(1.12);

}

.icon-btn:active {

    transform: scale(0.9);
    transition-duration: var(--transition-fast);
    transition-timing-function: var(--ease-bounce);

}

/* .icon-btn is a bare 32px circle with nothing padding out its tap area
   (unlike, say, .toggle-switch, which sits inside a clickable settings
   row) — well under the 44px minimum, and it's the modal close button, the
   calendar's prev/next month chevrons, every class's delete button, the
   sidebar collapse toggle... one of the most-repeated interactive elements
   in the app. pointer:coarse (not a width breakpoint — screen size doesn't
   tell you input method, a touchscreen laptop is still a laptop) leaves
   mouse/trackpad users at the tighter, denser 32px a fine pointer doesn't
   need, and only grows the target for touch/stylus input. */
@media (pointer: coarse) {

    .icon-btn:not([hidden]),
    .header-icon-btn {

        min-width: 44px;
        min-height: 44px;

    }

}

/* :not([hidden]) — same reasoning as .field/.icon-btn above; Settings'
   #profile-edit-actions toggles hidden and needs this to actually work. */
.card-footer:not([hidden]) {

    display: flex;
    justify-content: space-between;
    align-items:center;

    margin-top: var(--space-6);
    padding-top: var(--space-5);

    border-top: 1px solid var(--border);

}

.card-divider {

    height: 1px;

    background-color: var(--border);

    margin-bottom: 18px;

}

.card-body {

    display: flex;
    flex-direction: column;

    gap: 10px;

}

.card-badge {

    padding: 4px 10px;

    background-color: var(--surface-hover);

    color: var(--text-secondary);

    font-size: 12px;
    font-weight: 600;

    border-radius: 999px;

}

.card-badge-accent{

    background-color:rgba(200, 164, 110,.18);

    color:var(--primary-light);

}

.card h3 {
    margin-bottom: var(--space-3);

    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
}

.card p {
    color: var(--text-secondary);

    line-height: 1.6;
}


/* =========================
   AI Assistant
========================= */

.ai-message{

    padding:var(--space-4);

    margin-bottom:var(--space-4);

    background:rgba(200, 164, 110,.08);

    border:1px solid rgba(200, 164, 110,.18);
    border-radius:var(--radius-medium);

    line-height:1.6;

}

.ai-actions{

    justify-content:flex-end;

}


/* =========================
   Progress Component
========================= */

.progress-info {

    display: flex;
    justify-content: space-between;
    align-items: center;

    margin-bottom: var(--space-3);

    font-size: var(--text-sm);

    color: var(--text-secondary);

}

.progress-bar {

    width: 100%;
    height: 12px;

    background: var(--surface-hover);

    border-radius: 999px;

    overflow: hidden;

    margin-bottom: var(--space-5);

}

.progress-bar-thin{

    height:6px;

    margin-bottom:0;

}

.progress-fill {

    width: 62%;

    height: 100%;

    border-radius: inherit;

    background: linear-gradient(
        90deg,
        var(--primary) 0%,
        var(--primary-hover) 55%,
        var(--primary-light) 100%
    );

    box-shadow:
        0 0 18px rgba(200, 164, 110,.35);

    transform-origin: left center;
    animation: growIn .8s ease both;

    position: relative;

    overflow: hidden;

}

.progress-fill::after {

    content: "";

    position: absolute;

    top: 0;
    left: -30%;

    width: 30%;
    height: 100%;

    background: linear-gradient(
        to right,
        transparent,
        rgba(255,255,255,.25),
        transparent
    );

    animation: shimmer 2.5s infinite;

}

@keyframes growIn {

    from { transform: scaleX(0); }

    to { transform: scaleX(1); }

}

@keyframes shimmer {

    from { left: -30%; }

    to { left: 130%; }

}


/* =========================
   Study Ring
========================= */

.study-ring-row{

    display:flex;
    align-items:center;
    gap:var(--space-5);

}

.study-ring{

    position:relative;

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:96px;
    height:96px;

    border-radius:50%;

    background:conic-gradient(var(--primary) calc(var(--progress) * 1%), var(--surface-hover) 0);

}

.study-ring::before{

    content:"";

    position:absolute;
    inset:10px;

    border-radius:50%;

    background:var(--surface);

}

.study-ring-value{

    position:relative;
    z-index:1;

    font-size:var(--text-sm);
    font-weight:var(--weight-bold);

}

.study-ring-legend p{

    margin-bottom:4px;

}

.study-ring-legend strong{

    font-size:var(--text-lg);

}

.streak-calendar{

    display:flex;
    gap:var(--space-2);

    margin-bottom:var(--space-3);

}

.streak-day{

    display:flex;
    align-items:center;
    justify-content:center;

    width:32px;
    height:32px;

    border-radius:50%;

    background:var(--surface-hover);

    color:var(--text-muted);

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

}

.streak-day.active{

    background:rgba(127, 168, 137,.18);

    color:var(--accent-lime);

}

.streak-day.frozen{

    background:rgba(122, 147, 168,.18);

    color:var(--accent-cyan);

}

.streak-freeze-badge{

    display:inline-flex;

}


/* =========================
   Focus Spotlight
========================= */

.focus-card{

    display:flex;
    flex-direction:column;
    align-items:center;

    gap:var(--space-3);

    padding:var(--space-12);

    text-align:center;

}

.focus-ring-wrap{

    margin-bottom:var(--space-2);

}

/* Was inline (font-size:24px;font-weight:700;) directly on the h2 — the
   only heading in the app overriding weight to a raw 700 instead of the
   shared h1-h4 rule's --weight-semibold, for no documented reason. Same
   size role as .logo-text h2 (--text-title); weight now matches every
   other plain h2 in the system instead of standing out as bolder. */
.focus-title{

    font-size:var(--text-title);

}

/* Grounds the ceremonial ring in the student's actual day — what's
   immediately after this session, not just a countdown floating alone.
   Neutral, not gold: it's contextual information, not one of the Rarity
   Rule's five sanctioned roles (see DESIGN.md's Named Rules).
   :not([hidden]) — caught 2026-08-16 while redesigning the card around
   this element: js/modules/study.js#renderUpNext sets the hidden
   attribute when there's nothing upcoming, but an unconditional
   `display` here overrode it the same way .icon-btn/.study-session/
   .secondary-btn already had to guard against elsewhere in this file —
   a bare arrow icon with an empty label was rendering on its own line
   even with nothing to show. */
.focus-upnext:not([hidden]){

    display:flex;
    align-items:center;
    gap:6px;

    color:var(--text-secondary);

    font-size:var(--text-sm);

}

.focus-upnext .icon{

    font-size:var(--icon-sm);

}

.study-ring-lg{

    width:220px;
    height:220px;

}

.study-ring-lg::before{

    inset:20px;

}

.study-ring-lg .study-ring-value{

    font-size:36px;
    font-weight:var(--weight-extrabold);
    letter-spacing:-1px;

}

.focus-controls{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    margin-top:var(--space-3);

}

.icon-btn.lg{

    width:52px;
    height:52px;

    font-size:var(--icon-lg);

    background:var(--surface-hover);

}

/* Quick Focus, 2026-08-15 — the original unstructured timer, demoted from
   the page's primary card (.card-primary, gold gradient) to a plain
   .card-secondary and a smaller ring, now that the real recommendation
   card above owns the "primary" role. See DESIGN.md's Study section:
   "the timer still exists but is secondary."

   Restructured 2026-08-16, direct feedback that the page read as messy:
   the first pass shrank the ring (220px -> 160px) but kept the original
   centered, stacked-vertically layout (badge, ring, title, subtitle,
   up-next, controls each on their own row) — at ~470px tall it was
   still the single largest card on the page, still visually competing
   with the real recommendation hero above it despite the "secondary"
   intent. Compact horizontal layout instead: a small ring beside its
   text rather than a big one above it, controls in one slim row below
   — under 200px tall, reads as a quiet utility strip, not a second
   hero. */
.focus-card-quiet{

    /* flex-start, not the base .focus-card's centered items — a badge
       sized to its own content (below) would otherwise stretch to the
       card's full width and render as a bar instead of a small pill. */
    align-items:flex-start;
    text-align:left;

    gap:var(--space-4);

    padding:var(--space-5);

}

.focus-card-quiet .focus-card-main{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    /* This and .focus-controls below are the two rows that should span
       the card's full width even though the card's own align-items is
       flex-start (so the badge above stays pill-sized, not stretched). */
    align-self:stretch;

}

.focus-card-quiet .focus-ring-wrap{

    margin-bottom:0;

}

.focus-card-quiet .focus-info{

    display:flex;
    flex-direction:column;
    gap:2px;

    flex:1;
    min-width:0;

}

.focus-card-quiet .focus-title{

    font-size:var(--text-lg);

}

.focus-card-quiet .focus-upnext{

    margin-top:2px;

}

.focus-card-quiet .study-ring-lg{

    width:76px;
    height:76px;

}

.focus-card-quiet .study-ring-lg::before{

    inset:8px;

}

.focus-card-quiet .study-ring-lg .study-ring-value{

    font-size:15px;
    letter-spacing:normal;

}

.focus-card-quiet .focus-controls{

    justify-content:flex-start;

    align-self:stretch;
    margin-top:0;

}


/* =========================
   Study — recommendation, queue, review, session overlay. 2026-08-15
   rebuild against a full product spec (see DESIGN.md's Study section) —
   the real prioritization/mastery logic lives in js/core/studyEngine.js;
   this is only its presentation.
========================= */

.study-rec-card{

    position:relative;

    display:flex;
    align-items:center;
    gap:var(--space-6);

    overflow:hidden;

}

.study-rec-body{

    position:relative;
    z-index:1;

    flex:1;
    min-width:0;

}

.study-rec-label{

    display:flex;
    align-items:center;
    gap:8px;

    margin-bottom:10px;

    color:var(--primary-light);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.study-rec-title{

    margin-bottom:8px;

    color:var(--text);
    font-size:var(--text-xl);
    font-weight:var(--weight-bold);
    letter-spacing:-.3px;

}

.study-rec-reason{

    max-width:52ch;
    margin-bottom:var(--space-4);

    color:var(--text-secondary);
    font-size:var(--text-sm);
    line-height:1.5;

}

.study-rec-meta{

    display:flex;
    align-items:center;
    gap:var(--space-5);

    margin-bottom:var(--space-5);

    color:var(--text-muted);
    font-size:var(--text-xs);

}

.study-rec-mastery{

    display:flex;
    align-items:center;
    gap:8px;

}

.study-rec-mastery-bar{

    display:block;

    width:80px;
    height:6px;

    background:var(--surface-hover);
    border-radius:999px;

    overflow:hidden;

}

.study-rec-mastery-bar span{

    display:block;
    height:100%;

    background:linear-gradient(90deg, var(--primary) 0%, var(--primary-hover) 100%);
    border-radius:inherit;

}

.study-rec-actions{

    display:flex;
    align-items:center;
    gap:var(--space-4);

}

.study-rec-why-text{

    max-width:52ch;
    margin-top:var(--space-3);
    padding:var(--space-3) var(--space-4);

    background:rgba(200, 164, 110,.08);
    border-radius:var(--radius-medium);

    color:var(--text-secondary);
    font-size:var(--text-sm);
    line-height:1.5;

}

.study-rec-why-list{

    display:flex;
    flex-direction:column;
    gap:var(--space-1);

    padding-left:var(--space-4);

    list-style:disc;

}

/* Same real horizon photo Calendar/Nova/Friends/Classes already show as
   ambient environment — no Nova figure here (this card's own AI voice is
   already carried by the sparkle label + reasoning text, not a mascot). */
.study-rec-glow{

    position:absolute;
    top:0;
    right:0;
    bottom:0;

    width:38%;
    height:100%;

    object-fit:cover;
    object-position:65% 40%;

    -webkit-mask-image:linear-gradient(to right, transparent, black 40%);
    mask-image:linear-gradient(to right, transparent, black 40%);

    opacity:.5;

    pointer-events:none;

}

.study-time-row{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    flex-wrap:wrap;

}

.study-time-label{

    color:var(--text-secondary);
    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.study-time-chips{

    display:flex;
    flex-wrap:wrap;
    gap:8px;

}

.study-queue{

    display:flex;
    flex-direction:column;
    gap:var(--space-5);

}

/* One labeled cluster (Now/Next/Review) instead of one flat 8-row list,
   2026-08-16 — direct feedback that the queue read as "hideous,
   unorganized, messy": every row previously carried identical visual
   weight with only a small per-row badge telling groups apart, which
   read as one long repetitive wall rather than an organized queue. The
   group label now says that once per cluster instead of once per row,
   and rows within a group sit tighter together (4px) while groups
   themselves get real air (--space-5 above) — the grouping itself does
   the organizing work instead of decoration. */
.study-queue-group{

    display:flex;
    flex-direction:column;
    gap:4px;

}

.study-queue-group-label{

    margin:0 0 2px 2px;

    font-size:10px;
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.study-queue-group-label.urgency-now{ color:var(--danger); }
.study-queue-group-label.urgency-next{ color:var(--primary-light); }
.study-queue-group-label.urgency-review{ color:var(--success); }

.study-queue-item{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    /* Tightened from --space-4 (16px) all around, 2026-08-16 — paired
       with the grouping above so a cluster of several rows reads as one
       compact, scannable block instead of a stack of equally roomy
       cards each demanding the same attention. */
    padding:12px var(--space-4);

    background:var(--surface);
    border:1px solid var(--border);
    border-left:2px solid transparent;
    border-radius:var(--radius-medium);

    transition:border-color var(--transition-fast);

}

.study-queue-group.urgency-now .study-queue-item{ border-left-color:rgba(224, 104, 90,.5); }
.study-queue-group.urgency-next .study-queue-item{ border-left-color:rgba(200, 164, 110,.4); }
.study-queue-group.urgency-review .study-queue-item{ border-left-color:rgba(111, 174, 127,.5); }

.study-queue-item:hover{

    border-color:rgba(200, 164, 110,.3);
    border-left-color:rgba(200, 164, 110,.5);

}

.study-queue-info{

    display:flex;
    flex-direction:column;
    gap:2px;

    min-width:0;
    flex:1;

}

.study-queue-info strong{

    overflow:hidden;

    color:var(--text);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    white-space:nowrap;
    text-overflow:ellipsis;

}

.study-queue-info .text-muted{

    font-size:var(--text-2xs);

}

.study-queue-actions{

    flex-shrink:0;

    display:flex;
    align-items:center;
    gap:var(--space-2);

}

.study-review-card{

    display:flex;
    align-items:center;
    gap:var(--space-4);

}

.study-review-icon{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:44px;
    height:44px;

    background:rgba(200, 164, 110,.1);
    border-radius:50%;

    color:var(--primary);
    font-size:20px;

}

.study-review-body{

    display:flex;
    flex-direction:column;
    flex:1;

}

.study-review-body strong{

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-extrabold);

}

.study-review-body span{

    color:var(--text-muted);
    font-size:var(--text-xs);

}

.study-course-mastery-grid{

    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));
    gap:var(--space-4);

}

.study-course-mastery-card{

    padding:var(--space-4);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-medium);

}

.study-course-mastery-top{

    display:flex;
    align-items:center;
    gap:8px;

    margin-bottom:10px;

}

.study-course-mastery-top strong{

    overflow:hidden;

    color:var(--text);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    white-space:nowrap;
    text-overflow:ellipsis;

}

.study-course-mastery-card .progress-bar{

    margin-bottom:4px;

}

.study-course-mastery-card .text-muted{

    font-size:var(--text-2xs);

}

.study-week-stats{

    display:grid;
    grid-template-columns:1fr 1fr;
    gap:var(--space-4);

}

.study-week-stat strong{

    display:block;

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-extrabold);
    letter-spacing:-.3px;

}

.study-week-stat span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.study-recent-mastery{

    display:block;

    margin-top:2px;

    color:var(--success);
    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

}


/* =========================
   Study Session overlay — a focused, distraction-reduced full-screen
   panel (js/modules/study.js). body.study-session-active hides the app
   chrome behind it entirely, per the product spec's own "the student
   should feel: I'm in study mode now."
========================= */

body.study-session-active .container{

    display:none;

}

/* :not([hidden]) — an unconditional `display` here would defeat the
   `hidden` attribute js/modules/study.js relies on to show/close this
   overlay: author CSS always overrides the UA [hidden]{display:none}
   default at equal specificity by cascade order, same reasoning as
   .icon-btn/.verify-control's own :not([hidden]) guards elsewhere in
   this file. Caught by actually testing the closed state, not assumed —
   the overlay was rendering open on every page load before this fix. */
.study-session:not([hidden]){

    position:fixed;
    inset:0;

    /* --z-page-overlay, not --z-modal or higher — the "End this session?"
       confirm dialog (js/modules/study.js's #session-close handler) is a
       real .modal-overlay opened from inside this overlay and must render
       on top of it, same as any other modal in the app. Confirmed by
       testing: an unqualified 9500 here rendered the confirm dialog fully
       behind this overlay, making its buttons unclickable. */
    z-index:var(--z-page-overlay);

    display:flex;
    flex-direction:column;

    background:var(--background);

}

.study-session-header{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-5) var(--space-6);

    border-bottom:1px solid var(--border);

}

.study-session-header-text{

    flex:1;
    min-width:0;

}

.study-session-course{

    display:block;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.study-session-header-text h2{

    overflow:hidden;

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

    white-space:nowrap;
    text-overflow:ellipsis;

}

.study-session-timer{

    flex-shrink:0;

    color:var(--primary-light);

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);
    font-variant-numeric:tabular-nums;

}

.study-session-progress{

    display:flex;
    justify-content:center;
    gap:8px;

    padding:var(--space-4) 0;

}

.study-session-dot{

    width:8px;
    height:8px;

    background:var(--surface-hover);
    border-radius:50%;

    transition:background-color var(--transition-fast);

}

.study-session-dot.is-active{

    background:var(--primary);

}

.study-session-dot.is-done{

    background:rgba(200, 164, 110,.4);

}

.study-session-body{

    flex:1;

    display:flex;
    align-items:center;
    justify-content:center;

    padding:var(--space-6);

    overflow-y:auto;

}

.study-session-loading{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-4);

    color:var(--text-secondary);

}

.study-session-recall,
.study-session-learn,
.study-session-practice,
.study-session-confidence,
.study-session-complete{

    width:100%;
    max-width:640px;

}

.study-session-prompt{

    margin-bottom:var(--space-4);

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-semibold);
    text-align:center;

}

.study-session-nova-text{

    margin-bottom:var(--space-4);
    padding:var(--space-4) var(--space-5);

    background:rgba(200, 164, 110,.08);
    border:1px solid rgba(200, 164, 110,.18);
    border-radius:var(--radius-large);

    color:var(--text);
    font-size:var(--text-sm);
    line-height:1.6;

}

.study-recall-answer{

    width:100%;

    resize:vertical;

}

.study-confidence-scale{

    display:flex;
    justify-content:center;
    gap:var(--space-3);

    margin-bottom:8px;

}

.study-confidence-btn{

    display:flex;
    align-items:center;
    justify-content:center;

    width:52px;
    height:52px;

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:50%;

    color:var(--text);
    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast),
        color var(--transition-fast);

}

.study-confidence-btn:hover{

    border-color:rgba(200, 164, 110,.4);

}

.study-confidence-btn.active{

    background:var(--primary);
    border-color:var(--primary);

    color:#0d0e11;

}

.study-confidence-labels{

    display:flex;
    justify-content:space-between;

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.study-session-complete{

    display:flex;
    flex-direction:column;
    align-items:center;

    text-align:center;

}

.study-session-complete-icon{

    display:flex;
    align-items:center;
    justify-content:center;

    width:56px;
    height:56px;
    margin-bottom:var(--space-4);

    background:rgba(111, 174, 127,.14);
    border-radius:50%;

    color:var(--success);
    font-size:28px;

}

.study-session-complete h3{

    margin-bottom:4px;

    font-size:var(--text-xl);

}

.study-session-mastery-shift{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    margin:var(--space-4) 0;

    color:var(--text);
    font-size:var(--text-xl);
    font-weight:var(--weight-extrabold);

}

.study-session-mastery-shift i{

    color:var(--primary);
    font-size:20px;

}

.study-session-nova-assessment{

    max-width:52ch;
    margin-top:var(--space-4);
    padding:var(--space-4) var(--space-5);

    background:rgba(200, 164, 110,.08);
    border-radius:var(--radius-large);

    color:var(--text-secondary);
    font-size:var(--text-sm);
    line-height:1.5;

}

.study-session-footer{

    display:flex;
    align-items:center;
    justify-content:flex-end;
    gap:var(--space-3);

    padding:var(--space-5) var(--space-6);

    border-top:1px solid var(--border);

}

@media (max-width:700px){

    .study-rec-card{

        flex-direction:column;
        align-items:stretch;

    }

    .study-rec-glow{

        position:relative;
        width:100%;
        height:100px;

        order:-1;

        -webkit-mask-image:linear-gradient(to bottom, transparent, black 40%);
        mask-image:linear-gradient(to bottom, transparent, black 40%);

    }

    .study-week-stats{

        grid-template-columns:1fr;

    }

}


/* =========================
   Mini Bar Chart
========================= */

.mini-bar-chart{

    display:flex;
    gap:8px;

    height:70px;

    margin:var(--space-2) 0;

}

.mini-bar-col{

    display:flex;
    flex-direction:column;
    justify-content:flex-end;
    align-items:center;
    gap:6px;

    flex:1;

}

.mini-bar{

    width:100%;

    background:linear-gradient(180deg, var(--primary-light), var(--primary));

    border-radius:4px 4px 0 0;

    transform-origin:bottom center;
    animation:growInVertical .6s ease both;

}

.mini-bar-col span{

    color:var(--text-muted);

    font-size:var(--text-2xs);
    text-transform:uppercase;

}

@keyframes growInVertical{

    from{ transform:scaleY(0); }

    to{ transform:scaleY(1); }

}


/* =========================
   Timeline
========================= */

.timeline {

    display: flex;
    flex-direction: column;

    gap: var(--space-5);

}

.timeline-item {

    display: grid;

    grid-template-columns: 60px 18px 1fr;

    column-gap: var(--space-4);

    align-items: start;

    position: relative;

}

.timeline-time,
.timeline-content {

    /* min-width:0 overrides the grid item's implicit min-width:auto —
       without it, an unbroken long title/location string (no spaces to
       wrap at) could force this 1fr track wider than its share and push
       past the card's edge instead of wrapping. */
    min-width: 0;

    transition: transform var(--transition);

}

.timeline-item:hover .timeline-time,
.timeline-item:hover .timeline-content {

    transform: translateX(6px);

}

.timeline-time {

    color: var(--text-secondary);

    font-size: var(--text-sm);

    text-align: right;

}

.timeline-dot {

    width: 14px;
    height: 14px;

    border-radius: 50%;

    background: var(--surface-hover);

    border: 3px solid var(--background);

    position: relative;

    margin-top: 3px;

}

.timeline-dot::after {

    content: "";

    position: absolute;

    top: 16px;
    left: 50%;

    transform: translateX(-50%);

    width: 2px;

    height: 56px;

    background: var(--border);

}

.timeline-item:last-child .timeline-dot::after {

    display: none;

}

.timeline-content h3 {

    font-size: var(--text-base);

    font-weight: var(--weight-semibold);

    /* Event titles are free text with no maxlength short enough to
       guarantee a wrap point (150 chars, no spaces required) — this is
       the fallback for the pathological case, not the common one. */
    overflow-wrap: break-word;

}

.timeline-content p {

    margin-top: var(--space-1);

    color: var(--text-secondary);

    font-size: var(--text-sm);

}

.timeline-item.active .timeline-dot {

    background: var(--primary);

    box-shadow: 0 0 0 6px rgba(200, 164, 110,.18);

}

.timeline-item.active .timeline-content h3 {

    color: var(--primary);

}

/* Day view events carry data-event-id so they can be clicked straight to a
   delete-capable detail modal — recurring class meetings don't (they aren't
   removable this way), so the pointer cursor and hover accent are scoped to
   the attribute rather than the whole timeline. */
.timeline-item[data-event-id] {

    cursor: pointer;

}

.timeline-item[data-event-id]:hover .timeline-content h3 {

    color: var(--primary);

}


/* =========================
   Nova — chat interface
========================= */

.nova-content{

    padding:0;

    overflow:hidden;

    display:flex;

}

.nova-chat{

    display:flex;
    flex-direction:column;

    min-width:0;
    /* Required for a flex child to shrink below its own content's natural
       size at all (min-height defaults to auto, not 0) — same root cause
       and same fix as .sidebar-top's own comment elsewhere in this file.
       Without it, .nova-messages below (flex:1, meant to be the one
       scrollable region) had nothing real to flex against: .nova-chat
       just grew taller than the viewport to fit however many messages
       existed, .nova-messages never actually became shorter than its own
       content so its overflow-y:auto never had anything to scroll, and
       the input bar at the bottom got pushed off-screen with no way to
       reach it. Confirmed via direct measurement, not assumed: filling
       the chat with 8 real messages showed .nova-chat at 2311px tall in
       a 900px viewport and .nova-messages' scrollHeight exactly equal to
       its own clientHeight (nothing to scroll) before this fix. */
    min-height:0;

    border-left:1px solid var(--border);

    clip-path:inset(0 0 0 0);

}

/* Fullscreen swoop, rebuilt as two @keyframes animations, 2026-08-16 —
   real bug, confirmed by direct measurement (sampling getComputedStyle's
   clipPath on every animation frame, in both headless *and* a real headed
   GPU-accelerated browser): the previous version used a plain CSS
   `transition:clip-path` plus a manual "add both classes, force a state,
   remove one a beat later" dance (first a double requestAnimationFrame,
   later a synchronous forced reflow) to give the transition a starting
   value to animate from. Neither approach actually worked — every sample
   across 30+ animation frames showed clip-path jumping straight to its
   final value with no interpolation at all, i.e. the fullscreen panel
   snapped open/closed instantly with zero visible glide, exactly what
   "the gliding animation... does not work" was reporting. The likely
   cause: `clip-path` doesn't participate in layout, so forcing a layout
   flush (offsetHeight, or waiting on rAF-driven paint) doesn't reliably
   commit an intermediate *style* the transition system can treat as a
   real "before" state — both classes end up resolved within the same
   style-change event, leaving the transition nothing to animate between.
   A `@keyframes` animation has no such ambiguity: applying an
   animation-name always plays its `from` state through to `to` once,
   regardless of whatever the element's computed style was a moment
   before — no forced reflow, no rAF timing, nothing left to race. */
@keyframes nova-fullscreen-enter{
    from{ clip-path:inset(0 0 0 calc(100% - 400px)); }
    to{ clip-path:inset(0 0 0 0); }
}

@keyframes nova-fullscreen-exit{
    from{ clip-path:inset(0 0 0 0); }
    to{ clip-path:inset(0 0 0 calc(100% - 400px)); }
}

/* One class instead of the old .is-fullscreen/.is-entering pair — see
   js/modules/nova.js#setFullscreen, which now just adds/removes
   .is-fullscreen and .is-exiting directly and listens for
   `animationend` (not `transitionend`) to know when to clean up. */
.nova-chat.is-fullscreen{

    position:fixed;
    inset:0;

    z-index:var(--z-modal);

    /* Real bug, caught from a screenshot: .nova-chat itself never needed
       its own opaque background in normal flow (the shared page
       background behind .nova-content already showed through the gaps),
       but position:fixed lifts it out of that box entirely — without
       this, the dashboard content underneath showed straight through
       every part of the fullscreen panel that isn't already covered by
       one of its own opaque children, a confusing double-exposed look. */
    background:var(--background);

    border-left:none;

    clip-path:inset(0 0 0 0);

    animation:nova-fullscreen-enter .4s cubic-bezier(.16,.8,.3,1) both;

}

.nova-chat.is-fullscreen.is-exiting{

    animation:nova-fullscreen-exit .4s cubic-bezier(.16,.8,.3,1) both;

}

@media (prefers-reduced-motion: reduce){

    .nova-chat.is-fullscreen,
    .nova-chat.is-fullscreen.is-exiting{
        /* animation:none, not transition:none — this panel's swoop is a
           @keyframes animation now (see the comment above), not a CSS
           transition. js/modules/nova.js#setFullscreen still needs to
           know when to clean up after the exit case even with the
           animation suppressed here, so it also races a fixed timeout
           against `animationend` — animation:none makes animationend
           never fire at all, and the timeout is what catches that. */
        animation:none;
    }

}

.nova-chat-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding:var(--space-5) var(--space-6);

    background:rgba(43,45,49,.5);

    border-bottom:1px solid var(--border);

}

.nova-chat-identity{

    display:flex;
    align-items:center;
    gap:var(--space-3);

}

.nova-avatar{

    position:relative;
    flex-shrink:0;

    width:44px;
    height:44px;

    border-radius:50%;
    padding:3px;
    overflow:hidden;

    background:radial-gradient(circle at 32% 28%, var(--primary-light) 0%, var(--primary) 45%, var(--surface-hover) 85%, #111215 100%);

    box-shadow:
        inset -6px -4px 12px rgba(0,0,0,.35),
        0 0 10px rgba(200, 164, 110,.2);

}

.nova-avatar img{

    width:100%;
    height:100%;

    border-radius:50%;
    object-fit:cover;

}

/* A message "from Nova" inside a plain modal (the new-account Canvas
   onboarding ask, js/modules/dashboard.js#openCanvasOnboardingModal) —
   her avatar + plain paragraphs, deliberately not a chat bubble: this
   isn't the chat page, it's one message, so the bubble treatment would
   read as a stray fragment of a conversation that isn't happening. */
.nova-ask{

    display:flex;
    align-items:flex-start;
    gap:var(--space-3);

}

.nova-ask-message p{

    margin:0 0 var(--space-2);

}

.nova-ask-message p:last-child{

    margin-bottom:0;

}

.nova-chat-identity h2{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

/* Was an "online status" line (green text + a status dot on the avatar,
   both removed) — a presence indicator is a chatbot-product convention
   ("is the AI online right now?"), and Nova doesn't have an online/offline
   state to report; this is a fixed descriptor instead, so it reads as
   Planit's own neutral text tone, not a status signal. */
.nova-chat-identity p{

    color:var(--text-secondary);

    font-size:var(--text-xs);

}

.nova-messages{

    display:flex;
    flex-direction:column;
    gap:var(--space-5);
    flex:1;

    padding:var(--space-6);

    overflow-y:auto;

}

.msg-row{

    display:flex;
    align-items:flex-start;
    gap:var(--space-3);

    max-width:640px;

}

.msg-row.from-user{

    align-self:flex-end;

    flex-direction:row-reverse;

}

.msg-avatar{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:32px;
    height:32px;

    border-radius:50%;
    overflow:hidden;

    font-size:var(--text-xs);
    font-weight:700;

    color:#fff;

}

.msg-avatar img{

    width:100%;
    height:100%;

    border-radius:50%;
    object-fit:cover;

}

/* The gradient stays as a glowing ring frame around Nova's face (padding
   insets the image so the gradient peeks through at the edge) rather than
   being fully covered by the image. */
.msg-avatar.nova-mini{

    padding:2px;

    background:radial-gradient(circle at 32% 28%, var(--primary-light), var(--primary) 60%, var(--surface-hover) 100%);

}

/* Neutral, not gold — this is the student's own avatar, not Nova's; see
   .avatar's comment above for why "this is a person" isn't a brass role.
   nova-mini above keeps its gradient (Nova/AI is one of the five roles
   that is). Color needs its own override too, not just background — the
   shared .msg-avatar's color:#fff assumed a dark gold gradient behind it
   in both themes; var(--surface-hover) flips light in light theme, so
   white text on it would've gone invisible there. */
.msg-avatar.user-mini{

    background:var(--surface-hover);
    color:var(--text);

}

.msg-bubble{

    padding:14px 18px;

    border-radius:var(--radius-large);

    font-size:var(--text-sm);
    line-height:1.6;

}

.msg-row.from-nova .msg-bubble{

    background:rgba(200, 164, 110,.08);

    border:1px solid rgba(200, 164, 110,.18);
    border-top-left-radius:4px;

    color:var(--text);

}

/* Neutral, not gold — every message a student has ever sent to Nova was
   a solid brass bubble; brass is reserved for AI/progress/action/
   achievement/active-state, and this is none of those, it's just the
   user's own words. Nova's bubble above keeps its (already subtle, 8%
   opacity) brass tint — that one *is* the AI role. */
.msg-row.from-user .msg-bubble{

    background:var(--surface-hover);
    border-top-right-radius:4px;

    color:var(--text);

}

.msg-time{

    margin-top:6px;

    color:var(--text-muted);

    font-size:var(--text-2xs);

}

.msg-row.from-user .msg-time{

    text-align:right;

}

/* Structured content inside a Nova reply — see NovaChat.formatNovaText.
   Nova's page-load greeting always renders this way (built from real Store
   data, not prose); a live Claude reply renders this way whenever it
   answers with a markdown list (the system prompt asks for one on
   multi-item answers — see server/services/novaService.js). Never applied
   to a user's own message — their words render as plain escaped text,
   unparsed, so nothing they type is ever silently reinterpreted. */
.msg-list{

    margin:var(--space-2) 0;
    padding-left:1.1em;

}

.msg-list li{

    margin-top:4px;

}

.msg-list li::marker{

    color:var(--primary-light);

}

/* The "agenda-table" — a run of time+title rows, tabular-numeric so a
   column of times lines up like a real schedule instead of ragged text.
   The list wrapper owns the spacing (gap, and margin on both sides so
   whatever text comes before or after it in the same bubble never sits
   flush against it) rather than each row margining itself relative to a
   sibling that may or may not be there. */
.msg-agenda-list{

    display:flex;
    flex-direction:column;
    gap:4px;

    margin:8px 0;

}

.msg-agenda-row{

    display:flex;
    align-items:baseline;
    gap:var(--space-3);

    padding:8px 10px;

    background:var(--surface);

    border-radius:var(--radius-small);

}

.msg-agenda-time{

    flex-shrink:0;

    min-width:5.5em;

    color:var(--primary-light);

    font-weight:var(--weight-semibold);
    font-variant-numeric:tabular-nums;

}

.msg-agenda-title{

    color:var(--text);

}

/* The one thing Nova wants to stand out in a reply (see novaChat.js's
   RECOMMENDATION_LINE — server/services/novaService.js teaches Claude the
   same convention for its own real replies, js/modules/nova.js#buildGreeting
   uses it for the client-composed greeting's own Insights.generate() line)
   — a gold-tinted callout, not just another paragraph. Rarity Rule-
   sanctioned: this is the "AI/recommendation" role, and it's deliberately
   rare (one per reply, at most). */
.msg-recommendation{

    display:block;

    margin:8px 0;
    padding:var(--space-3) var(--space-4);

    background:rgba(200, 164, 110,.12);
    border:1px solid rgba(200, 164, 110,.25);
    border-radius:var(--radius-medium);

    color:var(--text);
    font-size:var(--text-sm);
    line-height:1.5;

}

.msg-recommendation-label{

    display:block;

    margin-bottom:4px;

    color:var(--primary-light);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

/* Nova's lifestyle-advisor memory, 2026-08-16 — a quiet acknowledgment
   that Nova just saved something real about the student (see
   NovaChat's REMEMBER_LINE and Store.addNovaMemoryNote), deliberately
   understated next to .msg-recommendation above: this isn't something
   the student needs to read and act on, it's disclosure — proof this
   is a real, visible, student-removable note (Settings > "What Nova
   knows about you"), not a black box quietly building a profile. */
.msg-remembered{

    display:flex;
    align-items:center;
    gap:6px;

    margin:8px 0;
    padding:6px 10px;

    background:rgba(127, 168, 137,.08);
    border:1px solid rgba(127, 168, 137,.2);
    border-radius:var(--radius-medium);

    color:var(--text-muted);
    font-size:var(--text-xs);
    font-style:italic;

}

.msg-remembered .icon{

    color:var(--accent-lime);
    font-size:var(--icon-sm);

}

/* Flashcards + quiz — Nova generates these directly from her own
   knowledge (see NovaChat's FLASHCARDS_BLOCK/QUIZ_BLOCK and
   server/services/novaService.js's system prompt for the fenced-block
   convention this renders), 2026-08-14 direct request. Both widgets are
   deliberately wider than an ordinary reply bubble — a study tool needs
   room to be legible, not squeezed into chat-bubble width — so they
   negate .msg-bubble's own padding rather than inheriting it. */
.msg-flashcards,
.msg-quiz{

    margin:10px -18px -14px;
    padding:14px 18px;

    background:var(--surface);
    border-top:1px solid rgba(200, 164, 110,.18);

}

.msg-flashcards-label,
.msg-quiz-label{

    display:flex;
    align-items:center;
    gap:8px;

    margin-bottom:10px;

    color:var(--primary-light);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.msg-flashcards-label i,
.msg-quiz-label i{

    font-size:14px;

}

.flashcard-stack{

    display:flex;
    flex-direction:column;
    gap:10px;

}

/* Click-to-flip, real 3D transform rather than a crossfade — the front/
   back distinction (question vs. answer) is exactly the kind of state a
   flip communicates better than a fade, and it's a single, cheap,
   GPU-composited transform, not a layout-affecting animation. */
.flashcard{

    perspective:1000px;

    min-height:88px;

    cursor:pointer;

}

.flashcard-inner{

    position:relative;

    width:100%;
    height:100%;
    min-height:inherit;

    transition:transform .4s var(--ease-spring);
    transform-style:preserve-3d;

}

.flashcard.is-flipped .flashcard-inner{

    transform:rotateY(180deg);

}

.flashcard-face{

    position:absolute;
    inset:0;

    display:flex;
    flex-direction:column;
    justify-content:center;

    padding:var(--space-4);

    background:var(--surface-light);
    border:1px solid var(--border);
    border-radius:var(--radius-medium);

    backface-visibility:hidden;

    color:var(--text);
    font-size:var(--text-sm);
    line-height:1.5;

}

.flashcard-back{

    transform:rotateY(180deg);

    background:rgba(200, 164, 110,.1);
    border-color:rgba(200, 164, 110,.28);

}

.flashcard-kicker{

    display:block;

    margin-bottom:4px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    letter-spacing:.04em;
    text-transform:uppercase;

}

.quiz-question{

    margin-bottom:14px;
    padding-bottom:14px;

    border-bottom:1px solid var(--border);

}

.quiz-question:last-of-type{

    margin-bottom:0;
    padding-bottom:0;

    border-bottom:none;

}

.quiz-question-text{

    display:flex;
    align-items:baseline;
    gap:8px;

    margin-bottom:10px;

    color:var(--text);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);
    line-height:1.5;

}

.quiz-question-number{

    flex-shrink:0;

    color:var(--primary-light);

}

.quiz-options{

    display:flex;
    flex-direction:column;
    gap:6px;

}

.quiz-option{

    display:flex;
    align-items:center;
    gap:10px;

    padding:9px 12px;

    background:var(--surface-light);
    border:1px solid var(--border);
    border-radius:var(--radius-small);

    color:var(--text);
    font-size:var(--text-sm);
    text-align:left;

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast);

}

.quiz-question:not(.is-answered) .quiz-option:hover{

    background:var(--surface-hover);
    border-color:rgba(200, 164, 110,.35);

}

.quiz-option:disabled{

    cursor:default;

}

.quiz-option-letter{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:22px;
    height:22px;

    background:var(--surface-hover);
    border-radius:50%;

    color:var(--text-secondary);
    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);

}

.quiz-option.is-correct{

    background:rgba(111, 174, 127,.14);
    border-color:var(--success);

}

.quiz-option.is-correct .quiz-option-letter{

    background:var(--success);
    color:#0d0e11;

}

.quiz-option.is-incorrect{

    background:rgba(224, 104, 90,.14);
    border-color:var(--danger);

}

.quiz-option.is-incorrect .quiz-option-letter{

    background:var(--danger);
    color:#0d0e11;

}

/* The detailed-feedback line — Claude's own REASON: text, shown right
   under the question the moment it's answered, correct or not, so the
   feedback teaches regardless of which way the guess went. */
.quiz-feedback{

    margin-top:10px;
    padding:10px 12px;

    border-radius:var(--radius-small);

    font-size:var(--text-sm);
    line-height:1.5;

}

.quiz-feedback.is-correct{

    background:rgba(111, 174, 127,.1);
    color:var(--text);

}

.quiz-feedback.is-incorrect{

    background:rgba(224, 104, 90,.1);
    color:var(--text);

}

.quiz-score{

    margin-top:4px;

    color:var(--primary-light);
    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.typing-row .msg-bubble{

    display:flex;
    align-items:center;

    padding:16px 18px;

}

.typing-dots{

    display:flex;
    gap:4px;

}

.typing-dots span{

    width:6px;
    height:6px;

    background:var(--primary-light);

    border-radius:50%;

    animation:typingBounce 1.4s ease-in-out infinite;

}

.typing-dots span:nth-child(2){ animation-delay:.2s; }
.typing-dots span:nth-child(3){ animation-delay:.4s; }

.nova-suggestions{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-2);

    padding:0 var(--space-6) var(--space-4);

}

.nova-input-bar{

    display:flex;
    /* flex-end, not center, 2026-08-16 — now that .nova-input is a real
       growing textarea (below), the attach/mic/send buttons should sit
       anchored to its bottom edge as it grows taller, not float at its
       vertical midpoint the way they could when it was always one fixed-
       height line. */
    align-items:flex-end;
    gap:var(--space-3);

    padding:var(--space-5) var(--space-6);

    background:rgba(43,45,49,.5);

    border-top:1px solid var(--border);

}

.nova-input-hint{

    padding:0 var(--space-6) var(--space-4);

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

/* Each slash example is now a real shortcut, not just a description of
   one — 2026-08-14 direct request, tying the hint text to the new
   flashcards/quiz capability instead of leaving it purely aspirational
   copy. Styled to read as part of the sentence (inline, same muted
   color) until touched, so it doesn't compete with the hint's own quiet
   role — only the hover/focus state signals it's interactive. */
.nova-hint-chip{

    padding:0;

    background:none;
    border:none;

    color:var(--text-secondary);
    font-size:inherit;
    font-family:inherit;

    text-decoration:underline;
    text-decoration-color:transparent;
    text-underline-offset:2px;

    cursor:pointer;

    transition:
        color var(--transition-fast),
        text-decoration-color var(--transition-fast);

}

.nova-hint-chip:hover,
.nova-hint-chip:focus-visible{

    color:var(--primary-light);
    text-decoration-color:currentColor;

}

/* A real <textarea> as of 2026-08-16 (was a single-line <input>, see
   js/core/novaChat.js#initAutoGrow) — a long message used to just scroll
   its own text sideways out of view instead of wrapping. border-radius
   dropped from a full 999px pill to --radius-large: a pill shape reads
   fine at one fixed line height but looks stretched/odd once the box
   can grow several lines tall; a normal rounded rect scales cleanly at
   any height, matching the reference "growing chat box" behavior
   (ChatGPT and similar) this was modeled on. */
.nova-input{

    flex:1;

    max-height:200px;

    padding:14px 20px;

    background:var(--surface-light);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

    color:var(--text);

    font-family:inherit;
    font-size:var(--text-sm);
    line-height:1.4;

    resize:none;
    overflow-y:hidden;

    outline:none;

    transition:
        border-color var(--transition-fast),
        box-shadow var(--transition-fast);

}

.nova-input:focus{

    border-color:var(--primary);

    box-shadow:var(--shadow-focus);

}

.nova-send-btn{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:48px;
    height:48px;

    background:var(--primary);
    border:none;
    border-radius:50%;

    color:#fff;

    font-size:var(--icon-md);

    cursor:pointer;

    transition:
        background-color var(--transition),
        transform var(--transition);

}

.nova-send-btn:hover{

    background:var(--primary-hover);

    transform:scale(1.06);

}

/* :not([hidden]) — see the note on .field above; an unconditional display
   here would defeat nova.js's container.hidden toggle the same way. */
.nova-attachment-preview:not([hidden]){

    display:flex;
    align-items:center;
    gap:var(--space-2);

    padding:var(--space-3) var(--space-6) 0;

}

.nova-attachment-chip{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    padding:6px 10px 6px 12px;

    background:var(--surface-light);
    border:1px solid var(--border);
    border-radius:999px;

    font-size:var(--text-xs);
    color:var(--text-secondary);

}

.nova-attachment-chip i.icon{

    font-size:var(--icon-sm);
    color:var(--primary-light);

}

.nova-attachment-chip button{

    display:flex;
    align-items:center;
    justify-content:center;

    width:18px;
    height:18px;

    background:transparent;
    border:none;

    color:var(--text-muted);
    cursor:pointer;

}

.nova-attachment-chip button:hover{

    color:var(--danger);

}


/* =========================
   Nova — workspace column (2026-08-12 rebuild)
   Replaces the old persistent .nova-threads rail with a middle "workspace"
   column (hero, priorities, timeline, Insight, Momentum) alongside a
   narrower chat on the right — see nova.js's file header and DESIGN.md's
   Nova section for the full reasoning.
========================= */

.nova-workspace-shell{

    display:grid;
    grid-template-columns:1fr 400px;

    width:100%;
    height:100%;

}

.nova-workspace{

    display:flex;
    flex-direction:column;
    gap:var(--space-6);

    padding:var(--space-6);

    overflow-y:auto;

}

#nova-menu-btn{

    position:relative;

}

/* ---- Hero -------------------------------------------------------------- */

.nova-hero{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-6);

    /* min-height:230px, not the base .card's inherited 180px, 2026-08-16
       real bug caught from a screenshot: with all 4 real stat metrics
       populated (eyebrow + subtitle + a wrapping "Assignments due this
       week" label), this card's own content needs ~194px before padding
       even starts, but the box was rendering at exactly 180px — overflow:
       hidden (load-bearing for the horizon photo/Nova figure's
       intentional bleed past the card edge below) was silently clipping
       the bottom few pixels off the real stat labels along with it,
       every time, not just in some edge case. 230px comfortably fits the
       real content with room to spare. */
    min-height:230px;

    overflow:hidden;
    position:relative;

}

.nova-hero-text{

    min-width:0;

}

.nova-hero-eyebrow{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    margin-bottom:4px;

    color:var(--text);

    font-family:var(--font-serif);
    font-size:var(--text-2xl);
    font-weight:500;
    letter-spacing:-.01em;

}

.nova-hero-eyebrow .icon{

    color:var(--primary);
    font-size:var(--icon-lg);

}

.nova-hero-eyebrow .hero-greeting-name{

    color:var(--primary);

}

.nova-hero-stats{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-6);

    margin-top:var(--space-5);

}

.nova-hero-stats .hero-metric{

    display:flex;
    align-items:center;
    gap:10px;

}

.nova-hero-stats .hero-metric > i{

    color:var(--primary);
    font-size:18px;

}

.nova-hero-stats .hero-metric div{

    display:flex;
    flex-direction:column;

}

.nova-hero-stats .hero-metric strong{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.nova-hero-stats .hero-metric span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.study-ring-xs{

    width:20px;
    height:20px;

}

.study-ring-xs::before{

    inset:3px;

}

/* Same asset/technique as Calendar's hero Nova (assets/nova-figure.png,
   real alpha as of 2026-08-14 — see that card's CSS comment for the full
   mix-blend-mode/alpha history — cropped at the bottom only via an
   oversized img inside an overflow:hidden wrapper) — see that card's CSS
   comment for the full explanation. A first pass here
   used object-fit:cover on a square box directly, which crops the sides
   too and squeezes/distorts the figure instead of just trimming its feet;
   this wrapper+child pattern is what avoids that. Sized down slightly
   since this hero card runs shorter (no Next Up/timeline sharing its
   height). */
/* Same horizon-photo glow as .calendar-hero-horizon (assets/horizon.png,
   mix-blend-mode:screen + radial mask) — see that card's CSS comment for
   the full explanation. Sits before .nova-hero-figure in the DOM so it
   paints behind Nova. Sized down slightly for this card's narrower
   figure. */
.nova-hero-horizon{

    position:absolute;
    right:-160px;
    bottom:-80px;

    /* Grown 380px -> 500px and the mask reworked, 2026-08-14 — matching
       the owner's reference screenshot directly: the reference's glow is
       a big, vivid presence behind Nova, not a faint sliver. The ellipse
       is bottom-right-biased (66% 72%) so the photo's actual sunburst/
       curve — already bottom-right in the source frame — lands there.
       `farthest-corner` sizing (not an explicit percentage) is what
       makes the fade actually complete: it scales the gradient to the
       image's own farthest corner, so stopping fully transparent at 88%
       (not 100%) guarantees zero opacity well before that corner —
       an explicit "130% 130%" tried here first ended up *larger* than
       the image's own box on this narrower card, meaning the gradient
       never reached its own transparent stop before the box's real
       edge, leaving that edge faintly visible as a rectangle (the same
       family of seam as the original bug, just from the glow's own
       box this time rather than Nova's). An earlier, much tighter fade
       (closest-side, 50%) avoided any seam but also made the glow read
       as small and dim — this is the version that's both fully seamless
       and matches the reference's scale. */
    width:500px;
    max-width:none;
    height:auto;

    mix-blend-mode:screen;
    -webkit-mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);
    mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);

    pointer-events:none;
    user-select:none;

}

@media (max-width:1200px){

    .nova-hero-horizon{

        display:none;

    }

}

/* Grown ~30%, 2026-08-14 — matching the owner's reference screenshot
   directly: Nova reads noticeably bigger and lower in the reference than
   this card previously rendered him. */
.nova-hero-figure{

    width:180px;
    height:208px;
    flex-shrink:0;

    align-self:flex-end;
    margin-bottom:calc(-1 * var(--space-6));

    position:relative;
    overflow:hidden;

}

.nova-hero-figure img{

    position:absolute;
    left:50%;
    /* Re-tuned 2026-08-14 alongside the new art below — the replacement
       source file is portrait (1084x1374) rather than the old square
       one, so the same width now renders noticeably taller and pushed
       the head/ring up out of the crop window at the old bottom:0/
       width:248px. Narrower + pushed down clears the head again. */
    bottom:-30px;
    transform:translateX(-50%);

    width:210px;
    max-width:none;
    height:auto;

    /* No mix-blend-mode — assets/nova-figure.png carries real alpha as
       of 2026-08-14; see .friends-hero-figure img's comment for the full
       story (a derived alpha-approximation asset was needed here for a
       few hours until the owner replaced the source art with a real one,
       then removed). */
    pointer-events:none;
    user-select:none;

}

@media (max-width:1200px){

    .nova-hero-figure{

        display:none;

    }

}

/* ---- Today's Priorities -------------------------------------------------- */

.section-label{

    margin-bottom:var(--space-3);

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.nova-priorities{

    display:grid;
    grid-template-columns:repeat(3, 1fr);
    gap:var(--space-4);

}

.nova-priority-card{

    display:flex;
    flex-direction:column;

    padding:var(--space-4);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

    cursor:pointer;

    transition:border-color var(--transition-fast), transform var(--transition-fast);

}

.nova-priority-card:hover{

    border-color:rgba(200, 164, 110,.32);
    transform:translateY(-2px);

}

.nova-priority-icon{

    display:flex;
    align-items:center;
    justify-content:center;

    width:34px;
    height:34px;

    margin-bottom:var(--space-3);

    border-radius:var(--radius-small);

    font-size:var(--icon-sm);

}

.nova-priority-card strong{

    margin-bottom:2px;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.nova-priority-subtitle{

    display:block;

    margin-bottom:var(--space-4);

    color:var(--text-muted);
    font-size:var(--text-xs);

}

.nova-priority-foot{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-2);

    margin-top:auto;

}

.nova-priority-badge{

    padding:3px 9px;

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    letter-spacing:.03em;

}

/* Filled priority badges are a deliberate, disclosed exception to the
   Rarity Rule's "medium/low stay neutral text" default (see .task-priority
   above, same data, opposite treatment) — this reference explicitly shows
   three distinct filled pills, and Today's Priorities is a small, capped-
   at-3 surface, not a long list where three competing colors would add up
   the way a whole Tasks table's worth would. */
.nova-priority-badge-high{

    background:rgba(224, 104, 90,.15);
    color:var(--danger);

}

.nova-priority-badge-medium{

    background:rgba(127, 151, 163,.18);
    color:var(--info);

}

.nova-priority-badge-low{

    background:rgba(200, 164, 110,.18);
    color:var(--warning);

}

.nova-priority-estimate{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.nova-priorities-empty{

    padding:var(--space-5);

    text-align:center;

}

/* ---- Timeline + rail row -------------------------------------------------- */

.nova-workspace-row{

    display:grid;
    grid-template-columns:1fr 300px;
    gap:var(--space-4);

    align-items:start;

}

.nova-timeline-card .card-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

}

.nova-timeline{

    display:flex;
    flex-direction:column;

}

.nova-timeline-row{

    position:relative;

    display:grid;
    grid-template-columns:56px 20px 1fr auto;
    align-items:center;
    gap:var(--space-3);

    padding:10px 0;

}

.nova-timeline-time{

    color:var(--text-muted);
    font-size:var(--text-xs);
    font-weight:var(--weight-medium);

}

.nova-timeline-track{

    position:relative;

    display:flex;
    justify-content:center;
    align-self:stretch;

}

.nova-timeline-track::before{

    content:"";

    position:absolute;
    top:0;
    bottom:-10px;

    width:1px;

    background:var(--border);

}

.nova-timeline-row:last-child .nova-timeline-track::before{

    bottom:50%;

}

.nova-timeline-dot{

    position:relative;
    z-index:1;

    width:9px;
    height:9px;
    flex-shrink:0;
    margin-top:2px;

    border-radius:50%;

    box-shadow:0 0 0 3px var(--surface);

}

.nova-timeline-dot.is-done{

    background:var(--success);

}

.nova-timeline-body{

    min-width:0;

    display:flex;
    flex-direction:column;

}

.nova-timeline-body strong{

    overflow:hidden;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);
    text-overflow:ellipsis;
    white-space:nowrap;

}

.nova-timeline-body span{

    overflow:hidden;

    color:var(--text-muted);
    font-size:var(--text-xs);
    text-overflow:ellipsis;
    white-space:nowrap;

}

.nova-timeline-end{

    flex-shrink:0;

}

.nova-timeline-badge{

    background:var(--surface-hover);
    color:var(--text-secondary);

    font-size:var(--text-2xs);
    white-space:nowrap;

}

.nova-timeline-done{

    color:var(--success);
    font-size:var(--icon-sm);

}

.nova-timeline-empty{

    padding:var(--space-5) 0;

    text-align:center;

}

.nova-workspace-rail{

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

}

.nova-insight-card,
.nova-momentum-card{

    padding:var(--space-5);

}

.nova-insight-label,
.nova-momentum-label{

    display:flex;
    align-items:center;
    gap:6px;

    margin-bottom:var(--space-3);

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    letter-spacing:.06em;
    text-transform:uppercase;

}

.nova-insight-label i,
.nova-momentum-label i{

    color:var(--primary);
    font-size:var(--icon-sm);

}

.nova-insight-card p{

    margin-bottom:var(--space-4);

    font-size:var(--text-sm);
    line-height:1.5;

}

.nova-momentum-body{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    margin-bottom:var(--space-4);

}

.nova-momentum-body .study-ring-value{

    display:flex;
    flex-direction:column;
    align-items:center;

}

.nova-momentum-body p{

    color:var(--text-secondary);
    font-size:var(--text-xs);
    line-height:1.5;

}

.nova-sparkline{

    display:block;

    width:100%;
    height:56px;

}

/* Below this, the fixed 400px chat column plus a flexible workspace
   column no longer both fit — first pass shipped without this breakpoint
   at all and the two columns just overlapped/clipped at phone widths.
   Stacks to one column and hands scrolling back to .nova-content (each
   column scrolled itself independently at desktop width; once there's
   only one column, that's just the page scrolling, same as every other
   page's .content already does). */
@media (max-width:1100px){

    .nova-content{

        display:block;
        overflow-y:auto;
        overflow-x:hidden;

    }

    .nova-workspace-shell{

        display:flex;
        flex-direction:column;

        height:auto;

    }

    .nova-workspace{

        overflow-y:visible;

    }

    .nova-chat{

        min-height:640px;

        border-left:none;
        border-top:1px solid var(--border);

    }

    .nova-workspace-row{

        grid-template-columns:1fr;

    }

}

@media (max-width:640px){

    .nova-priorities{

        grid-template-columns:1fr;

    }

    .nova-hero{

        flex-wrap:wrap;

    }

    /* The serif display size that reads fine at desktop width wraps
       "Good [icon] afternoon, Guest" across three lines in a ~350px-wide
       card — same fix Dashboard/Calendar's own hero titles didn't need
       since neither packs an inline icon into the heading text. */
    .nova-hero-eyebrow{

        font-size:var(--text-xl);

    }

    /* The rigid 4-column grid (time | dot | title | end-badge) has no room
       left for the badge once the title column shrinks this far — switch
       to flex so a wide badge ("Happening now") wraps onto its own line
       instead of squeezing the title into an ellipsis. */
    .nova-timeline-row{

        display:flex;
        flex-wrap:wrap;
        align-items:center;
        column-gap:var(--space-2);
        row-gap:4px;

    }

    .nova-timeline-time{

        width:44px;
        flex-shrink:0;

    }

    .nova-timeline-body{

        flex:1 1 120px;

    }

    .nova-timeline-body strong,
    .nova-timeline-body span{

        white-space:normal;

    }

    .nova-timeline-end{

        margin-left:auto;

    }

}

.icon-btn.recording{

    color:#fff;
    background:var(--danger);

    animation:novaMicPulse 1.4s ease-in-out infinite;

}

@keyframes novaMicPulse{
    0%, 100%{ box-shadow:0 0 0 0 rgba(224, 104, 90,.5); }
    50%{ box-shadow:0 0 0 6px rgba(224, 104, 90,0); }
}

.msg-attachment-chip{

    display:inline-flex;
    align-items:center;
    gap:6px;

    margin-bottom:8px;

    padding:5px 10px;

    background:rgba(255,255,255,.12);
    border-radius:999px;

    font-size:var(--text-xs);

}

/* Only actually a link (see novaChat.js#attachmentChipHtml) once the
   attachment made it to Storage — reset the browser's default anchor
   styling so it still reads as the same quiet chip, with just a hover cue
   that it's now something you can open. */
a.msg-attachment-chip{

    color:inherit;
    text-decoration:none;

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

a.msg-attachment-chip:hover{

    background:rgba(255,255,255,.22);

}


/* =========================
   Nova Launcher — the floating "Ask Nova" FAB + quick-chat panel present
   on every page except nova.html itself (see js/modules/novaLauncher.js).
   Reuses .nova-messages/.msg-bubble/.nova-input-bar wholesale so this reads
   as a compact version of the real thing, not a new visual language.
========================= */

/* The dock owns fixed positioning + the JS-driven "dodge toasts" offset
   (see novaLauncher.js's initToastDodge) — kept on a separate wrapper so
   that offset (a translateY driven by --nova-dodge) never fights with the
   fab's own continuous breathing animation, which also animates transform
   but on the child button. Sits flush bottom-right now (no more +68px
   push-up), with only a small edge margin. */
.nova-dock{

    position:fixed;
    right:var(--space-6);
    bottom:var(--space-6);

    z-index:var(--z-sticky);

    transform:translateY(calc(-1 * var(--nova-dodge, 0px)));
    transition:transform .45s var(--ease-spring);

}

.nova-launcher-fab{

    display:flex;
    align-items:center;
    justify-content:center;

    width:58px;
    height:58px;

    background:linear-gradient(135deg, var(--primary), var(--accent-cyan));
    border:none;
    border-radius:50%;
    padding:3px;
    overflow:hidden;

    color:#fff;
    font-size:var(--icon-lg);

    box-shadow:0 0 0 0 rgba(200, 164, 110,.5), var(--shadow-large);
    cursor:pointer;

    /* Idle breathing/float restored, 2026-08-16, direct request ("make
       the bottom right Nova animated again, slightly breathing and
       hovering up and down slightly") — this exact animation was
       removed in an earlier pass specifically for running "on every
       page, forever, regardless of whether Nova had anything to say."
       Direct current feedback overrides that: back as a genuine
       always-on idle loop, just deliberately subtle (a few px of float,
       ~3.5% scale) rather than the old version's presumably bolder
       motion, so it reads as alive without fighting for attention.
       .nova-launcher-dot below runs the identical keyframes/timing so
       the notification badge visually stays attached to the FAB rather
       than sitting still while the button bobs underneath it — they're
       siblings in the DOM (not nested — see that rule's own comment on
       why), so this is how the two stay in sync without restructuring
       the badge's overflow:hidden-safe positioning. */
    animation:novaLauncherBreathe 4.5s ease-in-out infinite;

    transition:box-shadow var(--transition), transform .3s var(--ease-spring);

}

@keyframes novaLauncherBreathe{
    0%, 100%{ transform:translateY(0) scale(1); }
    50%{ transform:translateY(-6px) scale(1.035); }
}

.nova-launcher-fab img{

    width:100%;
    height:100%;

    border-radius:50%;
    object-fit:cover;

}

.nova-launcher-fab:hover{

    transform:scale(1.14);

    box-shadow:0 0 0 0 rgba(200, 164, 110,.35), 0 0 18px rgba(200, 164, 110,.3), var(--shadow-large);

    animation:novaLauncherPulse 3.2s ease-in-out infinite;

}

@keyframes novaLauncherPulse{
    0%, 100%{ box-shadow:0 0 0 0 rgba(200, 164, 110,.25), 0 0 18px rgba(200, 164, 110,.3), var(--shadow-large); }
    50%{ box-shadow:0 0 0 6px rgba(200, 164, 110,0), 0 0 18px rgba(200, 164, 110,.3), var(--shadow-large); }
}

/* "I noticed something" badge, 2026-08-16 direct request ("much much
   more proactive") — same restraint the FAB's own comment above
   already holds to: this is *not* a return to the always-on decorative
   pulsing that was deliberately removed ("regardless of whether Nova
   had anything to say"). js/modules/novaLauncher.js only ever un-hides
   this when Planit.Common.currentNovaNotificationItem() finds a
   genuinely actionable insight — the exact same check, and the exact
   same restraint (nothing here for the "all caught up"/praise-only
   insights), that gates the notification bell's own new Nova entry.
   Reuses .notification-dot's own visual language (same ping keyframe,
   same size/color) rather than inventing a second badge pattern.
   Positioned on .nova-dock, not .nova-launcher-fab itself — the FAB's
   own overflow:hidden (load-bearing, keeps the circular avatar image
   contained) would otherwise clip both the dot and its ping ring. */
.nova-launcher-dot{

    position:absolute;
    top:2px;
    right:2px;

    width:14px;
    height:14px;

    border-radius:50%;
    border:2px solid var(--background);

    background:var(--danger);

    pointer-events:none;

    /* Same keyframes/timing as .nova-launcher-fab's own idle animation,
       2026-08-16 — see that rule's comment. Applied here too (not
       inherited — this is a sibling, not a child of the FAB) so the
       badge visually tracks the button's float/breathe instead of
       sitting motionless while it bobs underneath. */
    animation:novaLauncherBreathe 4.5s ease-in-out infinite;

}

.nova-launcher-dot::after{

    content:"";

    position:absolute;
    inset:0;

    border-radius:50%;

    background:var(--danger);

    animation:notificationPing 2s ease-out infinite;

}

/* :not([hidden]) — see the note on .field above; an unconditional display
   here would defeat novaLauncher.js's panel.hidden toggle the same way,
   leaving the (invisible but still laid-out) panel intercepting clicks on
   whatever's underneath it. */
.nova-launcher-panel:not([hidden]){

    position:fixed;
    right:var(--space-6);
    bottom:calc(var(--space-6) + 70px);

    z-index:var(--z-sticky);

    display:flex;
    flex-direction:column;

    width:340px;
    max-height:480px;

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

    box-shadow:var(--shadow-large);
    overflow:hidden;

    animation:modalPop .3s var(--ease-bounce) both;

}

.nova-launcher-panel .nova-messages{

    flex:1;

    min-height:220px;
    max-height:280px;

}

.nova-launcher-panel .nova-input-bar{

    padding:var(--space-3) var(--space-4);

}

.nova-launcher-header{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-4);

    border-bottom:1px solid var(--border);

}

.nova-launcher-header-text{

    flex:1;

}

.nova-launcher-header-text p{

    font-size:var(--text-xs);

}

.nova-launcher-full-link{

    display:flex;
    align-items:center;
    justify-content:center;
    gap:6px;

    padding:var(--space-3);

    border-top:1px solid var(--border);

    color:var(--primary-light);
    font-size:var(--text-xs);
    font-weight:var(--weight-medium);
    text-decoration:none;

}

.nova-launcher-full-link:hover{

    color:var(--primary-hover);

}

/* Settings already crowds the bottom-right corner with its own edit-lock
   affordances and confirm modals — Nova's floating launcher sits this
   page out entirely rather than fighting for the same space. */
body[data-page="settings"] .nova-dock,
body[data-page="settings"] .nova-launcher-panel{

    display:none;

}


/* =========================
   Settings
========================= */

.settings-layout{

    display:grid;
    grid-template-columns:240px 1fr;
    gap:var(--space-6);

    align-items:start;

}

.settings-tabs{

    position:sticky;
    top:0;

    display:flex;
    flex-direction:column;
    gap:4px;

    padding:var(--space-3);

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.settings-tab{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:12px 16px;

    background:transparent;
    border:none;
    border-radius:var(--radius-medium);

    color:var(--text-secondary);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

    text-align:left;
    text-decoration:none;

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        color var(--transition-fast);

}

.settings-tab:hover{

    background:var(--surface-hover);

    color:var(--text);

}

.settings-tab.active{

    background:rgba(200, 164, 110,.15);

    color:var(--primary);
    font-weight:var(--weight-semibold);

}

.settings-panel{

    display:flex;
    flex-direction:column;
    gap:var(--space-6);

}

.settings-section-header{

    display:flex;
    justify-content:space-between;
    align-items:flex-start;
    gap:var(--space-3);

    margin-bottom:var(--space-4);

}

.settings-section-header h3{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

    margin-bottom:4px;

}

.settings-section-header p{

    color:var(--text-secondary);

    font-size:var(--text-sm);

}

/* :not([hidden]) — #nova-memory-row (settings.html) is the first
   .settings-row toggled via the hidden attribute; an unconditional
   `display` here would defeat it the same way .icon-btn/.study-session/
   .secondary-btn/.focus-upnext already had to guard against elsewhere
   in this file. */
.settings-row:not([hidden]){

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-4);
    flex-wrap:wrap;

    padding:var(--space-4) 0;

    border-bottom:1px solid var(--border);

}

.settings-row:last-child{

    border-bottom:none;

    padding-bottom:0;

}

/* For a settings row whose control is more than one button — e.g. Canvas's
   "paste your feed URL" input — stacking the label above a full-width
   control row reads better than squeezing an input into the same
   space-between line every other (label + single toggle/button) row uses. */
.settings-row-stacked{

    flex-direction:column;
    align-items:stretch;

}

.settings-row-label h4{

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    margin-bottom:2px;

}

.settings-row-label p{

    color:var(--text-muted);

    font-size:var(--text-xs);

}

/* "What Nova knows about you" — js/modules/settings.js#renderNovaMemory,
   2026-08-16. Real, removable notes, styled as a plain quiet list rather
   than cards — this is disclosure, not a feature to showcase. */
.nova-memory-list{

    display:flex;
    flex-direction:column;
    gap:6px;

    margin-top:var(--space-3);

}

.nova-memory-item{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);

    padding:var(--space-2) var(--space-3);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-medium);

    color:var(--text-secondary);
    font-size:var(--text-xs);

}

/* A number input paired with its unit ("50 minutes", "7 hours / day") —
   Focus Mode's three goal/length rows. */
.field-suffix-group{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    font-size:var(--text-sm);

}

.settings-avatar-row{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    margin-bottom:var(--space-5);

}

/* Same max-height/opacity/margin recipe as .supporting-grid.is-collapsed
   (Dashboard/Study's "Show more" sections) — different selector because this
   wraps form field-rows, not a card grid, but kept to the same motion recipe
   for consistency. Base state is collapsed here rather than expanded, since
   unlike those sections this one needs to default shut for a fresh profile;
   settings.js toggles .is-expanded rather than the old hidden attribute,
   which could never have transitioned in the first place. */
.profile-more-details{

    max-height:0;
    opacity:0;

    overflow:hidden;

    transition:max-height var(--transition-slow), opacity var(--transition), margin-top var(--transition-slow);

}

.profile-more-details.is-expanded{

    max-height:2000px;
    opacity:1;

    margin-top:var(--space-1);

}

/* Neutral, matching .avatar's fix — same "this is a person, not a
   signal" reasoning. */
.settings-avatar-lg{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:72px;
    height:72px;

    border-radius:50%;

    background:var(--surface-hover);

    color:var(--text-secondary);

    font-size:var(--text-section);
    font-weight:700;

    overflow:hidden;

}

/* A real photo (Settings, header, sidebar, Friends cards) replaces the
   initials text inside any of these circles — see
   Planit.Common.renderAvatarInto in js/modules/common.js. */
.avatar img,
.settings-avatar-lg img{

    width:100%;
    height:100%;

    object-fit:cover;

}

/* Wraps #settings-avatar-preview as a sibling, not a parent-of-content —
   renderAvatarInto replaces that inner div's innerHTML on every render, so
   the hover overlay lives here instead, where it's never touched. */
.settings-avatar-clickable{

    position:relative;

    padding:0;
    border:none;
    background:none;

    border-radius:50%;
    cursor:pointer;

    transition:transform var(--dur) var(--ease-spring);

}

.settings-avatar-clickable:hover{

    transform:scale(1.05);

}

.avatar-edit-overlay{

    position:absolute;
    inset:0;

    display:flex;
    align-items:center;
    justify-content:center;

    background:rgba(13, 14, 17,.55);
    border-radius:50%;

    color:#fff;
    font-size:var(--icon-md);

    opacity:0;

    transition:opacity var(--transition);

}

.settings-avatar-clickable:hover .avatar-edit-overlay,
.settings-avatar-clickable:focus-visible .avatar-edit-overlay{

    opacity:1;

}

.field-row{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-4);

    margin-bottom:var(--space-4);

}

/* :not([hidden]) — an unconditional `display` here would override the UA
   default `[hidden]{display:none}` for any .field toggled via the hidden
   attribute (e.g. login.html's Confirm Password field) regardless of
   selector specificity, since author rules beat UA rules outright. */
.field:not([hidden]){

    display:flex;
    flex-direction:column;
    gap:6px;

    flex:1;
    min-width:180px;

}

.field label{

    color:var(--text-secondary);

    font-size:var(--text-xs);
    font-weight:var(--weight-medium);

}

/* Anchor point for a .dropdown-panel of suggestions rendered under a plain
   text input (e.g. the sign-up school picker) — see js/modules/login.js. */
.combobox-field{

    position:relative;

    /* Own stacking context, deliberately — the sign-up form's Major/
       Expected-graduation reveal (.auth-reveal, right below this field —
       see login.html) animates opacity, which creates a stacking context
       of its own for as long as it isn't exactly 1; without this, which
       one painted on top of the other became a coin flip depending on
       transition timing instead of always the dropdown. Pinning this
       field's z-index above it makes the suggestions panel (a child of
       this field — see UI.openDropdown) reliably win regardless. */
    z-index:2;

}

/* Sign-up-only fields live in two separate wrapper blocks now (name, above
   email/password; school/major/graduation, below — see login.html and the
   Login & Sign Up section of DESIGN.md for why they're split rather than
   one contiguous block), both toggled by the same class, shown/hidden as
   a block (see login.js#applyMode) — .auth-form's own gap only applies
   between ITS direct children, so each wrapper needs the same gap
   repeated for the fields/field-rows nested inside it. .field-row already
   carries its own margin-bottom for use elsewhere (e.g. Settings); zero
   it out here so it doesn't stack with this wrapper's gap. */
/* :not([hidden]) matters here, not just style — any author rule that sets
   `display` on this element overrides the UA default `[hidden]{display:none}`
   regardless of specificity, so an unconditional `display:flex` here would
   silently defeat login.js#applyMode's hidden toggle the moment you switch
   back to sign-in mode. */
.auth-signup-only:not([hidden]){

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

}

.auth-signup-only .field-row{

    margin-bottom:0;

}

/* The Major/Expected-graduation reveal under School (login.js#wireSchoolPicker
   toggles .is-open once the field has a value) — grid-template-rows 0fr->1fr
   is the standard CSS-only way to animate toward an "auto" height, which a
   plain max-height guess can't do without either clipping tall content or
   leaving dead space for short content. The inner wrapper is what actually
   clips during the animation; the outer grid is what's actually animating. */
.auth-reveal{

    display:grid;
    grid-template-rows:0fr;

    opacity:0;

    transition:
        grid-template-rows .3s var(--ease-bounce),
        opacity .2s ease;

}

.auth-reveal.is-open{

    grid-template-rows:1fr;
    opacity:1;

}

.auth-reveal-inner{

    overflow:hidden;
    min-height:0;

}

.auth-reveal .field-row{

    padding-top:2px;

}

/* :not([hidden]) — same gotcha as .field/.auth-signup-only above: an
   unconditional `display` here outranks the UA default [hidden]{display:
   none} on specificity alone, regardless of DOM order, so a hint toggled
   via .hidden = true (e.g. login.js#applyMode's email/password hints)
   would otherwise stay visibly rendered the whole time — found live via
   the sign-in <-> sign-up transition work, not something new this
   introduced, just newly caught by actually watching the two states
   side by side. */
.field small:not([hidden]){

    display:block;

    font-size:var(--text-xs);
    line-height:1.4;

}

.text-input,
.select-input{

    padding:10px 14px;

    background:var(--surface-light);

    border:1px solid var(--border);
    border-radius:var(--radius-medium);

    color:var(--text);

    font-family:inherit;
    font-size:var(--text-sm);

    outline:none;

    transition:border-color var(--transition-fast);

}

.text-input:focus,
.select-input:focus{

    border-color:var(--primary);

}

.text-input:disabled,
.select-input:disabled{

    opacity:.6;

    cursor:not-allowed;

}

/* Grid of built-in avatar choices (Settings > Change Photo) — see
   js/modules/settings.js#pfpGridHtml. */
.pfp-grid{

    display:grid;
    grid-template-columns:repeat(4, 1fr);
    gap:var(--space-3);

}

.pfp-option{

    padding:0;

    background:transparent;
    border:2px solid transparent;
    border-radius:50%;

    cursor:pointer;

    transition:border-color var(--transition-fast);

}

.pfp-option:hover{

    border-color:var(--border-light);

}

.pfp-option img{

    display:block;

    width:100%;
    aspect-ratio:1;

    border-radius:50%;
    object-fit:cover;

}

.toggle-switch{

    position:relative;

    display:inline-block;
    flex-shrink:0;

    width:44px;
    height:24px;

}

.toggle-switch input{

    position:absolute;

    width:0;
    height:0;

    opacity:0;

}

.toggle-slider{

    position:absolute;
    inset:0;

    background:var(--surface-hover);

    border-radius:999px;

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.toggle-slider::before{

    content:"";

    position:absolute;
    top:3px;
    left:3px;

    width:18px;
    height:18px;

    background:#fff;

    border-radius:50%;

    transition:transform var(--transition-fast);

}

.toggle-switch input:checked + .toggle-slider{

    background:var(--primary);

}

.toggle-switch input:checked + .toggle-slider::before{

    transform:translateX(20px);

}

.theme-swatches{

    display:flex;
    gap:var(--space-3);

}

.theme-swatch{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:6px;

    padding:var(--space-2);

    background:transparent;
    border:2px solid transparent;
    border-radius:var(--radius-medium);

    cursor:pointer;

    transition:border-color var(--transition-fast);

}

.theme-swatch.active{

    border-color:var(--primary);

}

.theme-swatch-preview{

    width:56px;
    height:36px;

    border-radius:8px;

    border:1px solid var(--border);

}

.theme-swatch span{

    color:var(--text-secondary);

    font-size:var(--text-xs);

}


/* =========================
   Notes
========================= */

.notes-toolbar{

    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-4);

    margin-bottom:var(--space-6);

}

.notes-filters{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-2);

}

.filter-chip{

    padding:8px 16px;

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:999px;

    color:var(--text-secondary);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

    cursor:pointer;

    transition:all var(--transition-fast);

}

.filter-chip.active{

    background:var(--primary);

    border-color:var(--primary);

    color:#fff;

}

.filter-chip:hover:not(.active){

    background:var(--surface-hover);

    border-color:var(--border-light);

}

.filter-chip-row{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-2);

    margin-bottom:var(--space-6);

}


/* =========================
   Tasks — a real, separate collection (see js/modules/tasks.js), not a
   filtered view of calendar events. Rows reuse the same "quiet by
   default, hover to notice" row language as .leaderboard-row/
   .suggestion-row/.event-item rather than inventing a new one.
========================= */

.task-list{

    display:flex;
    flex-direction:column;
    gap:2px;

    margin-bottom:var(--space-6);

}

.task-list-empty{

    padding:var(--space-3) var(--space-2);

}

.task-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3) var(--space-2);

    border-radius:var(--radius-medium);

    transition:background-color var(--transition-fast);

}

.task-row:hover{

    background:var(--surface-hover);

}

/* Completing a task is a small achievement — the Rarity Rule's own
   reasoning applies here the same as it does to the XP chip, so the
   checkbox is the one gold moment in this row family, not decoration. */
.task-checkbox{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:22px;
    height:22px;

    background:transparent;
    border:2px solid var(--border-light);
    border-radius:50%;

    color:transparent;

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast),
        color var(--transition-fast);

}

.task-checkbox .icon{

    font-size:12px;

}

.task-checkbox:hover{

    border-color:var(--primary);

}

.task-checkbox.checked{

    background:var(--primary);
    border-color:var(--primary);

    color:var(--background);

}

.task-row-body{

    flex:1 1 auto;

    min-width:0;

}

.task-row-title{

    color:var(--text);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.task-row.completed .task-row-title{

    color:var(--text-muted);

    text-decoration:line-through;

}

.task-row-meta{

    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:var(--space-3);

    margin-top:3px;

}

.task-priority{

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);

    text-transform:uppercase;
    letter-spacing:.4px;

    color:var(--text-muted);

}

/* High priority reuses the Calendar legend's existing "High Priority"
   meaning (class-dot-danger) — the same fixed-alert-color convention,
   not a second one invented for this page. Medium/low stay neutral on
   purpose: not every status deserves a color, only the one that's
   actually urgent. */
.task-priority-high{

    color:var(--danger);

}

.task-row-course{

    display:flex;
    align-items:center;
    gap:6px;

    color:var(--text-secondary);

    font-size:var(--text-xs);

}

.task-row-due{

    color:var(--text-muted);

    font-size:var(--text-xs);

}

.task-row-due.overdue{

    color:var(--danger);
    font-weight:var(--weight-semibold);

}

.notes-grid{

    display:grid;
    grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
    gap:var(--space-5);

}

.note-card{

    position:relative;

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

    padding:var(--space-5);

    background:rgba(27, 28, 33,.75);

    backdrop-filter:blur(18px);
    -webkit-backdrop-filter:blur(18px);

    /* Was a 3px accent top border — clashed with the rounded corners
       (a classic AI-UI tell). Uniform brass-hairline border instead,
       matching .card's engraved-plate treatment. */
    border:1px solid rgba(200, 164, 110,.16);
    border-radius:var(--radius-large);

    box-shadow:var(--shadow-small), var(--shadow-inset);

    cursor:pointer;

    transition:
        transform var(--transition),
        box-shadow var(--transition),
        border-color var(--transition);

}

.note-card:hover{

    transform:translateY(-6px);

    box-shadow:var(--shadow-medium);

}

.note-card-blurple{ border-top-color:var(--primary); }
.note-card-info{ border-top-color:var(--info); }
.note-card-success{ border-top-color:var(--success); }
.note-card-warning{ border-top-color:var(--warning); }

.note-card-header{

    display:flex;
    align-items:flex-start;
    justify-content:space-between;
    gap:var(--space-2);

}

.note-card h3{

    font-size:var(--text-base);
    font-weight:var(--weight-semibold);

}

.note-card-snippet{

    color:var(--text-secondary);

    font-size:var(--text-sm);
    line-height:1.6;

    display:-webkit-box;
    -webkit-line-clamp:3;
    line-clamp:3;
    -webkit-box-orient:vertical;

    overflow:hidden;

}

.note-card-footer{

    display:flex;
    align-items:center;
    justify-content:space-between;

    margin-top:auto;
    padding-top:var(--space-3);

    border-top:1px solid var(--border);

    color:var(--text-muted);

    font-size:var(--text-xs);

}


/* =========================
   Task Board
========================= */

.board{

    display:grid;
    grid-template-columns:repeat(3,1fr);
    gap:var(--space-5);

    align-items:start;

}

.board-column{

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

    padding:var(--space-4);

    background:rgba(255,255,255,.02);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

    min-height:200px;

}

.board-column-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding:0 4px;

}

.board-column-title{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    color:var(--text-secondary);

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:.5px;

}

.board-count{

    padding:2px 8px;

    background:var(--surface-hover);

    border-radius:999px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

}

.board-card{

    position:relative;

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

    padding:var(--space-4);

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-medium);

    cursor:pointer;

    transition:
        transform var(--transition-fast),
        border-color var(--transition-fast),
        box-shadow var(--transition-fast);

}

.board-card-select{

    position:absolute;
    top:var(--space-3);
    right:var(--space-3);

    display:none;

    width:18px;
    height:18px;

}

#task-board.selecting .board-card-select{

    display:block;

}

#task-board.selecting .board-card.is-selected{

    border-color:var(--primary);
    box-shadow:0 0 0 1px var(--primary);

}

/* :not([hidden]) — same reasoning as .field/.icon-btn above; without it
   js/modules/tasks.js's bar.hidden toggle wouldn't actually hide this. */
.bulk-action-bar:not([hidden]){

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding:var(--space-4) var(--space-5);
    margin-bottom:var(--space-6);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-medium);

    font-size:var(--text-sm);
    color:var(--text-secondary);

}

.bulk-action-bar-actions{

    display:flex;
    gap:var(--space-3);

}

.board-card:hover{

    transform:translateY(-3px);

    border-color:var(--border-light);

    box-shadow:var(--shadow-small);

}

.board-card h4{

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.board-card-tags{

    display:flex;
    flex-wrap:wrap;
    gap:6px;

}

.board-card-footer{

    display:flex;
    align-items:center;
    justify-content:space-between;

    margin-top:2px;

    color:var(--text-muted);

    font-size:var(--text-xs);

}

.board-column-done .board-card{

    opacity:.7;

}

.board-quick-add{

    margin-bottom:2px;

}

.board-column-done .board-card h4{

    text-decoration:line-through;

    text-decoration-color:var(--border-light);

}


/* =========================
   Calendar Grid
========================= */

.calendar-toolbar{

    display:flex;
    justify-content:space-between;
    align-items:center;

    margin-bottom:var(--space-5);

}

.calendar-toolbar-nav{

    display:flex;
    align-items:center;
    gap:var(--space-3);

}

.calendar-month-label{

    min-width:170px;

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

    text-align:center;

}

.calendar-view-tabs{

    display:flex;
    gap:4px;

    padding:4px;

    background:var(--surface-hover);

    border-radius:999px;

}

.calendar-view-tab{

    padding:6px 14px;

    background:transparent;
    border:none;
    border-radius:999px;

    color:var(--text-secondary);

    font-size:var(--text-xs);
    font-weight:var(--weight-medium);

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        color var(--transition-fast);

}

.calendar-view-tab.active{

    background:var(--primary);

    color:var(--text);

}

.calendar-weekdays{

    display:grid;
    grid-template-columns:repeat(7,1fr);
    gap:6px;

    margin-bottom:var(--space-2);

}

.calendar-weekdays span{

    padding-bottom:6px;

    color:var(--text-muted);

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    text-align:center;
    text-transform:uppercase;
    letter-spacing:.5px;

}

.calendar-grid{

    display:grid;
    grid-template-columns:repeat(7,1fr);
    gap:6px;

}

.calendar-day{

    position:relative;

    display:flex;
    flex-direction:column;
    gap:6px;

    min-height:112px;
    padding:10px;

    background:rgba(255,255,255,.02);

    border:1px solid transparent;
    border-radius:var(--radius-small);

    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast),
        transform var(--transition-fast);

}

.calendar-day:hover{

    background:var(--surface-hover);

    border-color:var(--border-light);

    transform:translateY(-2px);

}

.calendar-day.other-month{

    opacity:.35;

}

.calendar-day.today{

    background:rgba(200, 164, 110,.14);

    border-color:rgba(200, 164, 110,.4);

}

.day-number{

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.calendar-day.weekend .day-number{

    color:var(--text-muted);

}

.calendar-day.today .day-number{

    display:flex;
    align-items:center;
    justify-content:center;

    width:22px;
    height:22px;

    background:var(--primary);

    border-radius:50%;

    color:#fff;

    font-size:var(--text-xs);

}

.day-dots{

    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:4px;

    margin-top:auto;

}

/* The "+N" summary for a day whose dots were capped — see
   js/modules/calendar.js#dayDotsHtml. */
.day-dot-overflow{

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);
    line-height:1;

    color:var(--text-muted);

}


/* =========================
   Events
========================= */

.event-list{

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

}

.event-item{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3);

    border-radius:var(--radius-medium);

    transition:
        background-color var(--transition),
        transform var(--transition);

}

.event-item:hover{

    background:var(--surface-hover);

    transform:translateX(4px);

}

.event-accent{

    align-self:stretch;

    width:4px;
    flex-shrink:0;

    border-radius:999px;

}

.accent-blurple{ background:var(--primary); }
.accent-info{ background:var(--info); }
.accent-success{ background:var(--success); }
.accent-warning{ background:var(--warning); }
.accent-danger{ background:var(--danger); }
.accent-teal{ background:var(--course-teal); }
.accent-olive{ background:var(--course-olive); }

/* =========================
   Calendar Week view — a real time-grid, replacing the "each day stacks
   its events in document-flow order" version this used to be (an 8am
   event and an 8pm event looked identical — no relationship to
   time-of-day at all). Hours down the left, absolutely-positioned event
   blocks placed/sized from real start/end times — see
   js/modules/calendar.js's weekViewHtml/layoutDayEvents for the geometry.
   Every position here is a pixel value computed in JS from one shared
   constant (GRID_HOUR_HEIGHT), not independently guessed per rule, so the
   header, hour labels, gridlines, and event blocks all stay in lockstep.
========================= */

.week-timegrid{

    overflow-x:auto;

}

.week-timegrid-header,
.week-timegrid-body{

    display:grid;
    grid-template-columns:52px repeat(7,1fr);

    min-width:640px;

}

.week-timegrid-header{

    position:sticky;
    top:0;
    z-index:2;

    background:var(--surface);

}

.week-timegrid-day-header{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:2px;

    padding:8px 4px;

    border-bottom:1px solid var(--border);
    border-left:1px solid var(--border);

}

.week-timegrid-day-name{

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:.5px;

}

.week-timegrid-day-num{

    color:var(--text);

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.week-timegrid-day-header.today .week-timegrid-day-num{

    display:flex;
    align-items:center;
    justify-content:center;

    width:22px;
    height:22px;

    background:var(--primary);
    border-radius:50%;

    color:#fff;

}

.week-timegrid-hours{

    position:relative;

}

.week-timegrid-hour-label{

    position:absolute;
    right:8px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-variant-numeric:tabular-nums;

}

.week-timegrid-columns{

    display:grid;
    grid-template-columns:repeat(7,1fr);

    grid-column:2 / -1;

}

.week-timegrid-column{

    position:relative;

    border-left:1px solid var(--border);

}

.week-timegrid-column.today{

    background:rgba(200, 164, 110,.05);

}

.week-timegrid-hourline{

    position:absolute;
    left:0;
    right:0;

    border-top:1px solid var(--border);

}

.week-timegrid-event{

    position:absolute;

    overflow:hidden;

    padding:3px 6px;

    background:var(--surface-hover);
    border-left:3px solid;
    border-radius:6px;

    font-size:var(--text-2xs);
    line-height:1.3;

    transition:background-color var(--transition-fast);

}

.week-timegrid-event.compact{

    display:flex;
    align-items:center;

    padding:2px 6px;

}

.week-timegrid-event[data-event-id]{

    cursor:pointer;

}

.week-timegrid-event[data-event-id]:hover{

    background:var(--surface-light);

}

.week-timegrid-event-time{

    display:block;

    color:var(--text-muted);
    font-weight:var(--weight-semibold);

}

.week-timegrid-event-title{

    display:block;

    overflow:hidden;

    color:var(--text);

    font-weight:var(--weight-medium);
    text-overflow:ellipsis;
    white-space:nowrap;

}

/* "Right now," on today's column only — the one place this grid earns an
   accent color for something that isn't progress/AI/achievement (the
   Rarity Rule's five roles): danger, not gold, reused the same way a
   status color would be for "you are here" rather than borrowing brand
   identity for a live-position marker. */
.week-timegrid-now-line{

    position:absolute;
    left:0;
    right:0;

    height:2px;

    background:var(--danger);

    z-index:1;

}

.week-timegrid-now-line::before{

    content:"";

    position:absolute;
    left:-4px;
    top:50%;

    width:8px;
    height:8px;

    background:var(--danger);
    border-radius:50%;

    transform:translateY(-50%);

}

/* An event block's location line, under the title — only Today's Timeline
   (js/modules/calendar.js#timelineEventBlockHtml) uses this; Week view's
   blocks are too narrow for a third line to ever fit. */
.week-timegrid-event-sub{

    display:block;

    overflow:hidden;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    text-overflow:ellipsis;
    white-space:nowrap;

}


/* =========================
   Calendar page rebuild (2026-08-12) — hero + Next Up + a single-day
   Timeline in the main column, a rail carrying a mini month-calendar,
   Assignments, and a Nova suggestion. Reuses .rail-panel and friends
   verbatim from the Dashboard's own rail (js/modules/dashboard.js) rather
   than a parallel component set — see DESIGN.md's Calendar section.
========================= */

.calendar-layout{

    display:grid;
    grid-template-columns:1fr 360px;
    gap:var(--space-6);
    align-items:start;

}

.calendar-main{

    display:flex;
    flex-direction:column;
    gap:var(--space-6);

    min-width:0;

}

.calendar-rail{

    display:flex;
    flex-direction:column;
    gap:var(--space-5);

    position:sticky;
    top:var(--space-6);

}

/* ---- Hero ---- */

.calendar-hero{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-6);

    padding:var(--space-6);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

    /* Nova bleeds past the bottom padding on purpose (see
       .calendar-hero-nova below) — this clips that bleed cleanly at the
       card's rounded corners instead of letting it square off past them. */
    overflow:hidden;
    position:relative;

}

.calendar-hero-text{

    min-width:0;

}

.calendar-hero-text .hero-title{

    margin:2px 0 4px;

}

.calendar-hero-text .hero-title strong{

    color:var(--primary-light);

}

.calendar-hero-stats{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-6);

    margin-top:var(--space-5);

}

.calendar-hero-stats .hero-metric{

    display:flex;
    align-items:center;
    gap:10px;

}

.calendar-hero-stats .hero-metric i{

    color:var(--primary);
    font-size:18px;

}

.calendar-hero-stats .hero-metric div{

    display:flex;
    flex-direction:column;

}

.calendar-hero-stats .hero-metric strong{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.calendar-hero-stats .hero-metric span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

/* Real illustrated-Nova asset (assets/nova-figure.png, the full-body art
   — assets/nova-pfp.png is the separate head-only crop used everywhere
   Nova sits in a small circular avatar instead), not the hand-built SVG
   this replaced (2026-08-12) — the owner asked for the actual artwork,
   scaled up large with the bottom cropped by the card edge, per the
   reference image. Originally a solid-black-background PNG with no alpha
   (Format24bppRgb), composited via mix-blend-mode:screen instead of plain
   opacity/z-stacking — screen makes true-black pixels resolve to
   whatever's behind them (the card surface), while Nova's gold rim-light
   and glowing eyes stay bright, reading as a transparent cutout without
   an actual alpha channel. Superseded 2026-08-14: the owner replaced the
   source file with a version that carries a real alpha channel, so this
   card (like the other two hero cards) now uses plain normal
   compositing — no blend mode needed at all, and it composites correctly
   against literally anything behind it (relevant below, where the
   horizon glow sits directly behind Nova — see .calendar-hero-nova img's
   own comment for why that used to matter). */
/* The same real horizon photo used in the sidebar (assets/horizon.png),
   here as an ambient glow bled into the card behind Nova's shoulders —
   direct request, matching the owner's reference (the two images should
   "sit properly on the card together"). mix-blend-mode:screen drops the
   photo's black background the same way it drops Nova's; a radial mask
   fades the photo's hard rectangle edge into nothing so only the bright
   sunburst/horizon-curve glow itself is visible, not a floating photo
   tile. Sits before .calendar-hero-nova in the DOM (both are z-index:auto
   positioned children of the card) specifically so paint order puts it
   behind Nova, per the request. */
.calendar-hero-horizon{

    position:absolute;
    right:-210px;
    bottom:-100px;

    /* Grown 480px -> 640px and the mask loosened, 2026-08-14 — matching
       the owner's reference screenshot directly. See .nova-hero-horizon's
       comment for the fuller sizing/mask story. */
    width:640px;
    max-width:none;
    height:auto;

    mix-blend-mode:screen;
    -webkit-mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);
    mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);

    pointer-events:none;
    user-select:none;

}

@media (max-width:960px){

    .calendar-hero-horizon{

        display:none;

    }

}

.calendar-hero-nova{

    /* Width now matches the img's own rendered width below (zero side
       cropping, direct request 2026-08-12 — the ring and shoulders were
       visibly cut off left/right when this box was narrower than the
       image inside it) and height grew to reveal more of the torso/cape
       below the shoulders, also a direct request, without going all the
       way down to the legs. */
    width:230px;
    height:260px;
    flex-shrink:0;

    align-self:flex-end;
    margin-bottom:calc(-1 * var(--space-6));

    position:relative;
    overflow:hidden;

}

.calendar-hero-nova img{

    position:absolute;
    left:50%;
    /* Negative on purpose — pushes the image down so the crop window's
       top edge clears the head/ring instead of clipping into it, at the
       cost of a bit more cape/leg lost off the bottom. bottom:6px (an
       earlier pass) put it the other way: the ring was noticeably
       clipped at the top. Kept the same top framing as the previous
       (narrower/shorter) box when the container height grew — see the
       .calendar-hero-nova comment above for why. */
    bottom:-33px;
    transform:translateX(-50%);

    width:260px;
    max-width:none;
    height:auto;

    /* No mix-blend-mode — assets/nova-figure.png carries real alpha as
       of 2026-08-14; see .friends-hero-figure img's comment for the full
       story (a derived alpha-approximation asset was needed here for a
       few hours until the owner replaced the source art with a real one,
       then removed). */
    pointer-events:none;
    user-select:none;

}

@media (max-width:960px){

    .calendar-hero-nova{

        display:none;

    }

}

/* ---- Next Up ---- */

.calendar-next-up{

    padding:var(--space-4) var(--space-5);

    background:rgba(200, 164, 110,.08);
    border:1px solid rgba(200, 164, 110,.3);
    border-radius:var(--radius-large);

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.calendar-next-up:hover{

    background:rgba(200, 164, 110,.13);

}

.calendar-next-up-label{

    display:block;

    margin-bottom:var(--space-2);

    color:var(--primary);

    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);
    text-transform:uppercase;
    letter-spacing:.06em;

}

.calendar-next-up-row{

    display:flex;
    align-items:center;
    gap:var(--space-4);

}

.calendar-next-up-time{

    display:flex;
    flex-direction:column;
    align-items:center;
    flex-shrink:0;

    min-width:64px;

}

.calendar-next-up-time strong{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);
    font-variant-numeric:tabular-nums;

}

.calendar-next-up-time span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.calendar-next-up-info{

    flex:1;
    min-width:0;

}

.calendar-next-up-info h3{

    font-size:var(--text-base);
    font-weight:var(--weight-semibold);

}

.calendar-next-up-info p{

    color:var(--text-secondary);
    font-size:var(--text-xs);

}

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

.calendar-timeline-card .card-header{

    flex-wrap:wrap;
    gap:var(--space-3);

}

.calendar-timeline-actions{

    display:flex;
    align-items:center;
    gap:var(--space-4);

}

.calendar-day-nav{

    display:flex;
    align-items:center;
    gap:6px;

}

.primary-btn.small,
.secondary-btn.small{

    padding:7px 14px;

    font-size:var(--text-xs);

}

.calendar-timeline-card .card-footer{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);

}

.calendar-day-timeline{

    display:flex;

    overflow-x:auto;

}

.calendar-day-timeline .week-timegrid-hours{

    width:52px;
    flex-shrink:0;

}

.calendar-day-timeline-column{

    position:relative;
    flex:1;

    min-width:280px;

    margin-left:var(--space-3);
    padding-left:var(--space-3);

    border-left:1px solid var(--border);

}

/* ---- Mini calendar (rail) ---- */

.calendar-mini-cal-nav{

    display:flex;
    gap:2px;

}

.calendar-mini-cal-weekdays{

    display:grid;
    grid-template-columns:repeat(7,1fr);

    margin-bottom:6px;

}

.calendar-mini-cal-weekday{

    text-align:center;

    color:var(--text-muted);
    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

}

.calendar-mini-cal-grid{

    display:grid;
    grid-template-columns:repeat(7,1fr);
    gap:2px;

}

.calendar-mini-cal-day{

    position:relative;

    display:flex;
    align-items:center;
    justify-content:center;

    aspect-ratio:1;

    background:transparent;
    border:none;
    border-radius:8px;

    color:var(--text);
    font-size:var(--text-xs);

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.calendar-mini-cal-day:hover{

    background:var(--surface-hover);

}

.calendar-mini-cal-day.other-month{

    color:var(--text-muted);
    opacity:.45;

}

.calendar-mini-cal-day.today{

    color:var(--primary);
    font-weight:var(--weight-bold);

}

.calendar-mini-cal-day.selected{

    background:var(--primary);
    color:var(--background);
    font-weight:var(--weight-bold);

}

.calendar-mini-cal-dot{

    position:absolute;
    bottom:3px;
    left:50%;

    width:4px;
    height:4px;

    border-radius:50%;

    background:var(--primary);

    transform:translateX(-50%);

}

.calendar-mini-cal-day.selected .calendar-mini-cal-dot{

    background:var(--background);

}

.calendar-mini-cal-dot.multi{

    background:var(--text-secondary);

}

/* ---- Assignments (rail) ---- */

.calendar-rail-assignments{

    display:flex;
    flex-direction:column;
    gap:6px;

}

.calendar-rail-assignment{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:9px 10px;

    border-radius:var(--radius-medium);

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.calendar-rail-assignment:hover{

    background:var(--surface-hover);

}

.calendar-rail-assignment-icon{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:30px;
    height:30px;

    background:var(--surface-hover);
    border-radius:8px;

    color:var(--primary);

}

.calendar-rail-assignment-body{

    flex:1;
    min-width:0;

}

.calendar-rail-assignment-body strong{

    display:block;

    overflow:hidden;

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);
    text-overflow:ellipsis;
    white-space:nowrap;

}

.calendar-rail-assignment-body span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.calendar-rail-assignment-due{

    flex-shrink:0;

    color:var(--text-muted);
    font-size:var(--text-2xs);
    font-weight:var(--weight-medium);

}

.calendar-rail-assignment-due.overdue{

    color:var(--danger);

}

/* ---- Nova suggestion (rail) ---- */

.calendar-rail-nova-actions{

    display:flex;
    gap:var(--space-2);

}

.calendar-rail-nova-actions .secondary-btn,
.calendar-rail-nova-actions .primary-btn{

    flex:1;

    padding:9px 12px;

    font-size:var(--text-xs);

}

@media (max-width:960px){

    .calendar-layout{

        grid-template-columns:1fr;

    }

    .calendar-rail{

        position:static;

    }

}

.event-time{

    min-width:60px;
    flex-shrink:0;

    color:var(--text-secondary);

    font-size:var(--text-xs);

}

.event-info{

    /* Same reasoning as .timeline-content — an unstyled flex child
       otherwise keeps min-width:auto, letting an unbroken long title push
       past .event-item's edge instead of wrapping. */
    min-width: 0;

}

.event-info h4{

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

    overflow-wrap: break-word;

}

.event-info p{

    margin-top:2px;

    color:var(--text-muted);

    font-size:var(--text-xs);

}

.legend-list{

    list-style:none;

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

}

.legend-list li{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    color:var(--text-secondary);

    font-size:var(--text-sm);

}


/* =========================
   Scrollbars
========================= */

::-webkit-scrollbar {

    width: 10px;

}

::-webkit-scrollbar-track {

    background: transparent;

}

::-webkit-scrollbar-thumb {

    background: var(--surface-hover);

    border-radius: 999px;

    border: 2px solid transparent;

    background-clip: padding-box;

}

::-webkit-scrollbar-thumb:hover {

    background: var(--primary);

    background-clip: padding-box;

}


/* =========================
   Chips
========================= */

.chip{

    display:inline-flex;
    align-items:center;
    gap:6px;

    padding:6px 12px;

    border-radius:999px;

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

}

.chip-success{

    background:rgba(111, 174, 127,.15);

    color:var(--success);

}

.chip-warning{

    background:rgba(200, 164, 110,.18);

    color:var(--warning);

}

.chip-danger{

    background:rgba(224, 104, 90,.15);

    color:var(--danger);

}

.chip-focus{

    background:rgba(200, 164, 110,.16);

    color:var(--primary-light);

}

.board-card-focus{

    border:1px solid rgba(200, 164, 110,.4);
    box-shadow:0 0 0 1px rgba(200, 164, 110,.15);

}


/* =========================
   Achievements / badges
========================= */

.xp-summary-header{

    display:flex;
    justify-content:space-between;
    align-items:baseline;

    margin-bottom:var(--space-2);

    font-size:var(--text-sm);

}

.badge-grid{

    display:grid;
    grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));
    gap:var(--space-4);

    margin-top:var(--space-4);

}

.badge-tile{

    display:flex;
    flex-direction:column;
    align-items:center;
    text-align:center;
    gap:var(--space-2);

    padding:var(--space-4);

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-medium);

}

.badge-tile .icon{

    font-size:var(--icon-lg, 28px);

}

.badge-tile p{

    font-size:var(--text-xs);

    color:var(--text-secondary);

}

.badge-tile.unlocked{

    border-color:var(--primary);

    box-shadow:var(--shadow-glow, 0 0 16px rgba(200, 164, 110,.25));

}

.badge-tile.unlocked .icon{

    color:var(--primary);

}

.badge-tile.unlocked .badge-date{

    font-size:var(--text-xs);

    color:var(--text-muted);

}

.badge-tile.locked{

    opacity:.45;
    filter:grayscale(1);

}


/* =========================
   Sidebar — manual collapse
========================= */

.sidebar.is-collapsed{

    width:90px;

}

.sidebar.is-collapsed .sidebar-title,
.sidebar.is-collapsed .semester-card,
.sidebar.is-collapsed .pinned-classes,
.sidebar.is-collapsed .btn-label,
.sidebar.is-collapsed .sidebar-profile-info{

    display:none;

}

.sidebar.is-collapsed .nav-btn,
.sidebar.is-collapsed .action-btn{

    justify-content:center;

    padding:14px;

}

.sidebar.is-collapsed .sidebar-profile{

    justify-content:center;

}

.sidebar.is-collapsed .sidebar-group-header{

    justify-content:center;

}


/* =========================
   Modal
========================= */

.modal-overlay{

    position:fixed;
    inset:0;

    z-index:var(--z-modal);

    display:flex;
    align-items:center;
    justify-content:center;

    padding:var(--space-6);

    background:rgba(13, 14, 17,.55);

    backdrop-filter:blur(4px);
    -webkit-backdrop-filter:blur(4px);

    animation:overlayFade .2s ease both;

}

/* A bit more dim + blur than the default overlay — this one specifically
   needs to read as "everything else just stepped back" (see
   settings.js#openPhotoConfirmModal), not just a generic dialog. */
.photo-confirm-overlay{

    background:rgba(13, 14, 17,.7);

    backdrop-filter:blur(6px);
    -webkit-backdrop-filter:blur(6px);

}

.photo-confirm-preview{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-4) 0;

}

.photo-confirm-preview img{

    width:200px;
    height:200px;

    border-radius:50%;
    object-fit:cover;

    box-shadow:var(--shadow-large), 0 0 0 4px var(--primary);

}

/* =========================
   Photo crop — pan + zoom before an uploaded photo becomes a profile
   picture. The viewport itself is the circular crop window (overflow
   hidden + a circle radius), so whatever's visible there is exactly what
   gets exported — see settings.js#openPhotoCropModal.
========================= */

.photo-crop-wrap{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-2) 0;

}

.photo-crop-viewport{

    position:relative;

    width:280px;
    height:280px;

    overflow:hidden;
    border-radius:50%;

    background:#100f0d;

    cursor:grab;
    touch-action:none;

    box-shadow:0 0 0 4px rgba(200, 164, 110,.35), var(--shadow-large);

}

.photo-crop-viewport.dragging{

    cursor:grabbing;

}

.photo-crop-viewport img{

    position:absolute;
    top:0;
    left:0;

    max-width:none;

    transform-origin:top left;

    user-select:none;
    -webkit-user-drag:none;

}

.photo-crop-zoom-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    width:100%;
    max-width:280px;

    color:var(--text-secondary);

}

.photo-crop-zoom-row input[type="range"]{

    flex:1;

    accent-color:var(--primary);

}

.photo-crop-hint{

    text-align:center;
    font-size:var(--text-xs);

}

.modal{

    width:100%;
    max-width:520px;
    max-height:calc(100vh - var(--space-10));

    display:flex;
    flex-direction:column;

    background:var(--surface-light);

    border:1px solid var(--border-light);
    border-radius:var(--radius-large);

    box-shadow:var(--shadow-large);

    overflow:hidden;

    animation:modalPop .28s var(--ease-bounce) both;

}

.modal.modal-lg{

    max-width:680px;

}

/* Set by ui.js#closeModal right before it starts the exit animation; the
   overlay is removed once modalPopOut finishes (or a 200ms safety timeout
   fires, in case animationend doesn't for any reason). pointer-events:none
   stops a stray click landing during that last fading moment. */
.modal-overlay.is-closing{

    animation: overlayFadeOut .15s ease-in both;

    pointer-events: none;

}

.modal-overlay.is-closing .modal{

    animation: modalPopOut .15s ease-in both;

}

/* ---- Overdrive: modal shared-element morph (View Transitions API) ------
   Set by ui.js#openModal only when a real trigger element was given and
   the browser supports document.startViewTransition (see #morphSupported)
   — Chromium and Safari today, plain progressive enhancement everywhere
   else. The real modal is invisible for the transition's duration (the
   browser substitutes its own captured old/new snapshots), so the ordinary
   modalPop/overlayFade entrance is turned off here rather than left to
   race a transition that already owns the moment. */
.modal-overlay.vt-active,
.modal-overlay.vt-active .modal{

    animation: none;

}

/* The browser auto-generates ::view-transition-group(modal-morph) to hold
   the old→new interpolation (position, size, opacity) once both the
   trigger and the modal have carried view-transition-name:modal-morph at
   the right moments — this only retunes ITS timing to the app's own
   settle-in feel (--ease-bounce is this file's exponential-decel curve,
   not literal elastic, despite the name) rather than the browser's
   generic default. */
::view-transition-group(modal-morph){

    animation-duration: .32s;
    animation-timing-function: var(--ease-bounce);

}

/* Scoped to ui.js's withMorph (see its comment) — the rest of the page
   should hold still while just the trigger→modal group morphs, not
   crossfade along with it. Deliberately NOT a blanket rule on
   ::view-transition-old(root)/::view-transition-new(root): those same
   pseudo-elements carry this app's actual cross-document page-to-page
   navigation transition (page-transition-in/out, further down this file),
   and an unscoped version here would silently disable that too. */
html.vt-modal-scope::view-transition-old(root),
html.vt-modal-scope::view-transition-new(root){

    animation: none;
    mix-blend-mode: normal;

}

/* ---- Overdrive: command palette (⌘K / Ctrl+K) --------------------------
   common.js#initCommandPalette. Anchored near the top rather than centered
   like .modal-overlay — a command palette reads as "summoned over your
   current place," not as a dialog interrupting it; centering it would
   borrow the wrong metaphor. Higher z-index than modals so ⌘K still works
   to jump away even with one open. */
.command-palette-overlay{

    position: fixed;
    inset: 0;

    z-index: calc(var(--z-modal) + 10);

    display: flex;
    justify-content: center;
    align-items: flex-start;

    padding: 12vh var(--space-6) var(--space-6);

    background: rgba(13, 14, 17,.55);

    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);

    animation: overlayFade .18s ease both;

}

.command-palette-overlay.is-closing{

    animation: overlayFadeOut .15s ease-in both;

    pointer-events: none;

}

.command-palette{

    width: 100%;
    max-width: 560px;
    max-height: 60vh;

    display: flex;
    flex-direction: column;

    background: var(--surface-light);

    border: 1px solid var(--border-light);
    border-radius: var(--radius-large);

    box-shadow: var(--shadow-large);

    overflow: hidden;

    animation: modalPop .22s var(--ease-bounce) both;

}

.command-palette-input-row{

    display: flex;
    align-items: center;
    gap: var(--space-3);

    padding: var(--space-4) var(--space-5);

    border-bottom: 1px solid var(--border);

    color: var(--text-muted);
    font-size: var(--icon-md);

}

.command-palette-input{

    flex: 1;

    background: transparent;
    border: none;
    outline: none;

    color: var(--text);
    font-family: var(--font-body);
    font-size: var(--text-base);

}

.command-palette-input::placeholder{

    color: var(--text-muted);

}

.command-palette-input-row kbd{

    padding: 2px 7px;

    background: var(--surface-hover);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-small);

    color: var(--text-muted);
    font-family: var(--font-body);
    font-size: var(--text-xs);

}

.command-palette-list{

    overflow-y: auto;
    padding: var(--space-2);

}

.command-palette-item{

    display: flex;
    align-items: center;
    gap: var(--space-3);

    padding: var(--space-3) var(--space-3);

    border-radius: var(--radius-medium);

    color: var(--text-secondary);

    cursor: pointer;

    transition: background-color var(--transition-fast), color var(--transition-fast);

}

.command-palette-item .icon{

    flex-shrink: 0;

    color: var(--text-muted);
    font-size: var(--icon-md);

}

.command-palette-item-title{

    color: var(--text);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);

}

.command-palette-item-subtitle{

    margin-left: auto;

    color: var(--text-muted);
    font-size: var(--text-xs);

    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;

}

.command-palette-item.active,
.command-palette-item:hover{

    background: var(--surface-hover);

}

.command-palette-item.active .icon{

    color: var(--primary);

}

.command-palette-empty{

    padding: var(--space-6) var(--space-4);

    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;

}

.modal-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding:var(--space-5) var(--space-6);

    border-bottom:1px solid var(--border);

}

.modal-header h3{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.modal-body{

    padding:var(--space-6);

    overflow-y:auto;

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

}

.modal-footer{

    display:flex;
    justify-content:flex-end;
    gap:var(--space-3);

    padding:var(--space-5) var(--space-6);

    border-top:1px solid var(--border);

}

.modal-body textarea.text-input{

    resize:vertical;

    min-height:100px;

    font-family:inherit;

}


/* =========================
   Toasts
========================= */

.toast-stack{

    position:fixed;
    right:var(--space-6);
    bottom:var(--space-6);

    z-index:var(--z-toast);

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

    pointer-events:none;

}

.toast{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    min-width:260px;
    max-width:360px;
    padding:var(--space-3) var(--space-4);

    background:var(--surface-light);

    /* The left accent is functional here (color-codes toast severity via
       the .toast-success/-danger/-info/-warning/-xp variants below), not
       decorative — kept, just thinned for a more instrument-precise feel. */
    border:1px solid var(--border-light);
    border-left:2px solid var(--primary);
    border-radius:var(--radius-medium);

    box-shadow:var(--shadow-medium);

    color:var(--text);
    font-size:var(--text-sm);

    /* .toast-stack sets pointer-events:none (so its own empty space never
       blocks clicks on whatever's underneath) — without this, an
       individual toast inherited that too, and the new Undo button
       (below) would render but never actually be clickable. Timing
       itself (the animation shorthand) is set inline per-toast now, not
       here — see js/core/ui.js#toast, duration varies with message
       length as of 2026-08-16. */
    pointer-events:auto;

}

.toast-text{
    flex:1;
}

/* 2026-08-16 direct request: a real Undo affordance on toasts that
   confirm an action the user didn't just explicitly click "confirm" on
   themselves (js/core/ui.js#toast's own opts.onUndo doc comment covers
   which toasts qualify) — a small ghost-style button rather than a
   second .primary-btn/.secondary-btn, so it stays legible against every
   toast-*  accent color without needing its own per-variant palette. */
.toast-undo{

    flex-shrink:0;

    padding:4px 10px;

    background:transparent;
    border:1px solid var(--border-light);
    border-radius:var(--radius-medium);

    color:var(--primary-light);
    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    cursor:pointer;

    transition:background-color var(--transition-fast), border-color var(--transition-fast);

}

.toast-undo:hover{
    background:var(--surface-hover);
    border-color:var(--primary);
}

.toast.toast-success{ border-left-color:var(--success); }
.toast.toast-danger{ border-left-color:var(--danger); }
.toast.toast-info{ border-left-color:var(--info); }
.toast.toast-warning{ border-left-color:var(--warning); }
.toast.toast-xp{ border-left-color:var(--accent-lime); }

.confetti-canvas{

    position:fixed;
    inset:0;

    z-index:var(--z-toast);

    pointer-events:none;

}


/* =========================
   Dropdown Panel (search / notifications / filters)
========================= */

.dropdown-panel{

    position:absolute;
    top:calc(100% + 10px);

    z-index:var(--z-dropdown);

    width:340px;
    max-height:420px;

    display:flex;
    flex-direction:column;

    background:var(--surface-light);

    border:1px solid var(--border-light);
    border-radius:var(--radius-medium);

    box-shadow:var(--shadow-large);

    overflow:hidden;

    animation:dropdownIn .16s ease both;

}

.dropdown-panel.align-right{

    right:0;

}

.dropdown-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

    padding:var(--space-3) var(--space-4);

    border-bottom:1px solid var(--border);

    color:var(--text-secondary);

    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:.5px;

}

.dropdown-list{

    overflow-y:auto;

}

.dropdown-item{

    display:flex;
    flex-direction:column;
    gap:2px;

    width:100%;

    padding:var(--space-3) var(--space-4);

    background:transparent;
    border:none;

    text-align:left;
    text-decoration:none;

    color:var(--text);

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.dropdown-item:hover{

    background:var(--surface-hover);

}

.dropdown-item strong{

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.dropdown-item span{

    color:var(--text-muted);

    font-size:var(--text-xs);

}

.dropdown-empty{

    padding:var(--space-5) var(--space-4);

    color:var(--text-muted);

    font-size:var(--text-sm);

    text-align:center;

}


/* =========================
   Search-deep-link highlight
========================= */

.highlight-target{

    animation:highlightPulse 1.6s ease;

}


/* =========================
   Icon button active state (pin / toggle)
========================= */

.icon-btn.active{

    color:var(--warning);

    background:rgba(200, 164, 110,.15);

}


/* =========================
   Utilities
========================= */

.text-muted {
    color: var(--text-muted);
}

.text-secondary {
    color: var(--text-secondary);
}


/* =========================
   Animations
========================= */

/* This section used to redeclare .card{ animation: cardEntrance ... }
   here — same selector, same specificity as the real .card rule up in
   Cards, just later in the file, so it silently won the cascade and
   replaced that rule's card-pop-in (spring rise+scale) with a flatter
   opacity-only fade for every .card, .card-primary, and .card-ai element
   in the app. It also meant .card-primary's primaryGlow and .card-ai's
   aiGlow (both keyframes below, now removed) never ran despite being
   declared — dead code masquerading as a live "always-pulsing" card.
   Removed rather than fixed forward: card-pop-in is the better entrance
   and the app is quieter without an infinite glow either way. */

@keyframes notificationPing {

    0% { transform: scale(1); opacity: .7; }

    100% { transform: scale(2.4); opacity: 0; }

}

@keyframes typingBounce {

    0%, 60%, 100% { transform: translateY(0); opacity: .4; }

    30% { transform: translateY(-6px); opacity: 1; }

}

@keyframes overlayFade {

    from { opacity: 0; }

    to { opacity: 1; }

}

@keyframes modalPop {

    0% { opacity: 0; transform: scale(.94) translateY(10px); }

    60% { opacity: 1; transform: scale(1.02) translateY(0); }

    100% { opacity: 1; transform: scale(1) translateY(0); }

}

/* Every modal in the app arrived via overlayFade + modalPop's authored
   settle-in — but closeModal() used to just call .remove() with no exit
   motion at all, so a dialog you spent 280ms watching arrive vanished in a
   single frame. These are the departure halves: faster than the entrance
   (per motion timing convention, exits read as latency if they linger) and
   accelerating away (ease-in) rather than settling — decelerating into
   place makes sense for an arrival, not a exit. See ui.js#closeModal. */
@keyframes overlayFadeOut {

    from { opacity: 1; }

    to { opacity: 0; }

}

@keyframes modalPopOut {

    from { opacity: 1; transform: scale(1) translateY(0); }

    to { opacity: 0; transform: scale(.96) translateY(6px); }

}

@keyframes dropdownIn {

    from { opacity: 0; transform: translateY(-6px); }

    to { opacity: 1; transform: translateY(0); }

}

@keyframes toastIn {

    0% { opacity: 0; transform: translateX(40px) scale(.9); }

    70% { opacity: 1; transform: translateX(-4px) scale(1.02); }

    100% { opacity: 1; transform: translateX(0) scale(1); }

}

@keyframes toastOut {

    from { opacity: 1; }

    to { opacity: 0; }

}

@keyframes highlightPulse {

    0% { box-shadow: 0 0 0 0 rgba(200, 164, 110,.35); }

    70% { box-shadow: 0 0 0 10px rgba(200, 164, 110,0); }

    100% { box-shadow: 0 0 0 0 rgba(200, 164, 110,0); }

}


/* =========================
   Responsive - Laptop
========================= */

@media (max-width: 1400px){

    .supporting-grid{

        grid-template-columns:repeat(2,1fr);

    }

}


/* =========================
   Responsive - Tablet
========================= */

@media (max-width:1000px){

    .sidebar{

        width:90px;

    }

    .logo-text,
    .sidebar-logo-text,
    .sidebar-title,
    .collapse-btn,
    .semester-card,
    .pinned-classes,
    .btn-label,
    .sidebar-profile-info{

        display:none;

    }

    .sidebar-profile,
    .sidebar-logo{

        justify-content:center;

    }

    .nav-btn,
    .action-btn{

        justify-content:center;

        padding:14px;

    }

    .content{

        padding:var(--space-6);

    }

    .supporting-grid{

        grid-template-columns:1fr;

    }

    .header{

        padding:0 var(--space-5);

    }

    .search-bar{

        width:220px;

    }

    .search-bar:focus{

        width:260px;

    }

    .header-right{

        gap:var(--space-2);

    }

    .nova-threads{

        width:220px;

    }

}


/* =========================
   Responsive - Mobile
========================= */

@media (max-width:700px){

    .header{

        flex-wrap:wrap;

        height:auto;

        gap:var(--space-4);

        padding:var(--space-4);

    }

    .header-nav{

        width:100%;

    }

    .search-bar{

        width:100%;

    }

    .search-bar:focus{

        width:100%;

    }

    .header-right{

        width:100%;

        justify-content:flex-end;

    }

    .profile-info{

        display:none;

    }

    .xp-chip-info{

        display:none;

    }

    .container{

        flex-direction:column;

    }

    .sidebar{

        width:100%;
        height:auto;

        flex-direction:row;

        overflow-x:auto;
        overflow-y:hidden;

        /* This row scrolls (7 nav items don't fit at 390px — Classes and
           Settings were measured fully off-screen), but nothing hinted
           that: no arrow, no gradient, no visible scrollbar. A fading
           edge is the standard, low-risk affordance for "this scrolls" —
           doesn't require restructuring the nav itself, degrades to
           fully-visible content on browsers without mask-image support. */
        -webkit-mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
        mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);

    }

    .sidebar-logo{

        margin-bottom:0;
        margin-right:var(--space-2);

        padding:14px;

    }

    .sidebar-top{

        flex-direction:row;

        /* Resets the desktop-only internal-scroll fix (2026-08-15,
           .sidebar-top's own comment) back to visible here — at this
           width the *parent* .sidebar is what scrolls horizontally
           (flex-direction:row + overflow-x:auto, above), carrying this
           row's overflowing nav items along with it. overflow-x:hidden
           inherited from the desktop rule would clip those items inside
           this row instead of letting the parent's own scroll reach
           them — the exact "Resources and Settings measured fully off-
           screen" scroll this mask-image comment already exists for. */
        overflow:visible;

    }

    .sidebar-bottom{

        display:none;

    }

    .hero-title{

        font-size:var(--text-section);

    }

    .page-toolbar-title{

        font-size:var(--text-section);

    }

    .card{

        padding:var(--space-5);

    }

    .card-header{

        flex-direction:column;
        align-items:flex-start;

        gap:var(--space-3);

    }

    .card-actions{

        width:100%;

        justify-content:space-between;

    }

    .section-heading h2{

        font-size:var(--text-title);

    }

    .calendar-day{

        min-height:56px;

        padding:4px;

    }

    .calendar-toolbar{

        flex-wrap:wrap;

        gap:var(--space-3);

    }

    .calendar-month-label{

        min-width:0;

        font-size:var(--text-base);

    }

    .nova-threads{

        display:none;

    }

    .nova-chat-header,
    .nova-messages,
    .nova-suggestions,
    .nova-input-bar{

        padding-left:var(--space-5);

        padding-right:var(--space-5);

    }

    .msg-row{

        max-width:100%;

    }

    .leaderboard-row{

        grid-template-columns:28px auto 1fr;
        grid-template-areas:
            "rank avatar info"
            "actions actions actions";

    }

    .leaderboard-row .leaderboard-rank{ grid-area:rank; }
    .leaderboard-row .leaderboard-avatar{ grid-area:avatar; }
    .leaderboard-row .leaderboard-info{ grid-area:info; }
    .leaderboard-row .friend-level-ring{ display:none; }
    .leaderboard-row .leaderboard-streak{ display:none; }

    .leaderboard-row .leaderboard-actions{

        grid-area:actions;

        justify-content:flex-end;

        margin-top:var(--space-2);

    }

    .suggestion-row{

        flex-wrap:wrap;

    }

    /* .settings-layout's 240px+1fr grid was never given a narrow-viewport
       collapse — at 390px the 1fr content column was being squeezed to a
       sliver, clipping form fields and labels rather than wrapping them.
       Stacking to one column also means the tab list needs its own
       `position:sticky;top:0` turned off: sticky only made sense sitting
       *beside* scrolling content in the two-column layout, and left
       un-set here it would compete with the app header (also sticky at
       top:0) for the same spot the moment you scroll past it. */
    .settings-layout{

        grid-template-columns:1fr;

    }

    /* Horizontal scroll + edge fade, not wrap — seven tabs wrapped into a
       multi-row block reads as clutter; this instead reuses the exact
       pattern .sidebar already uses to solve the identical "too many nav
       items, one narrow row" problem on mobile, down to the same fade. */
    .settings-tabs{

        position:static;

        flex-direction:row;

        overflow-x:auto;
        overflow-y:hidden;

        -webkit-mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);
        mask-image: linear-gradient(to right, transparent 0, black 24px, black calc(100% - 24px), transparent 100%);

    }

    .settings-tab{

        flex-shrink:0;

    }

}


/* ---- People You May Know — real friends-of-friends suggestions
   (server/services/friendsService.js#suggestions). */

.suggestion-list{

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

    margin-bottom:var(--space-2);

}

.suggestion-row{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-3) var(--space-5);

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.suggestion-avatar{

    width:40px;
    height:40px;

    font-size:14px;

    flex-shrink:0;

}

.suggestion-info{

    flex:1 1 auto;

    min-width:0;

}

.suggestion-info h4{

    font-weight:var(--weight-semibold);

}

.suggestion-info p{

    margin:0;

    font-size:var(--text-xs);

}


/* ---- Activity — the Activity tab's feed (see friends.html's comment):
   every badge unlock across you and your accepted friends, newest first.
   Plain rows, same "quiet by default, hover to notice" language as
   .leaderboard-row/.suggestion-row rather than a new visual idiom. */

.activity-feed{

    display:flex;
    flex-direction:column;
    gap:2px;

}

.activity-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3) var(--space-2);

    border-radius:var(--radius-medium);

    transition:background-color var(--transition-fast);

}

.activity-row:hover{

    background:var(--surface-hover);

}

.activity-avatar{

    flex-shrink:0;

}

.activity-info{

    display:flex;
    flex-direction:column;
    gap:2px;

    min-width:0;

}

.activity-info p{

    margin:0;

    color:var(--text);

    font-size:var(--text-sm);

}

.activity-info .text-muted{

    font-size:var(--text-2xs);

}

/* Gold here is sanctioned — "achievement" is one of the Rarity Rule's five
   roles, same reasoning as the XP chip and streak-chip-hot. */
.activity-badge-name{

    display:inline-flex;
    align-items:center;
    gap:4px;

    color:var(--primary-light);
    font-weight:var(--weight-semibold);

}

.activity-badge-name .icon{

    font-size:var(--icon-sm);

}


/* =========================
   Progress — new page (see DESIGN.md's Progress section for the full
   palette-validation writeup). Two real charts, hand-built in plain SVG/
   HTML (no charting library, consistent with this app's no-build-step
   constraint) plus five stat tiles, all driven by js/modules/progress.js.
========================= */

.progress-view-tabs{

    display:flex;
    gap:4px;

    padding:4px;

    background:var(--surface-hover);

    border-radius:999px;

}

.progress-view-tab{

    padding:8px 18px;

    background:transparent;
    border:none;
    border-radius:999px;

    color:var(--text-secondary);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

    cursor:pointer;

    transition:
        background-color var(--transition-fast),
        color var(--transition-fast);

}

.progress-view-tab.active{

    background:var(--primary);

    color:var(--background);
    font-weight:var(--weight-semibold);

}

/* ---- Stat tiles ---- */

.stat-tile-row{

    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));
    gap:var(--space-4);

    margin-bottom:var(--space-6);

}

.stat-tile{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-4) var(--space-5);

    background:var(--surface);

    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.stat-tile-icon{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:40px;
    height:40px;

    background:rgba(200, 164, 110,.12);
    border-radius:var(--radius-medium);

    color:var(--primary-light);

    font-size:var(--icon-md);

}

.stat-tile-value{

    display:block;

    color:var(--text);

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.stat-tile-label{

    display:block;

    color:var(--text-secondary);

    font-size:var(--text-xs);

}

.stat-tile-caption{

    display:block;

    color:var(--text-muted);

    font-size:var(--text-2xs);

}

/* ---- Shared chart-card wrapper ---- */

.progress-chart-card{

    margin-bottom:var(--space-6);

}

/* ---- Bar chart: Deep Work Time ---- */

.progress-bar-chart{

    display:flex;
    align-items:flex-end;
    gap:10px;

    padding-top:var(--space-4);

}

.progress-bar-col{

    display:flex;
    flex-direction:column;
    align-items:center;
    gap:var(--space-2);

    height:100%;

}

.progress-bar-track{

    display:flex;
    align-items:flex-end;

    width:100%;
    flex:1 1 auto;

}

.progress-bar-fill{

    width:100%;

    background:var(--primary);

    border-radius:4px 4px 0 0;

    cursor:default;

    transition:
        height var(--dur-slow) var(--ease-spring),
        background-color var(--transition-fast);

}

.progress-bar-fill:hover{

    background:var(--primary-hover);

}

/* Today's bucket (or the bucket touching today, in Month/Term views) — a
   brighter fill, not a second hue, so it still reads as one sequential
   series with today emphasized rather than a second category. */
.progress-bar-fill.current{

    background:var(--primary-light);

    box-shadow:0 0 0 2px var(--background), 0 0 0 3px var(--primary);

}

.progress-bar-label{

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-variant-numeric:tabular-nums;

}

.progress-bar-label.current{

    color:var(--primary-light);
    font-weight:var(--weight-semibold);

}

/* ---- Donut chart: Focus Distribution ---- */

.progress-donut-layout{

    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:var(--space-8);

    padding-top:var(--space-4);

}

.progress-donut-svg-wrap{

    position:relative;
    flex-shrink:0;

    width:180px;
    height:180px;

}

.progress-donut-svg-wrap svg{

    width:100%;
    height:100%;

}

.progress-donut-arc{

    cursor:default;

    transition:opacity var(--transition-fast);

}

.progress-donut-arc:hover{

    opacity:.8;

}

.progress-donut-center{

    position:absolute;
    inset:0;

    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;

    pointer-events:none;

}

.progress-donut-center strong{

    color:var(--text);

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.progress-donut-center span{

    color:var(--text-muted);

    font-size:var(--text-2xs);

    text-transform:uppercase;
    letter-spacing:.5px;

}

.progress-donut-legend{

    display:flex;
    flex-direction:column;
    gap:var(--space-3);

    flex:1 1 200px;

}

.progress-donut-legend-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

}

.progress-donut-swatch{

    display:inline-block;
    flex-shrink:0;

    width:10px;
    height:10px;

    border-radius:3px;

}

.progress-donut-legend-name{

    flex:1 1 auto;

    color:var(--text);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);

}

.progress-donut-legend-value{

    color:var(--text-secondary);

    font-size:var(--text-xs);
    font-variant-numeric:tabular-nums;

}

.progress-donut-table-wrap{

    padding-top:var(--space-4);

}

.progress-donut-table-wrap .classes-table td:first-child{

    display:flex;
    align-items:center;
    gap:var(--space-2);

}

/* ---- Shared hover tooltip (bar chart + donut) ---- */

.progress-tooltip{

    position:fixed;
    z-index:var(--z-tooltip);

    padding:6px 10px;

    background:var(--surface-hover);
    border:1px solid var(--border-light);
    border-radius:var(--radius-small);

    color:var(--text);

    font-size:var(--text-xs);
    font-variant-numeric:tabular-nums;

    pointer-events:none;

    box-shadow:var(--shadow-medium);

}


/* =========================
   Resources — new page (js/modules/resources.js). A per-class link
   library: one .resource-group "folder" per course (matching the
   sidebar's folder icon), General last for anything not tied to a class.
   Rows reuse the same "quiet by default, hover to notice" language as
   .task-row/.leaderboard-row rather than a new one.
========================= */

.resource-group{

    margin-bottom:var(--space-6);

}

.resource-group-header{

    display:flex;
    align-items:center;
    gap:var(--space-2);

    margin-bottom:var(--space-2);

}

.resource-group-header h3{

    color:var(--text);

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.resource-group-header .text-muted{

    font-size:var(--text-2xs);

}

.resource-group-icon{

    display:flex;
    align-items:center;
    justify-content:center;

    width:8px;
    height:8px;

    color:var(--text-muted);

    font-size:10px;

}

.resource-list{

    display:flex;
    flex-direction:column;
    gap:2px;

}

.resource-list-empty{

    padding:var(--space-3) var(--space-2);

}

.resource-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    padding:var(--space-3) var(--space-2);

    border-radius:var(--radius-medium);

    transition:background-color var(--transition-fast);

}

.resource-row:hover{

    background:var(--surface-hover);

}

.resource-row-icon{

    display:flex;
    align-items:center;
    justify-content:center;
    flex-shrink:0;

    width:36px;
    height:36px;

    background:var(--surface-hover);
    border-radius:var(--radius-medium);

    color:var(--text-secondary);

    font-size:var(--icon-sm);

}

.resource-row-body{

    flex:1 1 auto;

    min-width:0;

}

.resource-row-title{

    display:block;

    overflow:hidden;

    color:var(--text);

    font-size:var(--text-sm);
    font-weight:var(--weight-medium);
    text-decoration:none;
    text-overflow:ellipsis;
    white-space:nowrap;

}

.resource-row-title:hover{

    color:var(--primary-light);
    text-decoration:underline;

}

.resource-row-category{

    display:block;

    margin-top:2px;

    color:var(--text-muted);

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

    text-transform:uppercase;
    letter-spacing:.4px;

}


/* =========================
   Friends — rebuilt 2026-08-12 as a gamified circle dashboard against an
   owner-supplied reference. Replaces the old Circle/Leaderboard/Activity
   tabs entirely — see DESIGN.md's Friends section for what backs each new
   piece (presence, real-time friend status, group focus sessions, and the
   weekly challenge are all real backend features built the same day, not
   fabricated display data).
========================= */

.friends-layout{

    display:grid;
    grid-template-columns:1fr 360px;
    gap:var(--space-6);

}

.friends-main{

    display:flex;
    flex-direction:column;
    gap:var(--space-6);

}

.friends-rail{

    display:flex;
    flex-direction:column;
    gap:var(--space-4);

}

/* ---- Hero: circle streak + Nova ------------------------------------------ */

.friends-hero{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-6);

    overflow:hidden;
    position:relative;

}

.friends-hero-text{

    min-width:0;

}

.friends-hero-title{

    display:flex;
    align-items:baseline;
    gap:10px;
    flex-wrap:wrap;

    margin:4px 0 4px;

    color:var(--text);

    font-family:var(--font-serif);
    font-size:var(--text-2xl);
    font-weight:500;
    letter-spacing:-.01em;

}

.friends-hero-title span[data-stat]{

    color:var(--primary);

}

.friends-hero-flame{

    font-size:var(--text-xl);

}

.friends-hero-stats{

    display:flex;
    flex-wrap:wrap;
    gap:var(--space-6);

    margin:var(--space-5) 0;

}

.friends-hero-stats .hero-metric{

    display:flex;
    align-items:center;
    gap:10px;

}

.friends-hero-stats .hero-metric > i{

    color:var(--primary);
    font-size:18px;

}

.friends-hero-stats .hero-metric div{

    display:flex;
    flex-direction:column;

}

.friends-hero-stats .hero-metric strong{

    font-size:var(--text-lg);
    font-weight:var(--weight-bold);

}

.friends-hero-stats .hero-metric span{

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

/* Same asset/technique as Calendar's and Nova's hero Nova (assets/nova-
   figure.png, mix-blend-mode:screen to drop its solid-black background,
   an oversized img cropped by an overflow:hidden wrapper) — see
   .calendar-hero-nova's CSS comment for the full explanation. */
/* Same horizon-photo glow as .calendar-hero-horizon (assets/horizon.png,
   mix-blend-mode:screen + radial mask) — see that card's CSS comment for
   the full explanation. Sits before .friends-hero-figure in the DOM so
   it paints behind Nova. This is the exact card the owner's reference
   photo shows (the "Your Circle" hero). */
.friends-hero-horizon{

    position:absolute;
    right:-190px;
    bottom:-90px;

    /* Grown 430px -> 620px and the mask loosened, 2026-08-14 — matching
       the owner's reference screenshot directly: the reference's glow is
       a big, vivid presence behind Nova, not a faint sliver. See
       .nova-hero-horizon's comment for the fuller sizing/mask story. */
    width:620px;
    max-width:none;
    height:auto;

    mix-blend-mode:screen;
    -webkit-mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);
    mask-image:radial-gradient(ellipse farthest-corner at 66% 72%, black 35%, transparent 88%);

    pointer-events:none;
    user-select:none;

}

@media (max-width:1200px){

    .friends-hero-horizon{

        display:none;

    }

}

/* Grown ~30%, 2026-08-14 — matching the owner's reference screenshot
   directly: Nova reads noticeably bigger and lower in the reference than
   this card previously rendered him. */
.friends-hero-figure{

    width:220px;
    height:260px;
    flex-shrink:0;

    align-self:flex-end;
    margin-bottom:calc(-1 * var(--space-6));

    position:relative;
    overflow:hidden;

}

.friends-hero-figure img{

    position:absolute;
    left:50%;
    /* Re-tuned 2026-08-14 alongside the new art below — see
       .nova-hero-figure img's comment for why (portrait source now,
       was square). */
    bottom:-95px;
    transform:translateX(-50%);

    width:260px;
    max-width:none;
    height:auto;

    /* No mix-blend-mode here, on purpose, and not because this card's own
       photo backdrop makes it optional — assets/nova-figure.png itself
       carries a real alpha channel as of 2026-08-14 (the owner replaced
       the source art directly; the old version had a solid black canvas
       that needed mix-blend-mode:screen or an object.png with real
       alpha to fake transparency). A prior pass here used a locally-
       generated derived asset (nova-figure-alpha.png, a canvas pass that
       approximated alpha from the old art's own brightness) specifically
       to route around a real browser limitation: mix-blend-mode:screen
       doesn't reliably composite against a *sibling* image/gradient's
       actual rendered pixels once .friends-hero-horizon (below) grew
       large and bright enough to sit directly behind Nova — confirmed
       with an isolated repro, not a bug in this file. Real, source-level
       alpha makes that whole workaround unnecessary: it composites
       correctly against literally anything behind it, no blend mode
       needed, and the derived asset was removed once the real one
       existed. */
    pointer-events:none;
    user-select:none;

}

@media (max-width:1200px){

    .friends-hero-figure{

        display:none;

    }

}

.friends-invite-badge{

    display:inline-flex;
    align-items:center;
    justify-content:center;

    min-width:18px;
    height:18px;
    padding:0 5px;
    margin-left:8px;

    background:var(--danger);
    border-radius:999px;

    color:#fff;
    font-size:var(--text-2xs);
    font-weight:var(--weight-bold);

}

/* ---- Section headers (Friends row, Buddy Pacts) --------------------------- */

.friends-section .section-header{

    display:flex;
    align-items:center;
    justify-content:space-between;

    margin-bottom:var(--space-3);

}

.friends-section .section-header .section-label{

    margin-bottom:0;

}

.friends-empty-hint,
.achievements-empty-hint{

    padding:var(--space-5);

    text-align:center;

}

/* ---- Friends row: real status, focus, and daily-goal progress ------------ */

.friends-cards{

    display:grid;
    grid-template-columns:repeat(4, 1fr);
    gap:var(--space-4);

}

.friends-cards-modal{

    grid-template-columns:repeat(2, 1fr);

}

.friend-card{

    position:relative;

    display:flex;
    flex-direction:column;

    padding:var(--space-4);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

    cursor:pointer;

    transition:border-color var(--transition-fast), transform var(--transition-fast);

}

.friend-card:hover{

    border-color:rgba(200, 164, 110,.32);
    transform:translateY(-2px);

}

/* The one friend actually in a live group session gets real visual
   priority — not decoration, a genuine "this is happening right now"
   signal the same way .calendar-timeline-now-line marks the live time. */
.friend-card.is-in-session{

    border-color:var(--status-session);
    box-shadow:0 0 0 1px var(--status-session);

}

.friend-card-nudge{

    position:absolute;
    top:10px;
    right:10px;

    width:26px;
    height:26px;

    display:flex;
    align-items:center;
    justify-content:center;

    background:transparent;
    border:none;
    border-radius:50%;

    font-size:14px;
    line-height:1;

    cursor:pointer;

    transition:background-color var(--transition-fast);

}

.friend-card-nudge:hover{

    background:var(--surface-hover);

}

.friend-card-head{

    display:flex;
    align-items:center;
    gap:var(--space-3);

    margin-bottom:var(--space-3);

}

.friend-card-avatar{

    width:44px;
    height:44px;
    flex-shrink:0;

}

.friend-card-name{

    min-width:0;

}

.friend-card-name h4{

    overflow:hidden;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);
    text-overflow:ellipsis;
    white-space:nowrap;

}

.friend-card-streak{

    display:flex;
    align-items:center;
    gap:4px;

    color:var(--warning);
    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

}

.friend-status{

    display:flex;
    align-items:center;
    gap:5px;

    margin-bottom:var(--space-2);

    font-size:var(--text-xs);
    font-weight:var(--weight-medium);

    color:var(--text-muted);

}

.friend-status i{

    font-size:8px;

}

.friend-status-studying,
.friend-status-online{ color:var(--success); }
.friend-status-in_session{ color:var(--status-session); }
.friend-status-offline{ color:var(--text-muted); }

.friend-card-focus{

    overflow:hidden;

    margin-bottom:var(--space-3);

    color:var(--text-secondary);
    font-size:var(--text-xs);
    text-overflow:ellipsis;
    white-space:nowrap;

}

.friend-card-focus-empty{

    visibility:hidden;

}

.friend-card-goal{

    display:block;

    margin-top:6px;

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

/* ---- Nova Insight + online row --------------------------------------------- */

.friends-insight-card p{

    margin-bottom:var(--space-4);

    font-size:var(--text-sm);
    line-height:1.5;

}

.friends-insight-actions{

    display:flex;
    align-items:center;
    gap:var(--space-4);

    margin-bottom:var(--space-4);

}

.friends-online-row{

    display:flex;
    align-items:center;
    gap:var(--space-3);

}

.friends-online-stack{

    display:flex;

}

.friends-online-avatar{

    width:28px;
    height:28px;

    margin-left:-8px;

    border:2px solid var(--surface);

    font-size:11px;

}

.friends-online-stack .friends-online-avatar:first-child{

    margin-left:0;

}

/* ---- Buddy Pacts (restyled as cards) --------------------------------------- */

.buddy-pact-cards{

    display:flex;
    flex-direction:column;
    gap:var(--space-2);

}

.buddy-pact-card{

    position:relative;

    display:flex;
    align-items:center;
    gap:var(--space-4);

    padding:var(--space-4) var(--space-5);

    background:var(--surface);
    border:1px solid var(--border);
    border-radius:var(--radius-large);

}

.buddy-pact-avatars{

    display:flex;
    flex-shrink:0;

}

.buddy-pact-avatars .avatar{

    width:36px;
    height:36px;

    border:2px solid var(--surface);

    font-size:12px;

}

.buddy-pact-avatars .avatar:last-child{

    margin-left:-14px;

}

.buddy-pact-body{

    flex:1 1 auto;
    min-width:0;

}

.buddy-pact-body h4{

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.buddy-pact-body p{

    margin:2px 0 6px;

    color:var(--text-secondary);
    font-size:var(--text-xs);

}

.buddy-pact-body .progress-bar{

    margin:0;

}

.buddy-pact-streak{

    flex-shrink:0;

}

.buddy-pact-pct{

    flex-shrink:0;

    color:var(--text-muted);
    font-size:var(--text-xs);
    font-weight:var(--weight-semibold);

}

.buddy-pact-chevron{

    flex-shrink:0;

    color:var(--text-muted);

}

.buddy-pact-actions{

    display:flex;
    align-items:center;
    gap:var(--space-2);
    flex-shrink:0;

}

.buddy-pact-card.is-pending{

    border-left:2px solid var(--warning);

}

.buddy-pact-card.is-ended{

    opacity:.7;

}

/* ---- Weekly Challenge ------------------------------------------------------- */

.weekly-challenge-title{

    margin-bottom:2px;

    font-size:var(--text-sm);
    font-weight:var(--weight-semibold);

}

.weekly-challenge-desc{

    margin-bottom:var(--space-3);

    font-size:var(--text-xs);

}

.weekly-challenge-hours{

    display:block;

    margin:6px 0 var(--space-3);

    color:var(--text-muted);
    font-size:var(--text-2xs);

}

.weekly-challenge-foot{

    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:var(--space-3);

}

.weekly-challenge-avatars{

    display:flex;
    align-items:center;

}

.weekly-challenge-avatar{

    width:24px;
    height:24px;

    margin-left:-8px;

    border:2px solid var(--surface);

    font-size:9px;

}

.weekly-challenge-avatars .weekly-challenge-avatar:first-child{

    margin-left:0;

}

.weekly-challenge-avatar-overflow{

    display:flex;
    align-items:center;
    justify-content:center;

    width:24px;
    height:24px;
    margin-left:-8px;

    background:var(--surface-hover);
    border:2px solid var(--surface);
    border-radius:50%;

    color:var(--text-muted);
    font-size:9px;
    font-weight:var(--weight-semibold);

}

/* ---- Achievements (compact rail card) -------------------------------------- */

.achievements-mini-grid{

    display:grid;
    grid-template-columns:repeat(2, 1fr);
    gap:var(--space-3);

}

.achievement-mini-tile{

    display:flex;
    flex-direction:column;
    align-items:center;

    padding:var(--space-3) var(--space-2);

    background:var(--surface-hover);
    border-radius:var(--radius-medium);

    text-align:center;

}

.achievement-mini-icon{

    display:flex;
    align-items:center;
    justify-content:center;

    width:40px;
    height:40px;

    margin-bottom:6px;

    background:rgba(200, 164, 110,.15);
    /* An octagon reads close enough to the reference's hexagon badges
       without hand-authoring a true hex clip-path for a 40px icon nobody
       examines that closely — clip-path support is universal here either
       way. */
    clip-path:polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);

    color:var(--primary);
    font-size:var(--icon-sm);

}

.achievement-mini-tile strong{

    font-size:var(--text-2xs);
    font-weight:var(--weight-semibold);

}

@media (max-width:1100px){

    .friends-layout{

        grid-template-columns:1fr;

    }

    .friends-cards{

        grid-template-columns:repeat(2, 1fr);

    }

}

@media (max-width:640px){

    .friends-cards,
    .friends-cards-modal{

        grid-template-columns:1fr;

    }

    .friends-hero{

        flex-wrap:wrap;

    }

    .friends-hero-title{

        font-size:var(--text-xl);

    }

    /* The rigid single-line row (avatars | name | streak chip | % | chevron)
       has no room left for the name once five siblings are fighting for
       ~350px total — "Sarah & You" was wrapping mid-phrase across three
       lines. Wraps the row instead: avatars+body (flex:1, already sized
       to absorb remaining space) keep the first line, since between them
       they were never the problem. flex-basis:100% on the streak chip
       forces it — and everything after it in source order — onto a second
       line instead, so it's the three fixed-width extras competing with
       body for room that move, not body itself. */
    .buddy-pact-card{

        flex-wrap:wrap;
        row-gap:var(--space-2);

    }

    .buddy-pact-streak{

        flex-basis:100%;

    }

}
