/* ─────────────────────────────────────────────────────────────────────
   Lucrux TYPE SYSTEM — a deliberate identity, self-hosted, NOT the platform's
   default Inter. For a family's study companion: future-facing yet warm,
   scholarly but not cold.
     · Hanken Grotesk — one family carries the app: headings differentiate by
       weight/size (--font-display aliases it), body/UI stay supremely readable.
     · Geist Mono — numerics: clean technical mono for mastery %, credits, tables.
   All woff2 self-hosted under /fonts (no render-blocking @import). font-display:swap
   with a metric-tuned fallback so first paint never stalls or shifts. ────────── */
@font-face {
  font-family: 'Hanken Grotesk'; font-style: normal; font-display: swap;
  font-weight: 100 900; src: url('/fonts/hanken-grotesk-var.woff2') format('woff2');
}
@font-face {
  font-family: 'Hanken Grotesk'; font-style: italic; font-display: swap;
  font-weight: 100 900; src: url('/fonts/hanken-grotesk-italic-var.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist Mono'; font-style: normal; font-display: swap;
  font-weight: 100 900; src: url('/fonts/geist-mono-var.woff2') format('woff2');
}
/* Metric-tuned fallback faces: local Arial / Courier New adjusted to each webfont's
   frequency-weighted advance and vertical metrics, so the swap moves no layout
   (the parent theme does the same for Inter). Numbers derive from the woff2s —
   recompute if a font file changes. */
@font-face {
  font-family: 'Hanken Fallback'; src: local('Arial');
  size-adjust: 101.52%; ascent-override: 98.5%; descent-override: 29.85%; line-gap-override: 0%;
}
@font-face {
  font-family: 'Geist Mono Fallback'; src: local('Courier New');
  size-adjust: 99.98%; ascent-override: 100.52%; descent-override: 29.5%; line-gap-override: 0%;
}

/*
 * able@ableu — default theme
 *
 * Inherits canonical tokens from `system-base-theme` (parent in meta.yaml):
 *   - `--surface-{base,card,subtle,raised,inverse,overlay,hover,active}`
 *   - `--text-{primary,secondary,muted,subtle,inverse,on-accent}`
 *   - `--border-{subtle,default,strong,inverse}`
 *   - `--accent`, `--accent-hover`, `--accent-active`, `--accent-soft`, `--on-accent`
 *   - `--success`, `--danger`, `--warning`, `--info` (each with `-soft` + `--on-X`)
 *   - `--grey-0..--grey-1000`
 *   - `--shadow-{sm,md,lg,xl}`, `--radius-{sm,md,lg,xl,2xl,full}`
 *   - `--space-1..--space-12`
 *   - `--font-{sans,mono,serif}` — Inter self-hosted with `Inter Fallback` for CLS=0
 *   - `--z-{base,sticky,dropdown,fixed,overlay,modal,offcanvas,popover,tooltip,toast}`
 *   - `--transition-{fast,base,slow}`, `--ease-{enter,move,drawer}`
 *
 * See `.claude/rules/27-template-styling.md` cascade model +
 *     `.claude/rules/28b-design-catalog.md` §2.0 canonical vocabulary +
 *     `.claude/rules/26b-site-authoring-with-blueprints.md` §4 anti-patterns.
 *
 * BANNED in this file (per 26b §4):
 *   - Aliasing non-existent `--theme_*` parent tokens — they do NOT exist on
 *     `system-base-theme`. Aliases like `--theme_text: var(--theme_color_text)`
 *     resolve to empty string and every page falls through to UA defaults
 *     (Times New Roman body, white-on-white text, etc.).
 *   - `@import url('https://fonts.googleapis.com/...')` — parent self-hosts
 *     Inter with `Inter Fallback` for CLS=0. Re-importing from CDN is
 *     redundant network cost + reintroduces layout shift on slow links.
 *   - Redeclaring `--space-N` / `--radius-*` / `--shadow-*` (parent already
 *     exposes them).
 *
 * Tenants are encouraged to:
 *   1. Edit the brand palette below to match their identity.
 *   2. Re-bind `--accent` to a brand color (defaults to platform navy).
 *   3. Keep the body rule and AbleClaw chrome rules as-is.
 */

:root {
  /* ────────────────────────────────────────────────
     Brand palette — REPLACE with your own scale.
     A 5-step scale (50, 200, 500, 700, 900) is usually enough.
     ──────────────────────────────────────────────── */
  --brand-50:   #eff6ff;
  --brand-200:  #bfdbfe;
  --brand-500:  #2563eb;
  --brand-700:  #1d4ed8;
  --brand-900:  #1e3a8a;

  /* ────────────────────────────────────────────────
     Brand mapping — bind canonical platform tokens to your
     brand palette. Pages compose with canonical names
     (--accent, --surface-base, --text-primary) — they
     resolve to your brand via this layer.
     ──────────────────────────────────────────────── */
  --accent:        var(--brand-500);
  --accent-hover:  var(--brand-700);
  --accent-active: var(--brand-900);
  --accent-soft:   var(--brand-50);
  --on-accent:     #ffffff;
  /* Accent-coloured TYPE (links, inline actions) — distinct from the button fill
     above. On light they are the same colour; the dark block re-binds ONLY this one,
     because a fill must stay dark enough for white text while type must go LIGHTER to
     clear AA on a dark card. Use this for accent text; keep --accent for fills. */
  --accent-text:   var(--brand-500);

  /* ── Lucrux type identity — override the platform Inter defaults. ── */
  --font-sans:    'Hanken Grotesk', 'Hanken Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-display: 'Hanken Grotesk', 'Hanken Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-serif:   var(--font-display);   /* editorial blueprints read --font-serif for display */
  --font-mono:    'Geist Mono', 'Geist Mono Fallback', ui-monospace, 'SF Mono', Menlo, monospace;

  /* ────────────────────────────────────────────────
     Font-size scale — fluid, clamps each value within a sane range.
     Parent system-base-theme exposes --font-sans/mono/serif but no
     numeric scale. This gives pages a single source for sizing.
     ──────────────────────────────────────────────── */
  --font-xs:   clamp(11px, 0.7rem + 0.1vw, 12px);
  --font-sm:   clamp(13px, 0.8rem + 0.15vw, 14px);
  --font-base: clamp(15px, 0.9rem + 0.2vw, 16px);
  --font-lg:   clamp(17px, 1rem   + 0.3vw, 18px);
  --font-xl:   clamp(20px, 1.2rem + 0.5vw, 24px);
  --font-2xl:  clamp(24px, 1.5rem + 0.8vw, 32px);
  --font-3xl:  clamp(32px, 2rem   + 1.5vw, 48px);

  /* ────────────────────────────────────────────────
     Mobile-first safe-area-inset aliases (iPhone notch + Android nav-bar).
     env() defaults to 0 when no inset; alias makes max() composition cleaner.
     ──────────────────────────────────────────────── */
  --safe-top:    env(safe-area-inset-top,    0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left,   0px);
  --safe-right:  env(safe-area-inset-right,  0px);

  /* Accessibility / touch target — iOS HIG says 44px. */
  --tap-target: 44px;

  /* App-header height — referenced by .app-shell-main padding math. */
  --app-header-height: 56px;

  /* ────────────────────────────────────────────────
     Sidebar (app-shell) palette — LIGHT mode.
     Defined here on the UNSCOPED :root (not in the shell's [data-cmp]-scoped
     styles.css) so the [data-theme="dark"] override below actually applies — a
     :root-prefixed selector inside scoped CSS gets mangled. The shell consumes
     these via var(--mx-*).

     ⚠ THE RAIL CARRIES NO SCHOOL PALETTE. It inherited ONE university's black +
     champagne, which is the one thing a multi-family product cannot ship: the
     next household is at a different school. The token NAMES stayed (shell + pages
     consume them, and `--mx-*` is this theme's own local prefixed family, which
     27-template-styling explicitly allows); only the VALUES moved, to a neutral
     product indigo that clears AA on the near-white rail here and on the
     near-black rail below. Per-school accents are the DATA follow-up — re-bind
     these six tokens off `student_profile.school` and the whole rail re-skins.
     ──────────────────────────────────────────────── */
  --mx-ink:       #FCFCFD;   /* sidebar bg — near-white */
  --mx-ink-2:     #F7F7FA;   /* gradient end — barely-there */
  --mx-accent:      #4F46E5;   /* accent text/icons — 6.6:1 on the light rail */
  --mx-accent-br:   #6366F1;   /* active bar/icon — the brighter step */
  --mx-accent-soft: rgba(99,102,241,.14);   /* active/hover tint */
  --mx-accent-line: rgba(99,102,241,.30);
  --mx-txt:       #15151A;   /* brand + hover text — near-black */
  --mx-txt-dim:   #3F3F46;   /* nav item text — neutral grey */
  /* Section headers. #A1A1AA measured 2.29:1 behind the brand sub-line and
     2.48:1 behind the nav captions — both far under the 4.5:1 AA floor these
     9-10px sizes owe, and the brand one was the single layout-audit FAIL on
     the console pages. This is the LIGHTEST value that clears 4.5:1 on BOTH
     shell surfaces (5.18:1 on #FBFBFC, 4.79:1 on #F2F2F4), so the captions
     stay as quiet as they were meant to be and simply become legible.
     The dark value below is untouched — it already measured 5.35-5.56:1. */
  --mx-label:     #6A6A73;   /* section headers — neutral grey, AA on both surfaces */
  --mx-active-txt:#15151A;
  --mx-divider:   #ECECEF;   /* thin grey divider on white */
  --mx-border:    #E6E7EB;   /* sidebar right border */
  --mx-fill-a:    #6366F1;   /* solid fill (brand mark/avatar) — bright in BOTH modes */
  --mx-fill-b:    #A5B4FC;

  /* Clean-white CONTENT surfaces (light only). Plain :root so the system theme's
     :root[data-theme="dark"] block still wins in dark via higher specificity.
     White cards pop via their borders, not bg contrast. */
  --surface-base:   #FFFFFF;   /* page bg — pure white, same as the cards */
  --surface-subtle: #F2F2F4;   /* faint neutral grey — dow/hover/chips (was bluish #f1f5f9) */
  --surface-hover:  #ECECEF;   /* neutral hover */
}

/* Sidebar palette — DARK mode. The rail was a WARM near-black chosen to sit under
   champagne; with the product indigo it is a neutral near-black, and the accents
   step LIGHTER (not darker) so they stay legible against it. Every value here is
   the dark twin of a light one above — never a token that only exists in one mode. */
:root[data-theme="dark"] {
  --mx-ink:#141520; --mx-ink-2:#0B0C13;
  --mx-accent:#A5B4FC; --mx-accent-br:#C7D2FE;
  --mx-accent-soft:rgba(165,180,252,.14); --mx-accent-line:rgba(165,180,252,.26);
  --mx-txt:#F2F3F8; --mx-txt-dim:#D6D8E2; --mx-label:#8C8FA0;
  --mx-active-txt:#fff;
  --mx-divider:rgba(255,255,255,.05); --mx-border:rgba(165,180,252,.16);
  --mx-fill-a:#6366F1; --mx-fill-b:#A5B4FC;
}

/* ────────────────────────────────────────────────
   Body — explicit canonical bindings. Parent system-base-theme
   ships Inter self-hosted with `Inter Fallback` for CLS=0; binding
   --font-sans here means we never fall through to UA serif default.
   ──────────────────────────────────────────────── */
html, body {
  font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
  font-size: var(--font-base, 16px);
  background: var(--surface-base);
  color: var(--text-primary);
}

/* The assistant's (AbleClaw) chat canvas — the shared blueprint hardcodes a light-blue tint
   (oklch .97 .012 250) on .aclaw-grid, which clashed with the clean-white app. Retint
   to the app's neutral surface tokens (per-app, light-DOM, higher specificity than the
   blueprint's bare class) so the chat matches everything else + flips with the theme. */
able-claw .aclaw-grid { background: var(--surface-base) !important; }
able-claw .aclaw-thinking-toggle,
able-claw .aclaw-dropzone-card { background: var(--surface-subtle) !important; }

/* ────────────────────────────────────────────────
   Standard page header — EVERY section uses this one compact band
   (.mx-pagehead) so headers are uniform and don't waste vertical
   space. Set --cat inline (or inherit it from a parent) for the
   per-category accent; falls back to --accent.
   ──────────────────────────────────────────────── */
.mx-pagehead {
  display: flex; align-items: center; gap: 12px;
  padding: 2px 0 9px; margin-bottom: 12px;
  border-bottom: 1px solid var(--border-default);
}
.mx-pagehead-emoji {
  flex: none; width: 42px; height: 42px; font-size: 24px; line-height: 1;
  display: flex; align-items: center; justify-content: center; border-radius: 12px;
  background: color-mix(in srgb, var(--cat, var(--accent)) 14%, transparent);
}
.mx-pagehead-text { flex: 1; min-width: 0; }
.mx-pagehead-title { margin: 0; font-size: 20px; font-weight: 800; letter-spacing: -0.01em; line-height: 1.15; }
.mx-pagehead-blurb { margin: 1px 0 0; font-size: 12.5px; color: var(--text-secondary); line-height: 1.35; }
.mx-pagehead-aside { flex: none; display: flex; align-items: center; gap: 8px; }
.mx-pagehead-chip { font-size: 11px; font-weight: 700; color: var(--cat, var(--accent)); white-space: nowrap;
  background: color-mix(in srgb, var(--cat, var(--accent)) 13%, transparent); padding: 4px 11px; border-radius: 999px; }
.mx-pagehead-link { font-size: 12.5px; font-weight: 700; color: var(--cat, var(--accent)); text-decoration: none; white-space: nowrap; }
.mx-pagehead-link:hover { text-decoration: underline; }
@media (max-width: 560px) { .mx-pagehead-blurb { display: none; } }

/* ────────────────────────────────────────────────
   AbleClaw on a light surface — the blueprint ships
   dark-only chrome (light text on dark cards). When
   the page sits on a light bg, claw labels render
   white-on-white and become invisible. These rules
   restore contrast for sidebar items, shortcut
   labels, input placeholders, and meta text.

   Specificity tip: claw's blueprint stylesheet uses
   single-class .aclaw-* rules; we beat them with
   double-class via .aclaw-grid .aclaw-shortcut +
   :where() to keep specificity manageable.
   ──────────────────────────────────────────────── */
:where(.aclaw-grid, .aclaw-sidebar, .aclaw-content) {
  color: var(--text-primary);
}
.aclaw-grid .aclaw-shortcut,
.aclaw-grid .aclaw-shortcut-label,
.aclaw-grid .aclaw-shortcut-text {
  color: var(--text-primary);
  font-size: var(--font-sm);
}
/* The blueprint's .aclaw-shortcut-icon span carries its own inline color via
   the colored badge variant; leave it untouched. */
.aclaw-grid .aclaw-shortcut {
  background: var(--surface-card);
  border: 1px solid var(--border-default);
}
.aclaw-grid .aclaw-shortcut:hover {
  background: var(--surface-subtle);
  border-color: var(--accent);
}
.aclaw-grid .aclaw-sidebar-title,
.aclaw-grid .aclaw-sidebar-section {
  color: var(--text-muted);
}
.aclaw-grid .aclaw-conv-empty {
  color: var(--text-subtle);
}
.aclaw-grid .aclaw-search input,
.aclaw-grid .aclaw-input textarea {
  color: var(--text-primary);
  background: var(--surface-card);
}
.aclaw-grid .aclaw-search input::placeholder,
.aclaw-grid .aclaw-input textarea::placeholder {
  color: var(--text-subtle);
}
.aclaw-grid .aclaw-welcome-title,
.aclaw-grid .aclaw-empty-title {
  color: var(--text-primary);
}
.aclaw-grid .aclaw-welcome-subtitle,
.aclaw-grid .aclaw-empty-subtitle,
.aclaw-grid .aclaw-or-text {
  color: var(--text-muted);
}
.aclaw-grid .aclaw-category {
  color: var(--text-muted);
  font-weight: 600;
}

/* On the home page (which IS full-mode claw), hide the global
   launcher pill — having both the page-level claw AND a floating
   pill that opens its own claw is confusing. Launcher stays for
   every other route. */
body:has(.home-claw) > able-claw[mode="launcher"] {
  display: none;
}

/* ────────────────────────────────────────────────
   Dark mode — keys on [data-theme="dark"], NOT on
   @media (prefers-color-scheme: dark).

   Why: the inline SSR script in TemplatePageRenderer
   resolves data-theme synchronously from:
     (1) localStorage('able-platform-theme') stored user choice
     (2) prefers-color-scheme media query

   So [data-theme="dark"] is the SINGLE source of truth — it
   respects OS pref when there's no stored value AND respects
   user toggle (light despite OS=dark, or dark despite OS=light).

   Keying tokens on @media (prefers-color-scheme: dark) would
   defeat the toggle: the body bg would follow OS while the
   header / claw chrome follow the attribute → inconsistent UI.
   ──────────────────────────────────────────────── */
:root[data-theme="dark"] {
  --surface-base:    #0d1117;
  --surface-card:    #161b22;
  --surface-raised:  #21262d;
  --surface-subtle:  #1c2128;
  --surface-hover:   #21262d;
  --surface-active:  #2d333b;
  --text-primary:    #e6edf3;
  --text-secondary:  #c9d1d9;
  --text-muted:      #8b949e;
  --text-subtle:     #6e7681;
  --border-default:  #30363d;
  --border-subtle:   #21262d;
  --accent-soft:     #1f2937;
  /* ── accent TEXT on a dark surface ────────────────────────────────────────────
     `--accent` (#2563eb) is a BUTTON FILL: it has to stay dark enough to carry
     white `--on-accent` text, so it cannot also be the colour of accent-coloured
     TYPE. On the dark card (#161b22) it measures 3.35:1 — under the 4.5:1 AA floor
     for small text — so every accent link in dark mode was failing (found on
     /signup's "Sign in" + "Show", 2026-08-14; the same applies to any accent text
     the app draws on a dark surface).
     This is the text-only twin, the same indigo the rail already re-binds
     `--mx-accent` to in dark: 8.68:1 on the card. Buttons keep `--accent`. */
  --accent-text:     #A5B4FC;
}
/* Body bg + text re-resolved automatically via the canonical tokens above. */

/* ── Display headings wear Hanken at display weights; body + UI wear Hanken (via --font-sans).
   Targets the app's big titles + the claw's display surfaces (light DOM, so a
   theme rule reaches inside <able-claw>). Numerics get Geist Mono. ── */
h1, h2, h3,
.claw-title, .claw-hero-title,
.aclaw-welcome-headline-h1, .aclaw-welcome-title,
.aclaw-artifact-type, .aclaw-stage-card-title,
.aclaw-dock-tab, .mx-brand-1 {
  font-family: var(--font-display);
  letter-spacing: -0.01em;
}
/* Tabular numerics — mastery %, credits, balances, countdowns. */
.claw-num, .aclaw-num, [data-num], .mx-num,
.aclaw-kpi-value, .aclaw-stat-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Full-bleed home (the claw copilot): .mx-body's breathing padding-bottom
   would push the claw's input bar below the fold — drop it on the home route. */
body:has(.home-claw) .mx-body { padding-bottom: 0; }

/* ⟪2026-08-25⟫ Same deal for the WEEK, and for the same reason in reverse: that
   page now FILLS the window, and its own trailing padding is deliberately 4-5px
   ("a 5px bottom empty border is sufficient" — Raul). The shell's 16px of
   breathing room is right for a document-scroll page that ends in text; under a
   filled page it just re-opens the gap under the DUE rail that this round
   closed — measured 21px below the rail (1 sub-pixel band + 4 page + 16 here)
   against the ~5px asked for. Dropping it leaves exactly the page's own 5px.
   The SAFE AREA is not lost with it: .wkx carries `var(--safe-bottom)` in its
   own padding, so a notched device still clears the home indicator. */
body:has(.wkx) .mx-body { padding-bottom: 0; }

/* ── .calbar — the calendar VIEW SWITCHER (Day · Week · Month) ────────────────
   Lives in the theme, not in a page, because it is worn by THREE pages
   (/calendar, /week, /day) and rule 27 § "What lives where" puts a rule shared by
   more than one page here. Additive: new class names only, nothing existing is
   touched, so a page that does not carry the markup is unaffected.
   To adopt it, a page renders exactly:
     <nav class="calbar" aria-label="…">
       <div class="calbar-views" role="group" aria-label="…">
         <a class="calbar-view" href="/day/<key>">Day</a>
         <span class="calbar-view is-on" aria-current="page">Week</span>
         <a class="calbar-view" href="/calendar?m=<yyyy-MM>">Month</a>
       </div>
       <div class="calbar-pager">…optional ‹ label › Today…</div>
     </nav>
   The CURRENT view is a <span>, never a link — a control that navigates to where
   you already are is a dead tap. Targets are server-rendered so the switcher works
   with no JS and middle-clicks into a new tab. */
.calbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin-bottom: var(--space-3);
}
.calbar-views {
  display: inline-flex; align-items: center; gap: 2px; padding: 3px;
  border: 1px solid var(--border-default); border-radius: var(--radius-lg);
  background: var(--surface-subtle);
}
.calbar-view {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 34px; padding: 0 var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--font-sm); font-weight: 600;
  color: var(--text-secondary); text-decoration: none; white-space: nowrap;
  transition: background var(--transition-fast) ease, color var(--transition-fast) ease;
}
.calbar-view.is-on {
  background: var(--surface-card); color: var(--text-primary); box-shadow: var(--shadow-sm);
}
@media (hover: hover) and (pointer: fine) {
  .calbar-view:not(.is-on):hover { background: var(--surface-hover); color: var(--text-primary); }
}
.calbar-view:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.calbar-pager { display: inline-flex; align-items: center; gap: var(--space-1); }
.calbar-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; min-height: 38px;
  border: 1px solid var(--border-default); border-radius: var(--radius-md);
  background: var(--surface-card); color: var(--text-secondary);
  font-size: 18px; line-height: 1; text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .calbar-arrow:hover { background: var(--surface-hover); color: var(--text-primary); }
}
.calbar-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.calbar-range {
  min-width: 9ch; padding: 0 var(--space-2); text-align: center;
  font-size: var(--font-sm); font-weight: 700; color: var(--text-primary);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.calbar-today {
  display: inline-flex; align-items: center; min-height: 38px;
  padding: 0 var(--space-3); margin-left: var(--space-1);
  border-radius: var(--radius-md);
  font-size: var(--font-sm); font-weight: 600;
  color: var(--accent); text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .calbar-today:hover { background: var(--surface-hover); }
}
.calbar-today:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (prefers-reduced-motion: reduce) {
  .calbar-view, .calbar-arrow { transition: none; }
}

/* ══ THE CALENDAR FAMILY (gcal refactor, docs/calendar/GCAL-DESIGN.md §8) ══════
   Shared by /week + /calendar (rule 27: a rule shared by more than one page
   lives in the theme). Three token families + the rail/main page shell.

   ── --cal-tone-* — the §2c tone map, THEME-OWNED ─────────────────────────────
   The v2 feed's `tone` column is a NAME; the surfaces resolve
   var(--cal-tone-<name>). Every fill below is chosen to carry WHITE ink at
   ≥4.5:1 in BOTH modes (solid Google-style blocks; one ink token), so no
   per-mode re-bind is needed — the fills are mid-saturation and sit correctly
   on light and dark cards alike. A tenant re-tones by overriding these. */
:root {
  /* ⟪RAUL RULING 2026-08-26⟫ NEUTRAL, because hue is the class now. These were ten
     saturated hues competing with the course palette for the same channel; a grid
     where a chore is teal and a club fair is violet teaches the eye that colour
     means "kind", which is the confusion the ruling names. They are now one slate
     for the foreground kinds and a lighter slate for the quiet ones — a non-class
     row is legible and present, and says nothing about identity.
     THE ONE EXCEPTION IS `warn` = DUE: R3 gives due-ness its own colour (pink), so
     it is the single non-class hue left on the surface, and it means one thing. */
  /* ⚠ THE HATCH IS `-x`, NOT THE BARE NAME. able-time-grid.js:134-143 declares
     `--cal-tone-<n>: var(--cal-tone-<n>-x, <semantic fallback>)` ON THE ELEMENT,
     and an element-level custom property beats :root no matter how specific the
     :root rule is. So the blueprint's own comment ("a tenant re-tones by declaring
     --cal-tone-* once") is true only through the -x door — declaring the bare name
     here is silently shadowed, which is exactly what happened on the first cut of
     this change: the grid kept painting due-amber and social-green from the
     blueprint's fallbacks while this file said pink and slate. Measured, not
     reasoned: the live block still resolved #b45309. */
  --cal-tone-primary-x:  #475569;            /* meeting  — slate-600            */
  --cal-tone-accent-x:   #475569;            /* focus                           */
  --cal-tone-info-x:     #475569;            /* appointment                     */
  --cal-tone-warn-x:     var(--cal-due-ink);  /* DUE — the one non-class colour  */
  --cal-tone-critical-x: #475569;            /* exam (course rows take the hue)  */
  --cal-tone-money-x:    #475569;            /* payment                         */
  --cal-tone-travel-x:   #475569;            /* travel                          */
  --cal-tone-muted-x:    #94A3B8;            /* chore    — the quietest lane    */
  --cal-tone-social-x:   #64748B;            /* personal — slate-500            */
  --cal-tone-neutral-x:  #94A3B8;            /* observance/note                 */
  --cal-block-ink:     #ffffff;            /* the ink on ANY tone fill        */

  /* ── --cal-due-ink — DUE-NESS, the one non-class hue (R3, 2026-08-26) ──────
     Pink because Raul picked pink, and because it is the one family the course
     palette does not use for slots 1-5 (crimson/lime/emerald/blue/magenta), so a
     deadline can never be mistaken for a class at a glance. It is drawn as a 9%
     ground under dark ink and as a 3px edge, never as a fill under white. */
  --cal-due-ink: #DB2777;

  /* ── --cal-cat-* — the 11 life-category hues, promoted from /day ────────── */
  --cal-cat-academics: #4f46e5;  --cal-cat-athletics: #059669;
  --cal-cat-finances:  #d97706;  --cal-cat-nutrition: #65a30d;
  --cal-cat-health:    #e11d48;  --cal-cat-career:    #0891b2;
  --cal-cat-social:    #7c3aed;  --cal-cat-travel:    #ea580c;
  --cal-cat-dorm:      #0d9488;  --cal-cat-housing:   #0ea5e9;
  --cal-cat-personal:  #64748b;
  --cal-cat-fallback:  #71717A;   /* unknown category — grey, fail-visible */

  /* ── --cal-member-N — per-member ribbon/swatch colours (assigned by stable
        member-id order; GCAL-DESIGN §8d). Distinct from the tone hues. ─────── */
  --cal-member-1: #E11D48;  --cal-member-2: #0EA5E9;  --cal-member-3: #16A34A;
  --cal-member-4: #F59E0B;  --cal-member-5: #8B5CF6;  --cal-member-6: #0D9488;
}

/* ── the rail + main shell both calendar pages wear ──────────────────────────
   The rail COLUMN is reserved by the grid track (CLS contract in
   able-api docs/calendar/CAL-RAIL-CONTRACT.md); the tag hydrates INSIDE it. */
.gcx { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
.gcx-strip { display: flex; align-items: center; gap: var(--space-2); }
.gcx-rail { display: none; }
@media (min-width: 1000px) {
  .gcx-shell {
    display: grid; grid-template-columns: 248px minmax(0, 1fr);
    /* space-2 (Angel 2026-08-28): the rail's inner layer now carries its own
       right air (space-2, week styles), so a 20px track gap doubled it. */
    gap: var(--space-2); align-items: start;
  }
  /* The rail column BREATHES inside its own reserved track (Raul round-3:
     "need some padding left of month" — the mini-month sat flush against the
     collapsed 76px nav). Horizontal inset only: the vertical budget belongs
     to the calendar columns, never stolen from them. */
  .gcx-rail { display: block; position: sticky; top: var(--space-3);
    padding: 0 var(--space-2) 0 var(--space-3); box-sizing: border-box; }
  .gcx-strip { display: none; }
}
/* mobile/tablet: Create pill + Filters button on one strip above the calbar */
.gcx-pill {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap-target, 44px); padding: 0 var(--space-4);
  border: 1px solid var(--border-default); border-radius: var(--radius-full);
  background: var(--surface-card); color: var(--text-primary);
  font: inherit; font-size: var(--font-sm); font-weight: 700; cursor: pointer;
  box-shadow: var(--shadow-sm);
}
@media (hover: hover) and (pointer: fine) { .gcx-pill:hover { background: var(--surface-hover); } }
.gcx-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gcx-pill [data-count]:not([data-count=""]):not([data-count="0"])::after {
  content: attr(data-count); margin-left: 2px;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-full);
  background: var(--accent); color: var(--on-accent);
  font-size: 11px; font-weight: 800; display: inline-grid; place-items: center;
}

