/* Ascent — production UI styles */

:root {
  /* issue #297 — v2 "Modernist" redesign, full color/radius/shadow/heat/font
     token replace (supersedes issue #206 "Alpenglow" the same way that pass
     superseded every direction before it — see docs/architecture.md Build
     Log and .claude/rules/design-system.md, the binding spec for this pass).
     Tokens the spec doesn't define (spacing scale, rem-based type scale,
     motion durations/easing, icon sizes, chip box-model, tracking) keep
     their pre-#297 name/value unchanged — listed at the bottom of this
     block, same convention #206 used. This is a Phase 1 (#297) token +
     base-component pass only — individual page layouts are Phases 2-5
     (#298-#301), not touched here. */
  --color-bg:             #F3F2F2;
  --color-surface:        #EAE9E9;
  --color-surface-raised: #F8F4F4;
  /* --color-border/--color-border-strong/--color-divider are aliases of the
     same 2px-rule divider color per design-tokens.css — kept as three names
     so every existing --color-border call site (there were ~5) keeps working
     without a rename; --color-border-strong is a fully-opaque version for
     rules that need to sit on top of a divider-colored border without the
     translucency compounding (e.g. a hover state layered over a divider). */
  --color-border:         color-mix(in srgb, #201E1D 40%, transparent);
  --color-border-strong:  #201E1D;
  --color-divider:        color-mix(in srgb, #201E1D 40%, transparent);
  --color-text:           #201E1D;
  /* design-system.md §2 spec's literal formula (color-mix 60%) only clears
     ~4.12-4.24:1 against --color-surface/--color-bg — short of WCAG AA's
     4.5:1 for normal text, caught live by the E2E a11y suite's axe scan on
     .auth-subtitle/.daily-todo-empty/.template-card-desc. Bumped to 64%
     (verified via relative-luminance calculation: 4.64:1 on --color-surface,
     4.80:1 on --color-bg — comfortably passing both) — dark theme's own 60%
     value already clears 6.1-6.4:1 and is untouched. */
  --color-text-muted:     color-mix(in srgb, #201E1D 64%, transparent);
  /* --color-text-faint has no exact spec value in design-system.md's token
     table (only bg/surface/text/text-muted/divider/accent/neutral are
     spec'd) — judgment call: same color-mix idiom as --color-text-muted,
     one step further toward transparent, preserving the pre-#297 "faint"
     role (e.g. .phase-index-adjacent tertiary text) rather than dropping it. */
  --color-text-faint:     color-mix(in srgb, #201E1D 45%, transparent);

  /* Accent ramp (OKLCH-derived per design-tokens.css) — replaces
     --color-brand-gold / --color-brand-rose / --gradient-alpenglow. */
  --color-accent:      #EC3013;
  --color-accent-100:  #FFF2EF;
  --color-accent-200:  #FFE0D9;
  --color-accent-300:  #FFC4B8;
  --color-accent-400:  #FF9783;
  --color-accent-500:  #FF563C;
  /* issue #297 follow-up — real, axe-confirmed contrast bug: neither white
     nor dark ground-colored text clears 4.5:1 against the base
     --color-accent (#EC3013 hosts white at 3.76:1, dark at 3.95-4.42:1 —
     both fail; live on .btn-primary's "Start for free" label, caught by the
     E2E a11y suite). This is new to the v2 palette — the old Alpenglow gold
     accent passed at 6.8-8.1:1 in the same role. Darkened from #DD2B0F so it
     doubles as the solid FILL for any body-text-size control on accent (not
     just a hover step) — white text on this value measures 4.94:1. Icons/
     large-numeral/UI-chrome uses of the base --color-accent (avatars, icon
     badges, dots) are unaffected and stay on the un-darkened value per
     design-system.md §2's own "fine for icons, large numerals, UI chrome"
     carve-out — only .btn-primary and .seg-item's active fill (both host
     real body-size text) were repointed to this token. */
  --color-accent-600:  #C92910;  /* solid fill for text-hosting controls + hover step */
  --color-accent-700:  #AE1800;  /* accent as paragraph-size text */
  --color-accent-800:  #7C1405;  /* text on accent-100 tints */
  --color-accent-900:  #4D170E;

  /* Neutral ramp (full --color-neutral-100...900 per design-tokens.css) */
  --color-neutral-100: #F8F4F4;
  --color-neutral-200: #EAE7E7;
  --color-neutral-300: #D7D3D3;
  --color-neutral-400: #BAB6B6;
  --color-neutral-500: #9B9797;
  --color-neutral-600: #7D7979;
  --color-neutral-700: #605D5D;
  --color-neutral-800: #444141;
  --color-neutral-900: #2D2B2B;

  /* Semantic roles collapse into the mono scheme per design-system.md §2:
     danger reuses the accent ramp; success/warning use neutral ink + tags,
     not new hues. --color-warning/-bg used to have a separate darkened
     "-ink" text variant (--color-warning-ink) under Alpenglow — that's gone
     now: --color-warning already IS a text-safe accent-700 value, so every
     old --color-warning-ink call site was repointed straight to
     --color-warning instead of keeping a now-redundant alias token. */
  --color-danger:      var(--color-accent-700);
  --color-danger-bg:   var(--color-accent-100);
  --color-success:     var(--color-neutral-800);
  --color-success-bg:  var(--color-neutral-200);
  --color-warning:     var(--color-accent-700);
  --color-warning-bg:  var(--color-accent-100);

  /* Priority mapping — design-system.md §2: "P0 = accent tint, P1 = lighter
     accent tint or outline, P2 = neutral tint." The v2 spec has only three
     priority tiers (P0/P1/P2); this app's data model has a fourth (P3).
     A prior pass here repointed every --color-p3/--color-p3-ink call site
     directly to --color-p2/--color-p2-ink, making P3 visually identical to
     P2 — reported live (screenshot) as "priority filter colors are missing"
     once someone actually clicked through P0-P3 in sequence and P2/P3 read
     as the exact same chip. Restored as its own token pair instead: a
     lighter neutral tint (--color-neutral-300, one step lighter than P2's
     -500) so P3 reads as "even more de-emphasized than P2," not identical to
     it — live-verified contrast (relative-luminance calc, this file's usual
     discipline): --color-p3-ink on its own 15%-tint background clears
     5.23:1 light / 6.89:1 dark, both comfortably passing WCAG AA for the
     9px badge text this pairs with. */
  --color-p0:     var(--color-accent);
  --color-p0-ink: var(--color-accent-800);
  --color-p0-bg:  var(--color-accent-100);
  --color-p1:     var(--color-accent-400);
  --color-p1-ink: var(--color-accent-700);
  --color-p1-bg:  var(--color-accent-100);
  --color-p2:     var(--color-neutral-500);
  --color-p2-ink: var(--color-neutral-800);
  --color-p2-bg:  var(--color-neutral-200);
  --color-p3:     var(--color-neutral-300);
  --color-p3-ink: var(--color-neutral-700);
  --color-p3-bg:  var(--color-neutral-100);

  /* Text on solid accent fills (buttons, filled badges) — replaces
     --color-ink-on-gold. */
  --color-ink-on-accent: #F3F2F2;

  --font-body:    "Archivo", system-ui, sans-serif;
  --font-heading: "Archivo", system-ui, sans-serif;
  /* --font-display is kept as an alias (not removed) so the ~8 existing
     call sites using this name don't all need a mechanical rename in this
     CSS-only pass — it now resolves to Archivo like everything else, closing
     the "no separate display font" gap design-system.md §1 calls for, with
     zero risk of missing a call site. New code should read --font-heading
     directly. */
  --font-display: var(--font-heading);
  /* --font-mono — judgment call: design-system.md doesn't explicitly forbid
     monospace for code/numeric contexts (tabular phase indices, the AI-import
     JSON paste area, feedback report reference codes), only that heading/body
     text must be Archivo. Dropped the old custom 'Geist Mono' webfont (no
     additional font family beyond Archivo should be loaded per §1) in favor
     of a system monospace stack — visually neutral, zero extra network cost,
     and unaffected by the CSP font-src change below since it needs no
     Google Fonts request. */
  --font-mono:    ui-monospace, "SF Mono", Consolas, monospace;

  /* Type scale (rem) — unrelated to this pass's token scope, unchanged. */
  --text-xs:   0.75rem;
  --text-sm:   0.875rem;
  --text-base: 1rem;
  --text-lg:   1.25rem;
  --text-xl:   1.5625rem;
  --text-2xl:  1.953rem;
  --text-3xl:  2.441rem;
  --text-4xl:  3.052rem;

  --font-weight-regular:  400;
  --font-weight-medium:   500;
  --font-weight-semibold: 600;
  --font-weight-bold:     700;
  /* Archivo's heaviest weight, used for headings per design-system.md §3. */
  --font-weight-extrabold: 800;

  --leading-tight:  1.2;
  --leading-snug:   1.3;
  --leading-normal: 1.5;
  --leading-loose:  1.7;

  --tracking-tight: -0.03em;
  --tracking-snug: -0.01em;
  --tracking-caps: 0.07em;

  /* Spacing scale — unrelated to this pass's token scope, unchanged. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;

  /* App shell topbar height — unrelated to this pass's token scope, unchanged. */
  --topbar-h: 56px;

  /* Icon size scale — unrelated to this pass's token scope, unchanged. */
  --icon-size-xs: 16px;
  --icon-size-sm: 20px;
  --icon-size-md: 24px;
  --icon-size-lg: 32px;

  /* Shared "metadata chip" box-model scale — height/padding unrelated to
     this pass's token scope, unchanged. --chip-radius is explicitly in
     scope: design-system.md §4/§5 calls for "no rounded... chips", zeroed
     alongside --radius-*/--radius-full below. */
  --chip-height: 22px;
  --chip-padding-x: 9px;
  --chip-radius: 0px;

  /* Radius scale — design-system.md §4: "Radius 0 everywhere... No rounded
     buttons, cards, inputs, checkboxes, chips, avatars, or modals."
     --radius-full is also zeroed for the same reason (pill-shaped chips/
     avatars/toggle tracks all go square too). */
  --radius-sm: 0px;
  --radius-md: 0px;
  --radius-lg: 0px;
  --radius-full: 0px;

  /* Shadow scale — design-system.md §4: "Elevation only for overlays
     (dialog, dropdown, command palette, toasts) via --shadow-sm/md/lg. Flat
     surfaces otherwise." --shadow-sm/md/lg are the spec's exact values.
     --shadow-xs/-xl have no spec equivalent (spec only defines 3 tiers) —
     kept defined (reduce risk of a dangling var()), derived from the same
     neutral-ink shadow family as -sm/-md/-lg rather than left at their old
     values. --shadow-brand had zero call sites at the time of this pass
     (grepped and confirmed) — no colored/gold shadow is allowed on a flat
     surface per §4/§9's checklist, so rather than delete the token outright
     (a future accent-emphasis use might want a shadow keyed to the accent
     color specifically, e.g. a hovering "you can drop this here" affordance)
     it's redefined as a low-opacity accent-tinted shadow, not a neutral one
     — documented here since it's a deliberate exception to "flat surfaces
     otherwise," to be used only if a genuine future need arises, not
     reintroduced as a default button/card shadow. */
  --shadow-xs: 0 1px 2px color-mix(in srgb, var(--color-neutral-900) 8%, transparent), 0 0 0 1px color-mix(in srgb, var(--color-neutral-900) 4%, transparent);
  --shadow-sm: 0 1px 2px  color-mix(in srgb, #2D2B2B 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2D2B2B 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2D2B2B 22%, transparent);
  --shadow-xl: 0 24px 48px color-mix(in srgb, var(--color-neutral-900) 20%, transparent), 0 8px 16px color-mix(in srgb, var(--color-neutral-900) 10%, transparent);
  --shadow-brand: 0 8px 24px color-mix(in srgb, var(--color-accent) 20%, transparent);

  /* Motion (durations/easing) — unrelated to this pass's token scope,
     unchanged; design-system.md §7 reuses --duration-fast/base and
     --ease-out for the 120-200ms opacity/transform hover/press rule. */
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-decelerate: cubic-bezier(0, 0, 0, 1);
  --ease-accelerate: cubic-bezier(0.3, 0, 1, 1);
  --ease-spring: cubic-bezier(0.2, 0.9, 0.3, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);

  --duration-instant: 80ms;
  --duration-fast: 120ms;
  --duration-base: 200ms;
  --duration-slow: 360ms;
  --duration-enter: 480ms;

  --stagger-base: 40ms;

  /* Activity heatmap intensity scale — design-system.md §5: "hard-edged
     squares, 5 steps" — the exact 5 hex stops from the spec/design-tokens.css. */
  --heat-0: var(--color-neutral-200);
  --heat-1: #FFC4B8;
  --heat-2: #FF9783;
  --heat-3: #FF563C;
  --heat-4: #DD2B0F;

  /* Old bare --neutral-* scale (no "color-" prefix) — a genuinely distinct,
     pre-existing, theme-INVARIANT scale (issue #6 Phase 1.3), separate from
     the new theme-flipping --color-neutral-* ramp above. Confirmed via grep
     before deciding to keep it: only --neutral-900 currently has a live call
     site (.tooltip-bubble's fixed-dark-in-both-themes background), but the
     whole scale is kept, unchanged, per this task's own "if genuinely still
     needed for something unrelated to the redesign, keep it" instruction —
     out of scope for the #297 color redesign, not deleted. */
  --neutral-50: #f8fafc;
  --neutral-100: #f1f5f9;
  --neutral-200: #e2e8f0;
  --neutral-400: #94a3b8;
  --neutral-500: #64748b;
  --neutral-700: #334155;
  --neutral-900: #0f172a;

  /* DELETED (do not reintroduce, issue #297): --color-brand-gold,
     --color-brand-rose, --gradient-alpenglow, --color-brand-gold-ink,
     --color-warning-ink, --color-brand-rose-ink, --color-ink-on-gold,
     old --accent-50/400/500 (orange set), bg-grid-glow / text-gradient-brand
     classes. --color-p3/--color-p3-ink were also removed here at the time,
     then reinstated in issue #301 (Phase 5) as their own token pair — see
     the priority-mapping comment above `:root`'s --color-p0 block for why. */
}

:root[data-theme='dark'] {
  --color-bg:             #141312;
  --color-surface:        #1E1C1B;
  --color-surface-raised: #252221;
  --color-border:         color-mix(in srgb, #F1EFED 32%, transparent);
  --color-border-strong:  #F1EFED;
  --color-divider:        color-mix(in srgb, #F1EFED 32%, transparent);
  --color-text:           #F1EFED;
  --color-text-muted:     color-mix(in srgb, #F1EFED 60%, transparent);
  --color-text-faint:     color-mix(in srgb, #F1EFED 45%, transparent);

  --color-accent:      #FF563C;
  --color-accent-100:  #38201A;
  --color-accent-200:  #4D170E;
  --color-accent-300:  #7C1405;
  --color-accent-400:  #DD2B0F;
  --color-accent-500:  #FF563C;
  --color-accent-600:  #FF6D55;  /* hover steps LIGHTER on dark ground */
  --color-accent-700:  #FFC4B8;  /* accent as text */
  --color-accent-800:  #FFC4B8;
  --color-accent-900:  #FFE0D9;

  --color-neutral-100: #1E1C1B;
  --color-neutral-200: #2B2827;
  --color-neutral-300: #3A3634;
  --color-neutral-400: #4A4643;
  --color-neutral-500: #6B6663;
  --color-neutral-600: #8C8783;
  --color-neutral-700: #ADA8A4;
  --color-neutral-800: #D7D3D3;
  --color-neutral-900: #EAE7E7;

  --color-danger:      var(--color-accent-700);
  --color-danger-bg:   var(--color-accent-100);
  --color-success:     var(--color-neutral-800);
  --color-success-bg:  var(--color-neutral-200);
  --color-warning:     var(--color-accent-700);
  --color-warning-bg:  var(--color-accent-100);

  --color-p0:     var(--color-accent);
  --color-p0-ink: var(--color-accent-800);
  --color-p0-bg:  var(--color-accent-100);
  --color-p1:     var(--color-accent-400);
  --color-p1-ink: var(--color-accent-700);
  --color-p1-bg:  var(--color-accent-100);
  --color-p2:     var(--color-neutral-500);
  --color-p2-ink: var(--color-neutral-800);
  --color-p2-bg:  var(--color-neutral-200);
  --color-p3:     var(--color-neutral-300);
  --color-p3-ink: var(--color-neutral-700);
  --color-p3-bg:  var(--color-neutral-100);

  --color-ink-on-accent: #141312;

  --heat-0: var(--color-neutral-200);
  --heat-1: #4D170E;
  --heat-2: #AE1800;
  --heat-3: #FF563C;
  --heat-4: #FF9783;

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.18);
  --shadow-sm: 0 1px 2px  rgba(0,0,0,.5);
  --shadow-md: 0 3px 10px rgba(0,0,0,.55);
  --shadow-lg: 0 12px 32px rgba(0,0,0,.65);
  --shadow-xl: 0 24px 48px rgba(0,0,0,.7), 0 8px 16px rgba(0,0,0,.4);
  --shadow-brand: 0 8px 24px color-mix(in srgb, var(--color-accent) 24%, transparent);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }

html { scroll-behavior: smooth; }
html, body { min-height: 100%; }

body {
  font-family: var(--font-body);
  /* issue #206 §5 follow-up — was a page-wide radial gold/rose glow behind
     every single screen (dashboard, onboarding, progress, settings); found
     live to read as a large, high-frequency gradient fill, the same category
     of thing the earlier "no gradients anywhere, solid colors only" user
     decision already removed from the brand mark/progress ring/template-card
     border (see `--gradient-alpenglow`'s own comment below — it has zero
     remaining call sites app-wide) — this was simply a spot that decision's
     original sweep had missed, since it targeted component-level gradient
     fills, not this page-wide ambient background. Solid `--color-bg` now,
     matching the token's own documented "warm neutral, not pure white/black"
     role. */
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  transition: background-color 200ms ease, color 200ms ease;
}

.app-shell { min-height: 100vh; min-height: 100dvh; }

/* issue #6 Phase 9 — off-screen until focused (Tab from the page's very top),
   then pinned to the top-left corner above everything else. `top: -100%`
   rather than `display: none`/`visibility: hidden` so it stays in the
   keyboard tab order — a hidden-but-still-focusable link is the whole point
   of a skip link. */
.skip-link {
  position: fixed;
  top: -100%;
  left: 12px;
  z-index: 999;
  background: var(--color-text);
  color: var(--color-bg);
  padding: 10px 18px;
  border-radius: var(--radius-md);
  font-weight: 700;
  font-size: var(--text-sm);
  text-decoration: none;
  transition: top var(--duration-fast) var(--ease-out);
}
.skip-link:focus {
  top: 12px;
}
.skip-link:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Animations */
/* Issue #102 follow-up — this used to be `... both`, which per the
   `animation-fill-mode: forwards` half of `both` keeps the `to` keyframe's
   declared properties applied forever after the animation completes, not
   just during it. `transform: translateY(0)` is a visual no-op at rest, but
   a *declared* `transform` (any value, even an identity one) still
   establishes a new containing block for every `position: fixed` descendant
   — which silently broke any fixed-position element anywhere inside
   `.app-shell-2.dashboard.fade-in` (e.g. a `position: fixed` dropdown menu,
   reported live: it rendered positioned relative to this div's full
   scrollable content height instead of the viewport, landing thousands of
   pixels off-screen). `.fade-in` is only ever applied once on initial mount
   of a fresh page-level container (`authShell.js`, `onboarding.js`,
   `dashboard.js` — never toggled on/off an existing node), so there is
   nothing to "snap back" to once the animation ends; dropping the
   fill-mode lets the element fall back to its normal cascade (`transform:
   none`) the instant the animation finishes, which is visually identical to
   `translateY(0)` and removes the phantom containing block. Never add
   `forwards`/`both` back to this rule without confirming nothing depends on
   the `to` keyframe's values persisting — if you need that, apply the
   equivalent resting styles as a normal (non-animation) rule instead, so a
   plain `transform: none` there can never be confused with a "some
   transform is set" state. */
.fade-in { animation: fade-in 420ms var(--ease-spring); }
@keyframes fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes spin { to { transform: rotate(360deg); } }

@keyframes slide-panel {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

/* Brand */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: inherit;
  transition: opacity 0.15s ease;
}
a.brand:hover { opacity: 0.85; }
a.brand:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; border-radius: var(--radius-md); }

/* intentional: fixed white glyph on solid accent fill, same in both themes (issue
   #116) — design-system.md §1's "triangle glyph, flat accent fill" brand mark. issue
   #298 — radius0'd (was a stray 12px rounded-square left over from Phase 1's token
   pass, since this file is component-level, not a token) and the glow shadow
   removed entirely (§2: "No glows, no drop-shadows on flat surfaces" — this was a
   leftover fixed gold-tinted rgba() glow from the pre-#297 Alpenglow direction). */
.brand-mark {
  width: 40px;
  height: 40px;
  background: var(--color-accent);
  color: #fff;
  font-weight: 900;
  font-size: 20px;
  display: grid;
  place-items: center;
}

/* issue #298 — design-system.md §1: "wordmark ASCENT in Archivo 800 uppercase
   +0.05em". Was missing the uppercase/tracking transform since Phase 1. */
.brand-name { font-weight: 800; font-size: var(--text-xl); text-transform: uppercase; letter-spacing: 0.05em; }
.brand-tagline { font-size: 12px; color: var(--color-text-muted); font-weight: 600; }

/* ============================================================
   ZeBeyond-inspired design language (issue #155) — eyebrow labels,
   gradient headline text, two-tone tag chips, icon tiles, floating pill
   nav, grouped icon buttons, and a decorative grid+glow background layer.
   See the "Visual design language" section of .claude/rules/ui-styling.md
   for the reference this replaces and which call sites use which class.
   ============================================================ */

/* Uppercase, letter-spaced kicker label ("CASE STUDIES", "WHY CHOOSE US"
   in the reference) — generalizes the ad hoc uppercase-label pattern
   already used piecemeal by .current-roadmap-badge/.toolbar-label/
   .panel-kicker into one reusable class. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--color-accent-700);
}
.eyebrow-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-accent); flex-shrink: 0; }
/* issue #155 v2 Phase C — landing/auth/shell recolor: swap the ZeBeyond mint accent for
   the v2 lime accent on the specific classes scoped to those three areas (this class is
   only ever used by landing.js/authMarketingPanel.js — see ui-styling.md). Falls back to
   --color-brand-gold in light theme, unchanged (no v2 reference material exists there). */
:root[data-theme='dark'] .eyebrow { color: var(--color-accent-700); }
:root[data-theme='dark'] .eyebrow-dot { background: var(--color-accent); }

/* Solid accent-color headline text, for one accent phrase inside an
   otherwise plain-color heading. Was a background-clip:text gradient;
   reverted to a flat color per the app-wide "no gradients, solid only"
   decision — the class name is kept as-is (matching every JS call site) even
   though it's no longer literally a gradient. */
.text-gradient-brand {
  color: var(--color-accent-700);
}

/* Two-tone tag-chip pair (the "EPOP CONCEPT" + "AUTOMOTIVE" chip pattern) —
   built on the same --chip-height/--chip-padding-x/--chip-radius scale
   .badge/.resource-count already use, so a tag chip is pixel-identical in
   height to every other small pill in the app; only the neutral/accent
   color pairing is new. */
.tag-chip {
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  white-space: nowrap;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
}
.tag-chip-accent { background: var(--color-warning-bg); border-color: var(--color-border-strong); color: var(--color-accent-700); }

/* Small rounded-square icon container for card headers (the icon-topped
   case-study-card pattern) — wraps a createIcon()/createDecorativeIcon()
   node, replacing a bare inline icon with no visual container. */
.icon-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  color: var(--color-accent-700);
}
/* issue #155 v2 Phase C — landing-scoped recolor; falls back to mint in light theme. */
:root[data-theme='dark'] .icon-tile { color: var(--color-accent-700); }

/* issue #297 — design-system.md §5 explicitly merges .btn-cta into
   .btn-primary ("`.btn-cta` is merged into `.btn-primary`"). landing.js's 3
   call sites now read className: 'btn-primary'/'btn-primary btn-lg'
   directly — no separate CTA class/shape survives this phase. */

/* Bordered pill container that visually groups adjacent icon-only buttons
   (the reference's search/mail/sign-in icon cluster) — a shared wrapper,
   not a change to the icon buttons themselves. */
.icon-btn-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: var(--chip-radius);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
}
/* Every real call site (topbar's search/notification/theme-toggle buttons)
   already resets its own border/background for its unstyled-until-hover
   look (.app-topbar-bell, .app-topbar-command-btn, .btn-ghost); this is a
   defensive reset for any future child that doesn't. */
.icon-btn-group > * { border: none; background: transparent; }

/* issue #297 — design-system.md §2 explicitly bans this class by name
   ("No glows, no bg-grid-glow"), not just gradient color washes generally —
   the previous repeating-linear-gradient grid-line texture (a structural,
   not color, gradient) is removed outright rather than retuned. Kept as a
   named no-op class (matching every existing JS call site in landing.js/
   authMarketingPanel.js, same "keep the name, drop the effect" treatment
   already used for .text-gradient-brand above) so this stays a CSS-only
   pass with zero JS diffs — the wrapping section is now a plain flat
   surface with no decorative layer at all, matching §1's "flat,
   architectural" identity. */
.bg-grid-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Theme toggle */
.theme-toggle {
  font-size: 17px;
  line-height: 1;
}

/* Buttons (issue #206 §3.1 — Alpenglow redesign). Base rule is
   variant-agnostic layout only; each variant below sets its own bg/text/
   border/hover/shadow. issue #297 — text-decoration: none moved here (was
   only ever set on the now-removed .btn-cta) so every .btn variant used on
   an <a> (landing.js's "Start for free"/"Sign in" links use
   .btn-primary/.btn-ghost/.btn-secondary, not just the old CTA class) is
   covered by construction instead of needing its own per-variant override. */
/* issue #338 — `justify-content: center` restored to the base rule, reversing
   issue #309's flush-left change: the product owner explicitly requested
   centered button labels after seeing the sign-in screen (see
   design-system.md §4, updated in the same PR to state the new rule). Any
   trailing icon that previously relied on `margin-left: auto` to push itself
   right against a flush-left label now just centers alongside the label text
   as part of the same centered group — checked every `.btn`-classed call
   site, none currently set `margin-left: auto` on a child, so no follow-up
   change was needed there. `.btn-icon` (below) already centers its single
   icon child; this rule now agrees with it instead of deliberately
   diverging, since a square icon-only button was never covered by the old
   flush-left rule to begin with. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 40px;
  padding: 10px var(--space-4);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  text-decoration: none;
  font: inherit;
  font-weight: var(--font-weight-bold);
  font-size: var(--text-base);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
}

.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }

/* Primary — filled accent, fixed ground-colored text in both themes (see
   --color-ink-on-accent's own comment above for the cross-theme contrast
   writeup this token exists to solve). issue #297: also the target of the
   .btn-cta merge above — every former .btn-cta call site (landing.js) now
   renders through this rule directly. Fill reads --color-accent-600, not the
   base --color-accent — see that token's own comment for the axe-caught
   contrast fix this is (dark theme's base --color-accent already passes
   with dark ground-text at 5.88:1 and is intentionally left as-is via that
   token's own light/dark values; only light theme needed the darkened fill). */
.btn-primary {
  background: var(--color-accent-600);
  color: var(--color-ink-on-accent);
  border-color: var(--color-accent-600);
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--color-accent-600) 88%, black); border-color: color-mix(in srgb, var(--color-accent-600) 88%, black); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.btn-primary:disabled:hover { box-shadow: none; transform: none; }

/* Secondary — outlined, surface fill. */
.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border-strong);
}
.btn-secondary:hover:not(:disabled) { border-color: var(--color-accent); }

/* Ghost/tertiary — transparent until hovered. */
.btn-ghost { background: transparent; border-color: transparent; color: var(--color-text-muted); }
.btn-ghost:hover:not(:disabled) { background: var(--color-surface-raised); }

/* Danger — filled, same lift treatment as primary. `--color-danger` is
   `--color-accent-700` (design-system.md's "danger reuses the accent ramp"
   rule) — a dark red in light theme, a light pink in dark theme (it's tuned
   for use as *text*, not a button fill). `color: var(--color-text)` here
   measured 2.32:1 light / 1.32:1 dark against that fill — both catastrophic
   contrast failures, found via a real relative-luminance check (issue #301
   follow-up). `--color-ink-on-accent` (the same text token `.btn-primary`
   already uses against the base `--color-accent` fill) clears 6.41:1 light /
   12.23:1 dark instead. */
.btn-danger {
  background: var(--color-danger);
  color: var(--color-ink-on-accent);
  border-color: var(--color-danger);
  box-shadow: var(--shadow-sm);
}
.btn-danger:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translateY(-1px); }
.btn-danger:disabled:hover { box-shadow: none; transform: none; }

/* Danger, text-only variant (e.g. an inline "Delete" affordance that isn't
   a full filled button). */
.btn-danger-text { background: transparent; border-color: transparent; color: var(--color-danger); }
.btn-danger-text:hover:not(:disabled) { background: var(--color-danger-bg); }

/* Issue #6 Phase 5.3 — page-agnostic submit-button loading spinner
   (setButtonLoading(), src/ui/utils/buttonLoading.js). Same sizing/animation
   as the pre-existing .save-badge .spin (dashboard.js), promoted to a
   standalone class so any button can use it, not just the save badge. */
.btn-spinner {
  width: 12px; height: 12px; border-radius: 50%;
  /* intentional: neutral gray track works against any --color-surface value in either theme; the spinning edge is currentColor (issue #116) */
  border: 2px solid rgba(127, 127, 127, 0.3);
  border-top-color: currentColor;
  animation: spin 1s linear infinite;
  flex-shrink: 0;
}
.btn-block { width: 100%; }
.btn-sm { min-height: 34px; padding: 6px 12px; font-size: var(--text-sm); }
.btn-lg { min-height: 48px; padding: 12px 24px; font-size: var(--text-base); }
/* Re-centers explicitly (issue #309) — `.btn`'s own base rule no longer
   centers, but a square icon-only button has no text "label" for the
   flush-left rule to apply to; its icon should stay centered in the box. */
.btn-icon { width: 36px; height: 36px; padding: 0; font-size: 22px; line-height: 1; justify-content: center; }

.link { color: var(--color-accent-700); font-weight: 700; text-decoration: none; }
/* issue #155 v2 Phase D3 (full completion pass) — the app's generic inline-link class;
   straight text swap. */
:root[data-theme='dark'] .link { color: var(--color-accent-700); }
.link:hover { text-decoration: underline; }

/* Form fields */
.field { display: grid; gap: 6px; }
.field-label { font-size: var(--text-sm); font-weight: 700; color: var(--color-text); opacity: 0.85; }
.field-input {
  width: 100%;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-base);
  transition: border-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard);
}
/* issue #206 §3.4 — 20% opacity via color-mix(), matching this file's
   established opacity pattern (see .bg-grid-glow above) rather than a
   separate rgba literal. */
