/*
 * Occupemo Web — Design System
 * Ported from Occupemo/App/Theme.swift (exact hex values)
 *
 * Usage:
 *   - Pages must include this file AND the Inter Google Font link:
 *     <link rel="preconnect" href="https://fonts.googleapis.com">
 *     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 *     <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
 *   - Dark mode: add class "dark" to <html>. initTheme() in ui.js handles
 *     prefers-color-scheme detection and localStorage override.
 *   - All component classes use CSS custom properties — never hardcode hex.
 */

/* ============================================================
   0. Type — Bricolage Grotesque (display, variable) + Manrope (body).
      Both have warmth and character that Inter lacks. The variable
      axis on Bricolage gives us distinctive headlines without a
      second download.
   ============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wdth,wght@12..96,75..100,400..800&family=Manrope:wght@400;500;600;700;800&display=swap');

/* ============================================================
   1. Color tokens — light mode (from Theme.swift)
   ============================================================ */
:root {
  /* Native form-control chrome (date-picker popups, calendar indicators,
     scrollbars) follows the active theme — the .dark block flips this to
     dark. Without it, dark mode paints Chromium's light-scheme black
     calendar icon on near-black inputs (invisible). */
  color-scheme: light;

  /* Primary accent — Circled Day terracotta */
  --color-accent:         #C0512B;
  --color-accent-light:   #D06A42;
  --color-accent-surface: #F6E3D3;
  /* Foreground for text/icons sitting ON a solid accent fill (buttons,
     badges, "today" circle). Flips to near-black in dark mode where the
     accent itself is light. Also backs --color-on-gradient (below) via
     var(), so the flat brand-accent surface (--gradient-text-safe) gets
     the same mode-correct label color. */
  --color-on-accent:      #FFFFFF;

  /* Backgrounds */
  --color-bg:             #FAF5EB;
  --color-surface:        #FFFDF6;
  --color-surface-dark:   #2A231A;

  /* Text — warm ink ramp. secondary measures ~5.5:1 on the cream bg;
     muted 4.53:1 (bg) / 4.84:1 (card surface) — meets WCAG AA 4.5:1. */
  --color-text-primary:   #2A231A;
  --color-text-secondary: #6C6252;
  --color-text-muted:     #7A6F5F;
  --color-text-inactive:  #C9C0AE;

  /* Borders */
  --color-border:         #E7DCC8;
  --color-border-dark:    #B9AE97;

  /* State colors (success gets a dark-mode variant; warning/error/coral
     are the same across modes) */
  --color-success:        #477A44;
  --color-warning:        #B57E14;
  --color-error:          #BA3846;
  --color-coral:          #D2495F;

  /* Legacy splash tokens — names kept for compatibility; no gradient is
     rendered anywhere anymore (Circled Day is flat print). Both resolve
     to ember shades. */
  --color-gradient-top:    #C0512B;
  --color-gradient-bottom: #D06A42;
  /* Foreground for text/icons ON a brand-accent surface. Tracks
     --color-on-accent so it resolves correctly per mode: white in light,
     ink #241407 in dark. Declared once here — the dark :root blocks
     don't redeclare --color-on-gradient, so the var() reference
     re-resolves through the cascade in dark mode. Pair it with
     --gradient-text-safe below. */
  --color-on-gradient:     var(--color-on-accent);
  /* Text-safe brand surface — a FLAT solid (Circled Day is flat print,
     no gradients): the accent terracotta/ember. --color-on-gradient
     clears 4.5:1 against it in both modes (white on #C0512B ~4.7:1 in
     light; ink #241407 on #E5824F ~6.5:1 in dark). Use for any accent
     surface that carries small text (Plus badges, gate banners, level
     pills). All consumers use the `background:` shorthand, which
     accepts a flat color. */
  --gradient-text-safe: var(--color-accent);

  /* Category colors (from Theme.swift:38-43) */
  --color-cat-academic:     #3D6C9B;
  --color-cat-sports:       #5C7A33;
  --color-cat-art:          #D9A62E;
  --color-cat-entertainment:#43948B;
  --color-cat-social:       #C97A92;
  --color-cat-unknown:      #8B8071;

  /* ── Contrast-retune text tokens (Circled Day fix wave) ──────────────
     Mode-scoped so a single component rule reads correctly in both modes.
     LIGHT: accent/warning darken toward black to clear AA on the pale
     terracotta surfaces; error text passes raw. See a11y diagnostic. */
  /* accent AS TEXT on --color-accent-surface (btn-secondary, active nav/
     chips, onboarding selections). 82% mix → #9D4223 = 5.20:1 on #F6E3D3
     (was raw accent 3.78:1). Dark keeps raw ember (4.95:1). */
  --color-accent-on-surface: color-mix(in srgb, var(--color-accent) 82%, black);
  /* Primary CTA hover fill — white label needs a DARKER terracotta than
     accent-light in light (white on #D06A42 was 3.61:1). #9D4223 → 6.49:1. */
  --color-accent-hover:      color-mix(in srgb, var(--color-accent) 82%, black);
  /* error AS TEXT (.input-error-msg, popover clear, End Schedule). Light
     passes raw (5.16/5.51); dark lightens (raw 2.84:1 on dark surface). */
  --color-error-text:        var(--color-error);
  /* overrun countdown text (warning). 80% mix → #916510 = 5.07:1 light
     (raw 3.46:1); dark lightens to the §20 70%-white idiom. */
  --color-warning-text:      color-mix(in srgb, var(--color-warning) 80%, black);
  /* break-card live countdown, pre-compensated for the 0.75 card dim over
     the cream bg. 55% mix → #6A2D18; after 0.75 dim = 4.96:1 (raw 2.93:1).
     Dark uses the §20 white idiom (5.51:1 after dim). */
  --break-countdown-text:    color-mix(in srgb, var(--color-accent) 55%, black);
  /* Category-badge TEXT on the 8% category wash (12px/700, floor 4.5).
     Dots + borders keep the FULL category color; only the label darkens
     (light) / lightens (dark academic+sports). art/social dots stay
     spec-accepted below the 3:1 graphic floor (icon+label pairing). */
  --cat-academic-txt:        #3D6C9B; /* 4.85:1 (raw passes) */
  --cat-sports-txt:          #577430; /* raw 4.37 → 4.75:1 */
  --cat-art-txt:             #89691D; /* raw 2.06 → 4.74:1 */
  --cat-entertainment-txt:   #36766F; /* raw 3.25 → 4.77:1 */
  --cat-social-txt:          #975C6E; /* raw 2.87 → 4.70:1 */
  --cat-unknown-txt:         #736A5E; /* raw 3.49 → 4.79:1 */

  /* Eisenhower quadrant accents — aliases of existing palette tokens so the
     matrix follows dark mode + Theme Studio. Consumed as var() strings by
     eisenhower.js QUADRANTS (matrix.js passes them through style.setProperty). */
  --quadrant-1: var(--color-coral);        /* Do First   */
  --quadrant-2: var(--color-accent);       /* Schedule   */
  --quadrant-3: var(--color-warning);      /* Delegate   */
  --quadrant-4: var(--color-cat-unknown);  /* Eliminate  */

  /* ============================================================
     2. Spacing tokens (from Theme.swift:60-69)
     ============================================================ */
  --space-xs:  4px;
  --space-sm:  8px;
  --space-md:  12px;
  --space-lg:  16px;
  --space-xl:  24px;
  --space-xxl: 32px;
  --screen-pad: 16px;
  --card-pad:   16px;
  --card-gap:   8px;

  /* ============================================================
     3. Radius tokens (from Theme.swift:73-79)
     ============================================================ */
  --radius-xs:   6px;
  --radius-sm:   8px;
  --radius-md:   10px;
  --radius-lg:   12px;
  --radius-xl:   16px;
  --radius-xxl:  20px;
  --radius-full: 360px;

  /* ============================================================
     4. Shadow tokens (from Theme.swift:83-95)
     ============================================================ */
  --shadow-card:    0 2px 8px rgba(0, 0, 0, 0.05);
  --shadow-tab-bar: 0 -10px 20px rgba(0, 0, 0, 0.05);
  --shadow-floating: 0 10px 30px rgba(0, 0, 0, 0.12);

  /* ============================================================
     4b. Motion tokens — shared with iOS Theme.Motion
         (BRAND_IDENTITY.md §8 — Motion language)
     ============================================================ */
  --motion-instant:    100ms;
  --motion-fast:       180ms;
  --motion-base:       240ms;
  --motion-slow:       360ms;
  --motion-cinematic:  600ms;

  /* Easings */
  --ease-out:    cubic-bezier(0.22, 0.61, 0.36, 1);   /* default — entrances */
  --ease-in:     cubic-bezier(0.55, 0, 0.68, 0.06);   /* exits */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);      /* state changes */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);   /* Occupemo bounce */

  /* Layout */
  --sidebar-width: 240px;
  --bottom-nav-height: 60px;
  --mobile-breakpoint: 768px;

  /* Z-index scale — a documented stack so we never have to play whack-a-mole
     with magic numbers. Always reach for these tokens (not raw numbers). */
  --z-sticky-header: 50;  /* sticky page headers (schedule/analytics mobile) — below the bottom nav */
  --z-bottom-nav:  100;   /* fixed mobile nav, sticky page headers below this */
  --z-dropdown:    200;   /* tooltips, small popovers */
  --z-modal:       500;   /* modal overlay */
  --z-popover:    1050;   /* popovers anchored to modal contents */
  --z-toast:      1100;   /* always on top */
}

/* ============================================================
   4c. prefers-reduced-motion — collapse durations to instant
       (browser-level honoring; component CSS still respects this
        via @media checks for transforms it can't disable here)
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-instant:   1ms;
    --motion-fast:      1ms;
    --motion-base:      1ms;
    --motion-slow:      1ms;
    --motion-cinematic: 1ms;
  }

  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   5. Dark mode — .dark class on <html>
      Values from Theme.swift dark variants.
      Automatically applied by initTheme() in ui.js based on
      prefers-color-scheme and localStorage.theme override.
   ============================================================ */