/* ── round-4 (Raul, live review): the calendar TOOLBAR is SHORT + SYMMETRIC ──
   "the header can still be not as tall … it also looks not symetric with top
   down padding." The band used to ride 0px above the controls and 24px below
   them (the theme's 12px margin-bottom + the page's 12px flex gap stacking).
   Now: the SAME air on both sides — margin 8px above, and the single 8px
   below-space comes from the page (week: the main's flex gap; day: this
   margin), with compact 32px controls. Scoped .gcx so only the calendar
   family wears it. Every pixel saved goes to the grid. */
.gcx .calbar { margin: var(--space-2) 0; row-gap: var(--space-1); }
.gcx .calbar-arrow { min-width: 32px; min-height: 32px; font-size: 16px; }
.gcx .calbar-today { min-height: 32px; }
.gcx .calbar-views { padding: 2px; }
.gcx .calbar-view { min-height: 28px; }

/* the a11y-only page title (the visual title is the calbar range/month label) */
.gcx-srtitle {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── CATEGORY IS NO LONGER A COLOUR — ⟪RAUL RULING 2026-08-26⟫ "on identity the
   dual for category and academic makes it confusing, we will have to reserve the
   colour for the class, thats it, and then rearrange the rest".

   The Aug-24 law made hue mean COURSE for academic rows and CATEGORY for the rest.
   Two meanings on one channel, decided per row: measured on Max's own week, seven
   rows all reading `data-cat="academics"` rendered in FIVE different hues, while
   the rail legend went on teaching "indigo = Academics". A reader cannot learn a
   key that changes meaning row by row.

   So the map below is GONE, and with it the last non-course use of hue. What makes
   this cheap is that the blueprint already wrote the fallback and it has NEVER
   fired: `_paintInk` emits `--atg-ink: var(--cat, var(--cal-tone-<tone>))`, and
   because `.gcx [data-cat]` always resolved, the tone arm measured 0 of 35 live
   resolutions. Deleting these rules lights that arm up with ZERO blueprint change.
   `--cal-tone-*` (re-bound to neutrals just above) is now the non-course channel.

   The 11 `--cal-cat-*` tokens are NOT deleted: the rail legend, the mini-month
   dots and /c/<category> still use them as a category VOCABULARY. They simply no
   longer paint a calendar block. Category becomes what it should have been — a
   FILTER, not a colour.

   What replaces hue for a non-class row: the type SHAPE (.cal-mark, below), the
   row's own emoji, and the due-pink ground. Colour on this grid now answers
   exactly one question — which class is this. ─────────────────────────────── */

/* ══ THE ACADEMIC VISUAL SYSTEM — docs/calendar/ACADEMIC-VISUAL-SYSTEM.md ══════
   ⟪RAUL RULING 2026-08-24⟫ "his main function is academic so need to color code
   the class, and the types need to have a different motif to differentiate."

   This does NOT add an axis. `--cat` stays the ONE identity-hue slot every
   surface already consumes; for an ACADEMIC row it is fed by the COURSE, for
   everything else by the category, exactly as above. A surface that renders
   var(--cat) today renders the new law with no change.

   Why: measured on Max's real week (Aug 24-30, the feed's own 35 rows) --
   21 rows carry a course (60%), 5 are academic with NO course (14%), 9 are
   non-academic (26%). The category map above renders all 26 academic rows in
   ONE indigo, so the hue is constant across the surface's dominant content and
   carries no information. Course hue gives FIVE hues where there was one.

   ── --cal-course-N — 12 slots, per mode. ───────────────────────────────────
   Unlike the tone/category families these DO re-bind for dark, because the hue
   is drawn as a 3px EDGE and a 45% border against the card -- never as a fill
   under text -- so its constraint is edge contrast against ITS OWN card, which
   light and dark cannot both satisfy from one hex. The HUE FAMILY is identical
   in both modes (crimson/lime/emerald/blue/magenta/...), so a course keeps its
   identity across a theme toggle; only lightness moves.

   Slots 1-5 are the CONCURRENT case (Max carries 5 enrolled courses) and are
   optimised for colour-vision deficiency, measured with a Vienot dichromat
   simulation: worst pairwise deltaE across normal + deuteranopia + protanopia
   + tritanopia is 25.9 (light) / 16.6 (dark). The naive hand-picked palette
   this replaced measured 0.4 -- two courses literally identical to a
   tritanope. Slots 6-12 fill the gaps and are NOT CVD-separable from the
   first five (all-12 worst deltaE 1.6): past five concurrent courses the hue
   degrades and the type mark + the course chip text carry the distinction.
   That is a documented floor, not an oversight. */
:root {
  --cal-course-1:  #72211D;  /* crimson */
  --cal-course-2:  #6B960D;  /* lime    */
  --cal-course-3:  #29A36C;  /* emerald */
  --cal-course-4:  #C2410C;  /* orange -- NOT blue: blue is the calendar's own accent chrome (band wash, today column), so a blue course reads as decoration, not a class */
  --cal-course-5:  #B910BC;  /* magenta */
  --cal-course-6:  #157384;  /* teal    */
  --cal-course-7:  #4D2593;  /* violet  */
  --cal-course-8:  #8D640C;  /* amber   */
  --cal-course-9:  #9A1D45;  /* rose    */
  --cal-course-10: #1D7817;  /* green   */
  --cal-course-11: #3D34C4;  /* indigo -- brightened so its wash is not a cool grey */
  --cal-course-12: #994315;  /* orange  */
}
:root[data-theme="dark"] {
  --cal-course-1:  #F37972;  --cal-course-2:  #CAF372;
  --cal-course-3:  #54D49A;  --cal-course-4:  #F97316;
  --cal-course-5:  #D531D8;  --cal-course-6:  #4BC7DD;
  --cal-course-7:  #9365E2;  --cal-course-8:  #EFB843;
  --cal-course-9:  #EA6690;  --cal-course-10: #64D65C;
  --cal-course-11: #7B76E5;  --cal-course-12: #F08C56;
}

/* The course binding. Deliberately (0,3,0) -- one step MORE specific than the
   category rules above -- so it wins by SPECIFICITY and never by source order.
   Three source-order bugs landed in round 7 alone (GCAL-DESIGN 9i.4, 9i.7); a
   rule that depends on position in the file is the next person's identical
   trap. An academic row with NO course keeps the academics indigo. */
.gcx [data-cat][data-course-slot="1"]  { --cat: var(--cal-course-1); }
.gcx [data-cat][data-course-slot="2"]  { --cat: var(--cal-course-2); }
.gcx [data-cat][data-course-slot="3"]  { --cat: var(--cal-course-3); }
.gcx [data-cat][data-course-slot="4"]  { --cat: var(--cal-course-4); }
.gcx [data-cat][data-course-slot="5"]  { --cat: var(--cal-course-5); }
.gcx [data-cat][data-course-slot="6"]  { --cat: var(--cal-course-6); }
.gcx [data-cat][data-course-slot="7"]  { --cat: var(--cal-course-7); }
.gcx [data-cat][data-course-slot="8"]  { --cat: var(--cal-course-8); }
.gcx [data-cat][data-course-slot="9"]  { --cat: var(--cal-course-9); }
.gcx [data-cat][data-course-slot="10"] { --cat: var(--cal-course-10); }
.gcx [data-cat][data-course-slot="11"] { --cat: var(--cal-course-11); }
.gcx [data-cat][data-course-slot="12"] { --cat: var(--cal-course-12); }

/* ── THE TYPE MOTIF — a SHAPE, never a second hue ───────────────────────────
   Type has to survive the sticky bottom rail cell: ~110px wide, carrying up to
   9 items on a real teaching day, so a chip is a hue and ONE mark and nothing
   larger fits. An icon is mush below 14px, a dashed border vanishes at 1px, and
   emoji are banned as UI icons (rule 28 2.6) and would fight the course hue.
   Geometric marks are what survive -- the scatter-plot-marker answer, and the
   ski-trail answer (green circle / blue square / black diamond), both of which
   people read with no legend. They also survive GREYSCALE, which is what
   discharges "colour is never the only signal" (rule 28 2.1).

   CARRIER: an explicit <i class="cal-mark" data-work="..."> element, never a
   ::before on the row. Several calendar rows are grid containers (.wkx-pk-row
   is `grid-template-columns: 62px 1fr`) and a generated child would silently
   become a third track. The carrier is layout-safe everywhere.

   `--cal-mark-size` lets each surface scale it: 10px rail chip, 9px band chip,
   9.5px grid block. Below ~7px no mark is legible -- see the mini-month note. */
.cal-mark {
  --cal-mark-size: 10px;
  flex: none; display: inline-block; box-sizing: border-box;
  width: var(--cal-mark-size); height: var(--cal-mark-size);
  color: var(--cat, var(--text-muted));
  background: currentColor;
}
/* submit (44.6% of coursework) -- the neutral majority: a solid dot is "an item" */
.cal-mark[data-work="submit"]  { border-radius: 50%; }
/* prep -- literally less ink for lower stakes */
.cal-mark[data-work="prep"]    { border-radius: 50%; background: transparent;
                                 border: 2px solid currentColor; }
/* engage -- two marks = two voices; plurality is the small-size cue for dialogue */
.cal-mark[data-work="engage"]  { background: none;
  background-image: linear-gradient(currentColor, currentColor),
                    linear-gradient(currentColor, currentColor);
  background-size: 33% 100%, 33% 100%;
  background-position: left center, right center;
  background-repeat: no-repeat; }
/* assess -- the ski-trail "hard"; sharp corners read as risk at any size, and
   it is 12px where the others are 10. MEASURED: as a same-size filled diamond it
   was only 20% different in ink from the filled disc -- the two heaviest marks
   sitting closest together, with the HIGHEST-stakes type nearest the default.
   At 12px the worst pair in the whole vocabulary moves to 36.8% (engage/admin),
   and the exam now carries more ink than any other mark, which is what an exam
   should do. (A visual review flagged "circle vs ring vs square vs diamond";
   measurement said those pairs were fine at 45%/54% and named a different one.) */
.cal-mark[data-work="assess"]  { transform: rotate(45deg); border-radius: 1px;
                                 --cal-mark-size: 12px; }
/* admin -- a form / a checkbox, not schoolwork */
.cal-mark[data-work="admin"]   { background: transparent;
                                 border: 2px solid currentColor; border-radius: 1px; }
/* chore -- household upkeep: square like admin, filled like a task, and smaller */
.cal-mark[data-work="chore"]   { border-radius: 1px;
                                 --cal-mark-size: 7px; opacity: .75; }
/* class -- a MEETING you attend. It gets no mark either: a timed block's own
   geometry (tall, in an hour column, at a fixed time) already says "class", and
   spending a mark there would put one on 13 of the week's ~21 course rows --
   the most common row, where it adds nothing. Marks are for the MARKER lane,
   where a row has no shape to speak with. */
.cal-mark[data-work="class"]   { display: none; }
/* event -- a dated fact nobody PERFORMS. It gets no mark at all, and the chip
   that carries it is a BAND rather than a pill (see .cal-chip--band). The two
   halves matter together: "no mark" alone would collide with `unknown`. */
.cal-mark[data-work="event"]   { display: none; }
/* unknown -- classification is a KEYWORD GUESS over the feed's SUMMARY (the
   Brightspace feed carries no CATEGORIES property at all), and a wrong mark
   misinforms where a missing one merely under-informs. So an unclassified row
   keeps its PILL shape -- it is still a task -- and simply shows no mark. */
.cal-mark[data-work=""],
.cal-mark:not([data-work]),
.cal-mark[data-work="unknown"] { display: none; }

/* done is a STATE that composes with hue+mark, never a type. Raul rejected the
   full-height strike-through: dim the ink and give the SPACE back. */
.cal-mark[data-done="y"] { background: transparent; border: 2px solid currentColor;
                           border-radius: 50%; opacity: .55; }

/* ── .cal-collapsed — the CHIP half of the done state ────────────────────────
   Opt-in utility a chip/row carries alongside data-done, so no existing row
   changes behaviour by accident. This is what makes "keep the row, collapse it"
   affordable: el calendario es el REGISTRO, so a finished thing must not vanish
   -- but a finished thing at FULL height is what made dropping it tempting.

   Half height + dimmed ink + a struck TITLE ONLY (never the whole row, per the
   ruling). It composes with the course hue and the type mark rather than
   replacing them: a done ACCT homework still reads as ACCT, still reads as
   homework, and reads as finished. Tenant-neutral -- nothing here is academic,
   so fryaway's promo/campaign markers wear it unchanged. */
.gcx .cal-collapsed {
  min-height: 0;
  padding-block: 0;
  opacity: .58;
  font-size: .92em;
}
.gcx .cal-collapsed > .cal-mark { opacity: .5; }
@media (hover: hover) and (pointer: fine) {
  .gcx .cal-collapsed:hover { opacity: 1; }
}

/* ── QUIET IS NOT INVISIBLE — the cross-zone note ───────────────────────────
   "Times are Indianapolis time (Max) · you're 3 h behind" is the one line
   standing between a parent in San Diego and misreading their kid's entire
   schedule. A visual review called it illegible and proposed fixing the token.

   MEASURED FIRST: --text-muted is #6b7280 on #FBFBFC = 4.67:1 light, #8b949e
   on #0d1117 = 6.15:1 dark. The token ALREADY PASSES AA in both modes, so
   changing it would have "fixed" a defect it did not cause -- and would have
   moved every other muted string on the app to chase this one line.

   The defect is SIZE: 10.5px, below even --font-xs (11px floor), and 4.67:1
   is only just over a floor written for larger text. So: 12px -- the same
   meta scale /week, /day and the day modal agreed on in round 7 -- and
   --text-secondary, which is 10.1:1 light / 12.3:1 dark. It stays subordinate
   by SIZE and POSITION against a 17px/800 range label, which is what
   subordinate is supposed to mean.

   Scoped (0,3,0) so it beats the page rule by SPECIFICITY, not source order,
   and so the page's file is untouched. WORDING is not ours. */
.gcx .calbar-note .tzx-note {
  font-size: 12px; line-height: 1.3;
  color: var(--text-secondary);
}

/* ── the mobile filters sheet (both calendar pages; the rail node moves here) ── */
.gcx-sheet { position: fixed; inset: 0; z-index: var(--z-offcanvas, 700); }
.gcx-sheet[hidden] { display: none; }
.gcx-sheet-scrim { position: absolute; inset: 0; background: var(--surface-overlay); }
.gcx-sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: min(78dvh, 560px);
  overflow-y: auto;
  background: var(--surface-card);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--shadow-xl);
  padding: var(--space-2) var(--space-4) calc(var(--space-6) + var(--safe-bottom));
}
.gcx-sheet-grab {
  width: 36px; height: 4px; border-radius: 999px;
  background: var(--border-strong); margin: var(--space-1) auto var(--space-3);
}