.field-input:focus { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent); }
.field-input.compact { padding: var(--space-2) 10px; font-size: var(--text-sm); }
/* issue #297 — design-system.md §5's "flat surface fill, 1px divider border,
   accent caret, accent border on focus" input spec. .field-input (above) is
   this app's one input base class (no separate .input exists app-wide) and
   already satisfied every part of that spec except the caret color. */
.field-input, .search-input, .import-paste-area { caret-color: var(--color-accent); }

/* Segmented control (design-system.md §5) — for exclusive-choice groups
   (priority filter, theme picker). Base class only this phase, same
   "built, not yet wired into a page" precedent as .tabs (issue #125) —
   the dashboard priority filter and theme toggle adopt this in a later
   phase alongside their own page's markup changes. */
.seg { display: inline-flex; border: 1px solid var(--color-border); }
.seg-item {
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  color: var(--color-text);
  border: none;
  font: inherit;
  font-weight: var(--font-weight-bold);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
}
.seg-item + .seg-item { border-left: 1px solid var(--color-border); }
/* --color-accent-600, not the base --color-accent — same axe-caught
   contrast fix as .btn-primary above, since this is also body-size text
   on a solid accent fill. */
.seg-item[aria-selected='true'], .seg-item.active { background: var(--color-accent-600); color: var(--color-ink-on-accent); }
.seg-item:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }

.form-message { min-height: 20px; font-size: var(--text-sm); font-weight: 600; color: var(--color-text-muted); }
.form-message.error { color: var(--color-danger); }
.form-message.success { color: var(--color-success); }
/* Issue #121 item 1 — deliberately not .form-message.error: a Playwright
   locator scoped to that class needs to match exactly one element
   (summaryMsg's "N things need fixing" line), never this callout too. */
.import-corruption-hint { font-size: var(--text-sm); font-weight: 600; color: var(--color-danger); margin: 0; }
.form-error { color: var(--color-danger); font-size: var(--text-sm); font-weight: 600; min-height: 18px; }
.field-error { color: var(--color-danger); font-size: var(--text-sm); font-weight: 600; min-height: 16px; }

/* Password field with show/hide toggle */
.field-input-wrap { position: relative; display: grid; }
.field-input-wrap .field-input { padding-right: 52px; }
.password-toggle {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 4px 6px;
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: 0;
  line-height: 1;
}
.password-toggle:hover { color: var(--color-text); }
.password-toggle:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* issue #155 v2 Phase D3 (full completion pass) — straight outline swap. */
:root[data-theme='dark'] .password-toggle:focus-visible { outline-color: var(--color-accent); }

/* Issue #6 Phase 5.3 — inline ✓/✕ blur validation icon, shares
   .field-input-wrap with the password-toggle button above but must not
   overlap it — .has-toggle shifts the icon left and gives the input extra
   right padding to clear both. */
/* issue #136 Phase 2 follow-up — was text-based ('✓'/'✕' glyphs, sized via
   font-size); now holds a createIcon() svg, so it needs inline-flex
   centering instead of relying on line-height/font-size for alignment. */
.field-validation-icon {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.field-validation-icon.valid { color: var(--color-accent-700); }
.field-validation-icon.invalid { color: var(--color-danger); }
/* issue #155 v2 Phase D3 (full completion pass) — straight icon-color swap. */
:root[data-theme='dark'] .field-validation-icon.valid { color: var(--color-accent-700); }
.field-input-wrap.has-toggle .field-validation-icon { right: 46px; }
.field-input-wrap.has-toggle .field-input { padding-right: 84px; }

/* Password strength meter — 4 segment bar below the password field */
.strength-meter { display: flex; gap: 4px; margin-top: 4px; }
.strength-segment { flex: 1; height: 3px; border-radius: 2px; background: var(--color-border); transition: background var(--duration-base) var(--ease-standard); }
.strength-segment.weak { background: var(--color-danger); }
.strength-segment.fair { background: var(--color-warning); }
.strength-segment.strong { background: var(--color-success); }

.field-label-row { display: flex; justify-content: space-between; align-items: center; }
.forgot-link { background: none; border: none; padding: 0; font-size: 12px; font-weight: 600; color: var(--color-text-muted); cursor: pointer; line-height: 1; }
.forgot-link:hover { color: var(--color-accent-700); text-decoration: underline; }
.forgot-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 2px; }
.forgot-link:disabled { opacity: 0.5; cursor: not-allowed; }
/* issue #155 v2 Phase D3 (full completion pass) — straight text/outline swap. */
:root[data-theme='dark'] .forgot-link:hover { color: var(--color-accent-700); }
:root[data-theme='dark'] .forgot-link:focus-visible { outline-color: var(--color-accent); }

.reset-success-msg { display: flex; gap: 10px; align-items: flex-start; font-size: var(--text-base); color: var(--color-text); line-height: 1.5; }
/* intentional: fixed white checkmark on a --color-brand-gold-colored badge, same in both themes (issue #116) */
.reset-success-icon { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent); color: var(--color-ink-on-accent); font-size: 13px; font-weight: 900; }
/* issue #155 v2 Phase D3 (full completion pass) — same "white text fails on lime"
   pattern as .import-step-badge: swaps both fill and text color, not just the fill. */
:root[data-theme='dark'] .reset-success-icon { background: var(--color-accent); }

/* Auth pages — issue #6 Phase 5.1: .auth-page is a two-column split
   (marketing panel + form) above the ≤1024px breakpoint tier, single-column
   (today's original centered-card layout) at/below it — see the responsive
   override at the bottom of this file, grouped with the other ≤1024px rules
   rather than a new breakpoint. */
.auth-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: minmax(360px, 1fr) min(440px, 100%);
  position: relative;
}

/* .auth-page-right carries the exact rules .auth-page itself used to have
   pre-Phase-5 (centers the card in its own column) — renamed, not rewritten. */
.auth-page-right {
  display: grid;
  place-items: center;
  padding: max(24px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(40px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  position: relative;
  min-width: 0;
}

/* issue #206 §5 follow-up — was two radial gold/rose glows covering the
   entire auth-page right column; removed for the same "ambient gradient
   background reads as a high-frequency fill" reason as `body`'s own fix
   above. `.auth-page-bg` is now solid `--color-bg`, effectively a no-op
   layer (it exists mainly so `.auth-page-right`'s content can sit at a
   z-index above it without a separate background rule) — left in place
   rather than deleted in case a future non-gradient background treatment
   (a subtle pattern, etc.) wants this same fixed full-bleed layer again.
   `position: absolute`, not `fixed` — real bug, found live: `.auth-page-bg`
   is a DOM descendant of `.auth-page-right` (the right column only), but
   `position: fixed` ignores DOM nesting entirely and sizes `inset: 0`
   against the full viewport, not its parent. On a wide two-column sign-in
   page this painted the light `--color-bg` fill across the *entire* page —
   including over the always-dark `.auth-marketing` panel on the left, whose
   own fixed-white text (`.auth-marketing-headline` etc.) then read as
   barely-visible pale text on light cream instead of white-on-near-black.
   `pointer-events: none` hid this from click/hit-testing (`elementFromPoint`)
   the whole time, which is why it wasn't caught visually in normal use —
   nothing was ever clickable through it, only visually broken. `position:
   absolute` correctly scopes `inset: 0` to `.auth-page-right` (already
   `position: relative`, immediately above), matching what this layer was
   always meant to cover. */
.auth-page-bg {
  position: absolute;
  inset: 0;
  background: var(--color-bg);
  pointer-events: none;
  z-index: 0;
}

/* Left marketing panel (issue #6 Phase 5, redesigned in a follow-up after
   user feedback: the first cut looked sparse/empty on wide monitors —
   content pinned to the top-left of a huge flat-gradient column, plain
   emoji icons read as placeholder rather than finished). Fixed near-black
   background regardless of the site's own light/dark theme — this panel is
   intentionally always-dark, matching the ZeBeyond direction's near-black
   marketing surface (issue #155) rather than the previous solid brand-teal
   gradient fill. issue #206 §5 follow-up removed the two radial teal/cyan
   glows this comment used to describe (a leftover from the pre-Alpenglow
   ZeBeyond/lime pass, never retuned to gold/rose) — solid near-black now,
   same "no ambient background gradient anywhere in the app" decision as
   `body`/`.auth-page-bg` above; the diagonal-line texture below is
   unchanged. */
/* intentional: fixed literal, not a token, since this panel never reads the
   site's own light/dark theme tokens — same reasoning as the rest of this
   always-dark panel, issue #116. */
.auth-marketing {
  background: #0a0a0a;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: max(40px, env(safe-area-inset-top)) 56px max(40px, env(safe-area-inset-bottom)) max(56px, env(safe-area-inset-left));
  position: relative;
  overflow: hidden;
}
/* intentional: fixed accent color on this panel's own fixed-dark background
   regardless of site theme (issue #116) — mirrors .eyebrow's layout (imported via the
   shared class) but can't read the theme-flipping --color-accent token here, same
   reasoning as every other fixed color on this panel. issue #297 — retuned to the
   dark-theme Modernist accent value (#FF563C, matching
   :root[data-theme='dark']'s --color-accent): since this panel is always-dark
   regardless of site theme, it always uses the dark-theme accent value directly. */
.auth-marketing-eyebrow { color: #FF563C; margin: 0 0 var(--space-4); }
.auth-marketing-eyebrow .eyebrow-dot { background: #FF563C; }
/* intentional: subtle repeating diagonal-line texture — fills the panel's empty space
   with a low-opacity design element instead of a flat void, fixed white regardless of
   site theme since .auth-marketing itself is always-dark (issue #116).
   A single CSS gradient, no image asset. */
.auth-marketing-bg-pattern {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0px, rgba(255, 255, 255, 0.06) 1px, transparent 1px, transparent 28px);
  pointer-events: none;
}
.auth-marketing-inner {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  max-width: 480px;
}
/* intentional: scoped override — .brand-name/.brand-tagline normally inherit --color-text (dark
   text), unreadable on this always-dark panel regardless of site theme (issue #116). */
.auth-marketing .brand-name, .auth-marketing .brand-tagline { color: #fff; }
.auth-marketing .brand-tagline { opacity: 0.75; }
.auth-marketing-brand { margin-bottom: 56px; }

/* Vertically centers the headline/subhead/features block in whatever room
   is left between the brand mark (top) and the stat line (bottom), instead
   of the whole panel just center-aligning one small flex item — this is
   what actually fixes the "too much empty space" complaint on tall/wide
   viewports, not a smaller panel. */
.auth-marketing-content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 28px; }

/* intentional: every color below is a fixed white/white-alpha value against .auth-marketing's
   always-dark panel, not a theme token — the panel itself never changes with site theme
   (issue #116). */
.auth-marketing-headline {
  margin: 0;
  color: #fff;
  font-family: var(--font-display);
  font-size: clamp(32px, 3.2vw, 46px);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.02em;
}
.auth-marketing-subhead {
  margin: 0;
  color: rgba(255, 255, 255, 0.8);
  font-size: var(--text-base);
  line-height: var(--leading-loose);
  max-width: 40ch;
}

.auth-marketing-values { display: grid; gap: 22px; list-style: none; padding: 0; margin: 8px 0 0; }
.auth-marketing-value { display: flex; gap: 16px; align-items: flex-start; }
/* intentional: fixed white-on-white-alpha, same always-dark panel as above (issue #116) */
.auth-marketing-value-icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: 0;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}
.auth-marketing-value-title { display: block; color: #fff; font-weight: 800; font-size: var(--text-base); margin-bottom: 2px; }
.auth-marketing-value-text { display: block; color: rgba(255, 255, 255, 0.72); font-size: var(--text-sm); line-height: var(--leading-normal); }

/* intentional: fixed white-alpha divider/text, same always-dark panel as above (issue #116) */
.auth-marketing-stat {
  position: relative;
  z-index: 1;
  margin: 32px 0 0;
  padding-top: 20px;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.72);
  font-size: var(--text-sm);
  font-weight: 600;
}

.auth-page-inner {
  width: min(440px, 100%);
  position: relative;
  z-index: 1;
  display: grid;
  gap: 20px;
}

.auth-top-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* Sign-out button added to the onboarding page's top row alongside the theme
   toggle — this page had no sign-out affordance anywhere before. */
.onboarding-top-actions { display: flex; align-items: center; gap: 8px; }

.auth-brand { justify-self: start; }

.auth-card-lg {
  background: var(--surface-glass);
  backdrop-filter: blur(16px);
  border: 1px solid var(--border-glass);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.auth-card-head { padding: 28px 28px 0; }
.auth-title { font-size: clamp(26px, 4vw, 32px); font-weight: 900; line-height: 1.1; letter-spacing: -0.02em; }
.auth-subtitle { margin-top: 8px; color: var(--color-text-muted); font-size: var(--text-base); }
.auth-card-body { padding: 24px 28px; display: grid; gap: 16px; }
.auth-card-foot { padding: 0 28px 24px; font-size: var(--text-base); color: var(--color-text-muted); text-align: center; }
.auth-form { display: grid; gap: 14px; }
.remember-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.remember-checkbox { width: 15px; height: 15px; accent-color: var(--color-accent); cursor: pointer; flex-shrink: 0; }
/* issue #155 v2 Phase D3 (full completion pass) — native checkbox accent-color, no
   text-contrast implication. */
:root[data-theme='dark'] .remember-checkbox { accent-color: var(--color-accent); }
.remember-label { font-size: var(--text-sm); color: var(--color-text-muted); font-weight: 600; user-select: none; }
.auth-divider {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  color: var(--color-text-muted);
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.auth-divider::before, .auth-divider::after { content: ''; height: 1px; background: var(--color-border); }
.auth-footnote { text-align: center; color: var(--color-text-muted); font-size: 12px; font-weight: 600; }

/* Onboarding template picker */
.onboarding-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: max(24px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(40px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  position: relative;
}

.onboarding-inner {
  width: min(880px, 100%);
  position: relative;
  z-index: 1;
  display: grid;
  gap: 28px;
}

.onboarding-head { text-align: center; display: grid; gap: 8px; justify-items: center; }
.onboarding-back { justify-self: start; }

.template-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  align-items: stretch;
  gap: 16px;
}

/* issue #206 §3.2 — clickable card (the whole card picks a template via the
   nested .template-card-pick button), so it gets the hover-lift treatment.
   Base surface swapped from --surface-glass to --color-surface per the
   spec's explicit "Base: var(--color-surface) bg" instruction — this drops
   the backdrop-filter glass effect this card family previously had. */
.template-card {
  height: 100%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  text-align: left;
  cursor: pointer;
  transition: transform var(--duration-base) var(--ease-decelerate), box-shadow var(--duration-base) var(--ease-decelerate), border-color var(--duration-base) var(--ease-decelerate);
  font-family: inherit;
  color: var(--color-text);
  width: 100%;
  position: relative;
}
.template-card:hover, .template-card:has(.template-card-pick:focus-visible) { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--color-border-strong); }
.template-card.is-disabled { cursor: not-allowed; opacity: 0.6; transform: none; pointer-events: none; }
.template-card.picking { opacity: 0.85; }
/* `store.switchRoadmap()` (onClick, buildCard()/buildCustomCard(),
   onboarding.js) is a real Firebase round-trip whenever the template isn't
   already cached this session — the faint opacity dim above was the only
   feedback during that wait, which read as unresponsive lag rather than
   "loading" on anything slower than an instant local network (real,
   reported bug). This overlay (buildPickingOverlay(), onboarding.js) covers
   the whole card with a spinner + "Opening…" for the duration of the click. */
.template-card-picking-overlay {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--surface-glass);
  backdrop-filter: blur(2px);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-accent-700);
}
/* issue #155 v2 Phase D3 (full completion pass) — plain text on a glass overlay. */
:root[data-theme='dark'] .template-card-picking-overlay { color: var(--color-accent-700); }
.template-card.picking .template-card-picking-overlay { display: flex; }
.template-card-picking-overlay .btn-spinner { width: 16px; height: 16px; border-width: 2px; }
/* issue #6 Phase 9 — the "pick this template" action is now a real <button>
   nested inside the (non-interactive) `.template-card` div, sibling to the
   corner hide/delete/info button, instead of `.template-card` itself
   carrying role="button" with a focusable button nested inside it — axe-core
   correctly flags a role="button"/role="checkbox" element that contains
   other focusable descendants (WCAG 4.1.2; ARIA's button/checkbox roles
   don't allow focusable children). This wrapper is deliberately unstyled
   beyond making it fill the card and behave like transparent flex-column
   content — `.template-card` itself keeps 100% of its original visual
   styling (background/border/shadow/padding) unchanged. */
.template-card-pick {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  min-width: 0;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
}
.template-card-pick:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* issue #206 §3.2 — 3px TOP border only; right/bottom/left stay the card's
   normal 1px solid --color-border, untouched by this rule (border-top only
   sets the top edge). Was a border-image gradient; reverted to a plain solid
   color per the app-wide "no gradients, solid only" decision — simpler now
   that there's no gradient to scope to one edge via border-image-slice. */
.template-card-current {
  border-top: 3px solid var(--color-accent);
}
.template-card-icon { display: inline-flex; font-size: var(--icon-size-lg); line-height: 1; }
.template-card-name { font-size: var(--text-lg); font-weight: 800; }
.template-card-desc { font-size: var(--text-sm); color: var(--color-text-muted); line-height: 1.5; }
.template-card-footer { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 8px; }
.template-card-count {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-accent-700);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* issue #155 v2 Phase D3 (full completion pass) — plain text, straight swap. */
:root[data-theme='dark'] .template-card-count { color: var(--color-accent-700); }
/* issue #206 §3.6 — "Current" gets a small solid-filled dot (::before) next
   to the label, no markup change needed (onboarding.js renders this as a
   single `el('span', { text: 'Current' })` with no child node). Was a
   gradient dot; reverted to solid gold per the app-wide "no gradients"
   decision. */
.template-card-current-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-accent-700);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-border-strong);
}
.template-card-current-badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  flex-shrink: 0;
}
.template-card-started-badge {
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
}
.template-card-started { border-color: var(--color-border-strong); }

/* Corner info button on "Create your own roadmap" (issue #4 follow-up) —
   same 24px circular-corner-button slot/sizing the old standalone
   .template-card-hide/-delete/-favorite buttons used (all three retired in
   issue #206 §4.1, collapsed into .template-card-overflow-btn below —
   "Create your own roadmap" is the one card that never had 2+ secondary
   actions, so it keeps its own single always-visible corner button rather
   than a menu), opening buildYourOwnGuide instead of hiding/deleting. Used
   to live only on the now-retired "blank" template as an inline text link;
   moved here since blank was its sole home. */
.template-card-info-corner {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-glass);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.template-card-info-corner:hover { color: var(--color-accent-700); border-color: var(--color-border-strong); }
/* issue #155 v2 Phase D3 (full completion pass) — straight text/border swap. */
:root[data-theme='dark'] .template-card-info-corner:hover { color: var(--color-accent-700); border-color: var(--color-border-strong); }

/* Card-action overflow menu (issue #206 §4.1) — collapses the favorite +
   hide/delete corner buttons (formerly `.template-card-favorite`/`-hide`/
   `-delete`, issue #177/#61, removed here) behind one ⋯ trigger.
   `.template-card-overflow` is `createDropdown()`'s own `.dropdown` wrapper
   (app.css's existing `.dropdown`/`.dropdown-menu`/`.dropdown-item` rules
   style the portaled menu itself) positioned absolutely in the same
   top-right corner slot the old standalone hide/delete button used to
   occupy — only one button now, so no `right: 44px` favorite-offset variant
   is needed. */
/* `.dropdown.template-card-overflow` (two classes), not `.template-card-overflow`
   alone — `.dropdown`'s own `position: relative` (below) is a single-class selector
   at equal specificity, declared later in this file, so a single-class override here
   would lose the cascade regardless of source order and silently fall back to
   `position: relative`. That's a real bug this was found from: it knocked the ⋯
   trigger out of its absolute corner slot into the card's normal flex flow, which fed
   a wrong trigger rect into `positionMenu()` (dropdown.js) and rendered the portaled
   menu partly off-screen — reported live as a blank white box overlapping the cards. */
.dropdown.template-card-overflow { position: absolute; top: 12px; right: 12px; }
.template-card-overflow-btn {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-glass);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
}
.template-card-overflow-btn:hover,
.template-card-overflow-btn[aria-expanded='true'] { color: var(--color-accent-700); border-color: var(--color-border-strong); }
/* issue #155 v2 Phase D3 (full completion pass) — straight text/border swap, same as .template-card-info-corner:hover above. */
:root[data-theme='dark'] .template-card-overflow-btn:hover,
:root[data-theme='dark'] .template-card-overflow-btn[aria-expanded='true'] { color: var(--color-accent-700); border-color: var(--color-border-strong); }