.dark {
  /* Native widget chrome (pickers, indicators, scrollbars) goes dark too. */
  color-scheme: dark;

  /* Accent lifts to ember #E5824F so it reads as text on the dark card;
     on-accent flips to ink #241407 since the accent is now light — filled
     accent controls in dark mode use ink labels, never white. */
  --color-accent:         #E5824F;
  --color-accent-light:   #EE9463;
  --color-accent-surface: #3C2A1E;
  --color-on-accent:      #241407;

  --color-bg:             #1D1812;
  --color-surface:        #27211A;
  --color-surface-dark:   #332B22;

  --color-text-primary:   #F2EADA;
  --color-text-secondary: #B0A48D;
  --color-text-muted:     #9A9080;
  --color-text-inactive:  #5F574B;

  --color-border:         #3B3327;
  --color-border-dark:    #57503F;

  /* Dark-tuned category + success values. Art/social/warning/error/coral
     inherit the light values (measured ≥3:1 on #1D1812). */
  --color-cat-academic:      #6389AF;
  --color-cat-sports:        #7D955C;
  --color-cat-entertainment: #69A9A2;
  --color-cat-unknown:       #A2998D;
  --color-success:           #6C9469;

  /* Contrast-retune text tokens — dark variants (see :root notes). */
  --color-accent-on-surface: var(--color-accent);                        /* ember on #3C2A1E = 4.95:1 */
  --color-accent-hover:      var(--color-accent-light);                  /* ink on #EE9463 = 7.68:1 */
  --color-error-text:        color-mix(in srgb, var(--color-error) 70%, white); /* #CF747E: 4.89 surf / 5.41 bg */
  --color-warning-text:      color-mix(in srgb, var(--color-warning) 70%, white); /* #CBA55B: 6.88:1 */
  --break-countdown-text:    color-mix(in srgb, var(--color-accent) 70%, white);  /* #EDA884: 5.51:1 after 0.75 dim */
  --cat-academic-txt:        #7697B9; /* raw 3.94 → 4.75:1 */
  --cat-sports-txt:          #869C67; /* raw 4.31 → 4.75:1 */
  --cat-art-txt:             var(--color-cat-art);           /* #D9A62E: 6.18:1 */
  --cat-entertainment-txt:   var(--color-cat-entertainment); /* #69A9A2: 5.22:1 */
  --cat-social-txt:          var(--color-cat-social);        /* #C97A92: 4.53:1 */
  --cat-unknown-txt:         var(--color-cat-unknown);       /* #A2998D: 4.98:1 */

  --shadow-card:    0 2px 8px rgba(0, 0, 0, 0.18);
  --shadow-tab-bar: 0 -10px 20px rgba(0, 0, 0, 0.18);
}

/* ============================================================
   6. Respect prefers-color-scheme as default (JS overrides via
      .dark class on <html> when localStorage.theme is set)
   ============================================================ */
@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    /* Keep in sync with the .dark block above (see contrast notes there). */
    color-scheme: dark;

    --color-accent:         #E5824F;
    --color-accent-light:   #EE9463;
    --color-accent-surface: #3C2A1E;
    --color-on-accent:      #241407;

    --color-bg:             #1D1812;
    --color-surface:        #27211A;
    --color-surface-dark:   #332B22;

    --color-text-primary:   #F2EADA;
    --color-text-secondary: #B0A48D;
    --color-text-muted:     #9A9080;
    --color-text-inactive:  #5F574B;

    --color-border:         #3B3327;
    --color-border-dark:    #57503F;

    /* Dark-tuned category + success values. Art/social/warning/error/coral
       inherit the light values (measured ≥3:1 on #1D1812). */
    --color-cat-academic:      #6389AF;
    --color-cat-sports:        #7D955C;
    --color-cat-entertainment: #69A9A2;
    --color-cat-unknown:       #A2998D;
    --color-success:           #6C9469;

    /* Contrast-retune text tokens — dark variants (keep in sync with .dark). */
    --color-accent-on-surface: var(--color-accent);
    --color-accent-hover:      var(--color-accent-light);
    --color-error-text:        color-mix(in srgb, var(--color-error) 70%, white);
    --color-warning-text:      color-mix(in srgb, var(--color-warning) 70%, white);
    --break-countdown-text:    color-mix(in srgb, var(--color-accent) 70%, white);
    --cat-academic-txt:        #7697B9;
    --cat-sports-txt:          #869C67;
    --cat-art-txt:             var(--color-cat-art);
    --cat-entertainment-txt:   var(--color-cat-entertainment);
    --cat-social-txt:          var(--color-cat-social);
    --cat-unknown-txt:         var(--color-cat-unknown);

    --shadow-card:    0 2px 8px rgba(0, 0, 0, 0.18);
    --shadow-tab-bar: 0 -10px 20px rgba(0, 0, 0, 0.18);
  }
}

/* ============================================================
   7. Base reset + body
   ============================================================ */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 15.5px;
  font-weight: 400;
  line-height: 1.55;
  color: var(--color-text-primary);
  background-color: var(--color-bg);
  letter-spacing: -0.005em;
  transition: background-color 0.2s ease, color 0.2s ease;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Display type — used on h1/h2/page titles via .page-title and friends */
h1, h2, h3, .page-title, .text-hero, .text-h1, .text-h2 {
  font-family: 'Bricolage Grotesque', 'Manrope', sans-serif;
  font-variation-settings: 'wdth' 92, 'opsz' 32;
  letter-spacing: -0.022em;
  font-weight: 700;
}

/* (A duplicate .text-hero/.text-h1/.text-h2 size block lived here — it was
   fully shadowed by the §8 typography scale below, which wins on source
   order at equal specificity. Deleted; §8 is the single source of truth.) */

a {
  color: var(--color-accent);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

img, svg {
  display: block;
  max-width: 100%;
}

button, input, select, textarea {
  font-family: inherit;
  font-size: inherit;
}

/* ============================================================
   8. Typography scale (from Theme.swift:47-56 + WEBSITE_SPEC §4.2)
   ============================================================ */
.text-hero {
  font-size: 34px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.5px;
}

.text-h1 {
  font-size: 27px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.3px;
}

.text-h2 {
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
}

.text-h3 {
  font-size: 18px;
  font-weight: 600;
  line-height: 1.3;
}

.text-body {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
}

.text-body-med {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.4;
}

.text-caption {
  font-size: 12px;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.2px;
}

.text-small {
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.1px;
}

/* Shared page header (goals/matrix/achievements emit the same markup).
   Promoted from goals.css — on pages that didn't load goals.css the title
   fell back to UA-default 31px/700 and the subtitle rendered as full-
   strength body text. */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-bottom: var(--space-xl);
  flex-wrap: wrap;
}

.page-title {
  font-size: 28px;
  font-weight: 800;
  color: var(--color-text-primary);
  margin: 0 0 4px;
  letter-spacing: -0.4px;
}

.page-subtitle {
  font-size: 14px;
  color: var(--color-text-secondary);
  margin: 0;
}

/* Utility color helpers */
.text-primary   { color: var(--color-text-primary); }
.text-secondary { color: var(--color-text-secondary); }
.text-muted     { color: var(--color-text-muted); }
.text-inactive  { color: var(--color-text-inactive); }
.text-accent    { color: var(--color-accent); }
.text-success   { color: var(--color-success); }
.text-warning   { color: var(--color-warning); }
.text-error     { color: var(--color-error); }

/* ============================================================
   9. Layout — App chrome
   App pages use a sidebar on desktop, bottom nav on mobile.
   Structure:
     <div class="app-layout">
       <div id="sidebar-root" class="sidebar"></div>   <!-- inner .sidebar-nav is the labeled landmark -->
       <main class="app-main">...</main>
     </div>
     <nav id="bottom-nav-root" class="bottom-nav"></nav>
   ============================================================ */
.app-layout {
  display: flex;
  min-height: 100vh;
}

.app-main {
  flex: 1;
  min-width: 0;
  padding: var(--space-xl);
  /* NO overflow-y here: the window is the real scroller (.app-main grows
     with content). An `overflow-y: auto` made .app-main the containment
     scrollport for position:sticky descendants — so the mobile sticky page
     headers on schedule/analytics could never stick. */
}

/* ============================================================
   10. Sidebar (desktop — shown >= 768px)
   --------------------------------------------------------------
   Mechanics mirror the mockup the user shared:
     • Top: bordered "logo frame" — large lockup expanded,
       small mark when collapsed.
     • Middle: nav list. Active item gets a tinted pill with a
       full border + accent-tinted icon + accent label.
     • Bottom: Plus badge, theme + sign-out icon utils, and the
       collapse toggle (rounded rect with a chevron pointing
       inward when expanded, outward when collapsed).
   ============================================================ */
.sidebar {
  --sidebar-pad-x: 14px;
  width: var(--sidebar-width);
  /* Pin to the viewport. align-self:flex-start opts out of .app-layout's
     default align-items:stretch — stretched to full document height, sticky
     had zero travel and the whole rail scrolled away on tall pages, leaving
     Sign out/collapse at the DOCUMENT bottom. height:100vh (not min-height)
     caps the box at the viewport so sticky engages; overflow-y:auto keeps
     the bottom utilities reachable on short viewports. */
  align-self: flex-start;
  height: 100vh;
  max-height: 100vh;
  background-color: var(--color-surface);
  border-right: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  padding: var(--space-lg) var(--sidebar-pad-x);
  position: sticky;
  top: 0;
  flex-shrink: 0;
  gap: var(--space-md);
  transition: width 0.22s cubic-bezier(0.2, 0.8, 0.2, 1),
              padding 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
  overflow-x: hidden;  /* clips content during the width collapse transition */
  overflow-y: auto;
}