/* ── the CREATE modal (both calendar pages): tell the agent, or fill the form ── */
.gcx-cre { display: flex; flex-direction: column; gap: var(--space-3); }
.gcx-cre-t { margin: 0; font-size: var(--font-lg); font-weight: 800; color: var(--text-primary); }
.gcx-cre-nl { display: flex; gap: var(--space-2); }
.gcx-cre-nlin {
  flex: 1; min-width: 0;
  min-height: 44px; padding: 0 var(--space-3);
  border: 1.5px solid var(--accent); border-radius: var(--radius-full);
  background: var(--surface-card); color: var(--text-primary);
  font: inherit; font-size: max(16px, var(--font-sm));
}
.gcx-cre-nlin::placeholder { color: var(--text-muted); }
.gcx-cre-nlgo {
  flex: none; display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 44px; padding: 0 var(--space-3);
  border: 0; border-radius: var(--radius-full);
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: var(--font-sm); font-weight: 700; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .gcx-cre-nlgo:hover { background: var(--accent-hover); } }
.gcx-cre-or {
  margin: 0; font-size: var(--font-xs); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted); text-align: center;
}
.gcx-cre-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.gcx-cre-f { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.gcx-cre-f--wide { grid-column: 1 / -1; }
.gcx-cre-f > span { font-size: var(--font-xs); font-weight: 700; color: var(--text-secondary); }
.gcx-cre-f input, .gcx-cre-f select {
  min-height: 42px; padding: 0 var(--space-3);
  border: 1px solid var(--border-default); border-radius: var(--radius-md);
  background: var(--surface-card); color: var(--text-primary);
  font: inherit; font-size: max(16px, var(--font-sm));
}
.gcx-cre-foot { display: flex; justify-content: flex-end; gap: var(--space-2); }
.gcx-cre-save {
  display: inline-flex; align-items: center;
  min-height: 40px; padding: 0 var(--space-5);
  border: 0; border-radius: var(--radius-full);
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: var(--font-sm); font-weight: 800; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .gcx-cre-save:hover { background: var(--accent-hover); } }
.gcx-cre-save:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 480px) { .gcx-cre-grid { grid-template-columns: 1fr; } }