.hidden-templates-section { display: grid; gap: 12px; justify-items: center; }
.hidden-templates-toggle { justify-self: center; }
.hidden-grid { width: 100%; }
.template-card-hidden {
  background: var(--color-surface-raised);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  opacity: 0.85;
}
.template-card-hidden .btn { margin-top: auto; align-self: flex-start; }

/* Issue #100 revamp — the plain dashed-border box read as an inert
   placeholder rather than the primary "start something new" action it is
   (real user feedback: "I don't think it is good we need to remake this").
   A glowing brand-colored border + a small "AI-powered" pill make it the
   visually loudest card in the grid — deliberately keeping the same
   .template-card `--color-surface` base every other card uses (never
   overriding `background` to a solid fill) so it reads as a highlighted
   variant of the existing card language, not a mismatched flat block. */
/* issue #297 — --color-brand-rose (violet "AI-assisted" hue) is deleted, not
   remapped to a second accent: design-system.md §1/§2 is explicit that this
   app has "one accent per theme," with no reserved second hue for AI-related
   affordances the way issue #136's "accent-2" concept had. Judgment call:
   this card/badge now reads on the neutral ramp instead — --color-neutral-700
   for the border/icon/name (a clearly darker-than-divider tone that still
   reads as a deliberate outline, not the plain --color-border every other
   card uses) and --color-neutral-800 for the badge's higher-contrast text
   role (--color-brand-rose-ink's old job). The card is still visually
   distinguished from `.template-card-current` (accent-colored) by simply
   not using the accent at all — "the AI card is the one that's NOT accent,
   everything else uses the scarce accent" reads as a valid distinction on
   its own, and doesn't need a second hue to do it. The old violet glow
   box-shadow (a decorative colored shadow) is also dropped outright — no
   glows on flat surfaces, per design-system.md §2/§4/§9's review checklist —
   in favor of the same plain --shadow-sm/--shadow-lg elevation every other
   card in this file already uses. */
.template-card-create {
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 6px;
  border: 1.5px solid var(--color-neutral-700);
  box-shadow: var(--shadow-sm);
}
.template-card-create:hover, .template-card-create:has(.template-card-pick:focus-visible) { box-shadow: var(--shadow-lg); }
.template-card-create .template-card-icon { color: var(--color-neutral-700); }
.template-card-create .template-card-name { color: var(--color-neutral-700); }
.template-card-ai-badge {
  align-self: center;
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-neutral-800);
  background: var(--color-neutral-200);
  border: 1px solid var(--color-border-strong);
}

/* Dashboard */
.dashboard { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }

/* App shell — sidebar + topbar (issue #6 Phase 2). `.app-shell-2` composes
   with `.dashboard` (kept on the same element — existing e2e/unit tests
   assert `.dashboard` as the "dashboard is rendered" marker) rather than
   replacing it. First grid column is `auto`, sized to `.app-sidebar`'s own
   `width` — collapsing the sidebar is then just changing that one width,
   no `:has()` or duplicated column tracks needed. */
/* minmax(0, 1fr), not a bare 1fr (issue #8 follow-up) — a bare `1fr` track's
   automatic minimum size is its content's min-content width, so any page
   with wide-enough content (the Progress page's ~800px heatmap/charts) could
   force this whole grid column, and therefore the page, wider than the
   viewport instead of letting that content scroll within its own container.
   dashboard.js/settings.js never had content wide enough to expose this —
   `.app-shell-main`'s own `min-width: 0` alone wasn't sufficient; the track
   itself needs the explicit minimum too. Same fix applied to both
   `@media`-overridden column definitions below. */
.app-shell-2 { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; min-height: 100vh; min-height: 100dvh; }

/* `position: sticky; top: 0` + a viewport-height cap, not the grid row's
   (`align-items: start` above already stops the row-stretch half of this) —
   without both, a long roadmap's `.dashboard-content` column can grow far
   taller than the viewport, and a plain-stretched sidebar would grow right
   along with it (seen at ~5700px tall against 484 seeded items) instead of
   staying pinned while the content scrolls past it. `overflow-y: auto` is a
   safety net for a future sidebar tall enough to exceed the viewport itself
   (more nav items than fit), not something today's short nav list needs. */
.app-sidebar {
  width: 240px;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  background: var(--color-surface);
  border-right: 1px solid var(--color-border);
  transition: width var(--duration-base) var(--ease-out);
}

/* issue #298 — design-system.md §4's 2px major-section rule, under the brand block. */
.app-sidebar-brand { padding: 0 var(--space-2) var(--space-4); margin-bottom: var(--space-2); border-bottom: 2px solid var(--color-divider); }

.app-sidebar-nav { display: flex; flex-direction: column; gap: var(--space-1); }

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-text-muted);
  text-decoration: none;
  font-weight: 700;
  font-size: var(--text-sm);
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.nav-item:hover { background: var(--color-surface-raised); color: var(--color-text); }
/* issue #206 §3.7 — left-edge 3px accent bar + raised bg, replacing the old
   flat gold-tinted fill. Used `box-shadow: inset 3px 0 0 ...` rather than a
   `border-left`/pseudo-element, per the issue's own stated preference — an
   inset shadow doesn't add to the box's rendered width/padding, so it can't
   shift `.nav-item-icon`/label layout the way a real border would.
   Switched border-radius from the old pill shape (--chip-radius) to
   --radius-md: a left-edge straight bar reads as clipped/curved against a
   fully rounded pill's left corner — a rect radius is the correct shape for
   an edge-indicator pattern. Flagging this shape change from the pre-#206
   pill treatment. */
.nav-item.active {
  background: var(--color-accent-100);
  color: var(--color-accent-700);
  border-radius: var(--radius-md);
  box-shadow: inset 3px 0 0 var(--color-accent);
}
.nav-item-icon { display: inline-flex; flex-shrink: 0; }

.app-sidebar-spacer { flex: 1; }

.app-sidebar-collapse-btn {
  align-self: flex-start;
  border: none;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  border-radius: var(--radius-md);
}
.app-sidebar-collapse-btn:hover { background: var(--color-surface-raised); color: var(--color-text); }

.app-sidebar-footer {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.app-sidebar-identity {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  /* No text node of its own — the avatar and .app-sidebar-user-email child
     each set their own color — but a button never inherits color from its
     ancestors by default, so this stays explicit for defensiveness (issue #116). */
  color: inherit;
  cursor: pointer;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  text-align: left;
}
.app-sidebar-identity:hover { background: var(--color-surface-raised); }
.app-sidebar-user-email {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Issue #123 — persistent local-only-data risk indicator next to "Guest
   session". Colored with --color-warning (not --color-danger) since it's informational,
   not an active error state. */
.app-sidebar-guest-risk {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--color-warning);
}

/* Collapsed — manual desktop icon-rail toggle, persisted to localStorage
   (sidebar.js). Independent of the automatic tablet-width rail below. */
.app-sidebar.collapsed { width: 64px; }
.app-sidebar.collapsed .nav-item-label,
.app-sidebar.collapsed .app-sidebar-user-email,
.app-sidebar.collapsed .app-sidebar-guest-risk,
.app-sidebar.collapsed .brand-name,
.app-sidebar.collapsed .brand-tagline { display: none; }
.app-sidebar.collapsed .nav-item { justify-content: center; }
.app-sidebar.collapsed .app-sidebar-collapse-btn { transform: scaleX(-1); }

/* Issue #102 — an icon-only rail (this 64px desktop-collapsed state, or the
   automatic 640-1023px tablet rail below) is too narrow for the identity
   avatar and sign-out button to sit side by side (footer's default row
   layout): 64px width minus this rule's own left/right padding leaves ~32px
   of content width, less than the avatar (28px) + gap + icon button (36px)
   combined, so the two visually overlapped instead of wrapping. Stack them
   vertically and shrink the sign-out button to match the avatar's footprint
   whenever the rail is icon-only — never let this rule apply to the full
   (expanded) sidebar width, where the row layout is correct. */
.app-sidebar.collapsed .app-sidebar-footer {
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}
.app-sidebar.collapsed .app-sidebar-identity { justify-content: center; padding: var(--space-1) 0; }
.app-sidebar.collapsed .app-sidebar-signout { width: 32px; height: 32px; font-size: var(--text-base); }

/* A near-black, higher-opacity, theme-agnostic overlay — not the lighter
   `rgba(15, 23, 42, 0.5)` `.modal-overlay` uses. That value reads fine over
   `.modal-overlay`'s content because a bright, opaque `.modal-card` sits
   centered on top of it, drawing the eye; the mobile sidebar drawer has no
   such focal card — the backdrop *is* most of the visible screen once the
   drawer is open — so in dark theme, where the page's own background is
   already close to this same navy, the old value barely dimmed anything and
   the dashboard behind it stayed fully readable, reading as a broken
   half-transparent mess rather than a real modal overlay (reported live,
   issue #18 follow-up). `0.78` opacity + `blur(2px)` fixed that — confirmed
   live in both themes. (A later pass briefly pushed this to `0.94`/`blur(10px)`
   to make dashboard text fully unreadable behind the drawer, but that read as
   too heavy/flat — reverted back to this value, which was already good; the
   real remaining problem turned out to be the layout-shift bug fixed below,
   not this backdrop.) */
/* intentional: theme-agnostic dark scrim, see comment above (issue #116) */
.app-sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(2, 6, 16, 0.78);
  backdrop-filter: blur(2px);
  z-index: 199;
}
.app-sidebar-backdrop.show { display: block; }

/* issue #299 — design-system.md §8: "Avatars: square, ink fill, ground-colored
   initial." Was a circular, accent-filled, fixed-white-text avatar left over
   from the pre-#297 direction (both fill and shape). */
.avatar {
  display: grid;
  place-items: center;
  background: var(--color-text);
  color: var(--color-bg);
  font-weight: 800;
  flex-shrink: 0;
}
.avatar-sm { width: 28px; height: 28px; font-size: var(--text-xs); }
.avatar-md { width: 36px; height: 36px; font-size: var(--text-sm); }
.avatar-lg { width: 56px; height: 56px; font-size: var(--text-lg); }

/* Issue #102 follow-up — `.dropdown-menu` used to be `position: absolute`
   relative to `.dropdown` (this wrapper), with its left/right/top/bottom set
   by the static `.dropdown-start`/`.dropdown-end` rules below. That broke the
   moment the wrapper's containing block was `.app-sidebar`: the sidebar's
   `overflow-y: auto` computes `overflow-x` to `auto` too per the CSS overflow
   spec (an unspecified axis next to a non-`visible` one is never left
   `visible`), so the sidebar silently clips anything absolutely-positioned
   inside it to its own narrow content box — invisible in the normal 240px
   sidebar (the 180px menu mostly fit inside it by coincidence) but a hard
   clip in the 56-64px icon-only rail, cutting the "Delete account" menu down
   to a sliver (reported live). `position: fixed` escapes that ancestor clip
   entirely (fixed elements are clipped by the viewport, not by a scrolling
   ancestor, as long as nothing between here and the viewport sets a
   transform/filter/perspective — `.app-sidebar` never does outside its own
   mobile-drawer transform, which isn't active in the collapsed/rail states
   this bug hit) — but a fixed element ignores `.dropdown`'s
   `position: relative` entirely, so `dropdown.js` now computes the menu's
   `left`/`right`/`top`/`bottom` from the trigger's `getBoundingClientRect()`
   at open time instead of leaning on static CSS offsets. z-index raised to
   sit above the mobile sidebar drawer (200) and its backdrop (199), since a
   fixed-position menu can now be triggered from inside that drawer too.
   Issue #121 follow-up: `position: fixed` is *positioned* relative to the
   viewport only as long as no ancestor between it and the root has a
   transform/filter/etc. — but it also *stacks* (z-index paint order) within
   whatever stacking context its containing block belongs to, so even a
   correctly-positioned fixed menu can render behind/in front of unrelated
   page content inconsistently if such an ancestor exists (reported live: the
   sidebar's avatar menu, nested inside the animated dashboard shell,
   intermittently rendered under phase-card content). `dropdown.js` now
   portals `.dropdown-menu` to a direct child of `document.body` on open (the
   same fix `.custom-select-listbox` below already uses for the identical bug
   class) — it is no longer a DOM descendant of `.dropdown`, so its
   open/closed visibility is driven by `.dropdown-menu.open` directly, not a
   `.dropdown.open .dropdown-menu` descendant selector. */
/* A portaled floating menu/listbox has no page-flow relationship to what it
   visually sits on top of — clicking a trigger near the top of a short card
   (the Daily Todos duration select is the reported case, issue #228 follow-up)
   can open a menu tall enough to cover that same card's own sibling controls
   (the "Add" button, 8px below the trigger) and spill well past the card into
   unrelated content beneath it. That's expected positioning (see the
   position: fixed comments below), but with no visual layer marking it as a
   floating overlay, it reads as corrupted layout rather than an open menu —
   a real, reported bug, not a hypothetical. `.floating-scrim` is a shared,
   fixed, full-viewport dim layer inserted just behind the listbox/menu (a
   sibling body child, not a wrapper — the listbox/menu keep their own
   independent position: fixed coordinates) whenever one is open, giving every
   portaled floating element the same "this is a layer above the page, not
   part of it" visual cue a native <select>'s OS-rendered dropdown gets for
   free. Purely visual — clicking it closes the menu via the existing
   click-outside-`wrap`-and-`listbox`/`menu` handler already in
   select.js/dropdown.js, since the scrim is neither. */
.floating-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45); /* intentional: a fixed dim overlay, not a themed surface — same reasoning as .modal-overlay's own scrim */
}
.custom-select-scrim { z-index: 1009; }
.dropdown-scrim { z-index: 209; }

.dropdown { position: relative; display: inline-flex; min-width: 0; }
.dropdown-menu {
  position: fixed;
  min-width: 180px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: var(--space-1);
  display: none;
  flex-direction: column;
  z-index: 210;
}
.dropdown-menu.open { display: flex; }
.dropdown-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border: none;
  background: none;
  border-radius: var(--radius-md);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--color-text);
  cursor: pointer;
}
.dropdown-item:hover, .dropdown-item:focus-visible { background: var(--color-surface-raised); outline: none; }
.dropdown-item-danger { color: var(--color-danger); }

/* Custom-styled listbox replacing a bare <select> (issue #136 Phase 3,
   src/ui/components/select.js) — same position: fixed + getBoundingClientRect()
   approach as .dropdown-menu above, for the same reason (escapes a clipping
   ancestor like .app-sidebar). .custom-select-trigger reuses .field-input's
   border/padding/background so it matches every other field in the same
   form exactly, with only the layout (flex row, space-between) added. */
.custom-select { position: relative; display: inline-block; width: 100%; }
.custom-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  text-align: left;
  cursor: pointer;
  color: var(--color-text);
  font: inherit;
}
.custom-select-trigger:focus-visible { outline: none; border-color: var(--color-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent); }
.custom-select-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.custom-select-caret { display: inline-flex; flex-shrink: 0; color: var(--color-text-muted); transform: rotate(90deg); transition: transform var(--duration-base) var(--ease-in-out); }
.custom-select.open .custom-select-caret { transform: rotate(-90deg); }
.custom-select-listbox {
  position: fixed;
  max-height: 260px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: var(--space-1);
  margin: 0;
  list-style: none;
  display: none;
  flex-direction: column;
  /* Unlike .dropdown-menu above (only ever triggered from topbar/sidebar
     chrome, never nested inside a modal), select.js is explicitly meant to
     be droppable into any form — including importRoadmapModal.js's Goal/
     context field, inside a `.modal-overlay` at z-index 1000. A portaled
     listbox at the same z-index: 210 as .dropdown-menu rendered visually
     underneath the modal card: geometrically correct (per the transformed-
     ancestor portal fix above) but click-intercepted by the modal's own
     content on top of it in the stacking order — found via a real E2E
     failure (issue #136 Phase 3 follow-up), not a visual review. Must stay
     above every modal in the app; .modal-overlay's z-index: 1000 is the
     highest of any component this can be nested inside. */
  z-index: 1010;
}
/* Not a descendant selector — select.js appends .custom-select-listbox
   straight to <body> while open (a portal), specifically so its
   `position: fixed` coordinates are never hijacked by a transformed
   ancestor like .item-panel's slide-in (see select.js's own comment). */
.custom-select-listbox.open { display: flex; }
.custom-select-option {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--color-text);
  cursor: pointer;
}
.custom-select-option:hover, .custom-select-option:focus-visible { background: var(--color-surface-raised); outline: none; }
.custom-select-option.active { color: var(--color-accent-700); font-weight: 700; background: var(--color-warning-bg); }
/* issue #155 v2 Phase D3 (full completion pass) — text-on-light-fill pattern. */
:root[data-theme='dark'] .custom-select-option.active { color: var(--color-accent-700); background: var(--color-warning-bg); }
/* Issue #36 — no focusable field renders under 16px on touch, but a listbox
   option isn't a text-zoom-triggering field; matches .field-input's own
   compact variant instead so density stays consistent with the rest of a
   dense form. */
.custom-select.compact .custom-select-trigger { padding: 8px 10px; font-size: var(--text-sm); }

/* issue #298 — design-system.md §1/§4: flat surfaces, 2px rules for major
   section boundaries (was a translucent/blurred "glass" surface + 1px rule,
   a decorative effect the "alignment and rules do the organising — never
   decoration" identity rule doesn't allow). */
.app-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-6);
  background: var(--color-surface);
  border-bottom: 2px solid var(--color-divider);
  position: sticky;
  top: 0;
  z-index: 20;
  /* issue #6 Phase 4.2 — a single source of truth for the topbar's own
     height, reused by .section-label's sticky offset below so a sticky
     section header never sits half-hidden under the topbar. Keep these two
     in lockstep rather than hardcoding the same pixel value twice. */
  min-height: var(--topbar-h);
}
.app-topbar-breadcrumb {
  font-weight: 700;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.app-topbar-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
/* Real, reported bug (issue #309): the guest/sync pill (32px, padding-driven),
   the "Create account" `.btn-sm` (34px), and `.icon-btn-group` (~44-48px,
   padding+border around its 36px icon buttons) had three different heights
   with no shared convention, reading as visibly out-of-sync in this one row.
   Scoped to this row specifically (not a global `.sync-pill`/`.btn-sm`
   change) since both classes are used elsewhere with their own, independently
   fine sizing — 36px matches the icon buttons' own natural size, the
   "anchor" control already appearing three times over in this same row. */
.app-topbar-actions .sync-pill,
.app-topbar-actions > .btn,
.app-topbar-actions .icon-btn-group {
  height: 36px;
  box-sizing: border-box;
}
.app-topbar-actions .sync-pill { display: inline-flex; align-items: center; }
/* Notification bell (issue #20) — same 36px hit target as .btn-icon, but a
   bare unstyled-until-hover button (matches .app-topbar-hamburger's
   treatment) since it sits among icon-only topbar actions, not a filled
   .btn. The unread dot is absolutely positioned over the bell glyph's
   top-right corner. */
.app-topbar-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: none;
  color: var(--color-text);
  cursor: pointer;
  border-radius: var(--radius-md);
}
.app-topbar-bell:hover { background: var(--color-surface-raised); }
.app-topbar-bell .notification-badge-dot { position: absolute; top: 6px; right: 6px; }
/* Command palette trigger (issue #125) — same unstyled-until-hover 36px
   treatment as .app-topbar-bell, since it sits among the same icon-only
   topbar actions. */
.app-topbar-command-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: none;
  color: var(--color-text);
  cursor: pointer;
  border-radius: var(--radius-md);
}
.app-topbar-command-btn:hover { background: var(--color-surface-raised); }
.app-topbar-hamburger {
  display: none;
  border: none;
  background: none;
  font-size: var(--text-lg);
  color: var(--color-text);
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
}

/* `grid-column: 2` is required, not decorative — without an explicit
   placement, CSS Grid's auto-placement algorithm places the next in-flow
   child into the first *available* cell. `.app-sidebar` going
   `position: fixed` when the mobile/tablet drawer opens (below) removes it
   from grid flow entirely, and without this rule `.app-shell-main` (the
   next in-flow child) would auto-place into column 1 instead of column 2 —
   shrinking to the sidebar's own column width and jumping to the left edge
   the instant the drawer opened, a real layout-shift bug distinct from (and
   found after fixing) the backdrop-visibility issue above (reported live,
   issue #18 follow-up). This selector has no visible effect while the
   sidebar stays in flow (desktop, or the closed tablet rail) — it only
   matters once something upstream can go out of flow. */
.app-shell-main { display: flex; flex-direction: column; min-width: 0; grid-column: 2; }
.app-content { flex: 1; }

/* Tablet (640–1023px, issue #6 Phase 2.1) — automatic icon-only rail,
   independent of the manual desktop collapse toggle above. */
@media (max-width: 1023px) {
  /* Locks the grid's first column to the closed rail's own width (56px),
     independent of `.app-sidebar`'s own `width` — see the fix below for why
     that decoupling matters. */
  .app-shell-2 { grid-template-columns: 56px minmax(0, 1fr); }
  .app-sidebar { width: 56px; padding: var(--space-3) var(--space-2); }
  .app-sidebar .nav-item-label,
  .app-sidebar .app-sidebar-user-email,
  .app-sidebar .app-sidebar-guest-risk,
  .app-sidebar .brand-name,
  .app-sidebar .brand-tagline { display: none; }
  .app-sidebar .nav-item { justify-content: center; }
  .app-sidebar-collapse-btn { display: none; }

  /* Issue #102 — same icon-rail footer overlap this width hits as the
     desktop `.collapsed` state above: stack the avatar and sign-out button
     instead of letting them collide in the narrow rail. */
  .app-sidebar-footer { flex-direction: column; align-items: center; gap: var(--space-1); }
  .app-sidebar-identity { justify-content: center; padding: var(--space-1) 0; }
  .app-sidebar-signout { width: 32px; height: 32px; font-size: var(--text-base); }

  /* Issue #102 — the manual desktop collapse toggle is hidden at this width
     (the rail here is automatic, not user-togglable) and the hamburger below
     used to be scoped to <640px only, leaving 640-1023px with no way to see
     nav labels or the account email. Reuse the same `_toggleMobile()` drawer
     mechanism (`sidebar.js`) the phone breakpoint already has — the
     hamburger becomes visible here too, and opening it expands the rail into
     the same full overlay drawer instead of staying icon-only. */
  .app-topbar-hamburger { display: inline-flex; }
  /* `.app-topbar`'s base `align-items: center` (desktop) centers every flex
     child against the row's own height — harmless there since nothing
     wraps. Below 1024px, `.app-topbar-actions` (Local only/Create account +
     the search/bell/theme icon group) has to `flex-wrap` onto a second line
     once the hamburger + breadcrumb + all those controls no longer fit one
     row, making that one flex item roughly twice as tall as the hamburger
     button next to it — `align-items: center` then centers the hamburger
     against that *combined* two-row height, so it visually floats between
     the two wrapped rows instead of sitting level with the first one (real,
     reported: screenshotted at 390px looking "crowded"/misaligned — issue
     #301 follow-up). `flex-start` keeps every child's top edge level
     regardless of how tall its neighbor grows once wrapped. */
  .app-topbar { align-items: flex-start; }
  /* `position: fixed` takes the sidebar out of grid flow the moment the
     drawer opens — without the `.app-shell-2` override above locking the
     grid's first column to a fixed 56px, the `auto`-sized column it used to
     occupy collapses (nothing in-flow left to size it against), and
     `.app-shell-main` reflows to fill the freed space: the whole dashboard
     visibly shifted/resized every time the drawer opened and closed, not
     just an overlay appearing on top of a static page (reported live,
     issue #18 follow-up). Locking the column width is the fix — never make
     it `auto` again at this breakpoint while `.mobile-open` can go fixed. */
  .app-sidebar.mobile-open {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 240px;
    padding: var(--space-6) var(--space-4);
    z-index: 200;
  }
  .app-sidebar.mobile-open .nav-item-label,
  .app-sidebar.mobile-open .app-sidebar-user-email,
  .app-sidebar.mobile-open .brand-name,
  .app-sidebar.mobile-open .brand-tagline { display: block; }
  .app-sidebar.mobile-open .nav-item { justify-content: flex-start; }
  .app-sidebar.mobile-open .app-sidebar-footer { flex-direction: row; align-items: center; gap: var(--space-2); }
  .app-sidebar.mobile-open .app-sidebar-identity { justify-content: flex-start; padding: var(--space-1); }
  .app-sidebar.mobile-open .app-sidebar-signout { width: 36px; height: 36px; font-size: 22px; }
}

/* Mobile (<640px) — sidebar hidden by default, opens as a drawer on hamburger tap. */
@media (max-width: 639px) {
  /* `.app-shell-main { grid-column: 2; }` (unconditional, above) is only
     correct while the explicit grid actually has 2 columns. At this
     breakpoint `.app-sidebar` is `position: fixed` (out of flow) and the
     grid collapses to a single explicit column — but `.app-shell-main`
     staying pinned to "column 2" forces the browser to synthesize an
     implicit second column to satisfy that placement, splitting the
     viewport width between a phantom empty column and the real content
     instead of giving it the full 1fr. This happened to resolve to 0px on
     `dashboard.js`'s specific content shape (invisible) but produced a real,
     visible blank gutter on `settings.js` (reported live, issue #16 follow-
     up, screenshotted at multiple narrow desktop-browser widths). Pin it
     back to column 1 — the only explicit column that exists here. */
  .app-shell-main { grid-column: 1; }
  .app-shell-2 { grid-template-columns: minmax(0, 1fr); }
  .app-sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 240px;
    z-index: 200;
    transform: translateX(-100%);
    transition: transform var(--duration-base) var(--ease-out);
  }
  .app-sidebar.mobile-open { transform: translateX(0); }
  .app-sidebar .nav-item-label,
  .app-sidebar .app-sidebar-user-email,
  .app-sidebar .brand-name,
  .app-sidebar .brand-tagline { display: block; }
  .app-sidebar .nav-item { justify-content: flex-start; }
  .app-topbar-hamburger { display: inline-flex; }
}