.sidebar.collapsed {
  width: 76px;
  --sidebar-pad-x: 10px;
}

/* ─── Logo (top) ──────────────────────────────────────────────── */
/* No frame, no border. Wordmark+clock expanded; clock-only collapsed. */
.sidebar-logo-link {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 4px 6px;
  text-decoration: none;
  min-height: 40px;
  transition: opacity 0.15s ease;
}

.sidebar-logo-link:hover {
  text-decoration: none;
  opacity: 0.85;
}

.sidebar-logo {
  display: block;
  width: auto;
  max-width: 100%;
}

/* Inline SVGs from appIcon.js fill their span; the span fixes the box. */
.sidebar-logo svg {
  display: block;
  width: 100%;
  height: 100%;
}

.sidebar-logo--large { height: 26px; width: 133px; }
.sidebar-logo--small { display: none; height: 32px; width: 32px; }

.sidebar.collapsed .sidebar-logo-link {
  justify-content: center;
  padding: 4px 0;
}

.sidebar.collapsed .sidebar-logo--large { display: none; }
.sidebar.collapsed .sidebar-logo--small { display: block; }

/* ─── Nav list ────────────────────────────────────────────────── */
.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  margin-top: var(--space-xs);
}

.sidebar-nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: var(--radius-lg);
  border: 1px solid transparent;
  color: var(--color-text-primary);
  font-size: 14.5px;
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  background: transparent;
  width: 100%;
  text-align: left;
  white-space: nowrap;
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}

.sidebar-nav-item:hover {
  background-color: var(--color-bg);
  text-decoration: none;
}

.sidebar-nav-item.is-active {
  /* Tinted pill with a full accent border — the brand purple equivalent
     of the green active state from the mockup. */
  background-color: var(--color-accent-surface);
  border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);
  color: var(--color-accent-on-surface);
  font-weight: 600;
}

.sidebar-nav-icon {
  display: inline-flex;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  color: var(--color-text-secondary);
  transition: color 0.18s ease;
}

.sidebar-nav-icon svg {
  width: 20px;
  height: 20px;
  stroke-width: 1.75;
}

.sidebar-nav-item:hover .sidebar-nav-icon {
  color: var(--color-text-primary);
}

.sidebar-nav-item.is-active .sidebar-nav-icon {
  color: var(--color-accent);
}

/* Collapsed nav — icon-only, centered, active state becomes a tinted square */
.sidebar.collapsed .sidebar-nav-item {
  justify-content: center;
  padding: 10px;
  gap: 0;
}

.sidebar.collapsed .sidebar-nav-item.is-active {
  background-color: var(--color-accent-surface);
  border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);
}

.sidebar.collapsed .sidebar-nav-label {
  display: none;
}

/* ─── Bottom stack: badge, utilities, collapse toggle ────────── */
.sidebar-bottom {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

.sidebar-utilities {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--color-border);
}

/* Util buttons inherit .sidebar-nav-item from the cascade. This modifier
   gives them a slightly muted resting color so they don't compete with
   the primary nav for attention. */
.sidebar-nav-item--util {
  color: var(--color-text-secondary);
  font-weight: 500;
  font-size: 13.5px;
}

.sidebar-nav-item--util .sidebar-nav-icon {
  color: var(--color-text-muted);
}

.sidebar-nav-item--util:hover {
  color: var(--color-text-primary);
}

/* ─── Collapse toggle (always visible, bottom-most) ──────────── */
.sidebar-collapse-toggle {
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
  padding: 8px 12px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.sidebar-collapse-toggle:hover {
  background: var(--color-accent-surface);
  border-color: color-mix(in srgb, var(--color-accent) 25%, var(--color-border));
  color: var(--color-accent-on-surface);
}

/* Plus badge in collapsed mode: hide (no icon-only fallback yet) */
.sidebar.collapsed .plus-badge {
  display: none;
}

/* Hide sidebar on mobile */
@media (max-width: 767px) {
  .sidebar {
    display: none;
  }
  .app-main {
    /* Safe-area-aware padding so notches in landscape don't clip content.
       env() falls back to 0 on devices/browsers that don't expose it. */
    padding: var(--space-lg);
    padding-left:   calc(var(--space-lg) + env(safe-area-inset-left, 0px));
    padding-right:  calc(var(--space-lg) + env(safe-area-inset-right, 0px));
    padding-bottom: calc(var(--bottom-nav-height) + var(--space-lg) + env(safe-area-inset-bottom, 0px));
  }
}

/* Landscape phones report widths >= 768px (e.g. iPhone Pro Max is 932pt
   wide), so they get the SIDEBAR layout — the 767px block above never
   protects them. Pad the sidebar away from a left notch and the main
   column away from a right one. env() falls back to 0 elsewhere. */
@media (min-width: 768px) {
  .sidebar {
    padding-left: calc(var(--sidebar-pad-x) + env(safe-area-inset-left, 0px));
  }
  .app-main {
    padding-right: calc(var(--space-xl) + env(safe-area-inset-right, 0px));
  }
}

/* ============================================================
   11. Bottom nav (mobile — shown < 768px)
   ============================================================ */
.bottom-nav {
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  /* Add safe-area padding via min-height so the bar still measures correctly
     on notched devices in landscape (env() values fall back to 0 on others). */
  min-height: var(--bottom-nav-height);
  background-color: var(--color-surface);
  border-top: 1px solid var(--color-border);
  box-shadow: var(--shadow-tab-bar);
  z-index: var(--z-bottom-nav);
  padding: 0 var(--space-sm);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.bottom-nav-inner {
  display: flex;
  align-items: stretch;
  height: var(--bottom-nav-height);
  /* Fill the bar — .bottom-nav is display:flex on phones, and a default
     flex:0 1 auto inner shrink-wrapped to ~264px, clustering the tabs left
     with ~100px of dead bar and sub-44px tap widths. */
  flex: 1;
  width: 100%;
}

.bottom-nav-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* secondary, NOT inactive — these are enabled primary-nav controls; the
     inactive token measures 1.78:1 (light) / 2.94:1 (dark), far below AA. */
  color: var(--color-text-secondary);
  font-size: 10.5px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  border: none;
  background: none;
  transition: color 0.15s ease;
  /* Minimum 44pt iOS / 48dp Material touch target. */
  min-height: 44px;
  padding: var(--space-sm) var(--space-xs);
  border-radius: var(--radius-sm);
  -webkit-tap-highlight-color: transparent;
}

.bottom-nav-item:hover {
  color: var(--color-text-primary);
  text-decoration: none;
}

.bottom-nav-item:active {
  background-color: var(--color-accent-surface);
}

.bottom-nav-item.active {
  color: var(--color-accent);
}

.bottom-nav-item .nav-icon {
  width: 24px;
  height: 24px;
}

/* ─── "More" overflow menu — sheet anchored above the bar ─────────────────
   Rendered by ui.js renderBottomNav() as a child of the fixed .bottom-nav
   (which is its absolute containing block), so the sheet sits fully above
   the bar and the safe-area padding underneath it. Theme tokens only —
   dark mode and Theme Studio palettes follow automatically. No entrance
   animation, so prefers-reduced-motion needs no special casing. */
.bottom-nav-more-menu {
  position: absolute;
  bottom: calc(100% + 8px);
  right: var(--space-sm);
  min-width: 208px;
  max-width: calc(100vw - 2 * var(--space-sm));
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-floating);
  padding: var(--space-xs);
  z-index: var(--z-dropdown);
}

.bottom-nav-more-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-sm);
  min-height: 44px;            /* Apple HIG 44pt touch target */
  padding: 0 var(--space-md);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}

.bottom-nav-more-item:hover {
  background-color: var(--color-accent-surface);
  text-decoration: none;
}

.bottom-nav-more-item.active {
  color: var(--color-accent-on-surface);
  background-color: var(--color-accent-surface);
}

.bottom-nav-more-item .nav-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

@media (max-width: 767px) {
  .bottom-nav {
    display: flex;
  }
}

/* ============================================================
   12. Card component
   ============================================================ */
.card {
  background-color: var(--color-surface);
  border-radius: var(--radius-xl);
  padding: var(--space-lg);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset, var(--shadow-card);
  border: 1px solid var(--color-border);
  transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}

/* ============================================================
   Category chip selector — shared by Tasks, Goals, AddTask modal.
   Was previously only loaded with tasks.css; promoted to global so
   every modal/picker gets the same chip aesthetic.
   ============================================================ */
/* Editor form scaffolding — shared by the tasks editor, goal editor, and
   the reflection sheet. Promoted from tasks.css/goals.css page copies. */
.editor-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.editor-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
}

.category-selector {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

.category-option {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px 6px 10px;
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border: 1.5px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text-secondary);
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
  font-family: inherit;
  white-space: nowrap;
}

.category-option:hover {
  border-color: var(--color-accent);
  color: var(--color-text-primary);
}

.category-option.selected {
  border-color: var(--accent-cat-color, var(--color-accent));
  background-color: color-mix(in srgb, var(--accent-cat-color, var(--color-accent)) 12%, transparent);
  color: var(--accent-cat-color, var(--color-accent));
}

.category-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-block;
}

.card:hover {
  border-color: var(--color-border-dark, var(--color-border));
}

/* (The old .accent-card component — the AccentCard.swift port — was removed:
   nothing in the JS or HTML generates that class anymore.) */

/* ============================================================
   13. Button components
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 10px var(--space-lg);
  border-radius: var(--radius-lg);
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  border: none;
  text-decoration: none;
  transition:
    opacity var(--motion-fast) var(--ease-out),
    transform var(--motion-fast) var(--ease-out),
    background-color var(--motion-fast) var(--ease-out),
    box-shadow var(--motion-fast) var(--ease-out);
  white-space: nowrap;
  user-select: none;
  will-change: transform;
}

.btn:active {
  transform: scale(0.97);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Branded focus ring — meets WCAG AA, doesn't fight the design */
.btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.btn-primary {
  background-color: var(--color-accent);
  color: var(--color-on-accent);
  /* Flat print — no glow. Depth comes from the hover lift alone. */
}

.btn-primary:hover:not(:disabled) {
  /* Light hover DARKENS (white on accent-light #D06A42 was 3.61:1); the
     token resolves to #9D4223 (white 6.49:1) in light, accent-light in dark
     (ink 7.68:1). Keeps --color-accent-light for non-text uses. */
  background-color: var(--color-accent-hover);
  color: var(--color-on-accent);
  text-decoration: none;
  transform: translateY(-1px);
}

.btn-primary:active:not(:disabled) {
  transform: scale(0.97);
}

.btn-secondary {
  background-color: var(--color-accent-surface);
  color: var(--color-accent-on-surface);
}

.btn-secondary:hover:not(:disabled) {
  opacity: 0.85;
  text-decoration: none;
  transform: translateY(-1px);
}

.btn-ghost {
  background-color: transparent;
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
}

.btn-ghost:hover:not(:disabled) {
  background-color: var(--color-surface);
  color: var(--color-text-primary);
  text-decoration: none;
}

.btn-sm {
  padding: 6px var(--space-md);
  font-size: 14px;
  border-radius: var(--radius-md);
}

.btn-danger {
  /* Darkened via color-mix so white-on-fill stays well clear of 4.5:1 in
     both modes (raw --color-error #BA3846 measures ~5.6:1 under white;
     the mix adds headroom). */
  background-color: color-mix(in srgb, var(--color-error) 80%, black);
  color: #FFFFFF; /* on the darkened fill white is correct in BOTH modes */
}

.btn-danger:hover:not(:disabled) {
  opacity: 0.85;
  text-decoration: none;
}

/* Quiet destructive action (End Schedule) — outline/ghost, not a heavy fill.
   Transparent surface, 1.5px error border, error-token text. Dark-adaptive:
   --color-error-text lightens in dark (raw #BA3846 is 3.14:1 on the near-black
   header; #CF747E → 5.41:1). Light text passes raw (5.16:1). */
.btn-danger-ghost {
  background-color: transparent;
  border: 1.5px solid var(--color-error);
  color: var(--color-error-text);
}

.btn-danger-ghost:hover:not(:disabled) {
  background-color: color-mix(in srgb, var(--color-error) 10%, transparent);
  text-decoration: none;
}

/* ============================================================
   14. Input component
   ============================================================ */
.input {
  width: 100%;
  padding: 10px var(--space-md);
  background-color: var(--color-bg);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-lg);
  color: var(--color-text-primary);
  font-size: 16px; /* ≥16px keeps iOS Safari from zooming on focus (project convention) */
  font-weight: 400;
  line-height: 1.4;
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.input::placeholder {
  /* muted (not inactive) — the inactive token measured 1.78:1 on the input
     bg, leaving placeholders like "Search (Cmd+K)" nearly invisible. */
  color: var(--color-text-muted);
}

.input:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-surface);
}

.input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.input-error {
  border-color: var(--color-error);
}

.input-error:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 12%, transparent);
}

.input-label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text-primary);
  margin-bottom: var(--space-xs);
}

.input-hint {
  font-size: 12px;
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
}

.input-error-msg {
  font-size: 12px;
  /* error AS TEXT — lightens in dark (raw #BA3846 was 2.84:1 on dark surface). */
  color: var(--color-error-text);
  margin-top: var(--space-xs);
  /* Apply role="alert" on the HTML element itself so screen readers announce. */
}

/* ============================================================
   15. Pill / PillPicker (segmented capsule selector)
   ============================================================ */
.pill-group {
  display: inline-flex;
  gap: var(--space-xs);
  background-color: var(--color-surface);
  border-radius: var(--radius-full);
  padding: 3px;
}

.pill {
  padding: 6px var(--space-md);
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
  cursor: pointer;
  border: none;
  background: none;
  transition: background-color 0.15s ease, color 0.15s ease;
  white-space: nowrap;
}

.pill:hover {
  color: var(--color-text-primary);
}

.pill-selected {
  background-color: var(--color-bg);
  color: var(--color-accent);
  box-shadow: var(--shadow-card);
}

/* ============================================================
   16. Category badge (colored dot + label — mirrors CategoryBadge.swift)
   ============================================================ */
.category-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px 3px 6px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 700;
  background-color: var(--color-surface);
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
}

.category-badge-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* Category-specific badge colors — tint derives from the category token via
   color-mix so it can't drift if the palette is retuned. */
/* TEXT uses the contrast-tuned *-txt token (AA on the 8% wash); dot + border
   keep the FULL category color. */
.category-badge[data-category="academic"]      { color: var(--cat-academic-txt); border-color: var(--color-cat-academic); background-color: color-mix(in srgb, var(--color-cat-academic) 8%, transparent); }
.category-badge[data-category="sports"]        { color: var(--cat-sports-txt); border-color: var(--color-cat-sports); background-color: color-mix(in srgb, var(--color-cat-sports) 8%, transparent); }
.category-badge[data-category="art"]           { color: var(--cat-art-txt); border-color: var(--color-cat-art); background-color: color-mix(in srgb, var(--color-cat-art) 8%, transparent); }
.category-badge[data-category="entertainment"] { color: var(--cat-entertainment-txt); border-color: var(--color-cat-entertainment); background-color: color-mix(in srgb, var(--color-cat-entertainment) 8%, transparent); }
.category-badge[data-category="social"]        { color: var(--cat-social-txt); border-color: var(--color-cat-social); background-color: color-mix(in srgb, var(--color-cat-social) 8%, transparent); }
.category-badge[data-category="unknown"]       { color: var(--cat-unknown-txt); border-color: var(--color-cat-unknown); background-color: color-mix(in srgb, var(--color-cat-unknown) 8%, transparent); }

/* ============================================================
   17. Chip (filter chip — smaller, dismissible variant)
   ============================================================ */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 5px var(--space-sm);
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  white-space: nowrap;
}

.chip:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.chip-active {
  background-color: var(--color-accent-surface);
  color: var(--color-accent-on-surface);
  border-color: var(--color-accent);
}

/* ============================================================
   18. Toast notification
   ============================================================ */