.scroll-locked { overflow: hidden; }

.offline-banner {
  display: none;
  padding: 10px 16px;
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-warning);
  color: var(--color-warning);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
}
.offline-banner.show { display: flex; align-items: center; justify-content: center; gap: 8px; }

.verification-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-border-strong);
  color: var(--color-accent-700);
  font-size: var(--text-sm);
  font-weight: 600;
}
/* issue #155 v2 Phase D3 (full completion pass) — text-on-light-fill pattern, app-wide
   banner shown across every page for an unverified account. */
:root[data-theme='dark'] .verification-banner {
  background: var(--color-warning-bg);
  border-bottom-color: var(--color-border-strong);
  color: var(--color-accent-700);
}
.verification-msg { flex: 1; min-width: 0; }
.verification-dismiss { font-size: 16px; line-height: 1; padding: 2px 8px; }

.backup-reminder-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-border-strong);
  color: var(--color-accent-700);
  font-size: var(--text-sm);
  font-weight: 600;
}
/* issue #155 v2 Phase D3 (full completion pass) — same pattern as .verification-banner
   above. */
:root[data-theme='dark'] .backup-reminder-banner {
  background: var(--color-warning-bg);
  border-bottom-color: var(--color-border-strong);
  color: var(--color-accent-700);
}
.backup-reminder-icon { font-size: var(--text-base); }
.backup-reminder-msg { flex: 1; min-width: 220px; font-weight: 500; }

/* Weekly progress digest banner (issue #284) — same shape as
   .backup-reminder-banner immediately above, reusing the identical
   warning-tint/border/text-token trio so every dismissible top-of-page
   banner in this app reads as one visual family. */
.progress-digest-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-border-strong);
  color: var(--color-accent-700);
  font-size: var(--text-sm);
  font-weight: 600;
}
:root[data-theme='dark'] .progress-digest-banner {
  background: var(--color-warning-bg);
  border-bottom-color: var(--color-border-strong);
  color: var(--color-accent-700);
}
.progress-digest-icon { display: inline-flex; }
.progress-digest-msg { flex: 1; min-width: 220px; font-weight: 500; }

/* intentional: theme-agnostic dark scrim behind an opaque, bright .modal-card
   (issue #116; issue #206 §3.5 — retuned to a neutral rgba(0,0,0,0.5), the
   spec's exact value, replacing the old slate-tinted rgba(15,23,42,0.55)). */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  /* `safe center` (not plain `center`) so a modal taller than the viewport
     stays scrollable to its very top and bottom — plain `center` on an
     overflowing flex container clips both ends equally and makes them
     unreachable no matter how far you scroll, a well-known flexbox+overflow
     trap. Found via the Daily Todos guide modal growing past the viewport
     on a short window (issue #56 follow-up) — applies to every modal in
     the app, not just that one. */
  align-items: safe center;
  justify-content: center;
  z-index: 1000;
  padding: 24px;
  overflow-y: auto;
}
.modal-card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 32px 28px 28px;
  width: 100%;
  max-width: 420px;
}
/* design-system.md §5 — "modal/dialog... 2px title rules" (issue #301
   follow-up; this modal title previously had no rule at all, just a margin
   gap). */
.modal-title {
  font-size: 18px;
  font-weight: 800;
  color: var(--color-text);
  padding-bottom: 16px;
  margin-bottom: 20px;
  border-bottom: 2px solid var(--color-divider);
}
.delete-modal-body {
  font-size: var(--text-base);
  color: var(--color-text-muted);
  line-height: 1.6;
}

.build-guide-card { max-width: 520px; display: grid; gap: 4px; }
.build-guide-intro { font-size: var(--text-base); color: var(--color-text-muted); line-height: 1.6; margin-bottom: 8px; }
.build-guide-heading { font-size: var(--text-base); font-weight: 800; color: var(--color-text); margin-top: 12px; }
.build-guide-body { font-size: var(--text-base); color: var(--color-text); line-height: 1.6; }
.build-guide-note { font-size: 12px; color: var(--color-text-muted); font-style: italic; margin-top: 12px; }

.confirm-dialog-card { max-width: 440px; }
.confirm-dialog-body { font-size: var(--text-base); color: var(--color-text-muted); line-height: 1.6; }
.confirm-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }

/* Share card preview modal (issue #8, Part C) */
.share-modal-card { max-width: 600px; display: grid; gap: 14px; position: relative; max-height: 90vh; overflow-y: auto; }
.share-modal-card .modal-close { position: absolute; top: 8px; right: 8px; }
.share-card-preview { width: 100%; height: auto; border-radius: var(--radius-md); box-shadow: var(--shadow-sm); }
.share-caption-label { display: grid; gap: 6px; font-size: var(--text-sm); font-weight: 600; color: var(--color-text); }
.share-caption-input {
  font: inherit;
  font-size: var(--text-base);
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  color: var(--color-text);
  resize: vertical;
}
.share-modal-actions { display: flex; flex-wrap: wrap; gap: 10px; }

/* Roadmap sharing — published snapshot link (issue #131) */
.share-roadmap-modal { display: grid; gap: 14px; max-width: 520px; }
.share-roadmap-modal-copy { color: var(--color-text-muted); font-size: var(--text-sm); }
.share-roadmap-modal-status { font-size: var(--text-sm); color: var(--color-text); word-break: break-all; }
.share-link-list { display: grid; gap: 10px; list-style: none; padding: 0; margin: 0; }
.share-link-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}
.share-link-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-text); }

.shared-view-container { min-height: 100vh; min-height: 100dvh; padding: 32px 20px; max-width: 900px; margin: 0 auto; }
.shared-view-loading { color: var(--color-text-muted); text-align: center; padding: 60px 0; }
.shared-view-state { display: grid; gap: 12px; justify-items: center; text-align: center; padding: 80px 20px; }
.shared-view-header { margin-bottom: 24px; }
.shared-view-subtitle { color: var(--color-text-muted); }
.shared-view-phases { display: grid; gap: 20px; }
.shared-phase-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  padding: 18px 20px;
}
.shared-section { margin-top: 12px; }
.shared-item-list { display: grid; gap: 8px; list-style: none; padding: 0; margin: 8px 0 0; }
.shared-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}
.shared-item-status { width: 18px; text-align: center; color: var(--color-accent-700); font-weight: 700; }
/* issue #155 v2 Phase D3 (full completion pass) — sharedRoadmapView.js's public
   read-only page; straight text swaps. */
:root[data-theme='dark'] .shared-item-status { color: var(--color-accent-700); }
.shared-item-title { flex: 1; min-width: 0; }
.shared-item-done .shared-item-title { color: var(--color-text-muted); text-decoration: line-through; }
.shared-item-resources { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 8px; }
.shared-resource-link { color: var(--color-accent-700); font-size: var(--text-sm); }
:root[data-theme='dark'] .shared-resource-link { color: var(--color-accent-700); }
.shared-view-footer { margin-top: 32px; text-align: center; }
.shared-view-attribution { display: inline-flex; align-items: center; gap: 6px; color: var(--color-text-muted); font-size: var(--text-sm); }

@media (max-width: 480px) {
  .share-modal-actions { flex-direction: column; }
  .share-modal-actions .btn { width: 100%; }
}

/* "Create your own roadmap" AI-creation modal (issues #4/#64, redesigned as a
   two-column build/paste layout in #100). Below the ≥1024px tier (see the
   breakpoint scale at the bottom of this file) this collapses to the single
   top-to-bottom column the flow always used — only the grid/footer split
   below is gated on that tier, not the content itself. */
.import-modal-card { max-width: 1040px; display: grid; gap: 16px; max-height: 88vh; overflow-y: auto; }
.import-modal-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; }
.import-column { min-width: 0; }
.import-column-paste { display: grid; gap: 12px; align-content: start; }
/* The build column is a flex column, not a plain scrolling block — the
   "Copy it" step (`.import-copy-sticky`) is a sibling of the scrollable
   `.import-column-scroll`, not a descendant of it, so it always renders in
   its own natural space below whatever height the scroll area is given,
   instead of requiring a scroll to reach it. This is what fixes "the prompt
   and Copy button are under the scrollable, not visible on first open"
   without removing the scroll itself — the topic/filters area still scrolls
   independently, the copy step never does. */
.import-column-build { display: flex; flex-direction: column; min-width: 0; }
.import-column-scroll { display: grid; gap: 14px; align-content: start; }
.import-step-heading { display: flex; align-items: center; gap: 10px; font-size: var(--text-sm); font-weight: 800; color: var(--color-text); margin: 6px 0 0; }
.import-step-heading:first-child { margin-top: 0; }
/* intentional: fixed white text on a solid --color-brand-gold background, same theme-agnostic pattern as .reset-success-icon/.template-card-delete:hover above (issue #116) */
.import-step-badge {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  font-size: 12px;
  font-weight: 800;
}
/* issue #155 v2 Phase D3 — modal-scoped recolor. Unlike every other Phase C/D2 recolor,
   this one must also swap its *text* color, not just its background: white text (the
   base rule above) fails badly on solid lime (the issue's own explicit callout — lime
   hosts near-black text, never white), so dark theme reads `var(--color-bg)` here instead
   of the fixed white the light-theme/pre-v2 rule above uses. Falls back to the
   original brand+white pairing in light theme. */
:root[data-theme='dark'] .import-step-badge { background: var(--color-accent); }
.field-hint { font-size: 12px; color: var(--color-text-muted); margin-top: -4px; }
.import-options { display: grid; gap: 14px; padding: 14px; background: var(--color-surface-raised); border: 1px solid var(--color-border); border-radius: var(--radius-md); }
.import-option-chips { display: flex; flex-wrap: wrap; gap: 6px; }
/* An implicit CSS Grid row's "auto" sizing (.import-column-scroll, the
   parent) gives a scroll-container item (overflow-y: auto here) an
   automatic minimum content size of 0 — per spec, a non-"visible" overflow
   value zeroes out an item's content-based minimum size, which collapsed
   this block to ~34px (just its padding/border) regardless of max-height,
   the real cause of issue #121 item 5's "renders very short" report, not
   the 260px cap alone. A definite `height` sidesteps the ambiguous
   "auto" track-sizing algorithm entirely — the box is always exactly this
   tall, scrolling its own overflow, rather than being derived from content. */
.import-prompt-block {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.7;
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 16px;
  height: 420px;
  overflow-y: auto;
  color: var(--color-text);
}
/* Always visible the instant the modal opens — see the .import-column-build
   comment above. A top border + panel background give it a subtle "this is
   a fixed footer, not just more scrolling content" cue even though it isn't
   position: fixed/sticky. */
.import-copy-sticky {
  display: grid;
  gap: 8px;
  padding-top: 14px;
  margin-top: 10px;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
}
.import-copy-hint { font-size: 12px; color: var(--color-text-muted); margin: 0; }
.import-copy-instructions { font-size: 12px; color: var(--color-text-muted); margin: 0; }
.import-paste-area { font-family: var(--font-mono); font-size: 12px; resize: vertical; }
.import-technical-toggle { justify-self: start; }
.import-errors {
  list-style: none;
  display: grid;
  gap: 4px;
  padding: 10px 12px;
  margin: 0;
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-md);
  color: var(--color-danger);
  font-size: 12px;
}
.import-errors:empty { display: none; }
.import-errors[hidden] { display: none; }
.import-modal-footer { display: grid; gap: 10px; }

/* ≥1024px (tablet landscape / small laptop and up) — the two-column
   "build your prompt" / "paste the AI's answer" split. Each column scrolls
   independently (not the whole .modal-card) so the prompt-building column
   can stay in view while scrolling the paste column, or vice versa — the
   exact disorientation a single shared scroll caused pre-#100. The paste
   textarea is set to fill its column so it reads as the visually dominant
   element there, per the issue's "attention should land on the paste box"
   requirement. */
@media (min-width: 1025px) {
  .import-modal-grid { grid-template-columns: 45% minmax(0, 1fr); gap: 32px; }
  .import-column-build { max-height: 60vh; }
  .import-column-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
  .import-prompt-block { height: min(420px, 40vh); }
  .import-column-paste { max-height: 60vh; overflow-y: auto; padding-right: 4px; }
  .import-column-paste .import-paste-area { min-height: 260px; flex: 1; }
}

/* issue #298 — same flat-surface/2px-rule fix as .app-topbar above. */
.dashboard-header {
  background: var(--color-surface);
  border-bottom: 2px solid var(--color-divider);
  padding: max(18px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) 20px max(20px, env(safe-area-inset-left));
}

.header-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.sync-pill {
  font-size: 12px;
  font-weight: 800;
  padding: 6px 10px;
  border-radius: 0;
  border: 1px solid var(--color-success);
  background: var(--color-success-bg);
  color: var(--color-success);
}
.sync-pill.guest { background: var(--color-warning-bg); color: var(--color-warning); border-color: var(--color-warning); }
.sync-pill.error { background: var(--color-danger-bg); color: var(--color-danger); border-color: var(--color-danger); }

/* issue #297 — design-system.md §2 collapses success into the neutral-ink
   scheme (no dedicated green/olive hue survives this pass); --color-success
   already resolves to --color-neutral-800 in both themes, so this dot no
   longer needs its own fixed literal to stay theme-consistent. */
.sync-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--color-success); display: inline-block;
}
.sync-dot.error { background: var(--color-danger); }

/* Small header notification for the soonest active Daily Todo (issue #56
   follow-up) — visible on every roadmap's dashboard, not just the
   onboarding page the todo list itself lives on, since todos are global.
   Reuses the same ok/warn/danger status-color families as
   daily-todo-remaining's countdown bands (src/ui/utils/dailyTodo.js
   remainingBand) rather than inventing new colors, so the urgency reads
   consistently wherever a todo countdown appears. Hidden entirely (not just
   emptied) when there is no active todo — see updateDailyTodoBadge in
   dashboard.js. */
.daily-todo-nav-badge[hidden] { display: none; }
.daily-todo-nav-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 6px 10px;
  border-radius: 0;
  text-decoration: none;
  border: 1px solid var(--color-success);
  background: var(--color-success-bg);
  color: var(--color-success);
  transition: transform 0.15s ease;
}
.daily-todo-nav-badge:hover, .daily-todo-nav-badge:focus-visible { transform: translateY(-1px); }
.daily-todo-nav-badge:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.daily-todo-nav-badge.warn { background: var(--color-warning-bg); color: var(--color-warning); border-color: var(--color-warning); }
.daily-todo-nav-badge.danger { background: var(--color-danger-bg); color: var(--color-danger); border-color: var(--color-danger); }
.daily-todo-nav-icon { display: inline-flex; flex-shrink: 0; }

/* Review-due nav badge (issue #134) — header pill showing topics due for
   spaced-repetition review, matching the .daily-todo-nav-badge pattern above.
   Hidden entirely (not just emptied) when nothing is due — see
   updateReviewDueBadge in dashboard.js. */
.review-due-nav-badge[hidden] { display: none; }
.review-due-nav-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: inherit;
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 6px 10px;
  border-radius: 0;
  cursor: pointer;
  border: 1px solid var(--color-warning);
  background: var(--color-warning-bg);
  color: var(--color-warning);
  transition: transform 0.15s ease;
}
.review-due-nav-badge:hover, .review-due-nav-badge:focus-visible { transform: translateY(-1px); }
.review-due-nav-badge:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.review-due-nav-icon { display: inline-flex; flex-shrink: 0; }

/* Roadmap header (issue #6 Phase 4.4) — identity badge + freshness meta row.
   Replaces the old hero-panel's marketing tagline entirely: a returning
   user's daily-driver dashboard doesn't need re-pitching the product on
   every visit, which is also the direction "enterprise SaaS, not marketing
   page" points (see the Phase 4 planning note in docs/architecture.md). */
.roadmap-header {
  max-width: 1160px;
  margin: 0 auto 16px;
}

.current-roadmap-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: fit-content;
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-accent-700);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-border-strong);
  border-radius: 0;
  padding: 6px 12px;
  margin-bottom: 10px;
}
/* issue #155 v2 Phase D3 (full completion pass) — dashboard.js's header badge;
   text-on-light-fill pattern. */
:root[data-theme='dark'] .current-roadmap-badge {
  color: var(--color-accent-700);
  background: var(--color-warning-bg);
  border-color: var(--color-border-strong);
}

.roadmap-meta-row {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Stat strip (issue #6 Phase 4.1) — deliberately 2 tiles, not the spec's 4:
   streak/est-time-left need persisted tracking data that doesn't exist yet
   (issue #8's job, gated on this issue). Numbers CountUp once on first
   render only (see dashboard.js's hasAnimatedStats guard) — a checkbox
   toggle updates these tiles directly with no animation replay. */
.stat-strip {
  max-width: 1160px;
  margin: 0 auto 18px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
}

/* issue #206 §3.2 — no onClick anywhere on .stat-tile (dashboard.js renders
   it as a plain div), so the old hover-lift was removed. Padding kept at
   its existing compact value rather than normalized to --space-6: this is
   a slim horizontal icon+number strip tile, not a content card, and
   --space-6 on all sides would roughly double its height for no layout
   benefit — flagging as an intentional exception to the padding audit. */
.stat-tile {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
}

.stat-tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  background: var(--color-warning-bg);
  color: var(--color-accent-700);
  flex-shrink: 0;
}
/* issue #155 v2 Phase D3 (full completion pass) — dashboard.js's own stat-strip
   (distinct component from progress.js's .kpi-tile, Phase D1); text-on-light-fill
   pattern. */
:root[data-theme='dark'] .stat-tile-icon { background: var(--color-warning-bg); color: var(--color-accent-700); }

.stat-tile-value { display: flex; align-items: baseline; gap: 4px; }
.stat-tile-number { font-size: var(--text-3xl); font-weight: 900; color: var(--color-accent-700); font-variant-numeric: tabular-nums; }
:root[data-theme='dark'] .stat-tile-number { color: var(--color-accent-700); }
.stat-tile-total { font-size: var(--text-base); color: var(--color-text-muted); font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-tile-label { display: block; font-size: var(--text-sm); color: var(--color-text-muted); font-weight: 600; }

.stat-tile-ring { justify-content: flex-start; }
.stat-tile-ring-wrap { position: relative; display: grid; place-items: center; flex-shrink: 0; }
/* issue #6 Phase 4.1 fix — was `display: grid` with no columns, which
   auto-places each child (the number, the "%" span) into its own implicit
   row instead of sitting inline, stacking "0" above "%" instead of reading
   "0%". flex keeps them on one line regardless of font-size mismatch
   between the two children. `align-items: center`, not `baseline` — this
   box (64px, sized to match the SVG ring) is much taller than the text's
   own line box, and baseline alignment only lines up items' text baselines
   with each other, it does not center that line within extra container
   height — the previous `baseline` value rendered "0%" pinned near the
   container's top-left instead of centered inside the ring (caught from a
   live screenshot, not visible in a DOM-position-only check, since the
   flex box's own bounding rect was already pixel-perfect). */
.stat-tile-ring-value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--color-accent-700);
}
:root[data-theme='dark'] .stat-tile-ring-value { color: var(--color-accent-700); }
/* percentStat (dashboard.js) reuses the shared .stat-tile-number class (32px,
   sized for the big "Items done" tile) — scope it down inside the 64px ring
   so it doesn't overflow/wrap. */
.stat-tile-ring-value .stat-tile-number { font-size: var(--text-lg); }
.stat-tile-ring-value span:not(.stat-tile-number) { font-size: var(--text-xs); margin-left: 1px; }

/* issue #155 v2 Phase B — a distinct new component, `.kpi-tile`, not a `.stat-tile`
   variant despite the issue's own bullet naming it "`.stat-tile`": `.stat-tile` above
   is already shipped (issue #6 Phase 4.1) as a horizontal icon-left/number-right row
   used by the dashboard's stat strip and progress ring — reusing that class name for
   the reference's structurally different vertical KPI card (label row with a corner
   badge, then a large number, then a delta caption) would collide with an existing,
   in-production component rather than extend it. Built and visually verified in
   isolation this phase — no page wires it in yet, that's Phase C/D's job. Composes
   with the shared `.card-arrow-badge` corner affordance below (also used by Phase D's
   planned person/customer cards, per the issue's spec). */
/* issue #300 — design-system.md §4: "Elevation only for overlays... Flat
   surfaces otherwise" (dropped the --shadow-sm this flat stat cell had). */
.kpi-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: var(--space-6);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}
.kpi-tile-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
/* issue #300 — spec's "uppercase kickers" for the progress stat row. */
.kpi-tile-label { font-size: var(--text-xs); font-weight: 700; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: var(--tracking-caps, 0.08em); }
.kpi-tile-number { display: flex; align-items: baseline; gap: 4px; font-size: var(--text-3xl); font-weight: 900; color: var(--color-text); font-variant-numeric: tabular-nums; }
/* issue #206 §6 — de-emphasized "still at zero" state: smaller, muted, less bold than
   a real earned number, so a fresh roadmap's stat strip doesn't read as a wall of loud
   "0"s. Paired with an action-oriented .kpi-tile-delta caption at each call site. */
.kpi-tile-number-zero { font-size: var(--text-xl); font-weight: var(--font-weight-semibold); color: var(--color-text-muted); }
.kpi-tile-delta { font-size: var(--text-xs); color: var(--color-text-muted); font-weight: 600; }
/* issue #155 v2 Phase D1 — progress.js's stat strip: a smaller inline caption after
   the number (e.g. "/ 200", "days"), reusing .kpi-tile-number's flex row via a
   trailing span rather than adding a second wrapper element. .kpi-tile-bar positions
   the optional mini progress bar (Items complete tile) below the number, mirroring
   the spacing .stat-tile-bar already used for the same visual role. */
.kpi-tile-total { font-size: var(--text-base); color: var(--color-text-muted); font-weight: 700; font-variant-numeric: tabular-nums; }
.kpi-tile-bar { margin-top: 2px; }

/* The reference's "exactly one hero-highlighted tile per screen" pattern (issue #155 v2
   redefinition comment) — a single solid-filled tile among otherwise-dark siblings. The
   old fallback pattern (var(--color-brand-gold, var(--color-brand-gold))) was already
   dead code by the time #297 touched this file — both arguments resolved identically,
   a leftover from the lime/gold token collapse during #206 — so it's simplified to a
   single --color-accent read rather than carrying the redundant fallback forward. */
/* issue #300 — real, live contrast bug found auditing this component post-#297:
   --color-text on the raw --color-accent fill measures 3.95:1 light / 2.75:1
   dark (dark theme fails even the large-text 3:1 floor) — the old lime/mint
   accent this was tuned against passed at 5-17:1 in the same role, and the
   axe CONTRAST_FALSE_POSITIVE_SELECTORS exemption for .kpi-tile-label/
   .kpi-tile-hero (ui-styling.md) predates the #297 token swap, so CI's a11y
   suite was never going to re-flag this. Fixed the same way .btn-primary was
   in Phase 1: fill reads the darkened --color-accent-600, text reads
   --color-ink-on-accent (4.94:1 light / 6.69:1 dark, verified) — the
   .kpi-tile-delta opacity dim is also dropped (0.75 opacity dropped it to
   3.33:1 light, still failing) rather than layering a second contrast risk
   on top of the fill/text fix. */
.kpi-tile-hero {
  background: var(--color-accent-600);
  border-color: var(--color-accent-600);
}
.kpi-tile-hero .kpi-tile-label,
.kpi-tile-hero .kpi-tile-number,
.kpi-tile-hero .kpi-tile-delta,
.kpi-tile-hero .kpi-tile-total { color: var(--color-ink-on-accent); }

/* Shared circular corner-badge affordance (issue #155 v2 Phase B) — used by `.kpi-tile`
   above and, per the issue's Phase D scope, planned for person/customer-style cards
   too. Parent needs `position: relative` (`.kpi-tile` already has it). */
.card-arrow-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text);
  flex-shrink: 0;
}
/* intentional: a fixed black-tint overlay reads as a subtle darkening on top of
   whichever solid accent color fills `.kpi-tile-hero` (lime in dark theme, brand teal
   in light theme via the fallback above) — using a token here would fight whichever
   accent is actually active, since neither --color-surface-raised nor --color-border is designed to sit on
   top of a bright solid fill (issue #155 v2 Phase B). */
.kpi-tile-hero .card-arrow-badge {
  background: rgba(0, 0, 0, 0.14);
  border-color: rgba(0, 0, 0, 0.14);
  color: var(--color-text);
}

.toolbar {
  max-width: 1160px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px;
  align-items: end;
}

.toolbar-block { display: grid; gap: 8px; min-width: 0; }
.toolbar-block.toolbar-right { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.toolbar-label { font-size: var(--text-xs); font-weight: 800; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.08em; }

.filter-row { display: flex; flex-wrap: wrap; gap: 8px; }

/* Issue #182 — pattern/concept tag filter chips + the review-due tag-grouping
   summary line; reuses .filter-chip/.filter-row as-is (see above), just two
   small container-level rules for the pieces that are new to this issue. */
.tag-filter-row[hidden] { display: none; }
.review-tag-group-banner[hidden] { display: none; }
.review-tag-group-banner { display: grid; gap: 4px; margin-top: 8px; }
.review-tag-group-line {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 6px 10px;
}

.filter-chip {
  position: relative;
  min-height: 36px;
  padding: 7px 12px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 800;
  cursor: pointer;
  overflow: hidden;
  transition: transform var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
}
.filter-chip:hover { transform: translateY(-1px); border-color: var(--color-border-strong); }
.filter-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.filter-chip.active { background: var(--color-text); border-color: var(--color-text); color: var(--color-bg); }
/* issue #206 §3.6 — same tinted-pill treatment as .badge.P0-P3 above, so the
   two priority-indicator patterns in the app read as one consistent system. */
.filter-chip[data-p="P0"].active { background: color-mix(in srgb, var(--color-p0) 15%, transparent); border-color: var(--color-p0); color: var(--color-p0-ink); }
.filter-chip[data-p="P1"].active { background: color-mix(in srgb, var(--color-p1) 15%, transparent); border-color: var(--color-p1); color: var(--color-p1-ink); }
.filter-chip[data-p="P2"].active { background: color-mix(in srgb, var(--color-p2) 15%, transparent); border-color: var(--color-p2); color: var(--color-p2-ink); }
.filter-chip[data-p="P3"].active { background: color-mix(in srgb, var(--color-p3) 15%, transparent); border-color: var(--color-p3); color: var(--color-p3-ink); }
.chip-count { opacity: 0.75; font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

/* Issue #6 Phase 4.3 — subtle hover shimmer: a diagonal highlight band swept
   in via background-position on hover, not a separate ::after animation, so
   it inherits the chip's own border-radius/overflow-hidden for free. */
.filter-chip::before {
  content: '';
  position: absolute;
  inset: 0;
  /* intentional: fixed white shimmer highlight, same in both themes (issue #116) */
  background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.35) 50%, transparent 60%);
  background-size: 220% 100%;
  background-position: 150% 0;
  transition: background-position var(--duration-slow) var(--ease-out);
  pointer-events: none;
}
.filter-chip:hover::before { background-position: -50% 0; }

.filter-chip-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 4px;
  opacity: 0.75;
  cursor: pointer;
}
.filter-chip-clear:hover { opacity: 1; }

/* issue #155 v2 Phase B — `.filter-chip-counted`, an embedded count sub-badge riding on
   top of the existing `.filter-chip` base (the reference's "Pending 20"/"Responded
   22"/"Assigned 16" status-chip row) — a genuinely new compound component per the
   issue's spec, not a retune: no existing chip class supported a second, visually
   distinct count badge inside itself before this. Apply alongside the base class
   (`class="filter-chip filter-chip-counted"`), not as a replacement — `.filter-chip`
   still owns the pill shape/hover/active-state mechanics, this only adds the badge
   slot. Built and visually verified in isolation this phase; no page wires it in yet. */
.filter-chip-counted { display: inline-flex; align-items: center; gap: 8px; }
.filter-chip-count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-full);
  background: var(--color-surface-raised);
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
/* Inverts against whichever fill `.filter-chip.active` (or a future accent-colored
   variant of it) ends up using, the same way the base chip's own active text/bg swap
   works — so the badge reads as "a dark circle on top of a bright fill" regardless of
   which specific accent colors the active chip, without hardcoding a color here. */
.filter-chip-counted.active .filter-chip-count-badge {
  background: var(--color-bg);
  color: var(--color-text);
}

.clear-filters-btn[hidden] { display: none; }

.search-input {
  width: min(340px, 100%);
  min-width: 200px;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-base);
  transition: width var(--duration-base) var(--ease-out), border-color var(--duration-fast) ease;
}
/* Issue #6 Phase 4.3 — grows toward (not literally to) full toolbar width on
   focus: .toolbar-right also holds the Expand/Collapse-all button, so an
   unbounded 100% growth would shove that button out of the row. */
.search-input:focus { outline: none; border-color: var(--color-accent); width: min(520px, 100%); }
.search-input:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* issue #155 v2 Phase D3 (full completion pass) — dashboard.js's search field. */
:root[data-theme='dark'] .search-input:focus { border-color: var(--color-accent); }

/* Daily Todos (issue #56) — a self-contained card, separate from the phase
   list, sourced from its own store (dailyTodoStore.js), not roadmapStore.
   Same --color-surface/--radius-lg/--shadow-sm base every other card in this
   pass uses (issue #206 §3.2 — glass chrome dropped, see .template-card's
   comment above for the same change), but with an --color-warning (not
   --color-brand-gold) border/icon color — --color-warning is already used
   app-wide as the "not the roadmap-teal" token (progress bar highlight), so
   reusing it here (rather than inventing a new color) is what makes it read
   as "a different kind of card" in both themes without a special case.
   Distinct from --color-brand-rose, reserved specifically for AI-related
   affordances. No hover-lift: this panel isn't itself clickable, only the
   todo rows inside it are. */
/* issue #298 — design-system.md §4: "Elevation only for overlays... Flat
   surfaces otherwise" (dropped the --shadow-sm this flat card had) and a 2px
   rule for a major section boundary (was a 1px colored border). */
.daily-todo-panel {
  max-width: 1160px;
  margin: 0 auto 18px;
  width: 100%;
  box-sizing: border-box;
  background: var(--color-surface);
  border: 2px solid var(--color-divider);
  padding: var(--space-6);
}
/* On the onboarding "all roadmaps" page, .onboarding-inner already caps
   width at 880px and every sibling (header, template-grid) sits flush
   against it with no extra inset of its own. */
.onboarding-inner > .daily-todo-panel {
  max-width: none;
  margin: 0;
}
.daily-todo-heading-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
}
.daily-todo-icon { display: inline-flex; flex-shrink: 0; color: var(--color-warning); }
.daily-todo-heading {
  margin: 0 auto 0 0;
  font-size: var(--text-base);
  font-weight: 800;
}
/* issue #136 Phase 2 follow-up — was 20x20 with an 'xs' (16px) icon, sitting
   directly next to .daily-todo-collapse-btn's 24x24 box with an 'sm' (20px)
   icon; two different button diameters and two different icon sizes side by
   side read as visibly inconsistent/misaligned (reported live, screenshot).
   Matches .daily-todo-collapse-btn's box + icon size exactly now.
   A second live report (screenshot) flagged the reminder/info/collapse trio
   as still visibly misaligned — a real `getBoundingClientRect()` check found
   all three already pixel-identical (same top/height, same 24px box, same
   'sm' icon size) in this codebase, so the box-model dimensions above were
   not the cause here. `line-height: 0` was added to all three (below) as a
   defensive hardening against the sub-pixel/baseline drift an inline-flex
   button with no text content can pick up from an inherited non-zero
   line-height at some zoom levels/OS display-scaling factors — harmless if
   that wasn't the actual cause, and removes one more variable if a fresh
   report ever reproduces this again. */
.daily-todo-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  line-height: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.daily-todo-info-btn:hover { color: var(--color-warning); border-color: var(--color-warning); }
.daily-todo-info-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Local reminder opt-in toggle (issue #132) — same 24x24/'sm'-icon box as
   .daily-todo-info-btn/.daily-todo-collapse-btn above (see the "Two same-row
   circular icon-buttons" rule). .active marks reminders as currently on. */
.daily-todo-reminder-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  line-height: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.daily-todo-reminder-btn:hover { color: var(--color-warning); border-color: var(--color-warning); }
.daily-todo-reminder-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.daily-todo-reminder-btn.active { color: var(--color-warning); border-color: var(--color-warning); }

/* Collapse/expand toggle (issue #83) — reuses the exact same chevron-rotate
   language as .phase-card/.chevron (dashboard.js's phase cards) so the two
   "expandable section" affordances in the app read consistently: pointing
   right (0deg) when collapsed, rotated to point down (90deg) when expanded.
   Unlike .phase-card, the *expanded* state is the rotated one here since
   that's this panel's default/common state. */
.daily-todo-collapse-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  line-height: 0;
  border-radius: 50%;
  border: 1px solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.daily-todo-collapse-btn:hover { color: var(--color-warning); border-color: var(--color-warning); }
.daily-todo-collapse-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.daily-todo-panel:not(.collapsed) .daily-todo-collapse-btn .chevron { transform: rotate(90deg); }

/* Only shown while collapsed (below) — a small "N active" reminder so
   shrinking the panel never fully hides whether there's anything to come
   back for. */
.daily-todo-count-badge {
  display: none;
  flex-shrink: 0;
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--color-text-muted);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: 0;
  padding: 3px 9px;
  white-space: nowrap;
}

/* Inner rows no longer stack as their own bordered boxes — they sit flush
   inside .daily-todo-panel's own card padding, divided by hairlines, so the
   whole thing reads as one card rather than a heading floating above a
   separate stack of boxes. */
.daily-todo-panel > .daily-todo-add-row,
.daily-todo-panel > .daily-todo-list,
.daily-todo-panel > .daily-todo-missed-toggle,
.daily-todo-panel > .daily-todo-missed-list {
  background: transparent;
  border: none;
}
/* Collapsed state (issue #83) — shrinks the panel down to just its heading
   row so a growing todo list doesn't push the roadmap card grid further
   down the page each time one is added. Never the first-run default — see
   the KEYS.DAILY_TODOS_COLLAPSED comment in localStorageKeys.js and the
   `collapsed` initialization in dailyTodoPanel.js. */
.daily-todo-panel.collapsed > .daily-todo-add-row,
.daily-todo-panel.collapsed > .daily-todo-list,
.daily-todo-panel.collapsed > .daily-todo-missed-toggle,
.daily-todo-panel.collapsed > .daily-todo-missed-list {
  display: none;
}
.daily-todo-panel.collapsed .daily-todo-heading-row { margin-bottom: 0; }
.daily-todo-panel.collapsed .daily-todo-count-badge { display: inline-flex; }
/* Real, reported mismatch (issue #309, same shape as .app-topbar-actions'
   fix above): the "Add a todo" `.field-input.compact` (39px) and its "Add"
   `.btn-sm` (35px) sat 4px apart in the same row. */
.daily-todo-add-row > .field-input,
.daily-todo-add-row > .btn {
  height: 39px;
  box-sizing: border-box;
}
.daily-todo-add-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0 0 14px;
  border-radius: 0;
  border-bottom: 1px solid var(--color-border);
}
.daily-todo-add-row .inline-add { flex: 1; min-width: 160px; }
.todo-duration-select { min-width: 110px; }
.todo-custom-hours { width: 90px; }

.daily-todo-list {
  display: grid;
  border-top: none;
}
.daily-todo-empty {
  margin: 0;
  padding: 14px 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

.daily-todo-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--color-border);
}
.daily-todo-item.done .daily-todo-title { text-decoration: line-through; color: var(--color-text-muted); }

/* flex: 1 so the title column absorbs all the row's slack width — without
   it, .daily-todo-remaining (below) had no fixed anchor and its horizontal
   position drifted per-row depending on how long the title text was. */
.daily-todo-checkbox {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  flex: 1;
  min-width: 0;
}
.daily-todo-checkbox input[type='checkbox'] { width: 18px; height: 18px; accent-color: var(--color-warning); flex-shrink: 0; }
.daily-todo-title { font-size: var(--text-base); font-weight: 600; overflow-wrap: anywhere; }
.daily-todo-linked-badge {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 700;
  color: var(--color-accent-700);
  background: var(--color-warning-bg);
  border-radius: 0;
  padding: 2px 8px;
  white-space: nowrap;
}
/* issue #155 v2 Phase D3 (full completion pass) — dailyTodoPanel.js; text-on-light-fill
   pattern. */
:root[data-theme='dark'] .daily-todo-linked-badge { color: var(--color-accent-700); background: var(--color-warning-bg); }

/* Fixed width + right-aligned, always rendered (see renderRow in
   dailyTodoPanel.js) — every row's status text now lines up flush against
   the same edge regardless of title length or done state, instead of
   floating at whatever position `justify-content: space-between` happened
   to leave it. */
.daily-todo-remaining {
  flex-shrink: 0;
  min-width: 84px;
  text-align: right;
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.daily-todo-remaining.ok { color: var(--color-accent-700); }
:root[data-theme='dark'] .daily-todo-remaining.ok { color: var(--color-accent-700); }
.daily-todo-remaining.warn { color: var(--color-p1-ink); }
.daily-todo-remaining.danger { color: var(--color-p0); }
.daily-todo-remaining.done { color: var(--color-text-muted); font-weight: 700; }

.daily-todo-delete {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 0;
}
.daily-todo-delete:hover { color: var(--color-danger); background: var(--color-danger-bg); }
.daily-todo-delete:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.daily-todo-missed-toggle {
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 0;
  border-top: 1px solid var(--color-border);
  border-radius: 0;
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
}
.daily-todo-missed-list {
  border-top: none;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.daily-todo-missed-list .daily-todo-item { opacity: 0.75; }
.daily-todo-missed-list .daily-todo-title { text-decoration: line-through; }

.dashboard-content {
  flex: 1;
  max-width: 1160px;
  margin: 0 auto;
  width: 100%;
  padding: 22px 20px 80px;
  display: grid;
  gap: 12px;
  align-content: start;
}

/* Phase cards. issue #206 §3.2 — .phase-card itself has no click handler
   (only its nested .phase-head button toggles the accordion, dashboard.js);
   removed the old whole-card hover-lift accordingly — a card that isn't
   itself clickable shouldn't imply it is by lifting under the cursor
   anywhere over its body content. Flagging: this changes existing behavior
   (the card used to lift on hover regardless of where inside it the cursor
   was), unlike .template-card above, which keeps its whole-card hover-lift
   because clicking anywhere on that card *does* trigger the pick action via
   its nested button covering the full card. */
.phase-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left-width: 3px;
  border-radius: var(--radius-lg);
  overflow: hidden;
}

/* Issue #6 Phase 4.2 — left-border priority accent, reusing the same
   --color-p0..--color-p3 tokens .badge/.priority-tag already use, so a phase card's
   priority reads consistently with every other priority-colored element. */
.phase-card[data-priority="P0"] { border-left-color: var(--color-p0); }
.phase-card[data-priority="P1"] { border-left-color: var(--color-p1); }
.phase-card[data-priority="P2"] { border-left-color: var(--color-p2); }
.phase-card[data-priority="P3"] { border-left-color: var(--color-p3); }

.phase-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 15px 16px;
  border: 0;
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 140ms ease;
}
.phase-head:hover { background: var(--color-surface-raised); }
.phase-head:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.phase-index { font-family: var(--font-mono); font-size: 12px; font-weight: 700; color: var(--color-text-muted); min-width: 28px; font-variant-numeric: tabular-nums; }
/* issue #6 Phase 4.2 — display font for the "cleaner head layout" polish. */
.phase-name { flex: 1; font-family: var(--font-display); font-weight: 700; font-size: var(--text-base); }
/* Ring (createProgressRing, src/ui/components/progressRing.js) is now the
   visible progress affordance in the phase head; this stays in the DOM as
   an sr-only label so assistive tech (and the existing unit test asserting
   on its text) keep working unchanged. */
.phase-progress.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; }
.progress-ring { flex-shrink: 0; }
.chevron { color: var(--color-text-muted); transition: transform var(--duration-base) var(--ease-in-out); }
.phase-card.open .chevron { transform: rotate(90deg); }

.phase-body { display: none; border-top: 1px solid var(--color-border); }
/* issue #6 Phase 7 — the "body-in" fade used to run here on every open, via
   `display: none -> block`, which can't itself be transitioned. A full
   re-render (rebuilding every phase-card, not just the toggled one) used to
   trigger it unintentionally on structural changes too. Expand/collapse now
   plays a true FLIP height animation instead, driven by animatePhaseBody()
   (dashboard.js) via Element.animate() — this rule only sets the resting
   `display: block` state for an already-open card on a full re-render (no
   animation replay), the same way it always has. */
.phase-card.open .phase-body { display: block; }

/* issue #6 Phase 4.2 — sticky while scrolling through a long open phase.
   --topbar-h keeps the offset in lockstep with .app-topbar's own height
   (defined near .app-topbar) so a stuck section header never sits half
   hidden underneath it. */
.section-label {
  position: sticky;
  top: var(--topbar-h);
  z-index: 5;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  padding: 12px 16px 6px;
  background: var(--color-surface-raised);
}
/* `.phase-body-animating` (animatePhaseBody(), dashboard.js) is applied only
   while `.phase-body` carries an inline `overflow: hidden` for its FLIP
   height animation. That `overflow: hidden` makes `.phase-body` a scroll
   container, which per the Position spec becomes the sticky positioning
   context for every `.section-label` inside it (normally the page is) —
   causing labels to jump/overlap against the still-animating 0→target
   height for the animation's duration, a real reported bug. Dropping to
   `position: static` for that window removes the sticky recalculation
   entirely; the class is removed (and sticky restored) the instant the
   animation's `onfinish` clears the inline `overflow`. */
.phase-body-animating .section-label { position: static; }

/* Custom roadmap phase/section CRUD (issue #4). Column layout with the
   .field-hint caption on its own line (issue #121 item 4) — without it, the
   rename input directly under the phase/section header reads as an
   unlabeled duplicate title rather than an obviously-a-rename-field
   control. */
.phase-manage-row, .section-manage-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 10px 16px;
  background: var(--color-surface-raised);
  border-bottom: 1px dashed var(--color-border);
}
.phase-manage-row .field-hint, .section-manage-row .field-hint { margin-top: 0; }
.phase-manage-row-controls, .section-manage-row-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}
.phase-manage-row .field-input, .section-manage-row .field-input { flex: 1; }

/* issue #206 §3.6 — pill badge, tinted (not flat-filled): background at ~15%
   opacity of the matching --color-p0..--color-p3 token via color-mix()
   (this file's established opacity pattern), text at that token's full
   opacity. No separate dark-theme text-color override needed any more —
   unlike the old flat-fill-plus-white-text version, a token-tinted
   background is contrast-safe against its own full-opacity text by
   construction in both themes. */
.badge {
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 800;
}
.badge.P0 { background: color-mix(in srgb, var(--color-p0) 15%, transparent); color: var(--color-p0-ink); }
.badge.P1 { background: color-mix(in srgb, var(--color-p1) 15%, transparent); color: var(--color-p1-ink); }
.badge.P2 { background: color-mix(in srgb, var(--color-p2) 15%, transparent); color: var(--color-p2-ink); }
.badge.P3 { background: color-mix(in srgb, var(--color-p3) 15%, transparent); color: var(--color-p3-ink); }

/* Checklist items */
.check-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--color-border);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard);
}
.check-item:last-of-type { border-bottom: none; }
.check-item:hover { background: var(--color-surface-raised); }
.check-item:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Issue #6 Phase 4.2 — stagger-in entrance for a newly-added topic row (see
   dashboard.js's knownItemIds id-diff). A capped set of discrete
   entering-delay-N classes, not an inline `style="animation-delay:…"` —
   index.html's CSP has no 'unsafe-inline' in style-src, so the browser
   silently drops any inline style attribute app-wide (confirmed live: it
   produced a CSP console violation per row and no visible stagger at all).
   Any future per-row animation delay must follow this same discrete-class
   pattern instead of reaching for an inline style. */
@keyframes item-entering {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}
.check-item.entering { animation: item-entering var(--duration-base) var(--ease-out) both; }
.check-item.entering-delay-0 { animation-delay: 0ms; }
.check-item.entering-delay-1 { animation-delay: calc(var(--stagger-base) * 1); }
.check-item.entering-delay-2 { animation-delay: calc(var(--stagger-base) * 2); }
.check-item.entering-delay-3 { animation-delay: calc(var(--stagger-base) * 3); }
.check-item.entering-delay-4 { animation-delay: calc(var(--stagger-base) * 4); }
.check-item.entering-delay-5 { animation-delay: calc(var(--stagger-base) * 5); }
.check-item.entering-delay-6 { animation-delay: calc(var(--stagger-base) * 6); }
@media (prefers-reduced-motion: reduce) {
  .check-item.entering { animation: none; }
}

/* Same item-entering keyframes/stagger token reused for the "Create your own
   roadmap" modal's numbered step headings (issue #100 revamp) — a short,
   staggered fade-in so the six steps read as a guided sequence instead of
   all snapping in at once. */
.import-step-heading.entering { animation: item-entering var(--duration-base) var(--ease-out) both; }
.import-step-heading.entering-delay-0 { animation-delay: 0ms; }
.import-step-heading.entering-delay-1 { animation-delay: calc(var(--stagger-base) * 1); }
.import-step-heading.entering-delay-2 { animation-delay: calc(var(--stagger-base) * 2); }
.import-step-heading.entering-delay-3 { animation-delay: calc(var(--stagger-base) * 3); }
.import-step-heading.entering-delay-4 { animation-delay: calc(var(--stagger-base) * 4); }
.import-step-heading.entering-delay-5 { animation-delay: calc(var(--stagger-base) * 5); }
.import-step-heading.entering-delay-6 { animation-delay: calc(var(--stagger-base) * 6); }
@media (prefers-reduced-motion: reduce) {
  .import-step-heading.entering { animation: none; }
}

/* Phase/roadmap completion confetti flourish (issue #181, confetti.js's
   triggerConfetti()) — fixed full-viewport overlay, pointer-events: none so
   it never blocks the underlying UI, self-removed by confetti.js after the
   animation ends. issue #297 — design-system.md §7 calls for "squares in
   the accent ramp colors (no circles, no gold)": border-radius zeroed
   (was 2px, a holdover from the old rounded-corner scale) and every piece
   now reads directly from the --color-accent-* ramp (was a mix of
   --color-accent/--color-warning/--color-brand-rose — the last of those a
   dangling reference to a deleted v3 token that would have resolved to
   nothing) so the 6-piece burst reads as one accent family at different
   steps, not an arbitrary color grab-bag. */
.confetti-burst {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 300;
  overflow: hidden;
}
.confetti-piece {
  position: absolute;
  top: -12px;
  left: calc(var(--confetti-x, 50%));
  width: 8px;
  height: 14px;
  border-radius: 0;
  animation: confetti-fall 1.4s var(--ease-out) both;
}
.confetti-piece-0 { background: var(--color-accent); left: 8%; }
.confetti-piece-1 { background: var(--color-accent-600); left: 24%; }
.confetti-piece-2 { background: var(--color-accent-700); left: 40%; }
.confetti-piece-3 { background: var(--color-accent); left: 56%; }
.confetti-piece-4 { background: var(--color-accent-800); left: 72%; }
.confetti-piece-5 { background: var(--color-accent-600); left: 88%; }
.confetti-piece-delay-0 { animation-delay: 0ms; }
.confetti-piece-delay-1 { animation-delay: calc(var(--stagger-base) * 1); }
.confetti-piece-delay-2 { animation-delay: calc(var(--stagger-base) * 2); }
.confetti-piece-delay-3 { animation-delay: calc(var(--stagger-base) * 3); }
.confetti-piece-delay-4 { animation-delay: calc(var(--stagger-base) * 4); }
.confetti-piece-delay-5 { animation-delay: calc(var(--stagger-base) * 5); }
.confetti-piece-delay-6 { animation-delay: calc(var(--stagger-base) * 6); }
@keyframes confetti-fall {
  from { opacity: 1; transform: translateY(0) rotate(0deg); }
  to { opacity: 0; transform: translateY(100vh) rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .confetti-piece { animation: none; display: none; }
}

/* issue #206 §3.3 — checkbox radius standardized to --radius-sm; checked
   state now fills --color-success (was --color-brand-gold), which reads as
   dark-enough in both themes to safely host a fixed white checkmark with no
   per-theme text-color override needed (unlike the old lime-based dark
   theme, which needed near-black text — that override is removed below).
   The scale micro-animation itself (§5) shipped separately, below, as
   `.check-pop` — kept apart from `.check-box`'s own base rule since it's a
   one-shot class `toggleDone()` (dashboard.js) adds/removes around the
   click, not a permanent style. */
.check-box {
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: var(--color-surface);
  transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
}
/* A real, reported gap (issue #309): `.check-box` is a genuinely focusable
   `role="checkbox"` element (`tabindex="0"`, `dashboard.js`) — every checklist
   row's checkbox had no visible keyboard-focus indicator at all. Same 2px
   accent outline every other interactive element in the app already uses. */
.check-box:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* issue #206 §5 follow-up — real feedback (screenshots, twice): the checked
   fill kept reading as an out-of-family green no matter how much the
   `--color-success` token itself was warmed, since it's still a genuinely
   different hue from the app's gold/rose palette. Dropped the semantic
   "success ≠ brand" distinction for this one highest-frequency surface and
   switched the checked fill to solid `--color-brand-gold` instead — the
   same accent every other "this is selected/complete/active" indicator in
   the app already uses (the active-roadmap badge, the sidebar's active-nav
   bar, `.progress-ring-fill`). `--color-success`/`-bg` tokens are
   unchanged and still used elsewhere (e.g. toast-success's accent bar) —
   only `.check-box.done`'s fill moved off them. `--color-ink-on-gold`
   (not fixed white) for the checkmark, since white-on-gold is the exact
   "fails badly in dark theme" bug `--color-ink-on-gold`'s own token
   comment already documents. */
.check-mark {
  display: inline-flex;
  color: var(--color-ink-on-accent);
  transform: scale(0);
  opacity: 0;
  /* issue #6 Phase 4.2 — spring bounce on toggle; pre-existing, kept as-is
     (not a new §5/PR2 motion addition). */
  transition: transform var(--duration-base) var(--ease-bounce), opacity 120ms ease;
}
.check-item.done .check-box {
  background: var(--color-accent);
  border-color: transparent;
}
.check-item.done .check-mark { transform: scale(1); opacity: 1; }
/* issue #298 — design-system.md §5: "done row text = line-through at 50% opacity".
   Was only a color-mute (--color-text-muted, a 64% mix per issue #297's contrast
   fix) with no line-through at all. */
.check-item.done .check-title { text-decoration: line-through; opacity: 0.5; }

/* issue #206 §5 — checkbox check micro-animation: a scale(1)->scale(1.15)->
   scale(1) pop on the checkbox itself, layered on top of (not replacing) the
   check-mark's own existing spring-bounce fade-in above. `--duration-fast`
   keeps the whole pop comfortably under the spec's 200ms ceiling — the
   app's highest-frequency interaction must never read as laggy under rapid
   repeated clicking. Class lifecycle (add, forced reflow, remove-on-
   `animationend`) lives in `toggleDone()`, dashboard.js — see that
   function's own comment for why a one-shot JS-toggled class was needed
   instead of a plain CSS transition here. */
@keyframes check-pop {
  0% { transform: scale(1); }
  50% { transform: scale(1.15); }
  100% { transform: scale(1); }
}
.check-box.check-pop { animation: check-pop var(--duration-fast) var(--ease-standard); }
@media (prefers-reduced-motion: reduce) {
  .check-box.check-pop { animation: none; }
}

.check-body { flex: 1; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; }
/* issue #6 Phase 7 — strike-through animates in via `::after` width 0% -> 100%
   instead of an instant `text-decoration: line-through`, per the animation
   inventory. `position: relative` + `display: inline-block` so the pseudo-element
   sizes itself to the title's own rendered width, not the flex row's. */
.check-title { position: relative; display: inline-block; font-size: var(--text-base); font-weight: 600; }
.check-title::after {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  height: 1px;
  width: 0%;
  background: currentColor;
  transition: width var(--duration-base) var(--ease-out);
}
.check-item.done .check-title::after { width: 100%; }

/* issue #136 Phase 3 — shares --chip-height with every boxed badge/pill in
   the same row (.resource-count, .badge) so they align vertically as one
   consistent row of "metadata chip" heights, even though .priority-tag
   deliberately stays bare colored text (no background/padding/radius) —
   adding a tinted background here would need its own WCAG contrast pass
   against every --color-p0–--color-p3 pair in both themes, which is out of scope for a
   pure box-model unification. */
.priority-tag {
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 800;
  opacity: 0.65;
}
.priority-tag.P0 { color: var(--color-p0-ink); opacity: 1; }
.priority-tag.P1 { color: var(--color-p1-ink); opacity: 1; }
.priority-tag.P2 { color: var(--color-p2-ink); opacity: 1; }
.priority-tag.P3 { color: var(--color-p3-ink); opacity: 1; }

/* issue #136 Phase 1 — recolored from --color-warning-bg/--color-brand-gold to a
   neutral pill: a resource count is metadata, not a status, and doesn't need
   its own hue — it was previously drawing from the exact same teal family as
   "this is selected"/"this is the brand," with nothing to tell them apart. */
.resource-count {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  font: inherit;
  border: 1px solid var(--color-border);
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--color-text-muted);
  background: var(--color-surface-raised);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
}
.resource-count:hover { transform: translateY(-1px); }
.resource-count:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Issue #100 follow-up — expanded, always-visible resource links shown on a
   checklist row when the "Resources" filter chip is active (dashboard.js's
   renderInlineResources()), alongside the existing collapsed count badge.
   `flex-basis: 100%` on the wrapper drops it onto its own line within
   .check-body's existing `flex-wrap: wrap` row — no structural change to
   .check-body needed, the same wrap behavior that already lets .priority-tag
   /.resource-count reflow on narrow widths handles this for free. */
.check-resources-inline { display: flex; flex-wrap: wrap; gap: 6px; flex-basis: 100%; margin-top: 2px; }
.resource-inline-link { text-decoration: none; cursor: pointer; transition: transform 140ms ease, border-color 140ms ease; }
.resource-inline-link:hover { transform: translateY(-1px); }
.resource-inline-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* issue #206 §3.3 — ghost-style chip, matching .resource-count's treatment
   (transparent bg, muted text, 1px border) instead of the old borderless
   icon-only button, so both metadata badges in a row read as one family. */
.notes-indicator {
  display: inline-flex;
  align-items: center;
  height: var(--chip-height);
  padding: 0 var(--chip-padding-x);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font: inherit;
  background: transparent;
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard);
}
.notes-indicator:hover { transform: translateY(-1px); border-color: var(--color-border-strong); }
.notes-indicator:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Set when a topic was last marked done via a linked Today's Todo (issue
   #56 follow-up) — cleared the moment the topic (or the linked todo) is
   unchecked again, so this never shows a stale completion date. Not
   interactive (no onClick) — the title/aria-label attribute alone carries
   the date, same as a native tooltip. */