.toast-container {
  position: fixed;
  /* Keep toasts clear of the bottom nav AND the bottom safe-area on iPhones. */
  bottom: calc(var(--bottom-nav-height) + var(--space-lg) + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  align-items: center;
  pointer-events: none;
}

@media (min-width: 768px) {
  .toast-container {
    bottom: var(--space-xl);
    left: auto;
    right: var(--space-xl);
    transform: none;
    align-items: flex-end;
  }
}

.toast {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 10px var(--space-lg);
  border-radius: var(--radius-xl);
  font-size: 14px;
  font-weight: 600;
  /* Inverted surface — uses --color-bg as the foreground so the toast
     reads correctly in both light and dark modes (was hardcoded #FFFFFF,
     which became invisible on the near-white dark-mode text color). */
  color: var(--color-bg);
  background-color: var(--color-text-primary);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  pointer-events: auto;
  animation: toast-in 0.25s ease;
  max-width: 320px;
  text-align: center;
}

/* Status-colored toasts (14px/600 → 4.5:1 AA floor). Retuned for the Circled
   Day palette — the old 30%/20%/30%-to-black mixes measured 3.16 (success L) /
   3.30 (error) / 4.15 (warning), all below floor. Now measured:
     · error   → white on #BA3846            = 5.61:1 (both modes)
     · warning → 15%-black shade on #B57E14   = 5.22:1 (both modes)
     · success → white on light fill #477A44  = 5.08:1;
                 20%-black shade on dark fill #6C9469 = 4.94:1
   Success alone is mode-split because its fill lightens in dark (white would
   drop to 3.46:1) while its light fill is too dark for a dark label (max black
   on #477A44 is 4.14:1). */
.toast-success { background-color: var(--color-success); color: #FFFFFF; }
.dark .toast-success,
:root:not(.light) .toast-success { color: color-mix(in srgb, var(--color-success) 20%, black); }
.toast-error   { background-color: var(--color-error);   color: #FFFFFF; }
.toast-warning { background-color: var(--color-warning); color: color-mix(in srgb, var(--color-warning) 15%, black); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes toast-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(8px); }
}

.toast-hiding {
  animation: toast-out 0.2s ease forwards;
}

/* ============================================================
   19. Modal
   ============================================================ */
.modal-overlay {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  animation: overlay-in 0.2s ease;
}

.modal-overlay.hidden {
  display: none;
}

.modal {
  background-color: var(--color-bg);
  border-radius: var(--radius-xxl);
  padding: var(--space-xl);
  max-width: 480px;
  width: 100%;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
  position: relative;
  animation: modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  max-height: 90vh;
  overflow-y: auto;
  /* iOS Safari: avoid the rubber-band scroll from leaking to the body. */
  overscroll-behavior: contain;
}

@media (max-width: 767px) {
  .modal-overlay {
    align-items: flex-end;
    padding: 0;
  }
  .modal {
    border-radius: var(--radius-xxl) var(--radius-xxl) 0 0;
    max-width: 100%;
    /* On phones use nearly the full viewport so big modals (Add Task,
       Theme Studio) don't get cropped behind a soft keyboard. The 4vh
       gap keeps a sliver of dimmed background visible so the user knows
       this is a sheet, not a full page. */
    max-height: calc(100vh - 4vh);
    padding-bottom: calc(var(--space-xl) + env(safe-area-inset-bottom, 0px));
    animation: modal-slide-up 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
}

.modal-close {
  position: absolute;
  top: var(--space-lg);
  right: var(--space-lg);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background-color: var(--color-surface);
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
  font-size: 16px;
  font-weight: 700;
  transition: background-color 0.15s ease;
}

.modal-close:hover {
  background-color: var(--color-border);
}

/* ─── Mobile touch targets ────────────────────────────────────────────────
   Shared small controls get >=40-44px hit areas on phones (Apple HIG 44pt).
   .modal-close keeps its 28px visual size; an invisible ::after overlay
   extends the tappable area to 44px. */
@media (max-width: 767px) {
  .btn,
  .btn-sm {
    min-height: 44px;
  }

  .chip,
  .pill {
    min-height: 40px;
  }

  .modal-close::after {
    content: '';
    position: absolute;
    inset: -8px;            /* 28px visual + 2×8px = 44px hit area */
    border-radius: 50%;
  }
}

@keyframes overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes modal-in {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes modal-slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* ============================================================
   20. Progress bar (mirrors ProgressBarView.swift)
   ============================================================ */
.progress-bar {
  width: 100%;
  height: 8px;
  background-color: var(--color-border);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.progress-bar-fill {
  height: 100%;
  background-color: var(--color-accent);
  border-radius: var(--radius-full);
  transition: width 0.4s ease;
}

.progress-bar-fill.success { background-color: var(--color-success); }
.progress-bar-fill.warning { background-color: var(--color-warning); }
.progress-bar-fill.error   { background-color: var(--color-error); }

/* ─── Live block progress bar (replaces the old Tools stopwatch) ──────────
   Rendered inside the "current" block on the dashboard timetable AND on
   the schedule active page. JS ticks at 1 Hz, updating fill width and the
   "12:34 left" / "+0:42 over" inline text. Designed so the visual sits
   naturally inside the card layout without restructuring it.

   --block-progress-tint is ALWAYS the accent (rebrand owner decision —
   category color lives on chips/dots only). The stylesheet default below
   is the single source; .is-overrun swaps it to the warning color. */
.block-progress {
  --block-progress-tint: var(--color-accent);
  margin-top: var(--space-sm);
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.block-progress-bar {
  flex: 1;
  height: 6px;
  background-color: var(--color-border);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.block-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--block-progress-tint); /* flat accent fill — no gradient, no glow */
  border-radius: var(--radius-full);
  transition: width 0.45s linear;
}

.block-progress-text {
  font-size: 12px;
  font-weight: 700;
  color: var(--block-progress-tint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  letter-spacing: 0.1px;
  min-width: 74px;
  text-align: right;
}

/* Dark mode: lighten the TEXT toward white while the bar keeps the raw
   tint. Recalibrated for the always-accent tint (was 55% for raw category
   hexes — that double-lightened ember to a pale peach). Measured on the
   dark card surface #27211A: ember #E5824F → #EDA884 = 7.99:1; overrun
   warning #B57E14 → #CBA55B = 6.88:1 (raw was a razor-thin 4.52:1). Also
   still rescues Theme Studio chromatic-dark presets, whose accent is the
   same hex in both modes (raw: Occupemo 2.98:1, Cocoa 2.68:1 — at 70%
   every preset measures >= 5.07:1). */
.dark .block-progress-text {
  color: color-mix(in srgb, var(--block-progress-tint) 70%, white);
}

/* Overrun countdown TEXT — light mode had no equivalent of the dark 70%-white
   rescue, so the raw warning measured 3.46:1. The mode-scoped --color-warning-text
   darkens in light (#916510 = 5.07:1) and lightens in dark (#CBA55B = 6.88:1).
   3-class specificity beats `.dark .block-progress-text`, so the token governs
   both modes. */
.block-progress.is-overrun .block-progress-text {
  color: var(--color-warning-text);
}

/* BREAK-card live countdown sits on the cream page bg (raw accent = 4.34:1) and
   is further dimmed by the 0.75 card opacity → effective 2.93:1. --break-countdown-text
   pre-compensates (light #6A2D18 → 4.96:1 after the dim; dark keeps the white
   idiom → 5.51:1). */
.block-row.break-block .block-progress-text {
  color: var(--break-countdown-text);
}

/* Overrun state — block ran past its scheduled end without being marked
   complete. Switches the visual to the warning color so users notice. */
.block-progress.is-overrun {
  --block-progress-tint: var(--color-warning);
}

.block-progress.is-overrun .block-progress-fill {
  background: var(--color-warning);
  animation: block-progress-overrun-pulse 1.6s ease-in-out infinite;
}

@keyframes block-progress-overrun-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.7; }
}

@media (prefers-reduced-motion: reduce) {
  .block-progress-fill {
    transition: none;
  }
  .block-progress.is-overrun .block-progress-fill {
    animation: none;
  }
}

/* ============================================================
   21. Pen circle (Circled Day brand signature — hand-drawn accent
   ellipse around the dashboard's current task title; geometry +
   lifecycle in assets/js/penCircle.js, mounted by dashboard.js)
   ============================================================ */
.occ-pen-circle {
  position: absolute;
  inset: 0; /* pre-measure default — penCircle.js sets left/top/size */
  overflow: visible;
  pointer-events: none;
  /* The global `img, svg { max-width: 100% }` reset (§7) would squash
     this — the ellipse is deliberately wider than its target text. */
  max-width: none;
  /* Decorative layer: sits above the card surface but never above
     interactive siblings' hit areas (pointer-events:none regardless).
     Raw `1` (no --z-* token): the --z-* scale starts at 100 (bottom-nav) and
     governs page-level stacking contexts; this is a hyper-local +1 lift inside
     the hero card's own context, well below any token tier — a token would
     overstate its scope. */
  z-index: 1;
}

.occ-pen-circle path {
  fill: none;
  stroke: var(--color-accent); /* terracotta light / ember dark — mode + Theme Studio follow automatically */
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1000; /* matches pathLength="1000" set by penCircle.js */
  /* Resting/fallback state: FULLY DRAWN. Covers prefers-reduced-motion and
     any path where the draw-on never runs — penCircle.js only offsets this
     when it is about to animate. */
  stroke-dashoffset: 0;
  filter: url(#occ-ink);
}

/* The hero card is already position:relative (dashboard.css) — asserted
   here too so the overlay can never silently anchor to the page if that
   rule moves. */
.now-hero-card {
  position: relative;
}

@media (prefers-reduced-motion: reduce) {
  /* JS renders the circle fully drawn with no animation under reduced
     motion; belt-and-braces against future CSS-driven motion here. */
  .occ-pen-circle path {
    transition: none;
    animation: none;
  }
}

/* ============================================================
   21b. Motion (Circled Day) — countdown digit roll
   (assets/js/motion.js — createSecondsRoller / staggerIn)
   ============================================================ */
/* Clip window for the countdown's rolling seconds pair ("12:34 left" — the
   "34"). Purely decorative structure: the 1 Hz timers fall back to a plain
   textContent write whenever it's absent (see motion.js guard).
   NOTE (baseline): an inline-block with overflow:hidden baselines at its
   BOTTOM MARGIN EDGE (CSS2 quirk) — that visibly raised the seconds above
   the "12:" prefix in harness shots. overflow:clip is not a scroll
   container, so the box keeps its natural text baseline and the seconds
   sit exactly on the surrounding line (verified in headless Chromium).
   hidden stays first as the graceful fallback for engines without clip —
   same clipping, digits ride ~0.2em high there (cosmetic only). */
.digit-roll {
  position: relative;
  display: inline-block;
  overflow: hidden;
  overflow: clip;
  height: 1em;
  line-height: 1;
  vertical-align: baseline;
}

.digit-roll__inner {
  display: block;
  /* tabular-nums so the rolling pair can never change width between ticks.
     .block-progress-text (§20) already sets this, but enforce it on the
     roller itself so any future countdown surface gets it for free. */
  font-variant-numeric: tabular-nums;
}

/* The load stagger (motion.js staggerIn) is pure WAAPI — no CSS hooks, and
   reduced motion is a JS-side no-op. Belt-and-braces: nothing under
   .digit-roll may ever animate via CSS on its own. */
@media (prefers-reduced-motion: reduce) {
  .digit-roll,
  .digit-roll__inner {
    transition: none;
    animation: none;
  }
}

/* ============================================================
   21c. Motion (Circled Day) — personality micro-visuals
   (assets/js/motion.js drawStrike/popCheck/flickFlame + the
   header date circle, mounted by dashboard.js + schedule.js.
   Owner-approved set of four — the plan's "§22" block; numbered
   21c because §22 was already taken by the Plus badge below.)
   ============================================================ */

/* Drawn strike-through (completion moment) — replaces text-decoration:
   line-through on done timeline rows (dashboard timetable + schedule
   active view; the old rules were removed from those page sheets).
   The line span is absolutely positioned (zero layout impact; ellipsis
   truncation unaffected — out-of-flow content doesn't count toward the
   inline overflow) and rests FULLY DRAWN: already-done rows and every
   reduced-motion/no-JS-animation path read final state. drawStrike only
   animates scaleX (left origin, pen-style) on a LIVE complete. */
.occ-struck {
  position: relative;
}

.occ-strike-line {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(50% - 1px);
  height: 1.5px;
  border-radius: 1px;
  /* currentColor — exactly what the replaced text-decoration used, so the
     strike keeps tracking each row state's title color (muted when done,
     accent mid-complete). */
  background: currentColor;
  transform: rotate(-0.6deg);    /* hand-skew */
  transform-origin: left center; /* the pen starts at the left */
  pointer-events: none;
  filter: url(#occ-ink);         /* same roughness as the pen circles */
}

/* Pencil hover underline — CSS-only affordance on timeline/list row titles
   (dashboard timetable + schedule active timeline). Absolutely positioned
   1px line (no layout shift), scaleX 0→1 left-origin 150ms like a light
   pencil pass; retracts on leave. :focus-within gives keyboard parity
   (dashboard rows are tabbable; schedule rows focus via their buttons).
   Wrapped in (hover: hover) so touch devices skip the machinery entirely. */
@media (hover: hover) {
  .timetable-block .block-name,
  .block-row .block-card-name {
    position: relative;
  }

  .timetable-block .block-name::after,
  .block-row .block-card-name::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--color-text-inactive);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 150ms var(--ease-out, ease);
    pointer-events: none;
  }

  .timetable-block:hover .block-name::after,
  .timetable-block:focus-within .block-name::after,
  .block-row:hover .block-card-name::after,
  .block-row:focus-within .block-card-name::after {
    transform: scaleX(1);
  }
}

/* ✓ glyph wrapper inside Complete buttons — popCheck's transform target.
   inline-block so scale applies (a non-replaced inline ignores transforms);
   sits inside a single label span, so the button's flex gap and accessible
   name ("✓ Complete") are unchanged. */
.occ-check {
  display: inline-block;
}

/* Flame flick pivots near the glyph's base so it reads as a flicker, not a
   spin. Static property — only motion.js's one-shot WAAPI call animates it. */
.streak-chip-icon {
  transform-origin: 50% 85%;
}

/* The header's date line hosts the second pen circle (the circled
   day-of-month numeral) — the overlay anchors to this positioned ancestor.
   Belt-and-braces mirror of the .now-hero-card assertion in §21. */
.dashboard-subline {
  position: relative;
}

/* Reduced motion: every micro-visual is a JS-side no-op (final state
   rendered instantly) — belt-and-braces against future CSS-driven motion,
   same pattern as §21/§21b. */
@media (prefers-reduced-motion: reduce) {
  .occ-strike-line,
  .occ-check,
  .streak-chip-icon {
    transition: none;
    animation: none;
  }
  .timetable-block .block-name::after,
  .block-row .block-card-name::after {
    transition: none;
  }
}

/* (The old §21 Empty state that lived here was a dead duplicate fully
   shadowed by the canonical §22a .empty-state component below. Deleted.) */

/* ============================================================
   22. Plus badge (sidebar bottom — upgrade prompt)
   ============================================================ */
.plus-badge {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--gradient-text-safe); /* flat accent surface — label color tracks --color-on-accent for AA in both modes */
  border-radius: var(--radius-lg);
  cursor: pointer;
  text-decoration: none;
  transition: opacity 0.15s ease;
}

.plus-badge:hover {
  opacity: 0.9;
  text-decoration: none;
}

.plus-badge-label {
  font-size: 13px;
  font-weight: 700;
  color: var(--color-on-gradient); /* tracks --color-on-accent per mode — see --gradient-text-safe above */
}

.plus-badge-sub {
  font-size: 11px;
  font-weight: 500;
  /* Full-strength on-gradient — the previous 80%-white measured 2.2-3.6:1.
     Hierarchy comes from the smaller size/weight, not from translucency. */
  color: var(--color-on-gradient);
}

/* Active Plus state — same flat accent fill with a sparkle and a hairline
   inset ring (zero-blur = border technique, not a glow) */
.plus-badge--active {
  position: relative;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18) inset;
}

.plus-badge-spark {
  font-size: 18px;
  color: var(--color-on-gradient); /* see .plus-badge-label */
}

/* ─── Plus gate (free users on Plus-only pages: Goals, Matrix) ──────────
   Shared component — canonical home is here so every gated page styles it
   (the goals.css/matrix.css copies were removed in favor of this one). */
.plus-gate {
  text-align: center;
  max-width: 460px;
  margin: var(--space-xxl) auto;
  padding: var(--space-xl);
  border-radius: var(--radius-xl);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.plus-gate-spark {
  font-size: 44px;
  background: var(--color-accent); /* flat — clip kept so glyphs render as accent silhouettes */
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin-bottom: var(--space-md);
}

.plus-gate-title {
  font-size: 22px;
  font-weight: 800;
  margin: 0 0 var(--space-sm);
  color: var(--color-text-primary);
}

.plus-gate-body {
  font-size: 14px;
  color: var(--color-text-secondary);
  line-height: 1.55;
  margin: 0 0 var(--space-lg);
}

/* ─── Upgrade sheet (subscription.js showUpgradeModal) ──────────────────
   Opened from goals/matrix/achievements/settings/schedule alike, so it
   lives here, not in a page stylesheet (it used to be schedule.css-only,
   leaving the modal unstyled on every other page). */
.upgrade-sheet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  text-align: center;
}

.upgrade-sheet-icon {
  font-size: 40px;
}

.upgrade-sheet-headline {
  font-size: 20px;
  font-weight: 700;
  color: var(--color-text-primary);
}

.upgrade-sheet-body {
  font-size: 15px;
  color: var(--color-text-secondary);
  line-height: 1.5;
  max-width: 340px;
}

.upgrade-sheet-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  width: 100%;
  margin-top: var(--space-sm);
}

/* "Coming soon to the App Store" note under the CTA (shown while the App
   Store URL is still a placeholder) — lets subscription.js drop its inline
   style. */
.upgrade-sheet-note {
  font-size: 12px;
  color: var(--color-text-secondary);
  margin: 10px 0 0;
}

/* ─── Celebration sheet ─────────────────────────────────────────────────── */
.celebration-sheet {
  text-align: center;
  padding: var(--space-xl) var(--space-lg);
  max-width: 460px;
}

.celebration-spark {
  font-size: 56px;
  background: var(--color-accent); /* flat — clip kept so glyphs render as accent silhouettes */
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin-bottom: var(--space-md);
  animation: spark-spin 1.6s ease-in-out 1;
}

@keyframes spark-spin {
  0%   { transform: scale(0.4) rotate(-12deg); opacity: 0; }
  60%  { transform: scale(1.18) rotate(8deg); opacity: 1; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}

.celebration-headline {
  font-size: 26px;
  font-weight: 800;
  background: var(--color-accent); /* flat accent, clipped to the text */
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin: 0 0 var(--space-sm);
  letter-spacing: -0.4px;
}

.celebration-body {
  font-size: 15px;
  line-height: 1.55;
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-xl);
}

.celebration-actions {
  display: flex;
  justify-content: center;
}

/* ─── Add Task modal (iOS AddTaskSheet parity) ──────────────────────────── */
.add-task-modal {
  max-width: 540px;
  width: 100%;
  padding: var(--space-md) var(--space-sm);
}

.add-task-modal-title {
  font-family: 'Bricolage Grotesque', 'Manrope', sans-serif;
  font-variation-settings: 'wdth' 92, 'opsz' 28;
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 var(--space-md);
  color: var(--color-text-primary);
}

.add-task-modal-nl {
  font-size: 16px; /* ≥16px keeps iOS Safari from zooming on focus */
  padding: 12px 14px;
}

.add-task-modal-nl-hint {
  margin: 6px 2px 0;
  font-size: 12.5px;
  color: var(--color-text-muted);
  line-height: 1.4;
}

.add-task-modal-nl-hint em {
  font-style: normal;
  color: var(--color-text-secondary);
  font-weight: 600;
}

.add-task-modal-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: var(--space-sm) 0 var(--space-lg);
}