.completed-via-todo-indicator {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  color: var(--color-warning);
  cursor: default;
}

.check-actions { opacity: 0; transition: opacity 140ms ease; }
.check-item:hover .check-actions, .check-item:focus-within .check-actions { opacity: 1; }
/* A touch/coarse-pointer device (or one with no hover at all) has no reliable
   :hover — width was previously used as a stand-in for this and broke on
   touchscreen laptops/Surfaces/iPads at desktop-sized viewports (issue #36). */
@media (hover: none), (pointer: coarse) {
  .check-actions { opacity: 1; }
}

.add-row {
  display: flex;
  gap: 8px;
  padding: 10px 16px 14px;
  background: var(--color-surface-raised);
  border-top: 1px dashed var(--color-border);
}
.inline-add { flex: 1; }

/* "+ Add phase" (issue #65 follow-up) renders as a direct sibling of
   .phase-card in .dashboard-content, not nested inside one — unlike
   "+ Add section"/"Add a custom topic…", which already sit inside a fully
   boxed .phase-body. Without its own framing it read as a bare flat strip
   next to the bordered/shadowed phase-cards below it. Matches .phase-card's
   box but keeps a dashed border throughout (rather than just the top edge)
   as the same "this creates something new" cue used by
   .template-card-create/.template-card-hidden. */
.add-row-standalone {
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: 12px 16px;
}

/* Item panel */
/* intentional: theme-agnostic dark scrim, same pattern as .modal-overlay above
   (issue #116; issue #206 §3.5 — matched to .modal-overlay's rgba(0,0,0,0.5),
   just kept at a lower alpha since this one also has a backdrop-filter blur
   doing part of the visual separation work). */
.panel-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(4px);
  z-index: 200;
  opacity: 0;
  transition: opacity 220ms ease;
}
.panel-overlay.show { opacity: 1; }

.item-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: min(480px, 100%);
  height: 100%;
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
  z-index: 201;
  display: flex;
  flex-direction: column;
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  transform: translateX(100%);
  transition: transform 280ms cubic-bezier(.2,.9,.3,1);
}
.item-panel.show { transform: translateX(0); }

.panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 20px 12px;
  border-bottom: 1px solid var(--color-border);
}
.panel-kicker { font-size: 12px; color: var(--color-text-muted); font-weight: 700; }
.panel-title { font-size: 22px; font-weight: 900; margin-top: 4px; }
.panel-body { flex: 1; overflow: auto; padding: 20px; display: grid; gap: 16px; align-content: start; }
.panel-footer {
  padding: 16px 20px;
  border-top: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.panel-footer-right { display: flex; gap: 8px; margin-left: auto; }

/* What's New changelog drawer (issue #20) — reuses .item-panel/.panel-* for
   the slide-in shell; everything below is specific to its content. */
.changelog-drawer-body { gap: 28px; }
.changelog-group + .changelog-group { padding-top: 20px; border-top: 1px solid var(--color-border); }
.changelog-group-header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.changelog-group-version { font-size: var(--text-base); font-weight: 800; }
.changelog-group-date { font-size: 12px; color: var(--color-text-muted); }
.changelog-item-list { display: grid; gap: 14px; }
.changelog-item { display: flex; gap: 10px; align-items: flex-start; }
.changelog-item-body { min-width: 0; }
.changelog-item-title { font-weight: 700; font-size: var(--text-sm); }
.changelog-item-desc { color: var(--color-text-muted); font-size: var(--text-sm); margin-top: 2px; }
.changelog-type-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: 999px; margin-top: 6px; }
.changelog-type-dot.changelog-type-feat { background: var(--color-accent); }
:root[data-theme='dark'] .changelog-type-dot.changelog-type-feat { background: var(--color-accent); }
.changelog-type-dot.changelog-type-fix { background: var(--color-warning); }
.changelog-type-dot.changelog-type-improvement { background: var(--color-text-muted); }
.changelog-type-label { display: inline-block; margin-top: 4px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); }

/* "New" feature badge (issue #20 Phase C) — a small pill next to a feature
   introduced by a changelog entry the user has already opened the drawer
   for; same rounded-pill treatment as .sync-pill, brand-colored since this
   is always positive/informational, never a warning/error state. */
/* intentional: fixed white text on a solid --color-brand-gold background, same in both themes (issue #116 precedent) */
.feature-new-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 0;
  background: var(--color-accent);
  color: #fff;
}
/* issue #155 v2 Phase D3 (full completion pass) — featureBadge.js; white-text-fails-on
   -lime pattern, swaps both fill and text. */
:root[data-theme='dark'] .feature-new-badge { background: var(--color-accent); }

.notes-textarea {
  resize: vertical;
  min-height: 88px;
  field-sizing: content;
  max-height: 320px;
  font-family: inherit;
  line-height: 1.5;
}
.notes-counter { font-size: var(--text-xs); }
.notes-status {
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--color-accent-700);
  opacity: 0;
  transition: opacity 200ms ease;
}
:root[data-theme='dark'] .notes-status { color: var(--color-accent-700); }
.notes-status.show { opacity: 1; }
.notes-status.error { color: var(--color-warning); }

/* Issue #180 — timer start/pause control + running total, itemPanel.js and
   dailyTodoPanel.js. */
.timer-row { display: flex; align-items: center; gap: 8px; }
.timer-toggle-btn.active { color: var(--color-accent-700); }
:root[data-theme='dark'] .timer-toggle-btn.active { color: var(--color-accent-700); }
.timer-display { font-size: var(--text-sm); font-variant-numeric: tabular-nums; color: var(--color-text-muted); }
.daily-todo-timer-btn {
  border: none;
  background: none;
  padding: 2px;
  color: var(--color-text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  font: inherit;
}
.daily-todo-timer-btn:hover, .daily-todo-timer-btn.active { color: var(--color-accent-700); }
:root[data-theme='dark'] .daily-todo-timer-btn:hover,
:root[data-theme='dark'] .daily-todo-timer-btn.active { color: var(--color-accent-700); }
.daily-todo-time-spent {
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.resource-list { display: grid; gap: 10px; }
/* issue #206 §6 — small solid gold dot instead of a plain muted-gray message.
   Was a gradient dot; reverted to solid per the app-wide "no gradients"
   decision. */
.resource-list-empty { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--color-text-muted); }
.resource-list-empty-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--color-accent); }
.resource-card {
  display: grid;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}
.resource-card-header { display: flex; align-items: center; }
.resource-meta { display: grid; gap: 6px; }
.resource-actions { display: flex; gap: 8px; }
.resource-add { display: grid; gap: 8px; margin-top: 10px; }
.resource-url-warning { margin: 0; }

/* Issue #329 — best-effort "Check link" result line. `.unverified` (the
   common, expected result — see itemPanel.js's own comment on why this is
   not an error) stays neutral/muted, never the danger color: a CORS-blocked
   response is not evidence the link is broken. `.reachable` uses the success
   token; `.error` (an invalid URL, caught before ever attempting a fetch)
   is the only state that uses the danger token. */
.resource-check-status { margin: 0; display: flex; align-items: center; gap: 4px; }
.resource-check-status.reachable { color: var(--color-success); }
.resource-check-status.error { color: var(--color-danger); }

/* Issue #12B — link-type badge, one per detectLinkType() result
   (src/ui/utils/linkDetector.js). Issue #353: per-type hex colors (predating the v2
   "Modernist" mono-scheme rule, design-system.md §2) collapsed to the shared mono
   scheme — visual distinction between link types now comes from each badge's own
   icon (linkDetector.js's LINK_TYPE_META), not color. `.link-badge-github` was
   already this shape and is the template every other type now matches. */
.link-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 3px 9px;
  border-radius: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
}
.link-badge-github { color: var(--color-text); background: var(--color-border); border-color: var(--color-border); }

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

/* Empty state */
.empty-state {
  text-align: center;
  padding: 64px 20px;
  color: var(--color-text-muted);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
}
/* issue #206 §6 — small solid gold icon badge, not a generic muted-gray
   glyph. Was a gradient fill; reverted to solid per the app-wide
   "no gradients" decision. */
.empty-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-3);
  border-radius: 50%;
  background: var(--color-accent);
  color: var(--color-ink-on-accent);
}
.empty-title { font-weight: 800; color: var(--color-text); margin-top: var(--space-2); }
.empty-message { margin-top: var(--space-1); }
.empty-state .btn { margin-top: var(--space-4); }

/* Icon system (issue #107) — base class for every createIcon()-produced
   <svg>; size comes from one of the four modifier classes below, which read
   the --icon-size-* tokens defined in :root. Every icon-wrapper class in
   this file (.nav-item-icon, .btn-icon, .stat-tile-icon, etc.) sets an
   explicit size this way — never let an icon glyph inherit surrounding
   body-text font-size (that's exactly what undersized the sidebar's
   Settings gear before this issue). */
.icon { flex-shrink: 0; }
.icon-xs { width: var(--icon-size-xs); height: var(--icon-size-xs); }
.icon-sm { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.icon-md { width: var(--icon-size-md); height: var(--icon-size-md); }
.icon-lg { width: var(--icon-size-lg); height: var(--icon-size-lg); }

/* Component library (issue #6 Phase 3) */

/* 3.1 Skeleton loaders — design-system.md §7: "flat neutral-200 blocks, no
   shimmer gradients — a simple opacity pulse" (issue #309; this used to be a
   `background-position`-animated `linear-gradient`, both a §2 "no gradients
   anywhere" violation and, now moot, a redundant one — its three color stops
   had all converged on the same `--color-surface-raised` value during an
   earlier token migration, so it hadn't even been visibly shimmering). */
@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.skeleton {
  background: var(--color-neutral-200);
  animation: skeleton-pulse 1.4s ease infinite;
  border-radius: var(--radius-md);
}
.skeleton-text { height: 14px; }
.skeleton-card { height: 72px; }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}

/* 3.4 Tooltip — portaled to document.body (issue #180 follow-up, see
   tooltip.js's own comment), so `position: fixed` + JS-computed left/top
   are the only positioning mechanism now; there is no longer a
   `.tooltip-trigger` descendant relationship to key CSS off of. */
.tooltip-trigger { position: relative; }
/* intentional: fixed white text on a fixed dark --neutral-900 tooltip background, same in both themes (issue #116) */
.tooltip-bubble {
  position: fixed;
  transform: translate(-50%, -100%);
  background: var(--neutral-900);
  color: #fff;
  padding: 4px 8px;
  border-radius: 0;
  font-size: var(--text-xs);
  white-space: nowrap;
  pointer-events: none;
  z-index: 60;
}
.tooltip-bubble.tooltip-below { transform: translate(-50%, 0); }

/* 3.5 Modal — spring entry on top of the existing .modal-overlay/.modal-card */
@keyframes modal-card-enter {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: scale(1); }
}
.modal-card-enter { animation: modal-card-enter var(--duration-enter) var(--ease-spring) both; }
@media (prefers-reduced-motion: reduce) {
  .modal-card-enter { animation: none; }
}

/* 3.5b Feature tour (issue #17) — a one-time spotlight walkthrough, portaled
   to document.body like every other floating element (see this file's
   "every floating/positioned element is a portal" convention). z-index 1100+
   deliberately sits above .modal-overlay (1000) and .custom-select-listbox
   (1010, the highest existing value in this file) — the tour can be
   re-triggered mid-session via "Take a tour" and must render above
   absolutely everything, including an already-open modal. */
.tour-scrim {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: transparent;
}
.tour-ring {
  position: fixed;
  z-index: 1101;
  border-radius: var(--radius-md);
  border: 2px solid var(--color-accent);
  /* The dimming layer itself — a huge-spread box-shadow fills the rest of
     the viewport, leaving this element's own box as a "cutout" over the
     highlighted target. Not clip-path'ing a separate overlay, per the
     issue's own spec — cheaper to keep in sync on resize/scroll, since
     repositioning is just two style writes rather than a recomputed path. */
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); /* intentional: theme-agnostic dim layer, same reasoning as .modal-overlay's own scrim; design-system.md §7 specifies "ink scrim at 50%" exactly (issue #301 follow-up — was 0.6) */
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .tour-ring {
    transition: top var(--duration-base) var(--ease-standard), left var(--duration-base) var(--ease-standard),
      width var(--duration-base) var(--ease-standard), height var(--duration-base) var(--ease-standard);
  }
}
.tour-popover {
  position: fixed;
  z-index: 1102;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 20px;
  max-width: 320px;
}
.tour-popover-progress {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.tour-popover-title { font-size: var(--text-base); margin: 0 0 6px; color: var(--color-text); }
.tour-popover-body { font-size: var(--text-sm); color: var(--color-text-muted); margin: 0 0 16px; }
.tour-popover-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.tour-popover-actions-right { display: flex; gap: 8px; }

.tour-welcome-overlay {
  position: fixed;
  inset: 0;
  z-index: 1102;
  background: rgba(0, 0, 0, 0.5); /* intentional: same theme-agnostic scrim as .modal-overlay */
  display: flex;
  align-items: safe center;
  justify-content: center;
  padding: 24px;
  overflow-y: auto;
}
.tour-welcome-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 32px 28px 28px;
  width: 100%;
  max-width: 420px;
}
/* Flush-left, not centered — design-system.md §4's "flush-left everything...
   never center hero copy or button labels" rule (issue #301 follow-up). This
   card was the one remaining centered modal in the app. */
.tour-welcome-brand { display: flex; justify-content: flex-start; margin-bottom: 12px; }
.tour-welcome-title { font-size: var(--text-lg); margin: 0 0 8px; color: var(--color-text); }
.tour-welcome-body { color: var(--color-text-muted); margin: 0 0 20px; }
.tour-welcome-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; }

/* 3.6 Tabs */
.tabs { display: flex; border-bottom: 1px solid var(--color-border); gap: 0; }
.tab {
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-text-muted);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
}
.tab[aria-selected='true'] { color: var(--color-accent-700); border-bottom-color: var(--color-accent); }
.tab:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fade-in var(--duration-base) var(--ease-out); }

/* 3.7 Progress ring (issue #206 §7) — flat solid stroke. Was a
   var(--gradient-alpenglow) <linearGradient> (progressRing.js built a real
   <defs>/<linearGradient>/<stop> tree, since CSS custom properties can't be
   read from an SVG `stop-color` presentation attribute); reverted to a
   plain CSS `stroke` declaration per the app-wide "no gradients" decision —
   simpler now that there's no gradient def to keep out of this rule's way. */
.progress-ring-track { stroke: var(--color-border-strong); }
.progress-ring-fill {
  stroke: var(--color-accent);
  transition: stroke-dashoffset var(--duration-enter) var(--ease-spring);
}

/* 3.8 Notification badge */
/* intentional: fixed white text on a solid --color-danger background, same in both themes (issue #116) */
/* issue #299 — design-system.md §8: "Notification badge: square accent dot,
   no pill." Was a pill-shaped, danger-colored badge left over from the
   pre-#297 direction. */
.notification-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  background: var(--color-accent);
  color: var(--color-ink-on-accent);
  font-size: 10px;
  font-weight: 800;
  line-height: 1;
}
.notification-badge-dot { min-width: 8px; width: 8px; height: 8px; padding: 0; }

/* 3.3 Command palette */
.command-palette-card { max-width: 560px; width: 100%; padding: 0; display: grid; grid-template-rows: auto 1fr auto; max-height: 70vh; }
.command-palette-input {
  font: inherit;
  font-size: var(--text-base);
  border: none;
  border-bottom: 2px solid var(--color-divider); /* design-system.md §5's "2px input rule" (issue #301 follow-up; was 1px) */
  padding: var(--space-4) var(--space-6);
  background: transparent;
  color: var(--color-text);
}
.command-palette-input:focus { outline: none; }
.command-palette-list { overflow-y: auto; padding: var(--space-2); display: grid; gap: 2px; }
.command-palette-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  text-align: left;
  font: inherit;
  /* No text node of its own — .command-palette-item-title/-subtitle each set
     their own color — kept explicit for defensiveness (issue #116). */
  color: inherit;
  border: none;
  background: none;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  cursor: pointer;
}
/* design-system.md §5 — "selected row = 3px accent bar + accent-100 fill
   (same pattern as sidebar's active nav item)" (issue #301 follow-up; this
   was still the pre-#301 neutral `--color-surface-raised` hover-style fill,
   `.nav-item.active`'s own exact treatment just below). */
.command-palette-item.active { background: var(--color-accent-100); box-shadow: inset 3px 0 0 var(--color-accent); }
.command-palette-item.active .command-palette-item-title { color: var(--color-accent-700); }
.command-palette-item-title { font-size: var(--text-sm); font-weight: 700; color: var(--color-text); }
.command-palette-item-subtitle { font-size: var(--text-xs); color: var(--color-text-muted); }
/* issue #283 — "Navigation"/"Topics" group headers, only rendered once the palette
   has more than one result group (cross-roadmap topic search wired in). Kicker
   styling per design-system.md §3 (uppercase, letter-spaced, accent color). */
.command-palette-group-label {
  padding: var(--space-2) var(--space-3) 4px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-accent-700);
}
.command-palette-footer {
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* Save badge — stacked directly above the feedback widget trigger, not
   sharing its bottom-right corner. Both are permanently fixed to that same
   corner (.feedback-widget-trigger below is mounted once on document.body
   for the whole app session — .claude/rules/roadmap-store.md's "never
   unmount" note on feedbackWidget.js), so whenever a save-badge event fired
   at the same time the feedback button visually sat on top of (and
   partially hid) the save badge's text — reported live with a screenshot
   ("Save f[ailed]" clipped behind "Feedback"). Moving to the opposite
   (bottom-left) corner isn't safe either — the dashboard/progress/settings
   app-shell sidebar (`.app-sidebar`, 240px, `position: sticky` but still
   occupying that full-height column) would sit underneath a fixed-position
   badge there. `84px` = the feedback trigger's own bottom offset (24px) +
   its rendered height (`padding: 12px 18px` + text-sm line-height + border,
   ~46px) + a 12px gap, rounded up — if the feedback trigger's own size ever
   changes, re-check this offset still clears it. */
.save-badge {
  position: fixed;
  bottom: calc(84px + env(safe-area-inset-bottom));
  right: calc(20px + env(safe-area-inset-right));
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  padding: 10px 14px;
  border-radius: 0;
  font-size: var(--text-sm);
  font-weight: 700;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--duration-base) ease, transform var(--duration-base) ease;
  pointer-events: none;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 8px;
}
.save-badge.show { opacity: 1; transform: translateY(0); }
/* issue #153 — an error-state badge now carries a real "Retry now" button
   (roadmapStore.js's retrySaveNow()), so pointer-events flips to auto only in
   this state; every other state stays click-through (pointer-events: none
   above) since it's purely informational. */
.save-badge.error { background: var(--color-danger-bg); color: var(--color-danger); border-color: var(--color-danger); pointer-events: auto; }
.save-badge-retry {
  color: inherit;
  border-color: currentColor;
  min-height: 0;
  padding: 3px 10px;
  font-size: var(--text-xs);
}
/* intentional: neutral gray hover wash reads correctly against either theme's --color-danger-bg fill, same reasoning as .btn-spinner's own neutral track just below (issue #116) */
.save-badge-retry:hover:not(:disabled) { background: rgba(127, 127, 127, 0.15); }
/* intentional: neutral gray track works against any --color-surface value in either theme, same as .btn-spinner above (issue #116) */
.save-badge .spin {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid rgba(127,127,127,0.2);
  border-top-color: var(--color-accent);
  animation: spin 1s linear infinite;
}
/* issue #155 v2 Phase D3 (full completion pass) — app-wide save-status spinner. */
:root[data-theme='dark'] .save-badge .spin { border-top-color: var(--color-accent); }

/* Toasts */
.toast-stack {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
  display: grid;
  gap: 8px;
  pointer-events: none;
}
/* issue #206 §3.8 — was a flat solid-fill pill (999px radius, white text on a
   saturated background); now a --color-surface card with a left-edge 3px
   accent bar colored by type, matching the modal/sidebar language elsewhere
   in this pass. Used a real `border-left` rather than an inset box-shadow —
   the toast has no other border to conflict with (unlike .nav-item.active,
   which already had layout-sensitive padding), and a border naturally
   respects border-radius clipping with no extra care needed. Switched
   border-radius from the old full pill (999px) to --radius-md: a sharp
   left-edge color bar reads as a cut corner against a fully rounded pill,
   the same reasoning as the sidebar active-item shape change in this pass —
   flagging this shape change. */
/* issue #206 §5 — entrance/exit timing switched from a hardcoded 220ms/ease
   to the shared duration/ease tokens (the spec's "only duration/ease custom
   property tokens" hard limit), matching the modal entrance's own duration-enter/
   ease-spring pairing in spirit: slide+fade in on show, a quicker fade+slide
   back on dismiss. `toast.js`'s own setTimeout delays (260ms before the DOM
   remove) stay in sync with `--duration-base`, not restated here since JS
   can't read a CSS custom property without an extra getComputedStyle call
   for a one-line constant — see that file's own comment. */
/* issue #206 §5 follow-up — real feedback (screenshot): `--color-surface`
   (near-white in light theme) has almost no contrast against `--color-bg`
   (also near-white) with only a `box-shadow` for separation — the toast
   read as barely-there plain text floating on the page rather than a
   card. A full 1px `--color-border` outline gives it a crisp edge
   regardless of how close the two background tokens happen to be, on top
   of (not instead of) the existing shadow and left accent bar. */
.toast {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  padding: 12px 18px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 700;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--duration-base) var(--ease-decelerate), transform var(--duration-base) var(--ease-decelerate);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast:not(.show) { transition-timing-function: var(--ease-accelerate); transition-duration: var(--duration-fast); }
.toast-success { border-left-color: var(--color-success); }
.toast-info { border-left-color: var(--color-accent); }
.toast-error { border-left-color: var(--color-danger); }

/* ==========================================================================
   Settings page (issue #16) — reuses the app-shell-2/sidebar/topbar chrome
   dashboard.js already established (same `.app-shell-2` outer class); only
   the content region below the topbar is new here.
   ========================================================================== */

.settings-content { padding: var(--space-6); display: grid; gap: var(--space-6); max-width: 720px; }
.settings-header h1 { margin: 0; font-size: var(--text-xl); }

.settings-sections { display: grid; gap: var(--space-6); }

/* issue #300 — design-system.md §4: flat surfaces, no shadow (was --shadow-sm). */
.settings-section {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  padding: var(--space-6);
  display: grid;
  gap: var(--space-4);
}
.settings-section-title { margin: 0; font-size: var(--text-lg); }

/* A single row's label + control sit on one line on wide viewports and stack
   on narrow ones — the same "don't rely on equal source lengths" caution as
   .card-grid layouts elsewhere (ui-styling.md), just for a horizontal row
   instead of a vertical stack. */
.settings-row { display: grid; gap: var(--space-2); }
.settings-row-main { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.settings-row-label { font-weight: 700; color: var(--color-text); }
.settings-row-label-group { display: inline-flex; align-items: center; gap: var(--space-2); }
.settings-row-value { color: var(--color-text-muted); font-size: var(--text-sm); margin-right: auto; }
.settings-row-hint { color: var(--color-text-muted); font-size: 12px; margin: 0; }
.settings-verified { color: var(--color-accent-700); font-weight: 700; }
/* issue #155 v2 Phase D2 — settings-only class, recolored; falls back to mint in light theme. */
:root[data-theme='dark'] .settings-verified { color: var(--color-accent-700); }
.settings-unverified { color: var(--color-p1-ink); font-weight: 700; }

.settings-select { max-width: 200px; }

.settings-inline-form {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-4);
}
.settings-inline-form-body { display: grid; gap: var(--space-3); max-width: 380px; }

.danger-zone { border-color: var(--color-danger); }
.danger-zone .settings-section-title { color: var(--color-danger); }

/* issue #206 §3.2 — base card treatment; kept --space-8 padding (bigger than
   the --space-6 default) since this is a deliberately emphasized guest CTA
   card, not a standard content card. */
.settings-guest-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-8);
  display: grid;
  gap: var(--space-3);
  justify-items: start;
  max-width: 480px;
}
.settings-guest-card h2 { margin: 0; }
.settings-guest-card p { margin: 0; color: var(--color-text-muted); }

@media (max-width: 480px) {
  .settings-row-main { flex-direction: column; align-items: flex-start; }
  .settings-select { max-width: 100%; width: 100%; }
}

/* ==========================================================================
   Progress page (issue #8) — reuses the app-shell-2/sidebar/topbar chrome
   dashboard.js/settings.js already established; only the content region
   below the topbar is new here.
   ========================================================================== */

/* grid-template-columns: minmax(0, 1fr) explicitly, not left implicit
   (issue #8 follow-up) — an implicit single-column grid track's automatic
   size is content-based (max-content), so this card list was rendering at
   its widest child's content width (~840px, the heatmap) regardless of the
   container's own real width, forcing the whole page wider than the
   viewport on a narrow screen. Found live via a phone-width screenshot;
   .progress-card's own `min-width: 0` alone wasn't enough — the track
   itself needs the explicit minimum. */
.progress-content { padding: var(--space-6); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); max-width: 1160px; margin: 0 auto; width: 100%; align-content: start; }

.progress-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
.progress-header h1 { margin: 0; font-size: var(--text-xl); }
.progress-header-subtitle { margin: 4px 0 0; color: var(--color-text-muted); font-size: var(--text-sm); }
.progress-header-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
/* Real, reported mismatch (issue #309, same shape as .app-topbar-actions'
   fix above): the "This Week/This Month/All Time" `.filter-chip`s (36px)
   and the "Share progress" `.btn-sm` (34px) sat 2px apart in the same row. */
.progress-header-actions .filter-chip,
.progress-header-actions > .btn {
  height: 36px;
  box-sizing: border-box;
}
.progress-range-toggle { display: flex; gap: var(--space-2); }

.progress-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  /* .progress-content has no explicit grid-template-columns, so its single
     implicit column defaults to `auto` sizing — a grid item's own default
     min-width is its content's min-content size, so the ~800px-wide
     heatmap/charts inside this card would otherwise force the whole grid
     track (and the page) wider than the viewport instead of letting the
     heatmap's own .heatmap-scroll (and the chart/table wrap containers)
     scroll internally. Same fix as .app-shell-main's own min-width: 0. */
  min-width: 0;
}
.progress-card-title { margin: 0 0 var(--space-4); font-size: var(--text-lg); }

/* Activity heatmap (B3) — see src/ui/components/heatmap.js for why this is
   plain HTML/CSS Grid, not literal SVG. Cell size/gap match the issue's own
   spec (12x12px, 3px gap). */
.heatmap { display: flex; flex-direction: column; gap: var(--space-3); }
.heatmap-body { display: flex; gap: 6px; }
/* Right padding so the final month label (e.g. "Jul") has room to render
   past its 12px column instead of being clipped by the scroll container's
   own edge — found live, the label reads fine everywhere except the very
   last column. `min-width: 0` overrides a flex item's default `auto` min
   width (its own content size) — without it, this ~800px-wide grid forced
   `.heatmap-body` (and the whole page) wider than the viewport instead of
   scrolling within its own box, found live on a phone-width screenshot. */
.heatmap-scroll { overflow-x: auto; padding-bottom: 2px; padding-right: 28px; min-width: 0; }
.heatmap-months { display: grid; grid-auto-flow: column; grid-auto-columns: 12px; gap: 3px; margin-bottom: 4px; }
.heatmap-month-label { font-size: var(--text-xs); color: var(--color-text-muted); white-space: nowrap; }
.heatmap-day-labels { display: grid; grid-template-rows: repeat(7, 12px); gap: 3px; width: 26px; flex-shrink: 0; margin-top: 18px; }
.heatmap-day-label { font-size: var(--text-xs); color: var(--color-text-muted); line-height: 12px; }
.heatmap-grid { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 12px); grid-auto-columns: 12px; gap: 3px; }
/* issue #297 — design-system.md §5: "Heatmap: hard-edged squares." Both
   border-radius values below were 2px, a holdover from the old rounded
   scale; zeroed to match --radius-*'s "0 everywhere" rule. */
.heatmap-cell { width: 12px; height: 12px; border-radius: 0; background: var(--heat-0); padding: 0; border: none; cursor: default; }
.heatmap-cell[data-level="1"] { background: var(--heat-1); }
.heatmap-cell[data-level="2"] { background: var(--heat-2); }
.heatmap-cell[data-level="3"] { background: var(--heat-3); }
.heatmap-cell[data-level="4"] { background: var(--heat-4); }
.heatmap-cell[data-today="true"] { outline: 1.5px solid var(--color-text); outline-offset: -1.5px; }
.heatmap-legend { display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--color-text-muted); }
.heatmap-legend-swatch { width: 10px; height: 10px; border-radius: 0; background: var(--heat-0); }
.heatmap-legend-swatch[data-level="1"] { background: var(--heat-1); }
.heatmap-legend-swatch[data-level="2"] { background: var(--heat-2); }
.heatmap-legend-swatch[data-level="3"] { background: var(--heat-3); }
.heatmap-legend-swatch[data-level="4"] { background: var(--heat-4); }
/* Row-only grid placement (see heatmap.js) — column is left to
   `grid-auto-flow: column`'s own auto-placement in document order. */
.gr-1 { grid-row: 1; }
.gr-2 { grid-row: 2; }
.gr-3 { grid-row: 3; }
.gr-4 { grid-row: 4; }
.gr-5 { grid-row: 5; }
.gr-6 { grid-row: 6; }
.gr-7 { grid-row: 7; }

/* issue #155 v2 Phase B — chartWrapper.js's `createBucketedBarChart()`/
   `createChartLegend()` support (the reference's multi-color value-bucketed bar chart
   + floating tooltip + legend row). Built and visually verified in isolation this
   phase; no page calls the new chartWrapper.js exports yet — that's Phase C/D's job.
   `.chart-tooltip` is deliberately a fixed white-bg/dark-text card in both themes, not
   theme-tokened — the reference shows a small white rounded-rect tooltip with dark
   text regardless of page theme, the same "fixed regardless of site theme" reasoning
   already used for `.auth-marketing`'s always-dark panel (issue #116). */
.chart-tooltip {
  position: absolute;
  pointer-events: none;
  z-index: 5;
  padding: 8px 12px;
  border-radius: var(--radius-md);
  /* intentional: fixed white card + dark text, same in both themes — mirrors the
     reference's tooltip exactly rather than following site theme (issue #155 v2). */
  background: #ffffff;
  color: #0f172a;
  font-size: var(--text-xs);
  font-weight: 700;
  box-shadow: var(--shadow-md);
  opacity: 0;
  top: 0;
  left: 0;
  transform: translate(-50%, calc(-100% - 10px));
  transition: opacity var(--duration-fast) ease;
  white-space: nowrap;
}
.chart-tooltip.visible { opacity: 1; }
.chart-tooltip-date { display: block; font-weight: 800; margin-bottom: 2px; }
/* intentional: paired with the fixed white tooltip background above — a muted gray
   that reads correctly only against white, not a theme token (issue #155 v2). */
.chart-tooltip-value { display: block; color: #475569; }
.chart-tooltip::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -5px;
  width: 10px;
  height: 10px;
  transform: translateX(-50%) rotate(45deg);
  /* intentional: matches the fixed white tooltip body above (issue #155 v2). */
  background: #ffffff;
}

.chart-legend { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 8px; }
.chart-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--color-text-muted); font-weight: 600; }
.chart-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
/* Per-bucket dot colors as discrete classes, not an inline `style` (index.html's CSP
   has no `unsafe-inline` for style-src — see the "Never set an inline style" rule
   above) — mirrors chartWrapper.js's own BUCKET_TOKENS mapping so the legend dots and
   the bars they describe always agree. */
/* issue #206 — --color-brand-gold is now defined in both themes (the
   Alpenglow token layer replaced the old dark-theme-only lime token), so the
   var() fallback this rule used to need is dead; dropped. */
.chart-legend-dot-high { background: var(--color-accent); }
.chart-legend-dot-medium { background: var(--color-text-faint); }
.chart-legend-dot-low { background: var(--color-border-strong); }

/* Chart containers (B4/B5) — Chart.js is given a fixed-height box with
   `maintainAspectRatio: false` (see chartWrapper.js) since a canvas has no
   intrinsic height of its own to size a CSS Grid/flex row from. */
.chart-container { position: relative; height: 260px; }
.chart-container canvas { width: 100% !important; height: 100% !important; }
/* issue #206 §6 — flat-zero chart empty-state note (progress.js's velocityEmptySlot).
   A small solid gold dot, not a full illustration. Was a gradient dot;
   reverted to solid per the app-wide "no gradients" decision. */
.chart-empty-note { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3); font-size: var(--text-sm); color: var(--color-text-muted); }
.chart-empty-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--color-accent); }
/* Issue #125 — a skeleton placeholder for the first Chart.js CDN load only
   (chartWrapper.js's dynamic import), shown/hidden by progress.js rather
   than CSS alone since it must disappear the instant the real chart mounts,
   not on a fixed timer. */
.chart-container .skeleton-card { position: absolute; inset: 0; height: 100%; }
.chart-canvas-loading { display: none; }

/* Phase breakdown (B6) — a flat list of horizontal bars, deliberately not
   .phase-card (that class carries expand/collapse chrome this list doesn't
   need). The bar itself is an inline SVG rect with a `width` *attribute*
   (not inline `style`, per the CSP rule) driven by setAttribute, same
   pattern progressRing.js already uses for its own dynamic stroke-dashoffset. */
.phase-breakdown-list { display: grid; gap: var(--space-3); align-content: start; }
.phase-breakdown-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  background: none;
  border: none;
  padding: var(--space-2) 0;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  border-radius: 6px;
}
.phase-breakdown-row:hover, .phase-breakdown-row:focus-visible { background: var(--color-surface-raised); outline: none; }
.phase-breakdown-dot { width: 10px; height: 10px; border-radius: 999px; flex-shrink: 0; }
.phase-breakdown-dot[data-priority="P0"] { background: var(--color-p0); }
.phase-breakdown-dot[data-priority="P1"] { background: var(--color-p1); }
.phase-breakdown-dot[data-priority="P2"] { background: var(--color-p2); }
.phase-breakdown-dot[data-priority="P3"] { background: var(--color-p3); }
.phase-breakdown-main { display: grid; gap: 4px; min-width: 0; }
.phase-breakdown-name { font-weight: 600; font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phase-breakdown-count { font-size: var(--text-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Generic linear progress bar (issue #8) — reused by the phase-breakdown
   rows above and the "Items complete" stat tile below. An inline SVG rect
   with its `width` set via `setAttribute` (never inline `style`), same
   pattern progressRing.js already uses for its own dynamic stroke-dashoffset. */
.mini-bar { width: 100%; height: 8px; display: block; }
.mini-bar-track { fill: var(--color-border-strong); }
.mini-bar-fill { fill: var(--color-accent); }
/* issue #155 v2 Phase D3 (full completion pass) — base rule, used by every mini-bar
   outside progress.js's already-recolored .kpi-tile-hero context (Phase D1). */
:root[data-theme='dark'] .mini-bar-fill { fill: var(--color-accent); }
/* issue #155 v2 Phase D1 — progress.js's "Items complete" kpi-tile-hero: a teal fill
   on top of the tile's own solid accent background would clash/low-contrast, same
   reasoning as .card-arrow-badge's hero override above. */
.kpi-tile-hero .mini-bar-fill { fill: var(--color-bg); }
/* intentional: matches .kpi-tile-hero .card-arrow-badge's black-tint-on-solid-accent
   treatment above (issue #155 v2 Phase D1). */
.kpi-tile-hero .mini-bar-track { fill: rgba(0, 0, 0, 0.14); }
.stat-tile-bar { margin-top: 6px; }

/* Priority x phase table (B7) */
.priority-table-wrap { overflow-x: auto; }
.priority-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.priority-table th, .priority-table td { padding: 8px 10px; text-align: center; white-space: nowrap; }
.priority-table th:first-child, .priority-table td:first-child { text-align: left; }
.priority-table thead th { color: var(--color-text-muted); font-weight: 600; border-bottom: 1px solid var(--color-border); }
.priority-table td { border-bottom: 1px solid var(--color-border); font-variant-numeric: tabular-nums; }
.priority-table tbody tr:last-child td { border-bottom: none; }
.priority-cell[data-band="empty"] { color: var(--color-text-muted); }
.priority-cell[data-band="low"] { background: var(--color-danger-bg); color: var(--color-danger); }
.priority-cell[data-band="mid"] { background: var(--color-warning-bg); color: var(--color-warning); }
.priority-cell[data-band="high"] { background: var(--color-success-bg); color: var(--color-success); }

/* Projected completion (B8) */
.projection-card-body { display: grid; gap: 4px; justify-items: center; text-align: center; }
.projection-pace { color: var(--color-text-muted); font-size: var(--text-sm); }
.projection-headline { font-size: var(--text-2xl); font-weight: 800; color: var(--color-accent-700); }
:root[data-theme='dark'] .projection-headline { color: var(--color-accent-700); }
.projection-boost { color: var(--color-text-muted); font-size: var(--text-sm); }
.projection-empty { color: var(--color-text-muted); text-align: center; }

/* Roadmap comparison modal (issue #285) — reuses .modal-overlay/.modal-card and
   .filter-chip (as a two-option mode toggle, same precedent as
   .progress-range-toggle) rather than inventing new chrome; only the
   comparison-specific result list below is new. Flush-left throughout, radius
   0, 2px/1px rules per design-system.md. */
.comparison-modal-card { width: min(640px, 92vw); max-height: 85vh; display: flex; flex-direction: column; gap: var(--space-4); }
.comparison-modal-subtitle { color: var(--color-text-muted); font-size: var(--text-sm); }
.comparison-mode-toggle { display: flex; gap: 8px; }
.comparison-other-select { display: flex; flex-direction: column; gap: 6px; }
.comparison-result-slot { overflow-y: auto; border-top: 2px solid var(--color-divider); padding-top: var(--space-4); }
.comparison-loading, .comparison-empty { color: var(--color-text-muted); text-align: left; }
.comparison-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); border: 1px solid var(--color-divider); margin-bottom: var(--space-4); }
.comparison-summary-item { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-left: 1px solid var(--color-divider); text-align: left; }
.comparison-summary-item:first-child { border-left: none; }
.comparison-summary-value { font-size: var(--text-xl); font-weight: 800; color: var(--color-text); font-variant-numeric: tabular-nums; }
.comparison-summary-label { font-size: var(--text-xs); color: var(--color-text-muted); }
.comparison-phase-groups { display: flex; flex-direction: column; gap: var(--space-4); }
.comparison-phase-title { margin: 0 0 6px; font-size: var(--text-sm); font-weight: 700; text-align: left; }
.comparison-row-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-divider); }
.comparison-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--color-divider); text-align: left; }
.comparison-row-title { color: var(--color-text); font-size: var(--text-sm); }
.comparison-row-status { font-size: var(--text-xs); color: var(--color-text-muted); white-space: nowrap; }
.comparison-row-status-both-done { color: var(--color-success); }
.comparison-row-added .comparison-row-status { color: var(--color-accent-700); }
.comparison-row-removed .comparison-row-status { color: var(--color-text-muted); }

@media (max-width: 768px) {
  .progress-content { padding: var(--space-4); }
  .progress-header { flex-direction: column; align-items: stretch; }
  .chart-container { height: 220px; }
}

@media (max-width: 480px) {
  .progress-card { padding: var(--space-4); }
  .priority-table { font-size: var(--text-xs); }
}

/* ==========================================================================
   Landing page (issue #6 Phase 6) — marketing page for signed-out visitors
   at '#/'. Self-contained section; nothing here is reused by any other page.
   ========================================================================== */

.landing-page { min-height: 100vh; min-height: 100dvh; background: var(--color-bg); overflow-x: hidden; }

/* issue #299 — design-system.md's flat "2px-ruled header bar" nav, replacing
   the pre-#297 floating/bordered/blurred pill-shaped bar (chip-radius, glass
   surface, backdrop-filter — none of which fit the flat Modernist identity). */
.landing-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  max-width: 1160px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-6);
  border-bottom: 2px solid var(--color-divider);
  background: var(--color-bg);
}
.landing-nav-links { display: flex; gap: var(--space-6); }
.landing-nav-link {
  background: none;
  border: none;
  color: var(--color-text-muted);
  font: inherit;
  font-weight: 600;
  font-size: var(--text-base);
  cursor: pointer;
  padding: 0;
}
.landing-nav-link:hover { color: var(--color-text); }
.landing-nav-link:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.landing-nav-actions { display: flex; align-items: center; gap: var(--space-3); }

/* issue #155 (ZeBeyond direction) — position:relative;overflow:hidden so the
   .bg-grid-glow decorative layer can sit behind the copy/mock without
   affecting layout; .landing-hero-copy/.landing-hero-mock get z-index: 1 so
   they stay above it. */
.landing-hero {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  align-items: center;
  gap: var(--space-12);
  max-width: 1160px;
  margin: 0 auto;
  padding: var(--space-12) var(--space-6) var(--space-16);
  position: relative;
  overflow: hidden;
}
.landing-hero-copy { position: relative; z-index: 1; }
.landing-hero-eyebrow { margin: 0 0 var(--space-4); }
.landing-hero-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-4xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-4);
  color: var(--color-text);
}
.landing-hero-subtitle {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--color-text-muted);
  margin: 0 0 var(--space-8);
  max-width: 46ch;
}
.landing-hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-6); }
.landing-hero-stat { font-size: var(--text-sm); color: var(--color-text-muted); margin: 0; }

.landing-hero-mock { display: flex; justify-content: center; position: relative; z-index: 1; }
.landing-mock-card {
  width: 100%;
  max-width: 380px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.landing-mock-head { display: flex; gap: 6px; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--color-border); }
.landing-mock-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--color-border-strong); }
.landing-mock-body { display: grid; gap: var(--space-4); padding: var(--space-6) var(--space-6); }
.landing-mock-row { display: flex; align-items: center; gap: var(--space-3); }
/* Square, matching the real .check-box's shape (design-system.md §5:
   "Checkboxes: squares") — this decorative landing-page mockup had its own
   stray radius (issue #309). */
.landing-mock-check {
  width: 18px;
  height: 18px;
  border-radius: 0;
  border: 2px solid var(--color-accent);
  background: var(--color-warning-bg);
  flex-shrink: 0;
}
/* issue #155 v2 Phase D3 (full completion pass) — the landing hero's mockup preview
   should match the real dashboard checklist it's depicting, now that
   .check-item.done .check-box is lime in dark theme too. */
:root[data-theme='dark'] .landing-mock-check { border-color: var(--color-accent); background: var(--color-warning-bg); }
/* Flat, not a pill — design-system.md §5 "Progress: flat bars (6-8px)... accent fill" (issue #301 follow-up). */
.landing-mock-bar { flex: 1; height: 10px; background: var(--color-border-strong); overflow: hidden; }
.landing-mock-bar-fill { display: block; height: 100%; background: var(--color-accent); }
:root[data-theme='dark'] .landing-mock-bar-fill { background: var(--color-accent); }
.landing-mock-fill-1 { width: 92%; }
.landing-mock-fill-2 { width: 68%; }
.landing-mock-fill-3 { width: 100%; }
.landing-mock-fill-4 { width: 45%; }

.landing-section-eyebrow { display: flex; justify-content: center; margin-bottom: var(--space-3); }
.landing-section-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-2xl);
  letter-spacing: var(--tracking-tight);
  text-align: center;
  margin: 0 0 var(--space-8);
  color: var(--color-text);
}

.landing-features, .landing-steps { max-width: 1160px; margin: 0 auto; padding: var(--space-12) var(--space-6); }

.landing-feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: stretch;
  gap: var(--space-6);
}
.feature-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-8);
}
.feature-card-icon {
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  background: var(--color-warning-bg);
  color: var(--color-accent-700);
  margin-bottom: var(--space-6);
}
/* issue #155 v2 Phase C — landing-only class, recolored; falls back to mint in light theme. */
:root[data-theme='dark'] .feature-card-icon { background: var(--color-warning-bg); color: var(--color-accent-700); }
.feature-card-title { font-family: var(--font-display); font-weight: 700; font-size: var(--text-lg); margin: 0 0 var(--space-2); color: var(--color-text); }
.feature-card-text { font-size: var(--text-base); line-height: var(--leading-loose); color: var(--color-text-muted); margin: 0; }

.landing-steps-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  align-items: stretch;
  gap: var(--space-6);
  list-style: none;
  padding: 0;
  margin: 0;
}
.step-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-8) var(--space-6) var(--space-6);
}
.step-card-number {
  position: absolute;
  top: var(--space-4);
  right: var(--space-6);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-2xl);
  color: var(--color-border-strong);
}
.step-card-icon {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-warning-bg);
  color: var(--color-accent-700);
  margin-bottom: var(--space-4);
}
/* issue #155 v2 Phase C — landing-only class, recolored; falls back to mint in light theme. */
:root[data-theme='dark'] .step-card-icon { background: var(--color-warning-bg); color: var(--color-accent-700); }
.step-card-title { font-family: var(--font-display); font-weight: 700; font-size: var(--text-base); margin: 0 0 var(--space-2); color: var(--color-text); }
.step-card-text { font-size: var(--text-sm); line-height: var(--leading-loose); color: var(--color-text-muted); margin: 0; }

.landing-proof { text-align: center; padding: 0 var(--space-6) var(--space-12); }
.landing-proof-text { font-family: var(--font-display); font-weight: 600; font-size: var(--text-lg); color: var(--color-accent-700); max-width: 52ch; margin: 0 auto; }
/* issue #155 v2 Phase C — landing-only, recolored; falls back to mint in light theme. */
:root[data-theme='dark'] .landing-proof-text { color: var(--color-accent-700); }

/* issue #299 — design-system.md §2: "the accent runs as a field in exactly
   one pattern: poster statements (landing closing banner). Nowhere else."
   Was a bordered, page-background card (a leftover pre-#297 treatment) —
   now a genuine full-bleed solid-accent poster. Heading text is large
   (--text-3xl, ~39px, weight 800 — comfortably WCAG "large text," only
   needs 3:1) so --color-ink-on-accent on the raw --color-accent field is
   safe at 3.76:1 light / 5.88:1 dark (verified via relative-luminance
   calculation); the button inside inverts to a ground-colored fill so it
   reads as a cutout against the field rather than a same-hue-on-same-hue
   button (accent-700 text on ink-on-accent bg measures 6.41:1 light /
   12.23:1 dark). */
.landing-cta {
  text-align: center;
  width: 100%;
  margin: var(--space-8) 0 0;
  padding: var(--space-16) var(--space-6);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  position: relative;
  overflow: hidden;
  background: var(--color-accent);
}
.landing-cta-title { font-family: var(--font-display); font-weight: 800; font-size: var(--text-3xl); letter-spacing: var(--tracking-tight); margin: 0; color: var(--color-ink-on-accent); position: relative; z-index: 1; max-width: 20ch; }
.landing-cta .btn-primary {
  position: relative;
  z-index: 1;
  background: var(--color-ink-on-accent);
  color: var(--color-accent-700);
  border-color: var(--color-ink-on-accent);
}
.landing-cta .btn-primary:hover:not(:disabled) { background: var(--color-ink-on-accent); border-color: var(--color-ink-on-accent); opacity: 0.9; }

.landing-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-8) var(--space-6);
  border-top: 1px solid var(--color-border);
}
.landing-footer-copy { font-size: var(--text-sm); color: var(--color-text-muted); margin: 0; }