.add-task-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 12px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text-secondary);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}

.add-task-chip:hover {
  border-color: var(--color-accent);
  color: var(--color-text-primary);
}

.add-task-chip--override {
  border-color: var(--color-accent);
  background-color: var(--color-accent-surface);
  color: var(--color-accent-on-surface);
}

.add-task-modal-field {
  margin-top: var(--space-md);
}

.add-task-modal-field-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}

.add-task-modal-more-toggle {
  background: transparent;
  border: none;
  color: var(--color-text-secondary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  padding: var(--space-md) 0 var(--space-xs);
  text-align: left;
}

.add-task-modal-more-toggle:hover {
  color: var(--color-accent);
}

.add-task-modal-more.is-hidden {
  display: none;
}

.add-task-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
}

/* ─── Popover (chip pickers) ─────────────────────────────────────────────── */
.add-task-popover {
  position: absolute;
  z-index: var(--z-popover);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 16px 38px -10px rgba(0, 0, 0, 0.22);
  padding: 6px;
  min-width: 200px;
  max-width: 320px;
  max-height: 320px;
  overflow-y: auto;
}

.add-task-popover-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px;
}

.add-task-popover-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.add-task-popover-option {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--color-text-primary);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  width: 100%;
  text-align: left;
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.add-task-popover-option:hover {
  background: var(--color-accent-surface);
}

.add-task-popover-option.is-active {
  /* Consume the per-option token addTaskModal.js sets (urgency options carry
     their category/urgency color) — same pattern as .category-option.selected. */
  background: color-mix(in srgb, var(--accent-cat-color, var(--color-accent)) 10%, transparent);
  color: var(--accent-cat-color, var(--color-accent));
  border-color: color-mix(in srgb, var(--accent-cat-color, var(--color-accent)) 30%, transparent);
}

.add-task-popover-date {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px;
}

.add-task-popover-clear {
  background: transparent;
  border: none;
  color: var(--color-error-text);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  padding: 4px 8px;
  text-align: left;
}

/* ─── Onboarding flow ──────────────────────────────────────────────────── */
.onboarding-sheet {
  max-width: 560px;
  width: 100%;
  padding: var(--space-md);
  text-align: left;
}

.onboarding-step-tag {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
}

.onboarding-spark {
  font-size: 36px;
  background: var(--color-accent); /* flat — clip kept so glyphs render as accent silhouettes */
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin-bottom: var(--space-md);
  display: inline-block;
}

.onboarding-title {
  font-family: 'Bricolage Grotesque', 'Manrope', sans-serif;
  font-variation-settings: 'wdth' 92, 'opsz' 32;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--color-text-primary);
  margin: 0 0 var(--space-sm);
}

.onboarding-title--gradient {
  /* legacy class name — now a flat accent, clipped to the text */
  background: var(--color-accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.onboarding-body {
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-md);
}

.onboarding-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm);
  margin: var(--space-md) 0;
}

@media (max-width: 480px) {
  .onboarding-grid { grid-template-columns: 1fr; }
}

.onboarding-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  border: 1.5px solid var(--color-border);
  background: var(--color-bg);
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  color: var(--color-text-primary);
  transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.12s ease;
}