/* ==========================================================================
   Responsive breakpoint scale (issue #36)

   Six tiers, replacing the old ad-hoc 920/640 pair. Written mobile-up in this
   comment for readability, but implemented as `max-width` overrides on top of
   a desktop-first base (matching every other rule in this file), plus one
   `min-width` tier for ultra-wide displays:

     ≤375px   small phone   (iPhone SE, older/small Android)
     ≤480px   phone         (most modern phones, portrait)
     ≤768px   tablet portrait (iPad portrait)
     ≤1024px  tablet landscape / small laptop (iPad landscape)
     1025–1599px  laptop / desktop — the existing base styles, untuned
     ≥1600px  large / ultra-wide desktop
     ≥2200px  true ultra-wide / 4K (issue #340)

   Touch-capable vs. hover-capable is handled separately via the `(hover: …)`
   / `(pointer: …)` media features above and below — never inferred from
   viewport width. A touchscreen laptop or iPad landscape session is exactly
   as capable of triggering the `pointer: coarse` tier as a phone is, and a
   plain-mouse desktop at a narrow window should not get touch-sized targets.

   issue #340 — the ≥1600px tier originally only reached 5 dashboard-related
   selectors, so every other top-level page content container (onboarding,
   settings, progress) stayed at its base-tier width forever, no matter how
   wide the monitor got — on a real 27"+ display this read as a small,
   centered, laptop-sized page floating in a sea of unused background.
   `.onboarding-inner` (880px base) and `.progress-content` (1160px base) now
   get the same ≥1600px treatment `.dashboard-content` already had, plus a
   second ≥2200px tier for true ultra-wide/4K so the column keeps growing
   rather than capping out at a still-modest width on a 3440px+ display.
   `.settings-content` is a form/reading-width column (720px) — verified
   against a real 27"+ screenshot and deliberately left unchanged; growing a
   form's line length past a comfortable reading width doesn't improve
   usability the way more chart/grid room does for the other two pages.
   Modal `max-width` values (`.import-modal-card`, `.command-palette-card`,
   etc.) were also checked and left as-is — a modal filling most of a 27"+
   screen would be its own usability problem, not a fix. */

@media (min-width: 1600px) {
  .header-top, .roadmap-header, .stat-strip, .toolbar, .dashboard-content { max-width: 1320px; }
  .onboarding-inner { width: min(1200px, 100%); }
  .progress-content { max-width: 1320px; }
}

@media (min-width: 2200px) {
  .header-top, .roadmap-header, .stat-strip, .toolbar, .dashboard-content { max-width: 1600px; }
  .onboarding-inner { width: min(1500px, 100%); }
  .progress-content { max-width: 1600px; }
}

@media (max-width: 1024px) {
  .toolbar { grid-template-columns: 1fr; }
  .toolbar-block.toolbar-right { justify-content: flex-start; }
  /* issue #6 Phase 5.1 — marketing panel hidden, .auth-page-right becomes
     the sole (full-width) column: today's original centered-card layout. */
  .auth-page { grid-template-columns: 1fr; }
  .auth-marketing { display: none; }
  /* issue #6 Phase 6/8 — hero collapses to a single column; mock preview
     drops below the copy instead of sitting beside it. Flush-left, not
     centered (issue #309) — design-system.md §4's "never center hero copy
     or button labels" applies at every width, not just desktop. */
  .landing-hero { grid-template-columns: 1fr; }
  .landing-hero-actions { justify-content: flex-start; }
  .landing-hero-mock { order: -1; }
}

@media (max-width: 768px) {
  .dashboard-header { padding: max(14px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) 16px max(16px, env(safe-area-inset-left)); }
  .dashboard-content { padding: 18px 16px 84px; }
  .landing-nav-links { display: none; }
}

@media (max-width: 480px) {
  .dashboard-header { padding: max(14px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) 16px max(12px, env(safe-area-inset-left)); }
  .dashboard-content { padding: 16px 12px 88px; }
  /* issue #6 Phase 8 — full-screen slide-up from the bottom instead of a
     side-slide panel on phone widths; the ≥481px side-slide behavior above
     is unchanged. */
  .item-panel {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: 88vh;
    height: 88dvh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding-top: 0;
    padding-bottom: env(safe-area-inset-bottom);
    transform: translateY(100%);
  }
  .item-panel.show { transform: translateY(0); }
  .panel-footer { flex-direction: column; align-items: stretch; }
  .panel-footer-right { margin-left: 0; width: 100%; }
  .panel-footer-right .btn { flex: 1; }
  .confirm-dialog-actions { flex-direction: column; }
  .confirm-dialog-actions .btn { width: 100%; }
  .landing-hero-title { font-size: var(--text-3xl); }
  .landing-hero-actions { flex-direction: column; align-items: stretch; }
  .landing-hero-actions .btn { width: 100%; }
  .landing-cta-title { font-size: var(--text-2xl); }
  /* issue #6 Phase 8 — a horizontal-scroll row instead of wrapping, so the
     priority filter chips stay a single tappable row on a phone width
     instead of eating vertical space with a second/third wrapped line.
     `scrollbar-width: none`/the `::-webkit-scrollbar` rule hide the
     scrollbar chrome — the chips themselves are the scroll affordance. */
  .filter-row { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
  .filter-row::-webkit-scrollbar { display: none; }
  .filter-chip { flex-shrink: 0; }
}

/* In-app feedback widget (issue #9) — floating trigger + modal + form
   primitives. z-index 150 sits above ordinary page content but below the
   sidebar mobile backdrop (199)/item panel (210)/modal-overlay (1000)/toast
   (300), per the issue's "above content but below modals and item panels"
   spec. Fixed to the viewport corner, so it needs the same safe-area-inset
   treatment every other fixed edge element in this app gets
   (.claude/rules/ui-styling.md). */
/* design-system.md §5 — "Feedback widget: solid accent square." Was a
   neutral --color-surface pill with a border (issue #301 follow-up); now a
   flat solid --color-accent fill with ground-colored text/icon, matching
   .btn-primary's own fill/text pairing. */
.feedback-widget-trigger {
  position: fixed;
  bottom: max(24px, env(safe-area-inset-bottom));
  right: max(24px, env(safe-area-inset-right));
  z-index: 150;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 18px;
  border-radius: 0;
  border: none;
  background: var(--color-accent);
  color: var(--color-ink-on-accent);
  font-weight: 700;
  font-size: var(--text-sm);
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  opacity: 0;
  transform: scale(0.85);
  transition: transform 220ms var(--ease-spring, ease), opacity 220ms ease, box-shadow 160ms ease, background-color 160ms ease;
}
.feedback-widget-trigger.feedback-widget-visible { opacity: 1; transform: scale(1); }
.feedback-widget-trigger:hover { box-shadow: var(--shadow-xl); background: var(--color-accent-600); }
.feedback-widget-trigger:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
:root[data-theme='dark'] .feedback-widget-trigger:focus-visible { outline-color: var(--color-accent); }
.feedback-widget-emoji { font-size: var(--text-lg); line-height: 1; }

.feedback-modal-card { max-width: 480px; width: 100%; position: relative; }
.feedback-modal-close { position: absolute; top: 16px; right: 16px; }
.feedback-modal-body { display: flex; flex-direction: column; gap: 16px; }
.feedback-type-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-right: 36px; }
.feedback-privacy-note { font-size: var(--text-xs); color: var(--color-text-muted); margin: 0; }

.feedback-type-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.feedback-type-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 20px 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  /* A <button> does not inherit `color`/`font` from the page by default (the
     UA stylesheet gives it its own `color: buttontext`, effectively black) —
     every custom button-like class in this app must set both explicitly, the
     same way the shared .btn base class does. Omitting this rendered black
     text on a dark-theme card, reported live (issue #9 follow-up). */
  color: var(--color-text);
  font: inherit;
  font-weight: 700;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color 140ms ease, transform 140ms ease;
}
.feedback-type-card:hover { border-color: var(--color-accent); transform: translateY(-1px); }
.feedback-type-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* issue #155 v2 Phase D3 — modal-scoped recolor; border/outline colors carry no
   text-contrast requirement of their own, so a straight token swap is safe here
   (unlike .import-step-badge above). Falls back to mint in light theme. */
:root[data-theme='dark'] .feedback-type-card:hover { border-color: var(--color-accent); }
:root[data-theme='dark'] .feedback-type-card:focus-visible { outline-color: var(--color-accent); }
.feedback-type-emoji { font-size: var(--text-2xl); }

.feedback-form { display: flex; flex-direction: column; gap: 16px; }
.feedback-field { display: flex; flex-direction: column; gap: 6px; }
.feedback-field-input { min-height: 42px; }
textarea.feedback-field-input { min-height: 88px; resize: vertical; }
.feedback-char-counter { align-self: flex-end; font-size: var(--text-xs); color: var(--color-text-muted); }
.feedback-field-input.field-input-invalid { border-color: var(--color-danger); }

/* issue #206 §5 follow-up — real feedback (screenshots): the Severity/
   frequency radio groups rendered as a plain bare `<fieldset>` (the
   browser's own inset-groove border + default legend padding, both never
   reset) around unstyled OS-native radio circles — a jarring, off-theme box
   next to every other themed field in the same modal. `.field.feedback-
   field` used as a `<fieldset>` className needed its own reset since
   `.field`'s existing rules (used everywhere else on a plain `<label>`/
   `<div>`) never accounted for `<fieldset>`'s UA-stylesheet defaults. */
.feedback-field.feedback-radio-fieldset { border: none; padding: 0; margin: 0; min-width: 0; }
.feedback-field.feedback-radio-fieldset > legend { padding: 0; }
.feedback-radio-group { display: flex; flex-direction: column; gap: 10px; }
.feedback-radio-option { display: flex; align-items: center; gap: 10px; font-size: var(--text-sm); color: var(--color-text); cursor: pointer; }
/* Custom-styled radio circle matching `.check-box`'s visual language
   (border → filled dot on check, same corner-radius/border-width idiom,
   just circular) instead of the unstyled OS-native control the screenshots
   showed. `appearance: none` strips every browser's native rendering
   uniformly; the filled dot is a `::before` positioned via the radio's own
   `position: relative` rather than a wrapping span, so no extra DOM node is
   needed per option. */
.feedback-radio-option input[type='radio'] {
  appearance: none;
  position: relative;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1.5px solid var(--color-border-strong);
  border-radius: 50%;
  background: var(--color-surface);
  flex-shrink: 0;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.feedback-radio-option input[type='radio']:checked { border-color: var(--color-accent); }
.feedback-radio-option input[type='radio']:checked::before {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--color-accent);
}
.feedback-radio-option input[type='radio']:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.feedback-system-info-checkbox { display: flex; align-items: flex-start; gap: 8px; font-size: var(--text-sm); color: var(--color-text); cursor: pointer; }
/* issue #206 §5 follow-up — was an unstyled native checkbox (OS-default blue
   check in the screenshots); `accent-color` recolors the native control to
   match the theme with no custom widget needed, same convention
   `.remember-checkbox`/`.daily-todo-checkbox input[type='checkbox']`
   already established elsewhere in this file. */
.feedback-system-info-checkbox input[type='checkbox'] { width: 16px; height: 16px; margin-top: 2px; accent-color: var(--color-accent); flex-shrink: 0; cursor: pointer; }
.feedback-system-info-summary { color: var(--color-text-muted); font-size: var(--text-xs); }

.feedback-form-actions { display: flex; gap: 12px; justify-content: flex-end; }
.feedback-cooldown-message { text-align: right; }

.feedback-success { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 12px 0; }
.feedback-success-icon { font-size: var(--text-4xl); }
.feedback-reference { font-family: var(--font-mono); font-weight: 700; color: var(--color-accent-700); }
/* issue #155 v2 Phase D3 — modal-scoped recolor; falls back to mint in light theme. */
:root[data-theme='dark'] .feedback-reference { color: var(--color-accent-700); }

.my-reports-list { display: flex; flex-direction: column; gap: 8px; max-height: 60vh; overflow-y: auto; }
.my-report-row { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; }
.my-report-summary { width: 100%; text-align: left; padding: 12px 14px; background: var(--color-surface-raised); color: var(--color-text); font: inherit; border: none; cursor: pointer; display: flex; flex-direction: column; gap: 4px; }
.my-report-title-row { display: flex; align-items: center; gap: 8px; font-weight: 700; }
.my-report-title { flex: 1; }
.my-report-meta { font-size: var(--text-xs); color: var(--color-text-muted); }
.my-report-severity { font-size: var(--text-xs); font-weight: 800; padding: 2px 8px; border-radius: 0; background: var(--color-danger-bg); color: var(--color-danger); }
.my-report-severity-low, .my-report-severity-medium { background: var(--color-warning-bg); color: var(--color-warning); }
.my-report-details { padding: 0 14px 14px; font-size: var(--text-sm); color: var(--color-text); display: flex; flex-direction: column; gap: 6px; }

/* Collapse the feedback trigger to icon-only below 480px (issue #9 §2.1) —
   the full "Feedback" pill is wide enough to crowd a phone-width corner
   otherwise. Stays a square (radius 0), not a circular FAB — design-system.md
   §5's "solid accent square" rule applies at every width, not just desktop
   (issue #301 follow-up; this was a leftover circular-FAB treatment from
   before that rule existed). */
@media (max-width: 480px) {
  .feedback-widget-label { display: none; }
  .feedback-widget-trigger { padding: 12px; }
}

@media (max-width: 375px) {
  .dashboard-content { padding: 14px 10px 88px; }
  .modal-card { padding: 26px 20px 22px; }
}

/* iOS Safari auto-zooms the page when a focused input's computed font size
   is under 16px. Scoped to phone/tablet widths (not a hover/pointer feature —
   there's no reliable way to detect "is this WebKit's iOS text-zoom
   behavior" — 1024px comfortably covers phones and tablets in either
   orientation without changing the denser desktop field styling). */
@media (max-width: 1024px) {
  .field-input,
  .field-input.compact,
  .search-input,
  .import-paste-area,
  .feedback-field-input {
    font-size: 16px;
  }
}

/* Touch targets — WCAG 2.5.5 / platform guidance recommend ~44x44px minimum.
   Gated on actual pointer capability, not viewport width (see the note atop
   the breakpoint scale above). Sized to 46px, not exactly 44px (issue #233):
   sub-pixel/DPI layout rounding in real browsers (and headless Chromium under
   CI) can measure an element sitting exactly on the 44px boundary as
   fractionally under it (e.g. 43.999996px), failing a strict `>= 44`
   assertion — reproduced live in PR #231's CI run, which passed only on
   Playwright retry. 2px of headroom absorbs that rounding without changing
   the visual intent of "~44px minimum". */
@media (pointer: coarse) {
  .btn-icon { width: 46px; height: 46px; }
  .btn-sm { min-height: 46px; }
  .filter-chip { min-height: 46px; }
  .check-item { min-height: 46px; }
  .daily-todo-delete, .daily-todo-info-btn, .daily-todo-collapse-btn, .daily-todo-reminder-btn { width: 46px; height: 46px; }
  .feedback-widget-trigger { min-height: 46px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Print/PDF export of a roadmap (issue #133 Part 2, restyled/branded in issue
   #160, restructured onto a real <table>/<thead>/<tfoot> and given a page
   watermark + browser-chrome suppression in a follow-up — see the block
   comments inside @media print below) — a dedicated print stylesheet, not a
   separate route: printRoadmap.js appends a `.print-roadmap` <table> snapshot
   (with a repeating `.print-page-header`/`.print-page-footer` brand pair
   inside its <thead>/<tfoot>) plus a sibling `.print-watermark` layer to
   document.body, and toggles `.print-mode` on it just for the duration of
   window.print(). `.print-include-notes-row` is the small on-screen checkbox
   in the pre-print modal; everything else here only applies inside an actual
   print/PDF context. */
.print-include-notes-row { display: flex; align-items: center; gap: 8px; margin: 16px 0; }
.print-roadmap,
.print-watermark { display: none; }

/* intentional: every literal color in this @media print block is fixed,
   theme-independent print output — a printed page always renders on
   assumed-white paper/PDF viewer background regardless of the app's active
   on-screen theme, so none of these should ever read from a --token that
   could resolve to a dark-theme value. The phase/priority hues below are the
   same P0-P3 hues the on-screen light theme uses (--color-p0..--color-p3,
   :root), copied as literals rather than tokens since a print stylesheet
   must never resolve to the dark-theme values of those same custom
   properties.
   Retuned in issue #301 (Phase 5) — the previous literals here (#dc2626
   red/#b45309 amber/#2563eb blue/#15803d green) were a leftover four-hue
   rainbow palette from well before the v2 "Modernist" single-accent+neutral
   redesign; the comment claiming they "matched --color-p0..--color-p3" had
   gone stale the moment Phase 1 (#297) repointed those tokens to
   accent/accent-400/neutral-500/neutral-300, and nothing print-specific was
   touched in any phase since. A solid-fill-plus-white-text badge (the old
   approach) cannot work with the new palette at all — re-verified via a real
   relative-luminance calculation, white text on these current tokens'
   literal hex values (#EC3013/#FF9783/#9B9797/#D7D3D3) measures only
   4.20/2.10/2.89/1.48:1, all failing WCAG AA. Print badges now follow this
   app's own on-screen `.badge` pattern instead — a light ~15% tint
   background with full-opacity ink-variant text, not a solid fill — which is
   exactly what already makes the on-screen badges contrast-safe: text in
   #7C1405/#AE1800/#444141/#605D5D (the light theme's --color-p0-ink through
   --color-p3-ink) on their own literal ~15%-tint-on-white background
   (#fce0dc/#ffefec/#f0efef/#f9f8f8) measures 10.72/7.17/10.10/6.52:1, all
   comfortably passing. Phase border-left accents (no text-contrast concern,
   just a colored rule) use the base hue directly:
   #EC3013/#FF9783/#9B9797/#D7D3D3. */
@media print {
  /* `@page { margin: 0 }` removes the blank margin band Chrome/Firefox
     otherwise reserve for their own browser-injected print chrome — the
     page URL and date/time in the top corners, the URL and page-number in
     the bottom corners (visible in a real export as "localhost:4173/#/app"
     during local dev — real feedback, screenshot). With no margin space to
     paint into, that chrome no longer renders at all, in both engines. This
     also means our own header/footer/content no longer get any "free"
     side/edge padding from the browser's default page margin — see the
     `padding: 0 40px`/`36px` additions below the table rules, which replace
     it. A user's own browser-level "Headers and footers" print-dialog
     toggle is a separate setting this can't reach either way — this fix
     just makes sure there's no default browser margin left for that chrome
     to use even if a user does have it enabled. */
  @page {
    margin: 0;
  }

  /* `.print-roadmap` is a real <table>, with the branded header/footer
     inside a genuine <thead>/<tfoot> (printRoadmap.js) — not a `position:
     fixed` div pair. Two fixed-positioning approaches were tried and
     rejected first, both confirmed broken via a real multi-page `page.pdf()`
     render, not just visual inspection: (1) `position: fixed` header/footer
     with `.print-roadmap`'s own padding reserving clearance — that padding
     only applies once, at the very top/bottom of the whole flowed document,
     so pages after the first flowed content flush against the page edge,
     underneath the fixed header (reported live via a 47-page multi-phase
     roadmap export, "Core Java (P0)" overlapping the brand header on every
     page after the first). (2) `position: fixed` + an `@page` margin to
     reserve that same space on every page — this looked like the correct
     fix, but Chromium's print pagination positions `fixed` elements
     relative to the *content* box (inside the page margin), not the page
     edge, so a fixed header at `top: 0` still landed at the same coordinate
     the flowing content starts at, reproducing the identical overlap
     regardless of margin size. `<thead>`/`<tfoot>` is the browser-native,
     spec-backed mechanism for exactly this ("repeat this row and reserve
     space for it on every printed page") and was the only one of the three
     that a real multi-page PDF render confirmed clean on every page.
     `.print-roadmap` itself deliberately carries no `background` (unlike an
     earlier version) — a printed page is white paper regardless, and an
     opaque table background would paint over `.print-watermark` (below),
     which needs to show through from underneath the flowing content. */
  body.print-mode > *:not(.print-roadmap):not(.print-watermark) { display: none !important; }

  /* Fallback for a native browser/OS print invoked directly (mobile Chrome's
     own three-dot "Print…", Ctrl/Cmd+P, a print button outside this app's
     own "Print roadmap…" flow) — i.e. `body.print-mode` was never toggled.
     Without this, `.app-sidebar` (`position: sticky; height: 100dvh;
     overflow-y: auto`) and `.app-topbar` (`position: sticky`) get paginated
     as their own single viewport-height slice by the print engine, while
     `.app-shell-main`'s actual content — a sibling grid column, not a
     descendant of the scroll-clipped sidebar — renders empty on every page:
     real bug, reported live from a phone, printing the onboarding page
     showed two near-blank pages containing only the sidebar nav labels.
     Hide the app chrome and reset every height/overflow constraint so
     whatever page is on screen flows and paginates normally instead. */
  body:not(.print-mode) .app-sidebar,
  body:not(.print-mode) .app-sidebar-backdrop,
  body:not(.print-mode) .app-topbar-hamburger,
  body:not(.print-mode) .command-palette,
  body:not(.print-mode) .feedback-widget-trigger,
  body:not(.print-mode) .save-badge,
  body:not(.print-mode) .toast-stack,
  body:not(.print-mode) .tour-scrim,
  body:not(.print-mode) .tour-ring,
  body:not(.print-mode) .tour-popover {
    display: none !important;
  }
  body:not(.print-mode) .app-shell-2,
  body:not(.print-mode) .app-shell-main,
  body:not(.print-mode) .app-topbar,
  body:not(.print-mode) .dashboard-content,
  body:not(.print-mode) .progress-content,
  body:not(.print-mode) .onboarding-page,
  body:not(.print-mode) .onboarding-inner {
    display: block !important;
    position: static !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }

  body.print-mode .print-roadmap {
    display: table;
    width: 100%;
    border-collapse: collapse;
    color: #111111; /* intentional: fixed print text color, see block comment above */
    position: relative;
    z-index: 1;
  }
  .print-roadmap thead td,
  .print-roadmap tfoot td { padding: 0 40px; }
  .print-roadmap tbody td { padding: 0 40px 24px; }

  /* A faint, blurred repeating watermark behind the content on every page —
     `position: fixed` repeats correctly per page on its own (confirmed
     during the header/footer investigation above); it doesn't need the
     `<thead>`/`<tfoot>` space-reservation trick since overlapping the
     content is the intent here, not a bug to avoid. Sized and centered
     against a Letter-ish page (816×1056px at 96dpi) so it reads as one
     centered mark per page rather than drifting off-page on a different
     paper size — `width`/`height: 100vw`/`100vh` plus flex-centering keeps
     it centered regardless of actual page dimensions. Low opacity + blur
     keeps the underlying checklist text legible on top of it, per the
     explicit "both should be visible" requirement. `print-color-adjust`
     isn't needed here (opacity/color always print by default, unlike
     `background-color`/`box-shadow`, which need the browser's "background
     graphics" print option enabled) — this deliberately uses `color`, not a
     `background`, so it always renders, that option or not. */
  .print-watermark {
    display: flex;
    position: fixed;
    inset: 0;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    z-index: 0;
    filter: blur(2px);
  }
  .print-watermark-mark {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    opacity: 0.07;
    color: #111111; /* intentional: fixed print watermark color, see block comment above */
  }
  .print-watermark-mark .brand-mark {
    width: 160px;
    height: 160px;
    box-shadow: none;
  }
  .print-watermark-mark .brand-mark svg { width: 100%; height: 100%; }
  .print-watermark-mark .brand-name {
    font-size: 64px;
    color: #111111; /* intentional: fixed print watermark color, see block comment above */
  }

  .print-page-header {
    display: flex;
    align-items: center;
    padding: 20px 0 12px;
    border-bottom: 1px solid #ccc; /* intentional: fixed print divider color */
  }
  .print-brand .brand-name,
  .print-brand .brand-tagline { color: #111111; } /* intentional: fixed print text color */
  .print-brand .brand-mark { box-shadow: none; }

  .print-page-footer {
    padding: 8px 0 20px;
    border-top: 1px solid #ccc; /* intentional: fixed print divider color */
    font-size: 11px;
    color: #666; /* intentional: fixed print text color */
    text-align: center;
  }

  .print-roadmap-title { font-size: 22px; margin: 0 0 16px; }
  .print-phase {
    margin: 0 0 16px;
    padding-left: 12px;
    border-left: 4px solid #94a3b8; /* intentional: fixed neutral print accent, phase priority unmapped */
    page-break-inside: avoid;
  }
  .print-phase-P0 { border-left-color: #EC3013; } /* intentional: fixed print accent, matches --color-p0 (light theme) */
  .print-phase-P1 { border-left-color: #FF9783; } /* intentional: fixed print accent, matches --color-p1 (light theme) */
  .print-phase-P2 { border-left-color: #9B9797; } /* intentional: fixed print accent, matches --color-p2 (light theme) */
  .print-phase-P3 { border-left-color: #D7D3D3; } /* intentional: fixed print accent, matches --color-p3 (light theme) */
  .print-phase-title { font-size: 18px; margin: 20px 0 8px; }
  .print-section-title {
    font-size: 12px;
    margin: 14px 0 6px;
    color: #444; /* intentional: fixed print text color */
    text-transform: uppercase;
    letter-spacing: 0.06em;
    border-bottom: 1px solid #ddd; /* intentional: fixed print divider color */
    padding-bottom: 4px;
  }
  .print-item-list { list-style: none; margin: 0; padding: 0; }
  .print-item { margin: 6px 0; page-break-inside: avoid; }
  .print-priority-badge {
    display: inline-block;
    font-size: 11px;
    font-weight: 700;
    padding: 1px 8px;
    margin-left: 6px;
    /* radius 0 — design-system.md §4, matches every on-screen chip/badge */
  }
  .print-priority-badge.print-priority-P0 { background: #fce0dc; color: #7C1405; } /* intentional: fixed print tint+ink, matches --color-p0-bg/-ink (light theme) */
  .print-priority-badge.print-priority-P1 { background: #ffefec; color: #AE1800; } /* intentional: fixed print tint+ink, matches --color-p1-bg/-ink (light theme) */
  .print-priority-badge.print-priority-P2 { background: #f0efef; color: #444141; } /* intentional: fixed print tint+ink, matches --color-p2-bg/-ink (light theme) */
  .print-priority-badge.print-priority-P3 { background: #f9f8f8; color: #605D5D; } /* intentional: fixed print tint+ink, matches --color-p3-bg/-ink (light theme) */
  .print-item-notes { font-size: 12px; color: #333; margin: 2px 0 2px 24px; white-space: pre-wrap; /* intentional: fixed print text color */ }
  .print-resource { font-size: 12px; color: #333; margin: 2px 0 2px 24px; font-style: italic; /* intentional: fixed print text color */ }
}