.onboarding-card:hover {
  border-color: var(--color-accent);
  transform: translateY(-1px);
}

.onboarding-card.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-surface);
  color: var(--color-accent-on-surface);
}

.onboarding-card-icon {
  width: 28px;
  height: 28px;
  color: var(--color-accent);
}

.onboarding-card-label {
  font-size: 15px;
  font-weight: 700;
}

.onboarding-card-blurb {
  font-size: 12px;
  color: var(--color-text-muted);
}

/* Drop the 0.75 dim (failed AA both modes) — the darkened token carries the
   contrast: #9D4223 on #F6E3D3 = 5.20:1 light, ember on #3C2A1E = 4.95:1 dark. */
.onboarding-card.is-selected .onboarding-card-blurb { color: var(--color-accent-on-surface); }

.onboarding-q {
  margin-bottom: var(--space-lg);
}

.onboarding-q-label {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--color-text-primary);
  margin-bottom: 8px;
}

.onboarding-q-options {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.onboarding-q-option {
  padding: 8px 14px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-border);
  background: var(--color-bg);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

.onboarding-q-option:hover {
  border-color: var(--color-accent);
}

.onboarding-q-option.is-selected {
  border-color: var(--color-accent);
  background: var(--color-accent-surface);
  color: var(--color-accent-on-surface);
}

.onboarding-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-md);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
}

.onboarding-skip-link,
.onboarding-back-link {
  background: none;
  border: none;
  padding: 8px 4px;
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}

.onboarding-skip-link {
  order: -1;                 /* sits to the left of the primary CTA */
  margin-right: auto;        /* pushes the CTA to the right */
}

.onboarding-back-link {
  order: -2;                 /* leftmost control in the row */
  margin-right: auto;
}

/* When Back is present it owns the left edge; Skip moves next to the CTA. */
.onboarding-back-link ~ .onboarding-skip-link {
  margin-right: 0;
}

.onboarding-skip-link:hover,
.onboarding-back-link:hover {
  color: var(--color-text-primary);
  text-decoration-color: currentColor;
}

.onboarding-skip-link:focus-visible,
.onboarding-back-link:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* Calibration animation */
.onboarding-calibration {
  text-align: center;
  padding: var(--space-xl) var(--space-md);
}

.onboarding-calibration-spinner {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: var(--space-lg);
}

.onboarding-calibration-orb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--color-accent); /* flat accent dot */
  animation: onboarding-orb-bounce 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.onboarding-calibration-orb:nth-child(2) { animation-delay: 0.15s; }
.onboarding-calibration-orb:nth-child(3) { animation-delay: 0.30s; }

@keyframes onboarding-orb-bounce {
  0%, 100% { transform: translateY(0) scale(0.9); opacity: 0.65; }
  50%      { transform: translateY(-12px) scale(1.0); opacity: 1; }
}

.onboarding-status {
  font-size: 14px;
  font-weight: 500;
  color: var(--color-text-secondary);
  transition: opacity 0.3s ease;
}

/* Mobile touch targets (Apple HIG 44pt) for onboarding controls — the
   shared block in §modal covers .btn-sm/.chip/.pill but not these. */
@media (max-width: 767px) {
  .onboarding-actions .btn,
  .onboarding-q-option,
  .onboarding-skip-link,
  .onboarding-back-link {
    min-height: 44px;
  }
}

/* ─── Reflection sheet ──────────────────────────────────────────────────── */
.reflection-sheet {
  max-width: 500px;
  width: 100%;
  padding: var(--space-md);
}

.reflection-sheet-title {
  font-size: 22px;
  font-weight: 800;
  margin: 0 0 4px;
  color: var(--color-text-primary);
}

.reflection-sheet-sub {
  font-size: 13px;
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-lg);
}

.reflection-moods {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

.reflection-mood {
  background: var(--color-bg);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-md) var(--space-sm);
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.12s ease;
}

.reflection-mood:hover {
  border-color: var(--color-accent);
}

.reflection-mood.selected {
  border-color: var(--color-accent);
  background: var(--color-accent-surface);
  transform: translateY(-2px);
}

.reflection-mood-emoji {
  font-size: 32px;
}

.reflection-mood-label {
  font-size: 14px;
  font-weight: 700;
  color: var(--color-text-primary);
  margin-top: 4px;
}

.reflection-mood-sub {
  font-size: 11px;
  color: var(--color-text-muted);
  margin-top: 2px;
}

.reflection-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

/* ============================================================
   22a. Empty state — canonical component (was duplicated in tasks.css)
   ============================================================ */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-xxl) var(--space-xl);
  text-align: center;
  gap: var(--space-md);
  /* Subtle entrance — not a scroll reveal since empty states appear in-place */
  animation: empty-state-in var(--motion-slow) var(--ease-out) both;
}

@keyframes empty-state-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .empty-state { animation: none; }
}

.empty-state-icon {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-full);
  background: var(--gradient-text-safe);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-on-gradient);
  font-size: 28px;
  font-weight: 700;
  line-height: 1;
  margin-bottom: var(--space-xs);
}

.empty-state-icon svg {
  width: 28px;
  height: 28px;
}

/* Modifier — error variant (red bg, white symbol).
   Use when the empty state represents a failure rather than an absence. */
.empty-state-icon--error {
  background: var(--color-error);
}

.empty-state-title {
  font-size: 17px;
  font-weight: 700;
  color: var(--color-text-primary);
  margin: 0;
}

.empty-state-body,
.empty-state-subtitle {
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-text-secondary);
  max-width: 320px;
  margin: 0;
}

.empty-state .btn {
  margin-top: var(--space-sm);
}

/* ============================================================
   22b. Loading skeleton — canonical (was duplicated in dashboard.css + schedule.css)
   ============================================================ */
.skeleton {
  background: linear-gradient( /* skeleton shimmer — sanctioned exception (neutral surface/border mix) */
    90deg,
    var(--color-surface) 25%,
    var(--color-border) 50%,
    var(--color-surface) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease infinite;
  border-radius: var(--radius-md);
}

@keyframes skeleton-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

.skeleton-line {
  height: 14px;
  margin-bottom: var(--space-sm);
}
.skeleton-line:last-child { margin-bottom: 0; }
.skeleton-line.short  { width: 45%; }
.skeleton-line.medium { width: 70%; }
.skeleton-line.full   { width: 100%; }

.skeleton-block {
  height: 72px;
  border-radius: var(--radius-xl);
  margin-bottom: var(--space-sm);
}

/* Reduce motion → freeze the shimmer */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
    background: var(--color-surface);
  }
}

/* ============================================================
   22c. Global focus-visible — branded ring for all interactive elements
        (browsers' default ring is faint; this matches the accent)
   ============================================================ */
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  /* NO border-radius here: outlines already follow the element's own
     border-radius in modern engines. A radius on the bare pseudo-class
     restyled the focused ELEMENT — every input morphed 12px→6px on click
     and the circular modal × snapped to a 6px square on keyboard focus.
     (.onboarding-skip-link sets its own radius deliberately — that one is
     a plain link with no resting radius.) */
}

/* ============================================================
   22c-2. Skip-to-content link — canonical shared component.
   Consolidated from 9 identical inline app-page <style> blocks and a
   divergent marketing.css variant. Auth pages get it for free once their
   anchors land. box-shadow lives on :focus only — in the hidden resting
   state the translated element's 30px-blur shadow bled into the top-left
   viewport corner on every page.
   ============================================================ */
.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: var(--z-toast);
  padding: 10px 16px;
  border-radius: var(--radius-md);
  background-color: var(--color-accent);
  color: var(--color-on-accent); /* matches .btn-primary */
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transform: translateY(calc(-100% - 16px));
}

.skip-link:focus {
  transform: translateY(0);
  box-shadow: var(--shadow-floating);
}

/* Skip-link targets receive programmatic focus (tabindex="-1") — the ring
   is noise there; focus order continues naturally from the main region. */
main[tabindex="-1"]:focus {
  outline: none;
}

/* .btn, .input, .chip, .pill have their own focus rules at the component
   level — global :focus-visible above falls back for everything else. */

/* ============================================================
   22d. Scroll-reveal — marketing page entrance animations
        Add data-reveal to any element to fade+lift it into view as
        it enters the viewport. JS toggles the class via IntersectionObserver.
        Honors prefers-reduced-motion (no animation, instant final state).
   ============================================================ */
[data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity var(--motion-slow) var(--ease-out),
    transform var(--motion-slow) var(--ease-out);
  will-change: opacity, transform;
}

[data-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

/* Staggered children: add data-reveal-stagger to a parent, and each
   direct [data-reveal] child gets a 60ms cumulative delay. */
[data-reveal-stagger] [data-reveal]:nth-child(1) { transition-delay: 0ms; }
[data-reveal-stagger] [data-reveal]:nth-child(2) { transition-delay: 60ms; }
[data-reveal-stagger] [data-reveal]:nth-child(3) { transition-delay: 120ms; }
[data-reveal-stagger] [data-reveal]:nth-child(4) { transition-delay: 180ms; }
[data-reveal-stagger] [data-reveal]:nth-child(5) { transition-delay: 240ms; }
[data-reveal-stagger] [data-reveal]:nth-child(6) { transition-delay: 300ms; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ============================================================
   23. Utility classes
   ============================================================ */
.hidden   { display: none !important; }
.sr-only  {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.flex        { display: flex; }
.flex-col    { flex-direction: column; }
.flex-1      { flex: 1; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-xs  { gap: var(--space-xs); }
.gap-sm  { gap: var(--space-sm); }
.gap-md  { gap: var(--space-md); }
.gap-lg  { gap: var(--space-lg); }
.gap-xl  { gap: var(--space-xl); }
.p-md    { padding: var(--space-md); }
.p-lg    { padding: var(--space-lg); }
.p-xl    { padding: var(--space-xl); }
.mt-sm   { margin-top: var(--space-sm); }
.mt-md   { margin-top: var(--space-md); }
.mt-lg   { margin-top: var(--space-lg); }
.mt-xl   { margin-top: var(--space-xl); }
.mb-sm   { margin-bottom: var(--space-sm); }
.mb-md   { margin-bottom: var(--space-md); }
.mb-lg   { margin-bottom: var(--space-lg); }
.mb-xl   { margin-bottom: var(--space-xl); }
.w-full  { width: 100%; }

/* Accent text (for the Occupemo wordmark — legacy class name, now flat) */
.gradient-text {
  background: var(--color-accent);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* (§24 Reduced motion lived here — a duplicate of the §4c catch-all near
   the top of the file, which also collapses the --motion-* tokens. Deleted.) */

/* ============================================================
   Theme Studio — inlined here so it can't fail to load.
   ============================================================ */
/* ─── Theme Studio — Occupemo's Arc-flavor color picker ───────────────── */

/* The modal-box from ui.js wraps us. Tighten the chrome around the studio. */
.modal-box:has(.ts) {
  padding: 0 !important;
  background: transparent !important;
  border: none !important;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.55);
  max-width: 560px;
  width: calc(100vw - 32px);
}

.ts {
  background: #14141a;
  color: rgba(255, 255, 255, 0.92);
  font-family: 'Manrope', sans-serif;
  border-radius: 22px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* ─── Top bar ─────────────────────────────────────────────────────────── */
.ts-topbar {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 14px 16px 6px;
}

.ts-mode {
  display: inline-flex;
  gap: 2px;
  background: rgba(255, 255, 255, 0.04);
  padding: 4px;
  border-radius: 12px;
}

.ts-mode-btn {
  background: transparent;
  border: none;
  color: rgba(255, 255, 255, 0.55);
  width: 36px;
  height: 30px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  transition: background-color 0.18s ease, color 0.18s ease;
  padding: 0;
}

.ts-mode-btn:hover {
  color: rgba(255, 255, 255, 0.92);
  background: rgba(255, 255, 255, 0.05);
}

.ts-mode-btn.is-active {
  background: rgba(255, 255, 255, 0.14);
  color: #ffffff;
}

.ts-mode-btn svg { width: 16px; height: 16px; }

/* ─── Canvas card ─────────────────────────────────────────────────────── */
.ts-canvas {
  position: relative;
  margin: 6px 16px 14px;
  height: 320px;
  border-radius: 18px;
  background: #0d0d12;
  overflow: hidden;
  isolation: isolate;
}

/* Color field — hue across X, lightness top→bottom. */
.ts-canvas-field {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom, /* ts-canvas lightness ramp — sanctioned functional UI */
      rgba(255, 255, 255, 0.55) 0%,
      rgba(255, 255, 255, 0)    50%,
      rgba(0, 0, 0, 0.55)       100%),
    linear-gradient(to right, /* ts-canvas hue ramp — sanctioned functional UI */
      hsl(0,   80%, 60%) 0%,
      hsl(45,  80%, 60%) 12.5%,
      hsl(90,  80%, 60%) 25%,
      hsl(135, 80%, 60%) 37.5%,
      hsl(180, 80%, 60%) 50%,
      hsl(225, 80%, 60%) 62.5%,
      hsl(270, 80%, 60%) 75%,
      hsl(315, 80%, 60%) 87.5%,
      hsl(360, 80%, 60%) 100%);
  opacity: 0.9;
  pointer-events: none;
}

/* Subtle dotted texture overlay. */
.ts-canvas-dots {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px); /* ts-canvas dot texture — sanctioned functional UI */
  background-size: 14px 14px;
  pointer-events: none;
  mix-blend-mode: overlay;
}

/* Balls. */
.ts-ball {
  position: absolute;
  width: 58px;
  height: 58px;
  padding: 0;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  border: 3px solid #ffffff;
  background-clip: padding-box;
  box-shadow:
    0 6px 22px -3px rgba(0, 0, 0, 0.55),
    inset 0 0 0 1px rgba(255, 255, 255, 0.18);
  cursor: grab;
  transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1),
              box-shadow 0.18s ease;
  touch-action: none;
}

.ts-ball:hover { transform: translate(-50%, -50%) scale(1.06); }

.ts-ball.is-active,
.ts-ball:active {
  cursor: grabbing;
  box-shadow:
    0 6px 22px -3px rgba(0, 0, 0, 0.55),
    0 0 0 4px rgba(255, 255, 255, 0.22),
    inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}

.ts-ball.is-primary   { width: 62px; height: 62px; }
.ts-ball.is-secondary { width: 50px; height: 50px; }

/* +/- counter at bottom-center of the canvas. */
.ts-counter {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 999px;
  padding: 3px;
}

.ts-counter-btn {
  width: 28px;
  height: 28px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: rgba(255, 255, 255, 0.7);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  padding: 0;
  transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}

.ts-counter-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.10);
  color: #ffffff;
}

.ts-counter-btn:disabled { opacity: 0.32; cursor: not-allowed; }
.ts-counter-btn svg { width: 14px; height: 14px; }

/* ─── Preset row ─────────────────────────────────────────────────────── */
.ts-presets {
  display: grid;
  grid-template-columns: 32px 1fr 32px;
  align-items: center;
  gap: 8px;
  padding: 0 16px;
  margin-bottom: 12px;
}

.ts-arrow {
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: rgba(255, 255, 255, 0.65);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  padding: 0;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.ts-arrow:hover {
  background: rgba(255, 255, 255, 0.10);
  color: #ffffff;
}

.ts-arrow svg { width: 14px; height: 14px; }

.ts-preset-strip {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none;
  padding: 2px 0;
}

.ts-preset-strip::-webkit-scrollbar { display: none; }

.ts-preset {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.06);
  cursor: pointer;
  padding: 0;
  font-family: inherit;
  transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.ts-preset:hover {
  transform: scale(1.12);
  border-color: rgba(255, 255, 255, 0.6);
  box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.45);
}

/* ─── Sliders row: hue + knob ────────────────────────────────────────── */
.ts-sliders {
  display: grid;
  grid-template-columns: 1fr 76px;
  align-items: center;
  gap: 14px;
  padding: 6px 16px 14px;
}

/* Hue slider — full-width track with decorative sine inside. */
.ts-hue {
  position: relative;
  height: 48px;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  touch-action: none;
  overflow: hidden;
}

.ts-hue-wave {
  position: absolute;
  inset: 6px 12px;
  width: calc(100% - 24px);
  height: calc(100% - 12px);
  color: rgba(255, 255, 255, 0.32);
  pointer-events: none;
}

.ts-hue-thumb {
  position: absolute;
  top: 50%;
  width: 18px;
  height: 34px;
  background: #ffffff;
  border-radius: 14px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  box-shadow: 0 3px 10px -2px rgba(0, 0, 0, 0.5);
}

/* Saturation knob. */
.ts-knob {
  position: relative;
  width: 60px;
  height: 60px;
  cursor: grab;
  touch-action: none;
}

.ts-knob:active { cursor: grabbing; }

.ts-knob-ring {
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  border: 2px dotted rgba(255, 255, 255, 0.22);
}

.ts-knob-dot {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #ffffff;
  transform: translate(-50%, -50%);
  box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* ─── App icon row ───────────────────────────────────────────────────── */
/* Swatches preview the icon variants against the app's live --color-bg so
   the chips show exactly what the sidebar/favicon will look like with the
   current palette. Sized ≥44px for touch; selection ring matches the white
   .is-active idiom used by the mode buttons and balls above. */
.ts-icons {
  padding: 0 16px 14px;
}

.ts-icons-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  margin-bottom: 8px;
}

.ts-icon-grid {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.ts-icon {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 14px;
  border: 2px solid rgba(255, 255, 255, 0.08);
  background: var(--color-bg, #ffffff);
  color: var(--color-text-primary, #1d2433);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  font-family: inherit;
  transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.ts-icon svg {
  width: 30px;
  height: 30px;
  display: block;
}

.ts-icon:hover {
  transform: scale(1.06);
  border-color: rgba(255, 255, 255, 0.45);
}

.ts-icon.is-active {
  /* Ring drawn OUTSIDE the chip, over the always-dark studio chrome — the
     chips are filled with var(--color-bg), so in light mode a border-based
     white ring composited over the white chip and vanished. (Literal whites
     are fine here: the studio panel is mode-invariant dark.) */
  border-color: rgba(255, 255, 255, 0.9);
  outline: 2px solid rgba(255, 255, 255, 0.9);
  outline-offset: 2px;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.18);
}

/* ─── Actions row ────────────────────────────────────────────────────── */
.ts-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 16px 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.05);
  gap: 10px;
}

.ts-action {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 9px 16px;
  border-radius: 10px;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
  border: 1px solid transparent;
}

.ts-action--ghost {
  background: transparent;
  color: rgba(255, 255, 255, 0.65);
  border-color: rgba(255, 255, 255, 0.10);
}

.ts-action--ghost:hover {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.04);
}

.ts-action--primary {
  background: var(--color-accent, #C0512B);
  color: var(--color-on-accent, #ffffff);
  border: none;
}

.ts-action--primary:hover {
  filter: brightness(1.08);
}

/* ─── Theme Studio mobile touch targets ──────────────────────────────────
   Desktop sizes (36×30 mode, 28×28 counters, 32×32 arrows, 30×30 presets)
   sit below the 40-44px floor. Must live AFTER the component rules above —
   media queries don't add specificity, so source order decides. The preset
   strip scrolls horizontally (wider swatches fit); control icons keep their
   fixed px sizes. */
@media (max-width: 767px) {
  .ts-mode-btn    { width: 44px; height: 40px; }
  .ts-counter-btn { width: 40px; height: 40px; }
  .ts-arrow       { width: 40px; height: 40px; }
  .ts-preset      { width: 40px; height: 40px; }
}
