/* Task tracker — terminal aesthetic.
   Palette inspired by a dark terminal: near-black bg, phosphor-green accent,
   warm amber for "today", red for overdue, muted grey ink for secondary. */

:root {
  /* color-scheme is the source of truth for native form controls
     (selects, time/date pickers, autofill, scrollbars). Default is
     dark; light themes override below. Without this, the OS's
     system preference wins and a `<select>` dropdown on a dark page
     renders with the OS light dropdown — out of sync with the theme. */
  color-scheme: dark;
  /* Terminal palette — every level was nudged to address a real
     discoverability issue users hit on the live deploy:
     - Cards barely separated from --bg (was #151815 vs #0a0b0a, ~1.8% L gap;
       now #1a1d1a, ~2.7% gap so cards visibly sit ON the background).
     - Hover cards barely changed (--bg-card-hov was 1.2% above bg-card; now
       ~3% above so the hover affordance reads).
     - Borders nearly invisible (--line was ~5% L on 0.7% bg, ~2:1 contrast;
       now ~9% so cards have a discernible edge).
     - Faint metadata text was ~3.5:1 contrast — failed WCAG AA. Bumping
       --ink-faint to ~22% L (was ~12%) brings every "+done count", "due in
       2d", "Inbox / Personal" label, schedule placeholder, etc up to ~5:1.
     User reported "new tasks weren't visible although they were clearly
     there" — the root cause was --ink-faint + --line-strong both vanishing
     into the deep-green bg, not the .new-task rule itself. */
  --bg:          #0a0b0a;
  --bg-raised:   #121412;
  --bg-cell:     #101110;
  --bg-card:     #1a1d1a;       /* was #151815 — clearer card elevation */
  --bg-card-hov: #232723;       /* was #1b1f1b — clearer hover affordance */
  --line:        #2a2f2a;       /* SOFT terminal-CRT line (1.44:1 on bg). Reverted from #686d68 WCAG-AA lift 2026-05-17 v3: terminal aesthetic doesn't have grey-spreadsheet borders; cards separate via bg-card tonal layering. See DESIGN.md "Soft-Border Theme Identity" rule. */
  --line-strong: #3a423a;       /* SOFT default-button border (1.90:1 on bg). Same Soft-Border tradeoff — slightly more present than --line for buttons + focused states but still terminal-soft. */
  --ink:         #e6ebe2;       /* design pass §2 — was #d7e0d4, brighter so titles win the hierarchy */
  --ink-dim:     #a3ada1;       /* design pass §2 — was #8a958a, lifted so secondary doesn't fight primary */
  --ink-faint:   #889388;       /* WCAG-AA on worst-case --bg-card-hov (4.74:1); was #7a857a = 3.94 — borderline on hover. Audit 2026-05-17. */

  --accent:       #5ef07a;
  --accent-glow:  #5ef07a44;    /* was 33 (20% alpha) — slightly punchier focus glow */
  --accent-dim:   #3a8a48;
  --amber:       #f0b95e;
  --amber-glow:  #f0b95e22;
  --red:         #ff6a6a;
  --red-glow:    #ff6a6a22;
  --blue:        #7ec7ff;
  --violet:      #c38bff;

  /* ---- Accent-as-text variants (audit 2026-05-17) -----------------------
     Same hue family as the fill/border token, AA-compliant when used as
     `color: var(--X-text)`. The fill tokens (--accent etc.) keep their
     existing values for borders, backgrounds, focus glow, drag affordance.
     :root aliases the text variants to the fill tokens — terminal theme
     passes 4.5:1 as text on its dark bgs, so no separate value needed.
     Themes override only where the fill token fails text contrast. */
  --accent-text:  var(--accent);
  --amber-text:   var(--amber);
  --red-text:     var(--red);
  --blue-text:    var(--blue);
  --break-text:   var(--break);
  --violet-text:  var(--violet);
  /* Break-overdue chip — its own semantic so themes can tune the
     "time to rest" hue without touching --red, which is reserved for
     destructive / blocked / overdue-task semantics. Default theme
     softens the alarm-red into a coral that still pulls focus but
     doesn't read as "danger". Each theme overrides if its --red feels
     too hot for break framing. */
  --break:       #ff8a5a;
  --break-glow:  #ff8a5a22;

  /* App-wide default body font — monospace by design (terminal aesthetic); the font picker overrides --mono per-user. */
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  --radius: 3px;

  /* ---- Type scale (design pass §1) -----------------------------------------
     Five roles. Body (14/500) carries every task title — the primary signal.
     Strong (600) is reserved for section headers + focused titles. Captions
     (11) are never bolded. Sweep `font-size:` literals onto these vars. */
  --fs-display:  22px;
  --fs-section:  15px;
  --fs-body:     14px;
  --fs-meta:     12px;
  --fs-caption:  11px;
  --fw-regular:  400;
  --fw-medium:   500;
  --fw-strong:   600;

  /* ---- Ink roles (design pass §2) ------------------------------------------
     Four levels of ink. Old --ink/--ink-dim/--ink-faint stay for back-compat
     (referenced in ~hundreds of places); the new role names alias to them via
     var() so each theme automatically gets a coherent ink hierarchy without
     re-declaring per theme. New code should prefer the role names. */
  --ink-primary:   var(--ink);          /* titles, brand, focused content */
  --ink-secondary: var(--ink-dim);      /* notes preview, longer-form supporting text */
  --ink-tertiary:  var(--ink-faint);    /* IDs, captions, separators */
  --ink-quiet:     color-mix(in oklab, var(--ink-faint) 50%, transparent);  /* empty placeholders */

  /* ---- Stacking context scale ---------------------------------------------
     One named token per overlay role. Adding a new overlay? Pick the role it
     fits and reuse the token; don't invent a new numeric value. The literals
     below are kept for backwards-compat with the few places (in-flow blocks,
     hover-promoted cards) that don't need names. */
  --z-promoted:        200;   /* drag visuals, FAB, hover-promoted blocks */
  --z-drawer-backdrop: 250;
  --z-drawer:          260;
  --z-palette-backdrop:300;   /* command palette dim */
  --z-popover:         400;   /* legend, notif, due, asg, date popovers */
  --z-pop-elevated:    600;   /* theme / zoom / appearance / stats popovers */
  --z-scanlines:      1000;   /* CRT effect, pointer-events: none */
  --z-tour-spotlight: 9998;
  --z-tour-tip:       9999;
  --z-overlay-top:   10000;   /* drag ghost, hover preview, app modal */
}

* { box-sizing: border-box; }

/* ---- RTL / i18n overrides -------------------------------------------------
   Layout uses logical properties above, so most rules flip for free when
   <html dir="rtl"> is set. These overrides cover the stragglers: things CSS
   logical properties don't handle — horizontal transforms, directional
   chevrons, and an Arabic-friendly font fallback.

   Numerals inside RTL-direction text (timers, IDs) get `direction: ltr` via
   the `.ltr-num` / `<bdi>` helpers so "25:00" never renders mirrored. */
html[dir="rtl"] body {
  font-family: "IBM Plex Sans Arabic", "SF Arabic", "Noto Naskh Arabic",
               var(--mono);
}

/* Per-locale script-specific font fallbacks. Order: var(--mono) FIRST so
   the user's picked code font handles Latin/digits; the script font kicks
   in only for glyphs the picked font lacks (per-character font fallback,
   not per-string). The :lang() pseudo-class matches via <html lang="...">,
   which i18n.js keeps in sync with state.settings.locale. */
html:lang(ja) body {
  font-family: var(--mono), "Noto Sans JP", "Hiragino Kaku Gothic ProN",
               "Yu Gothic", Meiryo, sans-serif;
}
html:lang(zh-CN) body {
  font-family: var(--mono), "Noto Sans SC", "PingFang SC", "Microsoft YaHei",
               "Heiti SC", sans-serif;
}
html:lang(hi) body {
  font-family: var(--mono), "Noto Sans Devanagari", Mangal,
               "Kohinoor Devanagari", sans-serif;
}
html:lang(bn) body {
  font-family: var(--mono), "Noto Sans Bengali", Vrinda,
               "Bangla Sangam MN", sans-serif;
}
html[dir="rtl"] .chev,
html[dir="rtl"] .chev-right,
html[dir="rtl"] .quote-next {
  transform: scaleX(-1);
  display: inline-block;
}
.ltr-num,
html[dir="rtl"] .elapsed,
html[dir="rtl"] .timer,
html[dir="rtl"] .time,
html[dir="rtl"] time,
html[dir="rtl"] code,
html[dir="rtl"] kbd {
  direction: ltr;
  unicode-bidi: isolate;
}

/* The `hidden` HTML attribute normally maps to display:none, but class-level
   display:flex on .palette-backdrop and .toast wins by specificity. Force it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  font-feature-settings: "liga" 0, "calt" 0;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Clip any horizontal overflow at the page level. Prevents wide-metrics
   fonts (Press Start 2P, Major Mono Display, etc.) from making the body
   wider than the viewport — which would force a horizontal scrollbar
   AND visually shift `position: fixed` elements like the settings drawer
   when the user scrolls. Vertical scroll on body continues to work, so
   sticky positioning on .topbar is preserved. */
/* Overflow-x lives on html only — setting it on body too forces body's
   overflow-y to compute to `auto`, which makes BODY a scroll container.
   Sticky elements (topbar, active-now, filter-bar, col-headers,
   project-label) all use the nearest scroll ancestor as their pin
   reference — and the nearest one becomes BODY, whose scrollTop stays
   at 0 because the actual page scroll happens on HTML (viewport
   propagation). Net effect: every sticky element silently scrolls
   away. Keeping the rule on html only leaves body with default
   overflow:visible, so sticky correctly references HTML's scroll. */
html {
  overflow-x: hidden;
  /* Always reserve a vertical-scrollbar gutter, whether or not the page
     actually scrolls. Without this, switching from board (page scrolls,
     gutter present, ~15px) to calendar (no page scroll, no gutter) shifts
     the entire viewport's content rightward by the scrollbar width. With
     it, layout width stays constant across the toggle. */
  scrollbar-gutter: stable;
}

/* Subtle CRT scanline vibe, barely visible */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0 2px,
    rgba(255,255,255,0.008) 2px 3px
  );
  z-index: var(--z-scanlines);
}

/* iOS Safari safe-area backdrops — paint solid --bg-raised behind the
   translucent top chrome (notch / dynamic island / sliding URL bar) and
   bottom URL bar. With viewport-fit=cover the layout viewport extends
   under those strips, and Safari's chrome is glass-blurred over whatever
   is rendered beneath. The .topbar (z 20) and .statusbar (z 21) already
   cover most of these regions; these html-level fills catch the gaps —
   views that hide one or both bars (calendar, drawers), and the
   transient blink while iOS Safari's URL bar slides on scroll. Sits
   below both bars so they paint on top normally. */
html::before,
html::after {
  content: "";
  position: fixed;
  inset-inline: 0;
  background: var(--bg-raised);
  pointer-events: none;
  z-index: 19;
}
html::before { top: 0;    height: env(safe-area-inset-top); }
/* Bottom safe-area: paint var(--bg) (a step darker than the topbar's
   --bg-raised) so the iOS home-indicator strip reads as a dimmer
   "shelf" below the statusbar, rather than the same shade as the
   chips. Visible (a) when the statusbar is hidden — calendar / drawer
   views — and (b) through the new statusbar safe-area gradient below. */
html::after  { bottom: 0; height: env(safe-area-inset-bottom); background: var(--bg); }

/* Design pass §6/§8 — buttons drop the resting border. Hover lifts onto
   a quiet bg tint; focus-visible adds an accent ring for keyboard users.
   Components that need a permanent edge (modals, palette, primary CTAs)
   re-add it explicitly via their own selector. */
button {
  font: inherit;
  color: inherit;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 4px 10px;
  cursor: pointer;
}
button:hover { background: var(--bg-raised); }
button:focus-visible { border-color: var(--accent); outline: none; }

input, textarea {
  font: inherit;
  color: var(--ink);
  background: transparent;
  border: none;
  outline: none;
  width: 100%;
}

kbd {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-dim);
  background: var(--bg-raised);
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 3px;
  padding: 1px 5px;
  margin: 0 1px;
}

/* ---------- Layout ---------- */

#app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.topbar {
  /* Flex (not grid) so the brand + filter cluster naturally on the left
     and the actions push to the right via margin-inline-start: auto on
     .topbar-actions. The earlier grid (auto | 1fr | auto) made the filter
     float in the middle of a stretched 1fr cell, which read as "unnatural
     dead space" between brand and filter.

     Single row, NO wrap. The redesigned action cluster is icon-led and
     small enough to always fit; width pressure is absorbed by the filter
     input shrinking (flex 0 1 320px, 180px min). Wrapping the action
     cluster to a second row was the 115%-zoom bug this redesign fixes. */
  display: flex;
  align-items: center;
  gap: 12px;
  /* iOS Safari (viewport-fit=cover): the layout viewport extends behind
     the notch / dynamic island / translucent URL bar at the top. Pad the
     topbar by `env(safe-area-inset-top)` so its --bg-raised fill paints
     under that strip too — without this the kanban cells that scroll
     under the topbar bleed through Safari's blurred top chrome. */
  padding: max(10px, env(safe-area-inset-top)) 20px 10px;
  /* Lock the topbar to the same outer height regardless of which view is
     active. Board view ships a `.filter-wrap` (~30px content) which makes
     `align-items: center` settle the bar at ~50px; calendar view drops the
     filter cluster entirely and the bar would shrink to ~42px on the icon-
     btns alone — the user-visible "header jumps shorter when I switch to
     plan-your-day" bug. 52px matches the canonical value the rest of the
     stylesheet already assumes (see --sticky-top-col-headers default in
     `.col-headers`), and `box-sizing: border-box` globally means iOS's
     safe-area padding still pushes the bar taller on devices with a notch
     instead of being absorbed into the floor. */
  min-height: 52px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-raised);
  position: sticky;
  top: 0;
  z-index: 20;
}

/* Motivational quote strip — one line, click to cycle. */
.quote-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 20px;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(to right, transparent, var(--bg-raised) 30%, var(--bg-raised) 70%, transparent);
  font-size: 13px;
  color: var(--ink-dim);
  font-style: italic;
  cursor: pointer;
  user-select: none;
  transition: color 120ms;
}
.quote-bar:hover { color: var(--accent-text); }
.quote-bar .quote-mark {
  color: var(--accent-text);
  font-style: normal;
  font-size: 15px;
  opacity: 0.85;
}
.quote-bar .quote-tag.tip {
  font-style: normal;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 2px 6px;
  border: 1px solid var(--accent-dim);
  border-radius: 3px;
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.quote-bar .quote-text { flex: 0 1 auto; }
.quote-bar .quote-author {
  color: var(--accent-dim);
  font-style: normal;
  opacity: 0.7;
  font-size: 11px;
}
.quote-bar .quote-next {
  margin-inline-start: auto;
  color: var(--accent-dim);
  font-style: normal;
  opacity: 0;
  transition: opacity 120ms;
  font-size: 14px;
}
.quote-bar:hover .quote-next { opacity: 0.7; }
/* Collapse / expand chevron. Lives at the far right of the bar in expanded
   mode and is the only visible element in collapsed mode. */
.quote-bar .quote-toggle {
  background: transparent;
  border: 0;
  color: var(--ink-faint);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 6px;
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: color 120ms, background 120ms;
  font-family: inherit;
}
.quote-bar .quote-toggle:hover { color: var(--accent-text); background: var(--accent-glow); }
.quote-bar .quote-toggle .chev { font-size: 10px; line-height: 1; }

.quote-bar.collapsed {
  padding: 2px 20px;
  cursor: default;
  background: var(--bg-raised);
  font-style: normal;
  justify-content: flex-end;
}
.quote-bar.collapsed .quote-toggle { margin-inline-start: 0; }

.brand {
  display: flex;
  align-items: center;
  color: var(--accent-text);
  flex: 0 0 auto;
}
/* Tappable when activeView is calendar — clicking the logo returns to
   the board. Cursor + focus ring only when the data attribute is set
   so it stays inert on the board view. */
.brand[data-clickable="1"] { cursor: pointer; }
.brand[data-clickable="1"]:hover .prompt { opacity: 0.7; }
.brand[data-clickable="1"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Full opacity — this is the brand name, not the dim shell-prompt the
   ~/todo wordmark used to be. */
.brand .prompt { color: var(--accent-text); }
/* The "." of aro.day — the brand accent. It pulses slowly (the brand's
   "alive" beat) rather than a separate cursor: a blinking cursor only
   made sense completing the old ~/todo $ shell prompt. */
.brand .brand-dot {
  color: var(--accent);
  animation: brand-dot-pulse 2.4s ease-in-out infinite;
}
@keyframes brand-dot-pulse { 50% { opacity: 0.4; } }
/* Calm, but it is persistent chrome — honour reduced-motion. */
@media (prefers-reduced-motion: reduce) {
  .brand .brand-dot { animation: none; }
}

/* Blink keyframes for the tour boot screen's typewriter cursor
   (.tour-boot-caret). */
@keyframes terminal-cursor-blink { 50% { opacity: 0; } }

.filter-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 4px 10px;
  /* Sits right next to the brand in the flex topbar. Starts at 320px,
     can grow up to 420px, can shrink to 180px when the actions need
     more room. Without this flex shorthand the input would either
     stretch to fill all available space (no max-width effect) or
     collapse to its content min (~40px). */
  flex: 0 1 320px;
  min-width: 180px;
  max-width: 420px;
  background: var(--bg-cell);
}
.filter-wrap:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px var(--accent-glow); }
.filter-wrap .slash { color: var(--ink-faint); }
.filter-wrap input { flex: 1; }
.filter-wrap .hint { color: var(--ink-faint); font-size: 11px; }
.filter-wrap .filter-hidden-chip {
  background: color-mix(in oklab, var(--amber, #f5a623) 18%, transparent);
  color: var(--amber, #f5a623);
  border: 1px solid color-mix(in oklab, var(--amber, #f5a623) 35%, transparent);
  border-radius: 9px;
  padding: 1px 7px;
  font-size: 11px;
  font-family: inherit;
  line-height: 1.4;
  cursor: pointer;
  white-space: nowrap;
  margin-inline-start: 4px;
}
.filter-wrap .filter-hidden-chip:hover {
  background: color-mix(in oklab, var(--amber, #f5a623) 28%, transparent);
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-dim);
  font-size: 11px;
  /* margin-inline-start: auto pushes this group to the right edge of
     the flex topbar, so brand + filter cluster naturally on the left.
     flex: 0 0 auto holds it at its natural width — it never wraps and
     never compresses; the filter input absorbs all width pressure. */
  margin-inline-start: auto;
  flex: 0 0 auto;
}

/* Topbar stats chip — single compact button that replaces the 5 loose
   counter spans (backlog · doing · hold · running · overdue). Always
   fits the topbar at any viewport width / UI zoom. Shows an aggregate
   (todo + doing + hold), with inline running-timer and overdue-count
   pips when either is non-zero. Click to open a full-breakdown popover. */
.topbar-stats-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex-shrink: 0;
}
.topbar-stats-chip .stats-dot {
  color: var(--accent-text);
  font-size: 10px;
  line-height: 1;
  opacity: 0.9;
}
.topbar-stats-chip .stats-summary { color: var(--ink); }
.topbar-stats-chip .stats-chip-running {
  color: var(--accent-text);
  padding-inline-start: 4px;
  border-inline-start: 1px solid var(--line-strong);
}
.topbar-stats-chip .stats-chip-overdue {
  color: var(--red-text);
  padding-inline-start: 4px;
  border-inline-start: 1px solid var(--line-strong);
}
.topbar-stats-chip.has-overdue { border-color: color-mix(in oklab, var(--red) 50%, var(--line-strong)); }

/* Stats popover — same shell as .theme-pop, one row per counter. */
.stats-pop {
  min-width: 180px;
  padding: 6px;
}
.stats-pop .stats-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 4px 8px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.stats-pop .stats-row-n { color: var(--ink); }
/* 1px vertical divider between the primary nav button and the utility
   cluster. A real rule, not a `│` glyph — stays crisp at every zoom. */
.topbar-actions .sep {
  width: 1px;
  height: 18px;
  flex: 0 0 auto;
  background: var(--line-strong);
}
.topbar-actions .meta { color: var(--ink-faint); flex-shrink: 0; }
.topbar-actions > span { flex-shrink: 0; white-space: nowrap; }

/* Google Drive logo (ICON_GOOGLE_DRIVE) — keeps its brand colours; the
   one non-monochrome icon, used in the `⋯` overflow's Drive item. The
   overflow sizes it via `.tb-overflow-icon svg`; this is its default. */
.drive-svg {
  display: block;
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}

/* PWA standalone (Add to Home Screen) — `apple-mobile-web-app-status-bar-
   style: black-translucent` makes the iOS status bar overlay the page,
   and the home indicator sits at the bottom of the layout viewport.
   `env(safe-area-inset-*)` reports both insets correctly on modern iOS,
   but in some PWA contexts (older iOS versions, iPad split-view, certain
   landscape orientations) it underreports — the brand/clock then collide
   at the top, or the bottom chips slip under the home indicator.
   Floor the insets to safe minimums (44px top to clear the status bar,
   20px bottom to clear the home indicator) only inside standalone mode,
   so plain in-browser viewing keeps its current minimal padding. */
@media (display-mode: standalone) {
  .topbar {
    padding-block-start: max(env(safe-area-inset-top), 44px);
  }
  body {
    padding-block-end: calc(var(--statusbar-h) + max(env(safe-area-inset-bottom), 20px));
  }
  .statusbar {
    padding-block-end: max(env(safe-area-inset-bottom), 20px);
  }
  /* Strengthen the safe-area backdrop fills in standalone too so the
     iOS status-bar overlay and home indicator always sit on solid
     --bg-raised, not transparent over scrolling cards. */
  html::before { height: max(env(safe-area-inset-top), 44px); }
  html::after  { height: max(env(safe-area-inset-bottom), 20px); }
}

/* ===================================================================
   Bottom status bar
   ===================================================================
   Fixed status bar below the kanban that surfaces ambient state — counts,
   focus-session timer, today's tracked time, locale, and the legal links
   Google's OAuth review requires to be visible on the home page.
   Mounted at body level (NOT inside #app) and `position: fixed`.
   The body has matching padding-bottom (--statusbar-h) so the kanban
   reserves space for the fixed bar. */
:root { --statusbar-h: 34px; }
/* Default (board view): statusbar is fixed-positioned overlay at viewport
   bottom. Calendar view overrides this to in-flow sticky (see body.cal-active
   rules below) so the calendar grid physically can't extend behind the
   footer. Two architectures, one element, scoped via body class. */
.statusbar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 21;
  display: flex;
  align-items: center;
  gap: 12px;
  padding-inline: 16px;
  /* iOS Safari PWA: pad below the chips by the home-indicator height so
     the bar's content sits above the indicator instead of behind it.
     `box-sizing: border-box` + an explicit height would clip the safe
     area, so we keep height as min-height + add bottom padding. */
  min-height: var(--statusbar-h);
  padding-block-end: env(safe-area-inset-bottom);
  box-sizing: border-box;
  border-top: 1px solid var(--line);
  /* Two-tone background: --bg-raised behind the chips (where the user
     interacts) and --bg (a step darker) behind the iOS home-indicator
     strip so the safe-area zone reads as a dim base, not a continuation
     of the chip area. The hard color stop at `100% - safe-area-inset-bottom`
     means desktop / non-notched browsers (env() = 0) collapse cleanly
     to a single --bg-raised fill — no gradient artefacts. */
  background: linear-gradient(
    to bottom,
    var(--bg-raised) 0,
    var(--bg-raised) calc(100% - env(safe-area-inset-bottom)),
    var(--bg)        calc(100% - env(safe-area-inset-bottom)),
    var(--bg)        100%
  );
  color: var(--ink-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  pointer-events: auto;
}
/* The static-legal corner footer in index.html is kept in the DOM so
   Google's OAuth raw-HTML check still finds the privacy/terms links, but
   visually it's redundant now — the same links live in the status bar's
   right section. Hide the corner element so it doesn't double up. */
footer.static-legal { display: none !important; }
.statusbar-section {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.statusbar-section.statusbar-center {
  flex: 1 1 auto;
  justify-content: center;
}
.statusbar-section.statusbar-right { margin-inline-start: auto; }

/* Each chip in the status bar — same affordance as a topbar icon-btn but
   denser and with no resting border. Hover lifts it onto a slightly
   raised surface so the click target reads. */
.statusbar-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid transparent;
  border-radius: 3px;
  background: transparent;
  color: var(--ink-secondary);
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: background 100ms, color 100ms, border-color 100ms;
}
.statusbar-chip:hover { color: var(--accent-text); background: var(--bg-card-hov, var(--bg-raised)); }
.statusbar-chip:focus-visible { border-color: var(--accent); outline: none; }
.statusbar-chip .chip-icon { font-size: 12px; line-height: 1; }
.statusbar-chip .chip-tasks { color: var(--ink); }

/* Stats chip — reuses .topbar-stats-chip rules for inner pips. */
.statusbar-chip.topbar-stats-chip { padding: 3px 8px; }

/* Notifications — same dense layout, badge inherits .notif-badge styles. */
.statusbar-notifs .notif-badge { margin-inline-start: 0; }

/* Timer chip — three states share a base, modifiers tweak color + accent. */
.statusbar-timer .chip-play {
  color: var(--accent-text);
  font-size: 9px;
  line-height: 1;
}
.statusbar-timer .chip-timer-time {
  color: var(--accent-text);
  font-weight: 600;
}
.statusbar-timer.is-overdue .chip-timer-time,
.statusbar-timer.is-overdue .chip-play {
  color: var(--red-text);
}
.statusbar-timer.is-overdue {
  border-color: color-mix(in oklab, var(--red) 40%, transparent);
}
.statusbar-timer .chip-multi {
  color: var(--accent-text);
  font-size: 10px;
  margin-inline-start: 2px;
}
.statusbar-timer.is-onbreak {
  border-color: var(--accent-dim);
  background: color-mix(in oklab, var(--accent) 6%, transparent);
}
.statusbar-timer.is-onbreak .chip-timer-time { color: var(--accent-text); }

/* Break CTA next to the running timer chip — tighter, icon-only variant
   keeps the cluster narrow on narrow screens. */
.statusbar-break-cta.is-icon-only { padding: 3px 6px; }

/* Today-time chip — read-mostly. Slightly muted suffix ("today") to keep
   the duration as the dominant glyph. */
.statusbar-today .chip-today-time { color: var(--ink); font-weight: 500; }
.statusbar-today .chip-today-suffix { color: var(--ink-faint); }

/* Hide the idle-state #sb-timer chip on DESKTOP only. On desktop
   `.statusbar-today` (right side) already opens Reports → Time Log, so
   the idle `today: Xm` chip would just be another copy. On MOBILE
   `.statusbar-today` is `display: none` (see the phone media block
   below) — the idle chip stays visible there as the only statusbar
   entry to Reports → Time Log. JS render is viewport-agnostic on
   purpose; visibility is decided in CSS. */
@media (min-width: 601px) {
  .statusbar-timer.is-idle { display: none; }
  /* Hide the bottom-left stats chip (●N queued+in-progress count) on
     DESKTOP. The same data is already glanceable from the kanban filter
     bar (per-status chip counts) and the topbar insights drawer; the
     statusbar chip just added visual noise. Mobile keeps it because the
     mobile topbar collapses into ⋯ with no stats indicator, and the
     kanban filter bar is hidden in calendar view — so on phones this
     chip is the only always-visible signal of how much is on the board.
     Hide the trailing separator too so the slot doesn't leave an
     orphaned divider next to the today chip. */
  .statusbar-left .topbar-stats-chip,
  .statusbar-left .topbar-stats-chip + .statusbar-sep { display: none; }
}

/* Locale chip — short uppercase code, looks like a tag. */
.statusbar-locale {
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.statusbar-locale .chip-locale-code { color: var(--ink-dim); }

/* Theme chip — colored accent dot + name. Chip background mirrors the
   active theme's own bg so it reads as a two-tone live swatch: bg in
   the chip body, accent as the circle. Border uses --line for definition
   against the status bar's --bg-raised fill. */
.statusbar-theme {
  background: var(--bg);
  border-color: var(--line);
}
.statusbar-theme .chip-tasks { font-size: 11px; }
/* Break-style chip — icon-only by default, name shown via tooltip. */
.statusbar-breakstyle .chip-icon { color: var(--accent-dim); }
.statusbar-sound .chip-icon { font-size: 13px; }

/* Vertical group separator — a thin 1px-wide line between sub-clusters
   in the status bar (left state pair, right settings cluster). Same
   visual vocabulary as the status bar's top border so dividers feel
   native to the strip. */
.statusbar-sep {
  display: inline-block;
  width: 1px;
  height: 16px;
  background: var(--line);
  flex-shrink: 0;
  margin-inline: 4px;
  align-self: center;
}

/* Privacy + Terms — quiet inline links at the trailing edge. The parent
   <span> wraps them so the separator dot sits on the same baseline as the
   anchor text without becoming a focusable target. */
.statusbar-legal {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-faint);
  font-size: 11px;
}
.statusbar-legal a {
  color: var(--ink-faint);
  text-decoration: none;
  transition: color 100ms;
}
.statusbar-legal a:hover { color: var(--accent-text); }
.statusbar-legal-sep { color: var(--ink-faint); opacity: 0.6; }

/* Narrow-viewport tweaks: collapse the today suffix and notification badge
   text below 640px; the status bar must never wrap. */
@media (max-width: 640px) {
  .statusbar {
    padding-inline: 10px;
    padding-block-start: 4px;
    /* Keep iOS safe-area inset on the bottom edge; the base rule already
       sets padding-block-end: env(safe-area-inset-bottom). */
    gap: 8px;
  }
  .statusbar-today .chip-today-suffix { display: none; }
  .statusbar-locale { padding: 3px 6px; }
  .statusbar-section.statusbar-center { flex-basis: auto; justify-content: flex-start; }
}

/* Phone viewports — the right section's customization chips
   (theme / break-style / calendar-hours / sound) all duplicate
   controls in the settings drawer, and on a 360–390px row they were
   pushed off-screen, leaving the centre's "take break" button clipped
   by the viewport edge in iOS Safari. Hide them on mobile and keep
   only the locale chip + legal links visible (legal is required by
   Google's OAuth review and the static fallback in index.html still
   covers no-JS / pre-render cases). */
@media (max-width: 600px) {
  .statusbar-theme,
  .statusbar-breakstyle,
  .statusbar-sound,
  .statusbar-right .statusbar-sep { display: none; }
  /* Phone widths can't fit stats + today-time + running-timer + break-CTA
     on one row without clipping "take break" off-screen. The today-time
     chip and the "1 task" / "N tasks" suffix on the running timer both
     duplicate info available via the timer popover (tap the timer chip
     to see per-task breakdown + total today), so they're the safest to
     drop. ⚡ multi-cue stays so the 2+ timer case still reads. */
  .statusbar-today { display: none; }
  /* Hide the "K tasks" sub-text on the running chip (the live timer is
     enough on a phone). The idle state's "today: K sessions" stays —
     it's the user's only entry point to the time log on mobile after
     the tomato emoji was dropped. */
  .statusbar-timer.is-running .chip-tasks { display: none; }
  /* Compact spacing — gap, padding, and break-CTA padding all tighten
     so the remaining chips have a chance to fit before overflow-x auto
     kicks in. */
  .statusbar { gap: 6px; padding-inline: 8px; }
  .statusbar-chip { padding: 3px 6px; }
  .statusbar-break-cta { padding: 3px 6px; }
  /* Pin left + right sections to the same flex basis so the center
     cluster (timer + break CTA) lands mathematically centered instead
     of getting shoved by whichever side has more chips. The right
     section's `margin-inline-start: auto` is no longer needed once it
     has an explicit flex grow; replace it with `justify-content: end`
     so its content (locale + legal) still hugs the right edge.
     The center gets a slightly wider inner gap so the timer pill and
     ☕ break CTA breathe instead of kissing.
     `justify-content: center` re-asserts centering — the (max-width:
     640px) block above flips center to flex-start, and that rule has
     the same specificity but loses on source order only if we restate
     it here. */
  /* Center the cluster mathematically: left and right both grow to fill
     remaining space; center stays at content size and lands at the
     midpoint between them. With `flex: 1 1 auto` on center the section
     itself grew, so its inner `justify-content: center` only centered
     the cluster within the section's allocation — and when min-content
     on the left forced left to expand, the center section shrank by
     the same amount and the cluster slid right. `flex: 0 0 auto` on
     center keeps the section at content size; `flex: 1 1 0` on left
     and right makes them perfectly symmetric, so the geometric centre
     of the bar always falls inside the centre section's content. */
  .statusbar-section.statusbar-left,
  .statusbar-section.statusbar-right { flex: 1 1 0; min-width: 0; }
  .statusbar-section.statusbar-right {
    margin-inline-start: 0;
    justify-content: flex-end;
  }
  .statusbar-section.statusbar-center {
    flex: 0 0 auto;
    justify-content: center;
    gap: 10px;
  }
  /* Allow horizontal scroll as a final escape hatch — if a long locale
     label or extra future chip pushes past viewport width, the user
     can still reach it without the bar overlapping the centre cluster. */
  .statusbar {
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .statusbar::-webkit-scrollbar { display: none; }

  /* On phones the statusbar is glanceable, not a control surface. The
     stats / today / locale / theme chips read as ambient state — making
     them tappable invites accidental popovers when reaching for the
     keyboard or browser chrome. Keep the break CTA (statusbar-break-cta)
     and the timer chip (statusbar-timer, both is-running and is-idle)
     interactive — is-running opens the timer popover, is-idle opens the
     time log drawer (the only mobile entry point now that the today-time
     chip is hidden). */
  .statusbar-chip:not(.statusbar-break-cta):not(.statusbar-timer) {
    pointer-events: none;
    cursor: default;
  }
  .statusbar-legal a { pointer-events: auto; }

  /* Round the bottom corners + lift slightly off the viewport edge so the
     bar fits inside the rounded display area on phones. iOS Chrome hides
     its URL bar on scroll, exposing the device's curved corners — a
     full-bleed rectangle gets clipped by them and looks broken. The
     inline inset + bottom inset (on top of the home-indicator safe area)
     keeps the bar visually contained at every scroll state. */
  .statusbar {
    inset-inline: 6px;
    bottom: max(env(safe-area-inset-bottom), 6px);
    padding-block-end: 0;
    border: 1px solid var(--line);
    border-radius: 14px;
    overflow: hidden;
    /* Drop the safe-area gradient — the bar no longer touches the bottom
       edge so the dim "home indicator strip" panel isn't needed. */
    background: var(--bg-raised);
  }
  /* Body padding-bottom (line ~563) reserves --statusbar-h + safe-area
     for the now-floating bar; the 6px lift is absorbed by that reserve. */
}

/* Migration banner — src/components/migration-banner.tsx.
   Shown ONLY on the legacy todo.motaz-abuelnasr.workers.dev origin while
   that domain is sunset. It carries a FIXED terminal palette (the
   boot-loader's #0a120a / #5ef07a from index.html) rather than the active
   theme's var()s on purpose: it is a transitional system notice that must
   read identically on every theme. Non-sticky — it scrolls away and the
   sticky topbar then pins to the viewport top, so it needs no slot in the
   --sticky-top-* chain. */
.migration-banner {
  display: flex;
  flex-wrap: wrap; /* phone widths: the action buttons drop to a second row */
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  padding: 10px 16px;
  background: #0a120a;
  color: #d8e0d8;
  border-block-end: 1px solid #5ef07a;
  font-size: 13px;
}

.migration-banner-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0; /* allow the text to shrink instead of forcing overflow */
}

.migration-banner-title {
  color: #5ef07a;
  font-weight: 600;
}

.migration-banner-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.migration-banner-btn {
  font: inherit;
  font-size: 12px;
  padding: 6px 12px;
  border: 1px solid #5ef07a;
  border-radius: 3px; /* matches the app's 3px corner language */
  background: transparent;
  color: #5ef07a;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.migration-banner-btn-primary {
  background: #5ef07a;
  color: #0a120a;
  font-weight: 600;
}

/* Explicit hover states. The banner carries a fixed palette on purpose, so
   a global button:hover (themed var()s) would otherwise bleed a near-black
   fill onto these and kill the contrast. */
.migration-banner-btn:hover { background: rgba(94, 240, 122, 0.14); }
.migration-banner-btn-primary:hover { background: #7dff7d; }
/* ---------- Board ---------- */

.board {
  flex: 1;
  padding: 16px 20px 80px;
  /* No overflow set — the body scrolls. Setting overflow-x: auto here would
     promote overflow-y to auto and steal sticky containment from .col-headers,
     which needs to stick to the viewport (below the sticky topbar). */
}

/* Column headers row — sticks below all the other sticky elements
   (topbar / active-now / filter-bar). The cumulative offset is computed in
   updateStickyTops() in app.js. Default to 52 (just the topbar) when active-
   now and filter-bar haven't been measured yet — the var updates on first
   render. */
.col-headers {
  position: sticky;
  top: var(--sticky-top-col-headers, 52px);
  display: grid;
  /* Top-header layout: 3 status columns. Group label is no longer a sticky
     side column — each .project-row carries its own header on top. */
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
  padding: 8px 0;
  background: var(--bg);
  z-index: 10;
  border-bottom: 1px solid var(--line);
  margin-bottom: 10px;
}
.col-header {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
  padding: 10px 12px 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.col-header .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-faint);
}
/* Status enum v9: queued + in-progress + done. The old "doing" / "hold"
   split was collapsed into in-progress; pause is now a card-level state
   (.card.paused) inside the in-progress column rather than a separate
   column. */
.col-header[data-col="queued"] .dot { background: var(--ink-dim); }
.col-header[data-col="in-progress"] .dot { background: var(--accent); box-shadow: 0 0 8px var(--accent-glow); }
.col-header[data-col="done"] .dot { background: var(--accent-dim); }
.col-header .n { color: var(--ink-faint); margin-inline-start: auto; font-size: 11px; }
/* Active-status emphasis — the header text and count for in-progress
   inherit the accent green so the column reads as the "go / active" lane
   at a glance, complementing the queued-fade rule further down. The dot
   was already green; coloring the rest of the header gives the column
   one cohesive identity instead of a green dot floating in dim text. */
.col-header[data-col="in-progress"]      { color: var(--accent-text); }
.col-header[data-col="in-progress"] .n   { color: var(--accent-text); opacity: 0.85; }

.project-row {
  /* Column layout: label on top, cells row below. Block layout (instead of
     grid-template-areas) is what makes .project-label's `position: sticky`
     actually engage — sticky needs the parent to be taller than the
     element so there's room to slide. With grid-template-areas, the label's
     grid cell was exactly the label's height, leaving no scroll room. */
  display: block;
  border-bottom: 1px solid var(--line);
  padding: 14px 0 18px 0;
  margin-bottom: 4px;
  /* Project-color accent on the inline-END edge (right in LTR). Was on
     inline-START before but that collided visually with the project
     rail's own right-edge border, producing a doubled / inconsistent
     stripe at the rail/board seam. */
  border-inline-end: 3px solid var(--project-color, var(--line));
  padding-inline-end: 8px;
  border-radius: 2px;
  transition: background 160ms;
}
.project-row > .cells-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
  align-items: start;
}
.board.hide-done .project-row > .cells-row { grid-template-columns: 1fr 1fr; }
.project-row.has-color {
  background: color-mix(in oklab, var(--project-color) 4%, transparent);
  border-bottom-color: color-mix(in oklab, var(--project-color) 35%, var(--line));
}
/* Subtle gradient on the project-label header — echoes the 3px colored rail
   so the row reads as "this is project X" at a glance, without tinting
   the cards underneath. Caps at ~10% saturation and fades out by ~30%
   across the label so the bar's segment colors keep their punch. The
   focused-project rule below has higher specificity and overrides this
   when the user clicks a label to focus it. RTL flips direction. */
.project-row.has-color:not(.focused-project) .project-label {
  background:
    linear-gradient(
      to right,
      color-mix(in oklab, var(--project-color) 10%, transparent),
      transparent 30%
    ),
    var(--bg);
}
[dir="rtl"] .project-row.has-color:not(.focused-project) .project-label {
  background:
    linear-gradient(
      to left,
      color-mix(in oklab, var(--project-color) 10%, transparent),
      transparent 30%
    ),
    var(--bg);
}
/* Drag-over highlight on the group row uses the GROUP color (with accent
   as a fallback for groups that haven't been assigned a color). The tint
   inherits the group's identity; the inset left bar gives an
   unmistakable "the card lands in THIS project" edge that reads even
   under the (now translucent) drag ghost. */
.project-row.dragging-over-row {
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--project-color, var(--accent)) 26%, transparent),
    transparent 45%);
  /* inset, not a border — a real border would shift row content 3px mid-drag */
  box-shadow: inset 3px 0 0 color-mix(in oklab, var(--project-color, var(--accent)) 80%, transparent);
}

/* Focused group — click a group label to select it; `n` creates a new task
   into this group. The 2px accent border + 12px padding sums to 14px so
   the label content lines up with unfocused labels (which use 14px padding
   and no border). Without that compensation, focusing a group nudges its
   contents 2px left and breaks vertical alignment with siblings. */
.project-row.focused-project .project-label {
  background: linear-gradient(90deg, var(--accent-glow), transparent 80%);
  border-inline-start: 2px solid var(--accent);
  padding-inline-start: 12px;
  cursor: pointer;
}
.project-row .project-label { cursor: pointer; }

/* Top-header layout — name + stats + bar share one horizontal row above
   the cells. The bar stretches to fill remaining space, becoming a long
   thin status indicator that visually anchors the row.
   Sticky so the header pins to the top of the scroll viewport while the
   user scrolls through the group's cards — restores the old side-column
   "label always visible" behavior in the new top-of-row layout. Anchored
   below the cumulative sticky stack (topbar + active-now + filter-bar +
   col-headers) via the --sticky-top-col-headers var. The grid-row inset
   below adds the col-headers height (~36px) so labels don't slide under
   the persistent column headers. */
.project-label {
  padding: 4px 14px 12px;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  position: sticky;
  /* --sticky-top-project-label is set dynamically by updateStickyTops()
     to filter-bar-bottom + col-headers-height, so the label hugs the
     last sticky strip above it (col-headers in multi-col, filter-bar
     in single-col where col-headers is display:none). The fallback
     +28px was a stale magic constant — col-headers is actually ~57px
     in multi-col, so labels were sliding under the column headers. */
  top: var(--sticky-top-project-label, calc(var(--sticky-top-col-headers, 52px) + 28px));
  z-index: 4;
  /* Opaque base so cells scrolling under the sticky label don't bleed
     through. Has-color groups layer the color gradient on top via a
     separate rule below. */
  background: var(--bg);
}
.project-label .bar {
  flex: 0 0 180px;
  min-width: 120px;
  max-width: 240px;
  margin-block-start: 0;
  margin-inline-start: 8px;
}
.project-label .name {
  color: var(--ink-primary);
  font-size: var(--fs-section);
  font-weight: var(--fw-strong);
  letter-spacing: 0.02em;
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
}
/* Visible-by-default chevron — was 10px ink-faint, which disappeared
   into the row background. Bumped to 14px + ink-dim, padded into a
   hit-target so the click area matches what the eye sees. Hovering or
   collapsing the row brightens it to the row's accent (or full ink) so
   the affordance isn't ambiguous when state matters. */
.project-label .name .chev {
  color: var(--ink-dim);
  transition: transform 150ms, color 120ms;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  user-select: none;
  border-radius: 4px;
}
.project-label .name .chev:hover {
  color: var(--ink);
  background: color-mix(in oklab, var(--ink) 10%, transparent);
}
.project-row.collapsed .project-label .name .chev {
  transform: rotate(-90deg);
  color: var(--ink);
}
.project-row.has-color .project-label .name .chev { color: var(--project-color, var(--ink-dim)); }
.project-row.has-color.collapsed .project-label .name .chev { color: var(--project-color, var(--ink)); }
.project-label .stats {
  color: var(--ink-tertiary);
  font-size: var(--fs-caption);
  letter-spacing: 0.02em;
  display: flex;
  flex-wrap: nowrap;
  column-gap: 10px;
  margin-inline-start: 10px;
  white-space: nowrap;
}
/* (.project-label is already position: sticky earlier — sticky establishes
   a containing block for the absolutely-positioned .stats, so no extra
   `position: relative` needed.) */
.project-label .stats .stat { white-space: nowrap; }
.project-label .stats .stat.zero { opacity: 0.4; }
/* Each stat carries the same color as its bar segment so the eye can
   match number → segment without re-reading. The base ink color is the
   neutral fallback for any stat that doesn't have a semantic class. */
.project-label .stats .stat.done    { color: var(--accent-text); }    /* green */
.project-label .stats .stat.doing   { color: var(--blue-text); }      /* blue */
.project-label .stats .stat.paused  { color: var(--amber-text); }     /* yellow */
.project-label .stats .stat.blocked { color: var(--red, #e55); } /* red */
.project-label .stats .stat.queued  { color: var(--ink-tertiary); } /* gray */
.project-label .stats .stat.hold    { color: var(--amber-text); }
.project-label .stats .stat-sep { display: none; }
.project-label .collapsed-count {
  color: var(--ink-quiet);
  font-size: var(--fs-caption);
  font-weight: var(--fw-regular);
  font-style: italic;
  margin-inline-start: 4px;
}
/* Progress bar — five semantic segments share the row, in this order:
     done (green) · doing (blue) · paused (amber) · blocked (red) · queued (gray)
   Queued is rendered last because "remaining work" is the natural read at
   the right edge of a left-to-right progress bar. Stats above the bar use
   matching colors so the user's eye can pair number ↔ segment.  */
.project-label .bar {
  height: 4px;
  background: var(--line);
  border-radius: 2px;
  display: flex;
  overflow: hidden;
  margin-block-start: 4px;
}
.project-label .bar span { display: block; height: 100%; }
.project-label .bar .b-done    { background: var(--accent); }       /* green */
.project-label .bar .b-queued  { background: var(--ink-quiet); }    /* gray */
.project-label .bar .b-doing   { background: var(--blue); }         /* blue */
.project-label .bar .b-paused  { background: var(--amber); }        /* yellow */
.project-label .bar .b-blocked { background: var(--red); }          /* red */

.cell {
  min-height: 60px;
  padding: 4px;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 6px;
  position: relative;
  transition: background 120ms, box-shadow 120ms;
}
/* Design pass §6/§7 — empty cells at rest render nothing. The cell is just
   a layout slot. Drag drop-target highlight handles the "you can drop here"
   cue dynamically; no need for a permanent outline. */
.cell.drop-target {
  /* Drop-target highlight inherits --project-color from the parent
     .project-row when set; falls back to accent for un-colored groups so
     the visual matches the dragged task's group identity. */
  /* 14% tint + a solid 2px outline: the destination cell must read
     clearly against the translucent ghost parked over it. */
  background: color-mix(in oklab, var(--project-color, var(--accent)) 14%, transparent);
  outline: 2px solid color-mix(in oklab, var(--project-color, var(--accent)) 75%, transparent);
  outline-offset: -2px;
  opacity: 1;
}
/* When the cell has the live placeholder, the placeholder itself shows
   exactly where the card will land — drop the cell-level outline so the
   two dashed boxes don't nest, and ditch the bg tint that was wrapping
   the "+ new task" row and the "no tasks" placeholder along with it. */
.cell.drop-target:has(.drop-placeholder) {
  outline: none;
  background: transparent;
}
/* During a drop-target hover, hide the trailing affordances so the cell
   visually reads as "card area only" — empty/new-task/show-more rows
   would otherwise suggest you can drop into them, which you cannot. */
.cell.drop-target .cell-empty,
.cell.drop-target .new-task,
.cell.drop-target .cell-show-more { opacity: 0.25; pointer-events: none; }
.cell.drop-dimmed { opacity: 0.35; }
/* Live "make room" placeholder — inserted into the flex-column on dragover
   between the cards above and below the cursor, pushing existing cards
   down by its own height so the user sees exactly where the dragged card
   will land. Height is set inline to match the dragged card. */
.drop-placeholder {
  flex-shrink: 0;
  min-height: 56px;
  border: 1px dashed var(--accent);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  pointer-events: none;
  transition: height 80ms ease;
}

.project-row.collapsed .cell { display: none; }

/* Cards */

/* Brief highlight applied to a freshly-added card after its scroll-into-view
   so the user can see exactly where the new task landed. The class is added
   in renderTimeSheet's create save handler and auto-removed after ~1.8s. */
.card.card-just-added {
  animation: card-just-added-pulse 1.6s ease-out;
}
@keyframes card-just-added-pulse {
  0%   { box-shadow: 0 0 0 2px var(--accent), 0 0 16px color-mix(in oklab, var(--accent) 50%, transparent); background: color-mix(in oklab, var(--accent) 12%, var(--bg-card)); }
  60%  { box-shadow: 0 0 0 2px var(--accent), 0 0 8px color-mix(in oklab, var(--accent) 25%, transparent); background: color-mix(in oklab, var(--accent) 6%, var(--bg-card)); }
  100% { box-shadow: 0 0 0 0 transparent, 0 0 0 transparent; background: var(--bg-card); }
}
@media (prefers-reduced-motion: reduce) {
  .card.card-just-added { animation: none; box-shadow: 0 0 0 2px var(--accent); }
}

/* Pinned-recent badge — applied to cards whose ids are still in the
   per-session recently-added set. The pin no longer auto-clears after
   30s (users lost track of just-typed cards as they resorted away).
   The badge is a tiny corner pill; the cell-level "↕ Sort N recent"
   button is the only way to release the pin. The ::after content is
   set per-cell so the locale rendered matches whatever i18n is active
   at render time. */
/* Inline RECENT flag — sits in the card's top row, after the checkbox
   and before the project chip. Inline (a real element, NOT an absolute
   ::after overlay) so it never collides with the play button at the
   card's top-right corner. Quiet accent-tinted chip, mono caps. */
.card-recent-flag {
  flex: 0 0 auto;
  align-self: center;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.5px;
  line-height: 1.4;
  padding: 1px 5px;
  border: 1px solid color-mix(in oklab, var(--accent) 35%, var(--line-strong));
  border-radius: 2px;
  color: var(--accent-text);
  background: color-mix(in oklab, var(--accent) 12%, var(--bg-card));
  white-space: nowrap;
  user-select: none;
}

/* "Sort N recent" toggle — symmetric with .cell-clear-done (top of the
   done cell). Sits at the top of the queued cell, between the + new
   task row and the first pinned card. Clicking releases the recent
   pins so the column resorts by the user's chosen sort mode. Same
   shape as cell-clear-done — full-width flex, solid border, ::before
   icon — but a neutral accent hover instead of red, because this
   reorganizes rather than destroys. */
.cell-sort-recent {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  margin-block-end: 6px;
  padding-block: 5px;
  padding-inline: 10px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-caption);
  text-align: start;
  cursor: pointer;
  transition: background 100ms, color 100ms, border-color 100ms;
}
.cell-sort-recent::before {
  content: "↕";
  font-size: 12px;
  line-height: 1;
  opacity: 0.7;
  flex: 0 0 auto;
}
.cell-sort-recent:hover {
  background: var(--bg-card-hov);
  color: var(--accent-text);
  border-color: var(--accent);
}
.cell-sort-recent:hover::before { opacity: 1; }

.card {
  background: var(--bg-card);
  /* Design pass §6 — drop the heavy --line-strong border. The card edge sits
     on --line; bg-tint + spacing carry the unit boundary. Hover/focus add
     the accent border back, so click affordance stays. */
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: grid;
  gap: 6px;
  cursor: grab;
  position: relative;
  transition: background 120ms, border-color 120ms, transform 120ms;
  /* Long-press on a card fires the app's own action menu (500ms hold;
     see card touchstart handler in app.js). Without these, iOS pops its
     "Copy / Look Up / Share" sheet and Android Chrome pops its text-
     selection toolbar on top of the in-app menu. Editable children
     (title, notes, inputs) opt back in so users can still copy task
     text. `-webkit-tap-highlight-color: transparent` kills the dark
     flash Android draws on every tap inside the card. */
  -webkit-touch-callout: none;     /* iOS callout */
  -webkit-user-select: none;       /* Older Android Chrome / Safari */
  user-select: none;                /* Modern */
  -webkit-tap-highlight-color: transparent; /* Android tap flash */
}
.card [contenteditable="true"],
.card input,
.card textarea {
  -webkit-touch-callout: default;
  -webkit-user-select: text;
  user-select: text;
}
/* Design pass — quieter hover. The translateY lift + heavy shadow drew the
   eye and broke the "calm everywhere" intent; bg-tint + a soft shadow now
   carry the affordance. Click feedback comes from the cursor:grab anyway. */
.card:hover {
  background: var(--bg-card-hov);
  border-color: var(--accent-dim);
  /* Drop shadow removed 2026-05-17 v3 — added perceptible "lift" weight
     that broke the calm/Notion-soft feel in compact view across all themes.
     bg-tint + the border-color shift carry the hover affordance alone now. */
}

/* Google Calendar meeting card in the Overview board — see
   src/features/board-cards/MeetingCard.tsx + features/board-meetings.ts.
   Reuses the .card shell; the blue inline-start accent marks it as a
   meeting, mirroring the gcal today-card identity (--blue). */
.card.card-meeting {
  cursor: default;                       /* not draggable, unlike task cards */
  border-inline-start: 3px solid var(--blue, #7ec7ff);
}
/* Title fills the row so the play badge sits at the trailing edge — the
   meeting card has no .drag-area spacer (it cannot be dragged). */
.card.card-meeting .title { flex: 1 1 auto; }
/* No leading checkbox, so the meta needn't indent past one. */
.card.card-meeting .meta { padding-inline-start: 0; }
/* Keep the blue identity rail through the shared .card:hover, which
   otherwise repaints every border to --accent-dim. */
.card.card-meeting:hover { border-inline-start-color: var(--blue, #7ec7ff); }
/* Tracked meeting — steady (no pulse: calm by design, a meaningful share
   of users have ADHD) blue emphasis, mirroring .today-card-gcal.tracking
   in active-now.css. */
.card.card-meeting.tracking {
  border-color: var(--blue, #7ec7ff);
  background: color-mix(in oklab, var(--blue, #7ec7ff) 8%, var(--bg-card));
}
/* "+ Plan" — always-visible click-to-create CTA (spawns a prep task).
   Dashed outline marks it actionable, like the empty pill CTAs. */
.card.card-meeting .meeting-prep {
  cursor: pointer;
  font: inherit;
  color: var(--ink-dim);
  border: 1px dashed var(--line);
}
.card.card-meeting .meeting-prep:hover {
  border-color: var(--blue, #7ec7ff);
  color: var(--accent-text);
}
/* Faint group-color tint on cards inside a colored group. --project-color
   cascades from .project-row (see :577). The mix sits at 5% so text contrast
   stays unchanged; .focused / .dragging / .done states layer on top
   normally because they restyle border/opacity, not background.

   IMPORTANT: only tint top / bottom / inline-end borders. The inline-start
   border carries the priority rail (.card[data-priority] sets red / amber /
   accent-dim / line-strong) and must keep its color in colored groups too.
   Using `border-color` shorthand here previously clobbered the priority
   color on every card inside a colored group, which made priority changes
   visually invisible — the user saw the same group-tinted left rail
   regardless of priority. */
.project-row.has-color .card {
  background: color-mix(in oklab, var(--project-color) 5%, var(--bg-card));
  /* Only tint top/bottom — both inline edges carry the priority rail.
     Softened 2026-05-17 v3: 25% project-color over --line-strong was making
     colored-project cards visibly pink/orange/etc. outlined at rest, breaking
     the calm-by-default feel on blossom/pearl/light themes. 12% over --line
     keeps the project-color hint faint and the overall edge near-invisible. */
  border-block-color: color-mix(in oklab, var(--project-color) 12%, var(--line));
}
.project-row.has-color .card:hover {
  background: color-mix(in oklab, var(--project-color) 10%, var(--bg-card-hov));
  /* Softened 2026-05-17 v3: 55% over --accent-dim was a strong hover ring;
     30% over --line-strong is a clear-but-calm affordance. */
  border-block-color: color-mix(in oklab, var(--project-color) 30%, var(--line-strong));
}
/* All-projects-focus view: the wrapping .project-row is the synthetic
   all-projects sentinel (no color), so the .has-color tint above never
   fires. Tint each card by its OWN project color via --task-color, which
   renderCard sets per-card from groupById(task.groupId).color. */
.board.all-projects-focus .card.has-task-color {
  background: color-mix(in oklab, var(--task-color) 5%, var(--bg-card));
  /* Same Soft-Border softening as .project-row.has-color .card above. */
  border-block-color: color-mix(in oklab, var(--task-color) 12%, var(--line));
}
.board.all-projects-focus .card.has-task-color:hover {
  background: color-mix(in oklab, var(--task-color) 10%, var(--bg-card-hov));
  border-block-color: color-mix(in oklab, var(--task-color) 30%, var(--line-strong));
}
.card.focused {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
}
/* Source card during drag — hidden entirely. The custom drag-ghost follows
   the cursor showing what's being dragged, and the drop-placeholder shows
   the destination. Keeping the source visible at low opacity created a
   visual doppelgänger when dragging within the same column (placeholder +
   faded original both rendered, looking like duplicates). */
.card.dragging { display: none; }

/* Group 2 empty CTAs — reserved-slot hover-reveal. The card meta row
   renders empty placeholders (`+ schedule` / `+ est` / `+ due` / `+ block`)
   at the trailing edge whenever their attribute is unset and the task isn't
   done. They live in the DOM at all times so hover doesn't reflow the row;
   `visibility: hidden` reserves the slot, fade-in on hover/focus.
   Filled pills sit before them in DOM order, so they never shift.
   `:focus-within` keeps them reachable by keyboard tab; `.card.focused`
   covers the click/custom-navigation case. The card-color-sq stays here
   too so the swatch behaves the same. */
.card .pill.schedule-pill.empty,
.card .pill.due-pill.empty,
.card .pill.est-pill.empty,
.card .pill.block-chip.empty,
.card .card-color-sq.no-color {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, visibility 0s linear 120ms;
}
.card:hover .pill.schedule-pill.empty,
.card:hover .pill.due-pill.empty,
.card:hover .pill.est-pill.empty,
.card:hover .pill.block-chip.empty,
.card:hover .card-color-sq.no-color,
.card:focus-within .pill.schedule-pill.empty,
.card:focus-within .pill.due-pill.empty,
.card:focus-within .pill.est-pill.empty,
.card:focus-within .pill.block-chip.empty,
.card:focus-within .card-color-sq.no-color,
.card.focused .pill.schedule-pill.empty,
.card.focused .pill.due-pill.empty,
.card.focused .pill.est-pill.empty,
.card.focused .pill.block-chip.empty,
.card.focused .card-color-sq.no-color {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transition: opacity 120ms ease;
}
.card .card-actions {
  display: none;
}
.card:hover .card-actions,
.card:focus-within .card-actions,
.card.focused .card-actions {
  display: flex;
}
/* Click affordance on the est-pill (full or empty) — visually mirrors
   the existing due-pill click cursor + hover treatment. */
.card .pill.est-pill { cursor: pointer; }
.card .pill.est-pill:hover { border-color: var(--accent); color: var(--accent-text); }

/* Custom drag ghost — a clone (or rebuilt block) that follows the cursor.
   Lives inside body, so it inherits the same CSS zoom as the source and the
   visual size always matches what the user is dragging. */
.drag-ghost {
  pointer-events: none;
  z-index: var(--z-overlay-top);
  /* Translucent so the drop-target cell + project row read THROUGH the
     ghost — it only needs to say "card in hand", not occlude the board. */
  opacity: 0.5;
  box-shadow: 0 12px 28px rgba(0,0,0,0.55);
  transition: none !important;
  cursor: grabbing;
}
.drag-ghost.card { transform-origin: top left; }
.drag-ghost.cal-block.cal-block-ghost {
  /* Initial dimensions for the rail-card → calendar drag preview. Height +
     width get rewritten live by the col.dragover snap handler so the ghost
     matches the actual minute-height of the dropped block; the values here
     are the placeholder before the cursor enters the calendar. */
  height: auto;
  min-height: 20px;
  padding: 6px 8px;
  background: color-mix(in oklab, var(--accent) 35%, var(--bg-card));
  outline: 1px dashed var(--accent);
  overflow: hidden;
}
.card.done { opacity: 0.55; }
.card.done .title { text-decoration: line-through; text-decoration-color: var(--ink-faint); }

@keyframes checkbox-unpop {
  0%   { transform: scale(1);    background: var(--accent-dim); border-color: var(--accent-dim); color: var(--bg); }
  20%  { transform: scale(1.35); background: var(--accent-dim); border-color: var(--accent-dim); color: var(--bg); }
  55%  { transform: scale(0.85); background: transparent;       border-color: var(--line-strong); color: transparent; }
  80%  { transform: scale(1.08); background: transparent;       border-color: var(--line-strong); color: transparent; }
  100% { transform: scale(1);    background: transparent;       border-color: var(--line-strong); color: transparent; }
}
.checkbox.checkbox-unchecking {
  animation: checkbox-unpop 320ms ease-out forwards;
}

@keyframes checkbox-pop {
  0%   { transform: scale(1);    background: transparent;       border-color: var(--line-strong); color: transparent; }
  25%  { transform: scale(1.45); background: var(--accent);     border-color: var(--accent);      color: var(--bg); }
  50%  { transform: scale(0.85); background: var(--accent);     border-color: var(--accent);      color: var(--bg); }
  75%  { transform: scale(1.1);  background: var(--accent);     border-color: var(--accent);      color: var(--bg); }
  100% { transform: scale(1);    background: var(--accent-dim); border-color: var(--accent-dim);  color: var(--bg); }
}
.checkbox.checkbox-checking {
  animation: checkbox-pop 360ms ease-out forwards;
}

@keyframes card-done-exit {
  0%   { opacity: 1; transform: scale(1); }
  50%  { opacity: 0.3; transform: scale(0.97) translateY(2px); }
  100% { opacity: 0; transform: scale(0.95) translateY(4px); }
}
.card.card-marking-done {
  animation: card-done-exit 300ms ease-out forwards;
  pointer-events: none;
}

.card .top {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.card .id {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.04em;
  flex-shrink: 0;
  padding-top: 2px;
}
.card .title {
  flex: 0 1 auto;
  color: var(--ink-primary);
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  line-height: 1.4;
  word-break: break-word;
  cursor: text;
}
.card .drag-area {
  flex: 1;
  min-width: 8px;
  cursor: grab;
  align-self: stretch;
}
.card .drag-area:active { cursor: grabbing; }
.card .title[contenteditable="true"] ~ .drag-area { display: none; }
.card .title[contenteditable="true"] {
  flex: 1;
  /* Subtle bg shift + hairline outline. The previous 1px accent outline at
     offset 2px glowed louder than the title text itself; the new look is
     closer to Reminders — barely a chrome, the caret is the affordance. */
  background: color-mix(in oklab, var(--accent) 4%, var(--bg));
  outline: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
  outline-offset: 1px;
  border-radius: 3px;
  cursor: text;
}
/* On mobile the title's tap target is widened to the full card width and
   given extra vertical padding so a fingertip lands cleanly into edit
   mode. The contenteditable still shows a normal I-beam. */
@media (max-width: 600px) {
  .card .title {
    padding-block: 6px;
    min-height: 30px;
    display: flex;
    align-items: center;
  }
  .card .title[contenteditable="true"] {
    min-height: 36px;
    padding-block: 8px;
  }
}
.card .checkbox,
.today-card .checkbox {
  width: 14px;
  height: 14px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  cursor: pointer;
  margin-top: 2px;
  color: transparent;
  font-size: 10px;
  line-height: 1;
  position: relative;
}
.card .checkbox:hover,
.today-card .checkbox:hover { border-color: var(--accent); }
.card.done .checkbox,
.today-card.done .checkbox { background: var(--accent-dim); border-color: var(--accent-dim); color: var(--bg); }

.card .notes {
  color: var(--ink-secondary);
  font-size: var(--fs-meta);
  font-weight: var(--fw-regular);
  line-height: 1.5;
  padding-inline-start: 22px;
  white-space: pre-wrap;
  word-break: break-word;
  /* Cap the kanban description preview at 2 lines so a long note doesn't
     stretch one card to 3× the height of its neighbours. The textarea
     opens inline at the full text on click. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  /* Single-click on description → edit (Reminders-style). I-beam cursor
     advertises the affordance before the user clicks. */
  cursor: text;
  border-radius: 3px;
  transition: background 120ms;
}
.card .notes:hover {
  background: color-mix(in oklab, var(--accent) 6%, transparent);
}

/* Inline-edit textarea for the description. Sits in the same slot as
   .notes so the row above (.top) and below (.meta) don't reflow when
   editing starts. Border style mirrors the title contenteditable so
   both edit affordances read at the same visual weight (hairline accent
   + soft bg tint, not a heavy glow). */
.card .notes-edit {
  margin-inline-start: 22px;
  background: color-mix(in oklab, var(--accent) 4%, var(--bg));
  border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
  border-radius: 3px;
  color: var(--ink-primary);
  font: inherit;
  font-size: var(--fs-meta);
  line-height: 1.5;
  padding: 6px 8px;
  resize: none;
  outline: none;
  width: calc(100% - 22px);
  box-sizing: border-box;
  min-height: 32px;
  max-height: 240px;
  transition: border-color 120ms, background 120ms;
}
.card .notes-edit:focus {
  border-color: color-mix(in oklab, var(--accent) 55%, transparent);
}

/* Inline keyboard hint shown below the description textarea on desktop.
   Hidden on mobile (touch users can't Shift+Enter anyway, and the line
   would crowd the small card). Tiny, dim, monospace — pure metadata. */
.card .notes-hint {
  display: block;
  margin-inline-start: 22px;
  margin-block-start: 4px;
  color: var(--ink-faint);
  font-size: var(--fs-caption);
  letter-spacing: 0.02em;
  user-select: none;
  pointer-events: none;
}
@media (max-width: 600px) {
  .card .notes-hint { display: none; }
}
@media (max-width: 600px) {
  /* Description font tracks the title proportion — 13px sits two ticks
     below the 15px title (matches the .card .notes preview at 13px on
     mobile, so tapping to edit doesn't change text size). 44px min-height
     keeps a fingertip target without the textarea being taller than a
     short title. */
  .card .notes-edit { min-height: 44px; font-size: 13px; padding: 8px 10px; }
  .card .notes      { font-size: 13px; }
  .card .add-desc   { font-size: 13px; padding-block: 4px; min-height: 24px; }
}

/* "Add description" affordance — appears only while the title is in edit
   mode and t.notes is empty. Reminders-style: a thin italic placeholder
   line just below the title, left-aligned with the description column,
   no border or background chrome. Click → notes-edit. The empty pills
   (`+ schedule` / `+ estimation` / `+ due` / `⊘ block`) below carry the
   "tertiary action chip" pattern; this one stays text-only so the eye
   doesn't read two competing CTAs for "add metadata to this task". */
.card .add-desc {
  display: inline-block;
  padding-inline-start: 22px;
  color: var(--ink-faint);
  font: inherit;
  font-size: var(--fs-meta);
  font-style: italic;
  line-height: 1.5;
  cursor: text;
  border-radius: 3px;
  width: fit-content;
  max-width: 100%;
  opacity: 0;
  animation: card-add-desc-in 160ms ease-out forwards;
  transition: color 120ms;
}
.card .add-desc:hover {
  color: var(--ink-secondary);
}
.card .add-desc:focus-visible {
  outline: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
  outline-offset: 2px;
  color: var(--ink-secondary);
}
@keyframes card-add-desc-in {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .card .add-desc { animation: none; opacity: 1; transform: none; }
}

.card .meta {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-inline-start: 22px;
  flex-wrap: wrap;
  font-size: var(--fs-caption);
  color: var(--ink-tertiary);
  letter-spacing: 0.02em;
}

/* Compact view — titles only. Hide everything but the top row (checkbox /
   play / title / assignees). Description preview, the "Add description"
   placeholder, the inline-edit textarea, and the description hint all
   stay hidden so a user editing a title in compact doesn't see secondary
   chrome (or worse, a textarea) appear next to the title row. */
.board.compact .card { padding: 4px 8px; gap: 0; }
.board.compact .card .notes,
.board.compact .card .notes-edit,
.board.compact .card .notes-hint,
.board.compact .card .add-desc,
.board.compact .card .thumbs,
.board.compact .card .meta,
.board.compact .card .card-actions,
.board.compact .card .block-reason-row { display: none; }
.board.compact .card .top { align-items: center; gap: 6px; }
.board.compact .card .checkbox { margin-top: 0; }

/* Design pass §6 — drop the box-around-every-pill chrome. Filled pills
   (due-today / overdue / due-soon / running) read by color + leading dot.
   Empty pills (.pill.empty, see below) keep a thin dashed outline so they
   still read as click-to-set CTAs. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-caption);
  letter-spacing: 0.02em;
  padding: 0;
  border-radius: 0;
  color: var(--ink-tertiary);
  background: transparent;
  border: none;
}
.pill.due-today   { color: var(--amber-text); }
.pill.overdue     { color: var(--red-text); }
.pill.due-soon    { color: var(--blue-text); }
.pill.notes-flag  { color: var(--ink-tertiary); }
/* Empty CTAs keep a thin dashed boundary so they still read as "+ click" */
.card .pill.empty {
  padding: 1px 6px;
  border: 1px dashed color-mix(in oklab, var(--ink-faint) 60%, transparent);
  border-radius: 2px;
  color: var(--ink-tertiary);
}
.card .pill.empty:hover { color: var(--accent-text); border-color: var(--accent-dim); }
.pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* New task row.
   Uses the accent at low opacity for both the dashed border and the text,
   so the affordance is legible against every theme's background. The old
   `--ink-faint` text + `--line-strong` border vanished into terminal's deep
   green bg (user-reported "new tasks weren't visible although they were
   clearly there"). Going through the accent keeps the cue subtle but
   discoverable, and the per-theme accent colour means terminal reads
   green-tinged, monokai lime-tinged, blossom pink-tinged, etc. */
/* Design pass §6/§7 — the "+ new task" affordance reads as muted text at
   rest and lights up only when the row is hovered. The dashed outline is
   reserved for the hover/edit state, where the user has signalled intent. */
/* Default state is permanently lit at a soft accent fade so the row is
   discoverable without hovering — users were missing it entirely when it
   was fully transparent. Hover escalates to the bold accent treatment. */
.new-task {
  background: transparent;
  border: 1px dashed color-mix(in oklab, var(--accent) 30%, var(--line));
  border-radius: var(--radius);
  padding: 8px 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: text;
  color: color-mix(in oklab, var(--accent) 35%, var(--ink-dim));
  font-size: var(--fs-meta);
  transition: color 160ms, border-color 160ms, background 160ms;
}
.new-task:hover {
  border-color: var(--accent);
  color: var(--accent-text);
  background: color-mix(in oklab, var(--accent) 6%, var(--bg-cell));
}
.new-task .plus {
  color: var(--accent-text);
  font-weight: 600;
}
/* Keyboard hint on desktop new-task / new-project rows. Quiet by
   default, lights up with the row on hover. Mobile (≤600px)
   suppresses rendering at the call site — no CSS hide needed. */
.new-task-kbd {
  padding: 1px 6px;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 10px;
  line-height: 1.2;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: color-mix(in oklab, var(--bg-card) 60%, transparent);
  color: var(--ink-faint);
  pointer-events: none;
}
.new-task:hover .new-task-kbd,
.new-project-row:hover .new-task-kbd {
  border-color: color-mix(in oklab, var(--accent) 30%, var(--line-strong));
  color: var(--ink-dim);
}
.new-task.editing {
  border-style: solid;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
  color: var(--ink);
}
.new-task input {
  flex: 1;
  font-size: var(--fs-body);
}
.new-task .hint { color: var(--ink-faint); font-size: 11px; }

@keyframes new-task-first-run {
  0%   { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 8%, var(--bg-cell)); color: var(--accent-text); }
  50%  { border-color: color-mix(in oklab, var(--accent) 35%, transparent); background: transparent; color: var(--ink-quiet); }
  100% { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 8%, var(--bg-cell)); color: var(--accent-text); }
}
.new-task-first-run .new-task {
  animation: new-task-first-run 1.6s ease-in-out 3;
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 8%, var(--bg-cell));
  color: var(--accent-text);
}
@media (prefers-reduced-motion: reduce) {
  .new-task-first-run .new-task {
    animation: none;
    border-color: var(--accent);
    color: var(--accent-text);
  }
}

/* Design pass §7 — empty cells: render nothing visible at rest. The text
   fades in on row hover so the cell still tells you what column it is when
   you're looking. Empty becomes signal: "this column is clear here." */
.cell-empty {
  color: var(--ink-quiet);
  font-size: var(--fs-caption);
  padding: 6px 12px;
  font-style: italic;
  opacity: 0;
  transition: opacity 200ms;
  pointer-events: none;
}
.project-row:hover .cell-empty,
.project-row.focused-project .cell-empty { opacity: 0.7; }

/* "+N more" overflow button — visible pill so it doesn't disappear at
   the bottom of a long column. Centered, accent-tinted background +
   ring so it reads as an actionable affordance, not stray text. */
.cell-show-more {
  display: block;
  width: 100%;
  margin-block-start: 8px;
  padding: 8px 12px;
  background: color-mix(in oklab, var(--accent) 12%, var(--bg-card));
  border: 1px solid color-mix(in oklab, var(--accent) 35%, var(--line-strong));
  border-radius: var(--radius);
  color: var(--accent-text);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  letter-spacing: 0.02em;
  text-align: center;
  cursor: pointer;
  transition: background 120ms, color 120ms, border-color 120ms, transform 120ms;
}
.cell-show-more:hover {
  background: color-mix(in oklab, var(--accent) 22%, var(--bg-card-hov));
  border-color: var(--accent);
  transform: translateY(-1px);
}
.cell-show-more.cell-collapse {
  /* "Collapse" gets a quieter treatment than "+N more" — the user has
     already seen the overflow; they're just folding it back. */
  background: var(--bg-card);
  border-color: var(--line);
  color: var(--ink-dim);
}
.cell-show-more.cell-collapse:hover {
  background: var(--bg-card-hov);
  border-color: var(--line-strong);
  color: var(--ink);
}

/* Mobile-only compact kanban filter chip — desktop never sees it. */
.kanban-filter-bar .kfb-mobile-chip { display: none; }

/* The `⋯` topbar overflow button shows on every viewport — it carries
   the secondary actions on desktop and (plus the collapsed utilities)
   on mobile. No display rule needed; it uses the default .icon-btn. */

/* Mobile-only search toggle + close buttons. Hidden on desktop; the
   mobile media block shows the toggle inline and the close-button
   only while the topbar is in `is-search-open` mode. */
.topbar #btn-search-toggle,
.topbar #btn-search-close { display: none; }

/* Topbar overflow popover — opens from the `⋯` button (desktop + mobile).
   Reuses the legend-popover positioning (top-anchored, right-aligned). */
.tb-overflow-popover {
  position: fixed;
  z-index: 60;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 6px;
  min-width: 200px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
  font-size: 13px;
}
.tb-overflow-popover .tb-overflow-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  color: var(--ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
  border-radius: var(--radius);
}
.tb-overflow-popover .tb-overflow-item:hover,
.tb-overflow-popover .tb-overflow-item:focus-visible {
  background: var(--bg-card-hov, var(--bg));
  outline: none;
}
/* Overflow item icons — inline SVG from the shared topbar icon set. */
.tb-overflow-popover .tb-overflow-icon {
  display: inline-flex;
  flex: 0 0 auto;
}
.tb-overflow-popover .tb-overflow-icon svg { width: 17px; height: 17px; display: block; }
.tb-overflow-popover .tb-overflow-item .tb-overflow-badge {
  margin-inline-start: auto;
  color: var(--accent-text);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* Mobile rail toggle button — hidden on desktop. Only paints inside
   `main.calendar.mobile` (the mobile-media block flips it to flex). */
.cal-rail-mobile-toggle { display: none; }

/* Mobile schedule picker — bottom sheet on phones, centered modal on
   desktop. Wraps a native <input type="datetime-local"> so the OS
   picker fires on focus. Shared by the rail-card tap (first-time
   schedule) and the calendar-block menu's Reschedule action. */
.cal-sched-picker-wrap {
  position: fixed;
  /* Stretch top→bottom of the viewport. Using inset:0 (instead of the
     prior `top + explicit height` approach) means the wrap's bottom
     edge always matches the ACTUAL viewport bottom regardless of any
     --sheet-vvp quirk (e.g. Chrome DevTools mobile-emulation where
     visualViewport.height can report the emulated viewport while the
     visible browser area is smaller due to docked devtools). The
     iOS-keyboard case is still handled because the visualViewport
     listener now translates the wrap via padding-top instead of a
     calculated height. */
  inset: var(--sheet-vv-top, 0px) 0 0 0;
  z-index: 1100;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 0;
}
.cal-sched-picker-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
}
.cal-sched-picker {
  position: relative;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 16px 16px 0 0;
  padding: 10px 18px calc(18px + env(safe-area-inset-bottom)) 18px;
  width: 100%;
  max-width: 460px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
  animation: cal-sched-picker-up 180ms ease-out;
  /* Clamp to visible viewport height. The body wrapper takes the
     overflow; the sheet itself is overflow:hidden so its flex children
     (head, body, actions) all stay within the viewport bounds. */
  max-height: min(calc(var(--sheet-vvp, 100svh) - 8px), 560px);
  overflow: hidden;
  transition: max-height 200ms ease, border-radius 200ms ease;
}
/* Scrollable body — sits between head and actions. min-height:0 lets
   it shrink below its content's natural height so the overflow:auto
   actually engages and the actions row below stays in view. */
.cal-sched-picker-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Mirror the sheet's horizontal padding so the body's scrollbar
     doesn't sit flush against text. */
  margin: 0 -2px;
  padding: 0 2px;
}
/* Expanded state — toggled by dragging the .sheet-drag-handle UP past
   the threshold (see attachSheetDragGestures in calendar.js). Sheet
   fills the full visible viewport minus the safe-area top inset, and
   squares off its top corners to feel like a full-page surface. */
.cal-sched-picker.expanded {
  max-height: calc(var(--sheet-vvp, 100svh) - env(safe-area-inset-top, 0px) - 8px);
  border-radius: 8px 8px 0 0;
}
/* Drag handle pill — native-sheet visual cue. Now interactive: dragging
   it UP expands the sheet to full-viewport (adds .expanded class);
   dragging DOWN past 100px dismisses. The hit area extends past the
   visible pill via pseudo-element padding so fingers can grab it. */
.sheet-drag-handle {
  position: relative;
  width: 36px;
  height: 4px;
  background: var(--line-strong);
  border-radius: 2px;
  align-self: center;
  flex-shrink: 0;
  cursor: grab;
  touch-action: none; /* let pointer-drag bypass the browser's scroll-gesture handling */
}
.sheet-drag-handle:active,
.sheet-drag-handle.is-dragging {
  cursor: grabbing;
  background: var(--accent, var(--green));
}
/* Expand hit area to a comfortable 44x20 finger target without growing
   the visible pill. */
.sheet-drag-handle::before {
  content: "";
  position: absolute;
  inset: -10px -8px;
}
@keyframes cal-sched-picker-up {
  from { transform: translateY(100%); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .cal-sched-picker { animation: none; }
}
@media (min-width: 601px) {
  .cal-sched-picker-wrap {
    inset: 0;
    height: auto;
    align-items: center;
  }
  .cal-sched-picker {
    border-radius: 12px;
    padding: 18px;
    max-height: 90vh;
  }
  .sheet-drag-handle { display: none; }
  @keyframes cal-sched-picker-up {
    from { transform: scale(0.96); opacity: 0; }
    to   { transform: scale(1);    opacity: 1; }
  }
}
.cal-sched-picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.cal-sched-picker-title {
  font-weight: 600;
  font-size: 14px;
  color: var(--ink);
}
.cal-sched-picker-close {
  background: transparent;
  border: 0;
  color: var(--ink-faint);
  font-size: 18px;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  cursor: pointer;
}
.cal-sched-picker-close:hover { background: color-mix(in oklab, var(--ink) 8%, transparent); }
.cal-sched-picker-task-hero {
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.3;
  word-break: break-word;
  padding-block: 4px 8px;
  border-block-end: 1px solid var(--line);
}
.cal-sched-picker-caption {
  font-size: 12px;
  color: var(--ink-faint);
  margin-block-start: 4px;
}
/* Editable title input replaces the read-only `.cal-sched-picker-task-hero`
   in create / edit modes. Same visual weight as the hero (the user is
   typing the heading of the card) but with an underline + focus ring. */
.cal-sched-picker-title-input {
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  color: var(--ink);
  font-size: 18px;
  font-weight: 600;
  padding: 10px 12px;
  width: 100%;
  line-height: 1.3;
  margin-block-start: 4px;
}
.cal-sched-picker-title-input:focus { border-color: var(--accent); outline: none; }
/* Project picker row (create / edit modes). The select and the inline
   "+ New project" input occupy the same slot — only one is visible at a
   time. A small label sits above. */
.cal-sched-picker-proj-label {
  font-size: 11px;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-block-start: 8px;
}
.cal-sched-picker-proj-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.cal-sched-picker-proj-select,
.cal-sched-picker-proj-new {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 10px 12px;
  border-radius: 8px;
  font: inherit;
  font-size: 15px;
  min-height: 44px;
}
.cal-sched-picker-proj-select:focus,
.cal-sched-picker-proj-new:focus { border-color: var(--accent); outline: none; }
.cal-sched-picker-dt-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-block-start: 4px;
}
.cal-sched-picker-dt-btn {
  position: relative;
  flex: 1;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 12px 14px;
  border-radius: 10px;
  font: inherit;
  font-size: 15px;
  cursor: pointer;
  text-align: start;
  min-height: 48px;
  overflow: hidden;
}
.cal-sched-picker-dt-btn:focus-within { border-color: var(--accent); outline: none; }
.cal-sched-picker-dt-icon { font-size: 16px; opacity: 0.85; }
.cal-sched-picker-dt-label { flex: 1; }
.cal-sched-picker-dt-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  border: 0;
  padding: 0;
  margin: 0;
  background: transparent;
  color: transparent;
  font: inherit;
  cursor: pointer;
}
.cal-sched-picker-dt-input::-webkit-calendar-picker-indicator { opacity: 0; }
.cal-sched-picker-dt-sep {
  color: var(--ink-faint);
  font-size: 13px;
}
.cal-sched-picker-dur-label {
  font-size: 11px;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-block-start: 8px;
}
.cal-sched-picker-dur-row {
  /* Single-row pill cluster — no wrap. On narrow screens it scrolls
     horizontally so the custom input stays inline with the presets
     instead of dropping to a new row. */
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.cal-sched-picker-dur-row::-webkit-scrollbar { display: none; }
.cal-sched-picker-dur-pill {
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 6px 10px;
  border-radius: 999px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  min-height: 32px;
  flex: 0 0 auto;
  white-space: nowrap;
}
.cal-sched-picker-dur-pill:hover { color: var(--ink); }
.cal-sched-picker-dur-pill.on {
  background: color-mix(in oklab, var(--accent) 15%, transparent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 600;
}
.cal-sched-picker-dur-custom {
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 6px 10px;
  border-radius: 999px;
  font: inherit;
  font-size: 13px;
  min-height: 32px;
  width: 64px;
  flex: 0 0 auto;
  text-align: center;
}

/* Mobile week strip — Google-Calendar-style row of 7 day pills sitting
   flush under the calendar header. Replaces the older day-stepper.
   Each pill: weekday-short on top, date number below, up to 3 colored
   dots showing event density (overflow shown as "+N"). Today gets an
   accent-tinted circle around the date; the active anchor (in day mode)
   gets a ring outline. Mounted only in day mode — 3-day and week have
   their own per-column day headers in the grid. */
.cal-week-strip {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0;
  padding: 4px 6px 8px;
  background: var(--bg-card);
  border-block-end: 1px solid var(--line-strong);
}
.cal-week-strip-day {
  appearance: none;
  background: transparent;
  border: none;
  padding: 6px 2px 4px;
  font: inherit;
  color: var(--ink-dim);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  border-radius: 8px;
  transition: background 120ms;
}
.cal-week-strip-day:hover { background: var(--bg-raised); }
.cal-week-strip-wd {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  line-height: 1;
}
.cal-week-strip-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 28px;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  border-radius: 50%;
  font-variant-numeric: tabular-nums;
}
/* Today: accent-tinted circle around the date number. Mirrors Google
   Calendar's "today" treatment. */
.cal-week-strip-day.today .cal-week-strip-wd { color: var(--accent-text); }
.cal-week-strip-day.today .cal-week-strip-num {
  background: var(--accent);
  color: var(--bg);
}
/* Active anchor (day mode): outline ring around the date number — only
   added when the anchor isn't today, otherwise the today circle wins. */
.cal-week-strip-day.active:not(.today) .cal-week-strip-num {
  box-shadow: inset 0 0 0 2px var(--accent);
  color: var(--accent-text);
}
.cal-week-strip-day.active:not(.today) .cal-week-strip-wd { color: var(--accent-text); }
/* Event-density bar under the date number — up to 3 colored dots,
   then a "+N" overflow chip. Task dots are accent-colored, external
   (gcal) dots are blue, so the user reads at a glance whether a busy
   day is mostly self-scheduled or imported. */
.cal-week-strip-dots {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 6px;
  margin-block-start: 2px;
  font-variant-numeric: tabular-nums;
}
.cal-week-strip-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
}
.cal-week-strip-dot.external { background: var(--blue, #7ec7ff); }
.cal-week-strip-overflow {
  font-size: 9px;
  color: var(--ink-faint);
  margin-inline-start: 2px;
  line-height: 1;
}
.cal-sched-picker-dur-custom:focus {
  border-color: var(--accent);
  outline: none;
  width: 88px;
}
.cal-sched-picker-dur-custom::placeholder { color: var(--ink-faint); }
.cal-sched-picker-textarea {
  background: var(--bg);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 12px 14px;
  border-radius: 10px;
  font: inherit;
  font-size: 15px;
  width: 100%;
  resize: vertical;
  min-height: 72px;
  max-height: 200px;
  margin-block-start: 6px;
}
.cal-sched-picker-textarea:focus { border-color: var(--accent); outline: none; }
.cal-sched-picker-actions {
  display: flex;
  gap: 8px;
  margin-block-start: 6px;
  /* Pinned to the sheet's bottom as the last flex child — no `position:
     sticky` math needed because the sheet is a flex column and the
     body sibling takes the overflow. */
  flex: 0 0 auto;
  background: var(--bg-raised);
  padding-block: 8px 0;
  border-block-start: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
}
.cal-sched-picker-clear,
.cal-sched-picker-cancel,
.cal-sched-picker-save,
.cal-sched-picker-delete {
  flex: 1;
  padding: 12px 16px;
  border-radius: 8px;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  border: 1px solid var(--line-strong);
  min-height: 44px;
}
.cal-sched-picker-clear {
  background: transparent;
  color: var(--ink-dim);
  flex: 0 0 auto;
  padding-inline: 14px;
}
.cal-sched-picker-clear:hover { color: var(--ink); }
.cal-sched-picker-delete {
  background: transparent;
  color: var(--red-text);
  border-color: color-mix(in oklab, var(--red) 40%, var(--line-strong));
  flex: 0 0 auto;
  padding-inline: 14px;
}
.cal-sched-picker-delete:hover { background: color-mix(in oklab, var(--red) 8%, transparent); }
.cal-sched-picker-cancel {
  background: transparent;
  color: var(--ink-dim);
}
.cal-sched-picker-cancel:hover { color: var(--ink); }
.cal-sched-picker-save {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
  font-weight: 600;
}
.cal-sched-picker-save:hover { filter: brightness(1.05); }

/* Calendar grid slide animation — one-shot when the anchor date changes
   via setCalendarAnchorAnimated() (picker save lands on a different day,
   future drag-to-other-day flows). Logical translate doesn't auto-mirror
   in RTL, so RTL gets swapped keyframes per project i18n rule §5. */
@keyframes cal-grid-slide-from-end   { from { transform: translateX( 24px); opacity: 0; }
                                       to   { transform: translateX(0);     opacity: 1; } }
@keyframes cal-grid-slide-from-start { from { transform: translateX(-24px); opacity: 0; }
                                       to   { transform: translateX(0);     opacity: 1; } }
@media (prefers-reduced-motion: no-preference) {
  .cal-grid-slide-in-next { animation: cal-grid-slide-from-end   180ms ease-out; }
  .cal-grid-slide-in-prev { animation: cal-grid-slide-from-start 180ms ease-out; }
  [dir="rtl"] .cal-grid-slide-in-next { animation-name: cal-grid-slide-from-start; }
  [dir="rtl"] .cal-grid-slide-in-prev { animation-name: cal-grid-slide-from-end; }
}

/* Long-press arming pulse on calendar blocks (mobile). Shows the user
   their press is being held; transitions into the drag ghost once the
   250ms timer fires. Subtle so it doesn't startle on every tap. */
.cal-block.lp-arming {
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 50%, transparent);
  transition: box-shadow 220ms ease-out;
}
.cal-block.cal-block-source { opacity: 0.35; }

/* Calendar cards / blocks aren't paragraphs — touching them long enough
   to start a drag must NOT paint a native text selection. iOS in particular
   shows a magnifier + selection range on long-press by default; suppress. */
.cal-rail-card,
.cal-block,
.today-card,
.today-strip-block {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Body class set while a touch drag is active — disables page scroll so
   the finger drives the ghost instead of the viewport. Allows pointermove
   events to flow without the browser stealing them for pan-y scrolling.
   Also kills text selection / iOS callout so the long-press doesn't paint
   a selection range over whatever text the finger started on. */
body.cal-touch-dragging {
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
body.cal-touch-dragging * {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
body.cal-touch-dragging .cal-grid,
body.cal-touch-dragging .cal-rail-sheet { touch-action: none; }

/* FAB drop target — block dragged over the floating Unscheduled chip
   highlights to confirm the drop will unschedule. Amber to match the
   .would-unschedule ghost variant used by the rail in desktop. */
.cal-rail-fab.drop-target {
  background: color-mix(in oklab, var(--amber) 16%, var(--bg-raised));
  border-color: var(--amber);
  color: var(--amber-text);
}

/* ===================================================================
   Single-column board layout
   ===================================================================
   Stacks each group's three status cells vertically, ordering as
   in-progress → queued (faded) → done. Drag-and-drop is unaffected
   because cells still carry [data-status] and DnD locates targets
   by that attribute, not by grid position.
   The mobile breakpoint already does this same trick — these rules
   bring the same pattern to desktop, gated on .board.layout-single. */
.board.layout-single .col-headers {
  /* Per-cell ::before headers replace the sticky strip — collapsing it
     also hands a few px of vertical room back to the cards. */
  display: none;
}
/* Single-col override removed — the dynamic --sticky-top-project-label
   now reads col-headers' actual rendered height (0 in single-col since
   it's display:none), so the label automatically hugs the filter-bar
   bottom in single-col without a per-mode rule. */
.board.layout-single .project-row > .cells-row {
  grid-template-columns: 1fr;
  gap: 10px;
}
.board.layout-single.hide-done .project-row > .cells-row {
  /* Same single-column layout when "done" is hidden — only two cells
     render, but they keep the same width and stacking order. */
  grid-template-columns: 1fr;
}
.board.layout-single .cell {
  /* CSS `order` reshuffles cells without touching the DOM, so DnD's
     [data-status] lookups, focus order, and sibling references stay
     intact. In-progress moves to the top, queued in the middle, done
     at the bottom. */
  order: 2;
}
.board.layout-single .cell[data-status="in-progress"] { order: 1; }
.board.layout-single .cell[data-status="queued"]      { order: 2; }
.board.layout-single .cell[data-status="done"]        { order: 3; }
/* Single-column-specific queued-cell ordering is set above. The visual
   "queued reads quieter than in-progress" treatment lives further down,
   under the layout-agnostic rules — it now applies in multi-column too. */
/* Per-cell mini header — uses the same data-status-label attribute the
   mobile rules already populate (cell.dataset.statusLabel in
   renderCell). On desktop layout-single the strip has more room, so
   give it a touch more padding and a thin accent dot to match the
   sticky col-headers visual language. */
.board.layout-single .cell::before {
  content: attr(data-status-label);
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-dim);
  padding: 6px 4px 6px;
  border-block-end: 1px solid var(--line);
  margin-block-end: 4px;
}
/* Status-colored dot before each label, mirroring .col-header[data-col=...] .dot.
   Implemented as ::before content + a tiny inline-block via box-shadow trick
   would be ugly — instead we render the dot via the cell's own ::after
   anchored to top, but pseudo-elements only allow one ::before/::after each.
   So we collapse the dot into the label's letter-spacing trick: prefix the
   pseudo content with a U+2022 bullet. Color the bullet via the cell's
   per-status color override below. */
.board.layout-single .cell[data-status="queued"]::before      { color: var(--ink-dim); }
.board.layout-single .cell[data-status="in-progress"]::before { color: var(--accent-text); }
.board.layout-single .cell[data-status="done"]::before        { color: var(--accent-dim); }
/* Empty cells in single-column shouldn't visually disappear — show the
   label so the user understands the status exists, plus a soft em-dash
   placeholder. Matches mobile's :empty rule. */
.board.layout-single .cell:not(:has(.card)):not(:has(.new-task))::after {
  content: "—";
  color: var(--ink-faint);
  opacity: 0.5;
  font-size: 11px;
  padding: 2px 4px;
}

/* ===================================================================
   Queued vs In-Progress visual hierarchy (layout-agnostic)
   ===================================================================
   The queued column is "next up" — important to see, but not where the
   eye should land first. In-progress is the active stack and should
   read brightest. We fade queued cards (and the sticky col-header dot
   when present) to ~0.78 so the brightness gradient does the visual
   sorting, then restore on hover / focus-within / drop-target so the
   moment a user reaches into the queue, it lights up to full contrast.

   Applies in BOTH multi-column and single-column views — same rule,
   no layout gating. The mobile breakpoint also gets it for free, which
   is fine: phones already have the same in-progress > queued mental
   model and benefit from the same affordance. */
.board .cell[data-status="queued"] {
  opacity: 0.78;
  transition: opacity 140ms ease;
}
.board .cell[data-status="queued"]:hover,
.board .cell[data-status="queued"]:focus-within,
.board .cell[data-status="queued"].drop-target {
  opacity: 1;
}
/* In multi-column the sticky `QUEUED · IN PROGRESS · DONE` strip is
   visible — fade its queued header to match the cells underneath, so
   the column reads as one continuous "quieter" lane rather than a
   bright header on top of dim cards. layout-single hides .col-headers
   entirely (display:none above), so this rule has no effect there. */
.board .col-header[data-col="queued"] {
  opacity: 0.78;
  transition: opacity 140ms ease;
}
.board .col-header[data-col="queued"]:hover {
  opacity: 1;
}

/* Carry-over card variant — overdue tasks pulled forward into Today's
   cards row. Red accent + leading border so the "this is overdue" status
   reads at a glance against the regular today-card colors. */
.today-card.today-card-overdue-carryover {
  --card-accent: var(--red);
  border-inline-start: 2px solid var(--red);
  /* Carry-over cards have no estimation, no time-range row, and one chip
     in their meta. Without `align-self: start` they get stretched by the
     grid to match the tallest sibling (a regular task with 2-line title
     + chips), leaving a wide vertical void between title and play button.
     Sitting them at the top of the cell keeps the carry-over compact and
     puts the empty space outside the card border, where it reads as
     ordinary row whitespace rather than a broken card. */
  align-self: start;
}
.today-card.today-card-overdue-carryover .today-card-title-row {
  /* Don't stretch the title row vertically inside the card either —
     `flex: 1 1 auto` (the default for task cards) is what pushes meta
     to the bottom on tall cards. With `align-self: start` we already
     stop being tall, so the stretch is unwanted. Without this rule a
     reflow that briefly forces the card tall (e.g. grid recalculation
     on agenda nav) flashes the empty mid-card gap. */
  flex: 0 0 auto;
}
.today-card.today-card-overdue-carryover .today-card-label {
  color: var(--red-text);
}
/* Carry-over → today separator. Painted as a border on the first regular
   card via .today-card-after-carryover so the boundary takes no extra
   horizontal space (`today-cards-row` auto-sizes columns at 180-240px,
   so an inline divider element would eat a whole blank column). */
.today-card.today-card-after-carryover {
  border-inline-start: 1px dashed var(--line-strong);
  margin-inline-start: 8px;
  padding-inline-start: 14px;
}


/* ===================================================================
   Focus-view round 2: header redesign (color · name · bar · delete-on-hover
   · stats-on-hover), rail item simplification (color · name · count ·
   delete-on-hover, no ⋯), calendar multi-select project filter, view-done
   + compact SVG button layout, active-now compact mode, col-header height
   reduction.
   ================================================================ */

/* --- Focus-view-header redesign --- */
.focus-view-header {
  padding: 6px 12px 8px;
  border-block-end: 1px solid var(--line);
  position: sticky;
  top: var(--sticky-top-focus-header, var(--sticky-top-filter-bar, 52px));
  z-index: 12;
  background: var(--bg);
}
.focus-view-header .fvh-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.focus-view-header .fvh-color {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: transparent;
  padding: 0;
  cursor: pointer;
  color: var(--ink-faint);
  font-size: 10px;
  line-height: 1;
  flex: 0 0 auto;
}
.focus-view-header .fvh-color.has-color {
  border-color: var(--project-color, var(--line-strong));
}
.focus-view-header .fvh-name {
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
  cursor: text;
  flex: 0 0 auto;
  max-width: min(38vw, 360px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.focus-view-header .fvh-bar {
  position: relative;
  /* Bar grows to fill remaining horizontal space between name and stats,
     but shrinks down to 60px before clipping content. Stats sit beside
     it on the same row as a separate flex item (no longer below). */
  flex: 1 1 80px;
  min-width: 60px;
  height: 4px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
  display: flex;
}
.focus-view-header .fvh-bar > span {
  display: block;
  height: 100%;
}
.focus-view-header .fvh-bar .b-done    { background: var(--accent-dim); }
.focus-view-header .fvh-bar .b-doing   { background: var(--accent); }
.focus-view-header .fvh-bar .b-paused  { background: var(--amber, #d4a017); }
.focus-view-header .fvh-bar .b-blocked { background: var(--red); }
.focus-view-header .fvh-bar .b-queued  { background: var(--ink-faint); opacity: 0.4; }
.focus-view-header .fvh-delete {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  line-height: 1;
  font-size: 14px;
  padding: 0;
  opacity: 0;
  transition: opacity 120ms ease;
}
.focus-view-header:hover .fvh-delete,
.focus-view-header:focus-within .fvh-delete {
  opacity: 1;
}
.focus-view-header .fvh-delete:hover {
  color: var(--red-text);
  border-color: var(--red);
}
.focus-view-header .fvh-stats {
  /* Stats sit BESIDE the bar on the same row (no longer below it). They
     stay visible always — moving them into the flex row means hiding-
     and-showing would jolt the bar's width on every hover, which reads
     as visual noise. Compact whitespace + faded color keep them quiet
     without disappearing. */
  font-size: 11px;
  color: var(--ink-faint);
  display: inline-flex;
  align-items: center;
  flex-wrap: nowrap;
  white-space: nowrap;
  gap: 5px;
  flex: 0 0 auto;
}
.focus-view-header .fvh-color[hidden] { display: none; }
.focus-view-header .fvh-stat.open {
  color: var(--ink);
  font-weight: 500;
}
.focus-view-header .fvh-sep {
  color: var(--ink-faint);
  opacity: 0.5;
}

/* --- Project-rail item redesign (no ⋯; color · name · count · ×-on-hover) --- */
.project-rail-item .pri-color {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: transparent;
  padding: 0;
  cursor: pointer;
  color: var(--ink-faint);
  font-size: 8px;
  line-height: 1;
  flex: 0 0 auto;
}
.project-rail-item .pri-color.has-color {
  border-color: var(--project-color, var(--line-strong));
  background: var(--project-color, var(--ink-faint));
}
.project-rail-item .pri-color.pri-all {
  width: 14px;
  height: 14px;
  border: 0;
  color: var(--ink-faint);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.project-rail-item .pri-color.pri-all svg {
  width: 14px;
  height: 14px;
  display: block;
}
.project-rail-item .pri-delete {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  line-height: 1;
  font-size: 15px;
  padding: 0;
  opacity: 0;
  transition: opacity 120ms ease;
  margin-inline-start: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.project-rail-item:hover .pri-delete,
.project-rail-item:focus-within .pri-delete {
  opacity: 1;
}
.project-rail-item .pri-delete:hover {
  color: var(--red-text);
  border-color: var(--red);
}
/* Mobile: rail items live inside a bottom sheet, hover is unreliable on
   touch, and the touch target needs to clear the 38×38 floor. Always
   show the delete on the .in-sheet rail, and size it for fingertips. */
@media (max-width: 720px) {
  .project-rail.in-sheet .pri-delete {
    width: 38px;
    height: 38px;
    font-size: 20px;
    opacity: 1;             /* hover doesn't apply on touch */
    border-color: var(--line);
  }
  .project-rail.in-sheet .pri-color {
    width: 16px;
    height: 16px;
  }
  /* Slightly more padding on rail items so the delete + color + name
     have proper breathing room at touch sizes. */
  .project-rail.in-sheet .project-rail-item {
    padding: 10px 12px;
    gap: 10px;
  }
}

/* --- Kanban filter bar: view-done + compact SVG buttons --- */
.kfb-svg-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 4px 10px;
}
.kfb-svg-btn .kfb-svg-glyph svg {
  width: 14px;
  height: 14px;
  display: block;
}
.kfb-svg-btn .kfb-svg-label {
  font-size: 11px;
}
/* Mobile: drop the text label, bump icon size + padding + min-height so each
   button meets a comfortable touch-target floor (~38×38px). The previous
   22×30px hit area was below WCAG 2.5.5 (≥24×24), Apple HIG (≥44×44), and
   Material 3 (≥48dp) — and read as visually small on phones. */
@media (max-width: 720px) {
  .kfb-svg-btn {
    padding: 8px 10px;
    min-height: 38px;
    min-width: 38px;
  }
  .kfb-svg-btn .kfb-svg-glyph svg {
    width: 18px;
    height: 18px;
  }
  .kfb-svg-btn .kfb-svg-label { display: none; }
  .kfb-svg-btn .done-count-badge {
    font-size: 12px;
    padding: 2px 6px;
  }
  /* Focus-mode toggle on mobile lives in .kfb-focus-row — match the same
     touch-target floor so the row feels balanced with the view-done /
     density buttons below it. */
  .kfb-focus-row .btn-focus-toggle {
    padding: 8px 10px;
    min-height: 38px;
    min-width: 38px;
  }
  .kfb-focus-row .btn-focus-toggle svg {
    width: 18px;
    height: 18px;
  }
  /* Layout segmented toggle (multi/single) — same treatment so the whole
     kanban-filter-bar control cluster reads as consistently sized on
     mobile, not a mix of tiny and big. */
  .kfb-layout-btn {
    padding: 8px 12px;
    min-height: 38px;
    min-width: 38px;
  }
  .kfb-layout-btn .kfb-layout-glyph,
  .kfb-layout-btn .kfb-focus-glyph {
    width: 18px;
    height: 18px;
  }
}

/* --- Active-now compact mode --- */
.active-now.compact .active-row {
  display: none;
}
.active-now.compact {
  /* Compact state: only the head row renders. Reduce padding so the strip
     becomes a single thin line. */
  padding-block: 4px;
}
.active-now .ac-compact-toggle {
  /* position:relative so the NEW badge inside it anchors against the
     button rather than against the active-now bar. */
  position: relative;
  margin-inline-start: auto;
  width: 22px;
  height: 22px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* The whole .active-head is now clickable (toggles compact mode); the
   only exclusions are .ac-end-session and .next-break, which have
   stopPropagation in their own click handlers. cursor: pointer hints
   the affordance on the chrome around the chevron. */
.active-now .active-head { cursor: pointer; }
.active-now .active-head .ac-end-session,
.active-now .active-head .next-break { cursor: pointer; }

/* SVG chevron for quote-toggle — replaces the old ▴ / ▾ unicode glyphs
   with a properly sized inline SVG (uses ICON_CHEVRON_UP / _DOWN). The
   wrapper sets the box; the SVG inside is sized via height:100% so it
   inherits the chev's font-size scaling. */
.chev.chev-svg {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  line-height: 0;
  vertical-align: middle;
  color: currentColor;
}
.chev.chev-svg svg {
  width: 100%;
  height: 100%;
  display: block;
}
/* Override the bar's overflow:hidden — that was for an old max-height
   animation (the current keyframe animates opacity + translateY only),
   and it clips the floating NEW badge that the compact toggle hosts.
   Visible overflow is also needed for the focus-mode badge that pokes
   above the kanban-filter-bar (already handled there), and is the same
   pattern: badges float above their button into ancestor territory. */
.active-now { overflow: visible; }
.active-now .ac-compact-toggle:hover {
  border-color: var(--line);
  color: var(--ink);
}
.active-now .ac-compact-toggle svg {
  width: 14px;
  height: 14px;
  display: block;
}

/* --- Column header height reduction (existing UI) --- */
.col-header {
  padding: 4px 12px 4px;       /* was 10px 12px 8px */
}
.col-headers {
  padding: 4px 0;              /* was 8px 0 */
  margin-bottom: 4px;          /* was 10px */
}

/* The card's project chip is the shared <ProjectPickerChip> in its
   "card" variant — clicking it opens the project-change picker.
   `on-card` just makes the shared chip compact enough for a card row. */
.proj-picker-chip.on-card {
  font-size: 10px;
  padding: 2px 6px;
  max-width: 140px;
}

/* --- Calendar multi-select project filter --- */
.cal-project-filter-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
}
.cal-project-filter-btn.has-filter {
  color: var(--accent-text);
  border-color: var(--accent);
}
.cal-project-filter-btn .cpf-chev {
  font-size: 9px;
  color: var(--ink-faint);
}
.cal-project-filter-popover {
  z-index: 9000;
  width: min(360px, calc(100vw - 16px));
  max-height: 70vh;
  overflow-y: auto;
  padding: 10px;
  border-radius: 8px;
}
.cal-project-filter-popover .cpfp-bulk {
  display: flex;
  gap: 6px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--line);
  margin-block-end: 8px;
}
.cal-project-filter-popover .cpfp-bulk-btn {
  flex: 1 1 0;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg-card);
  color: var(--ink);
  font-size: 12px;
  cursor: pointer;
}
.cal-project-filter-popover .cpfp-bulk-btn:hover {
  border-color: var(--accent);
  color: var(--accent-text);
}
.cal-project-filter-popover .cpfp-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.cal-project-filter-popover .cpfp-row {
  display: grid;
  grid-template-columns: 20px 14px minmax(0, 1fr);
  align-items: center;
  gap: 10px;
  min-height: 38px;
  padding: 7px 9px;
  border: 1px solid transparent;
  border-radius: 7px;
  cursor: pointer;
  font-size: 12px;
  user-select: none;
}
.cal-project-filter-popover .cpfp-row:hover {
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
.cal-project-filter-popover .cpfp-row.is-on {
  color: var(--ink);
  border-color: color-mix(in oklab, var(--accent) 22%, transparent);
  background: color-mix(in oklab, var(--accent) 6%, transparent);
}
.cal-project-filter-popover .cpfp-row input {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
}
.cal-project-filter-popover .cpfp-row .cpfp-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink-faint);
  flex: 0 0 auto;
}
.cal-project-filter-popover .cpfp-row .cpfp-name {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Focus-view-header sort pill ---
   Compact control that opens a popover with 4 sort modes. Sits between the
   stats and the delete × inside .fvh-row. The row already has flex-wrap, so
   on narrow viewports the pill wraps onto a second line without crowding
   the name or the progress bar. The pill is always visible (unlike the ×
   which fades in on hover) — sort is a primary affordance for focus mode.
   When the active mode is "project" (default), the pill is quiet: faint
   border, faded ink. When the user has picked a non-default mode, the pill
   reads as "active" with the accent color so they don't lose track of the
   filter shaping their view. */
.focus-view-header .fvh-sort {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 3%, transparent);
  color: var(--ink);
  font-size: 11px;
  line-height: 1.4;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease, background-color 120ms ease;
}
.focus-view-header .fvh-sort:hover,
.focus-view-header .fvh-sort:focus-visible {
  border-color: color-mix(in srgb, var(--ink) 35%, transparent);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  outline: none;
}
.focus-view-header .fvh-sort.is-active {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--line));
  color: var(--accent-text, var(--accent));
  background: color-mix(in oklab, var(--accent) 12%, transparent);
}
.focus-view-header .fvh-sort.is-active:hover,
.focus-view-header .fvh-sort.is-active:focus-visible {
  background: color-mix(in oklab, var(--accent) 16%, transparent);
}
.focus-view-header .fvh-sort-icon {
  width: 12px;
  height: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
.focus-view-header .fvh-sort-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}
.focus-view-header .fvh-sort-label {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.focus-view-header .fvh-sort-chev {
  font-size: 9px;
  opacity: 0.7;
  line-height: 1;
}

/* Mobile: touch-target floor (38×38). The pill grows vertically and the
   font reads at a comfortable size on phones. Wraps naturally to a second
   row via the parent's flex-wrap. */
@media (max-width: 720px) {
  .focus-view-header .fvh-sort {
    padding: 8px 12px;
    min-height: 38px;
    font-size: 12px;
    gap: 8px;
  }
  .focus-view-header .fvh-sort-icon {
    width: 14px;
    height: 14px;
  }
}

/* Popover header — short label above the radio rows so the popover reads
   as a labelled chooser, not an unexplained list. Uses the same muted ink
   the rest of the chrome uses; tabular-nums kept for visual rhythm. */
.focus-sort-popover {
  min-width: 200px;
}
.focus-sort-popover .fsp-head {
  padding: 6px 12px 4px;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.focus-sort-popover .tb-overflow-item.is-active {
  color: var(--accent-text, var(--accent));
}

/* --- Move focus toggle out of layout-seg into its own slot --- */
.kfb-controls-cluster .kfb-focus-toggle-slot {
  display: inline-flex;
  align-items: center;
  margin-inline-start: 6px;
}
/* Strip the inline-start border that used to separate it from the
   multi/single buttons — it's now standalone so no separator needed. */
.kfb-controls-cluster .btn-focus-toggle {
  border-inline-start: none;
  margin-inline-start: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 4px 8px;
}
.kfb-controls-cluster .btn-focus-toggle.on {
  border-color: var(--accent);
}
/* ---- Calendar (Day + Week) ---- */
main.calendar {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--ink);
  flex: 1;
  min-height: 0;
}
.cal-header {
  display: flex;
  align-items: center;
  gap: 8px 12px;
  flex-wrap: wrap;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line-strong);
  background: var(--bg-card);
  font-size: 12px;
}
/* Left cluster: navigation + title + unscheduled chip stay together. */
.cal-header-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}
/* Right cluster: view-mode toggle + GCal + hours + gear. Uses
   margin-inline-start: auto so the cluster sits on the trailing edge
   when the row has space, and drops to its own line as a unit when it
   doesn't — instead of stranding day/week on a third row. */
.cal-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-inline-start: auto;
}
.cal-nav, .cal-today, .cal-mode-btn {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink);
  border-radius: 2px;
  padding: 4px 10px;
  cursor: pointer;
  font: inherit;
}
.cal-nav:hover, .cal-today:hover, .cal-mode-btn:hover { border-color: var(--accent-dim); }
/* Active day/week pill — outline-only with a tinted fill, not a solid block. */
.cal-mode-btn.on {
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  color: var(--accent-text);
  border-color: var(--accent);
}
.cal-title { font-weight: 600; margin-inline-start: 4px; color: var(--ink); }
.cal-mode { display: flex; gap: 2px; }

.cal-grid {
  flex: 1;
  overflow: auto;
  min-height: 0;
  /* Small visual cushion below 24:00 line at max-scroll. cal-body is now
     pinned to exactly 24h × pxPerHour (height, not min-height), so late-
     night blocks no longer push it taller — the 80px safety margin from
     earlier iterations is no longer needed. 16px gives the 24:00 line
     a tiny breath above cal-grid's bottom edge without producing a big
     empty area below 11 PM. */
  padding-block-end: 0px;
  scroll-padding-block-end: 0px;
  /* Stop scroll-chaining: once cal-grid hits its top or bottom boundary,
     the browser would otherwise pass the remaining wheel/touch delta up
     to the page (html), causing the page to scroll, main.calendar to slide
     out of position, and the sticky day-headers + all-day row inside
     cal-grid to visually disappear. `contain` keeps the scroll trapped
     in the cal-grid scroller. */
  overscroll-behavior: contain;
}
.cal-day-headers {
  display: grid;
  grid-template-columns: 50px repeat(var(--day-count), 1fr);
  border-bottom: 1px solid var(--line-strong);
  background: var(--bg-card);
  position: sticky;
  top: 0;
  /* Must outrank .cal-block, which uses z-index: calc(1 + var(--lane-index))
     and reaches 3+ when events overlap into multiple lanes. */
  z-index: 30;
}
.cal-day-header {
  padding: 6px 4px;
  text-align: center;
  font-size: 11px;
  color: var(--ink-dim);
  border-inline-start: 1px solid var(--line-strong);
}
.cal-day-header.today { color: var(--accent-text); background: color-mix(in oklab, var(--accent) 8%, transparent); }
.cal-day-num { display: block; font-size: 13px; color: var(--ink); margin-top: 2px; }

.cal-all-day {
  display: grid;
  grid-template-columns: 50px repeat(var(--day-count), 1fr);
  border-bottom: 1px solid var(--line-strong);
  background: var(--bg-card);
  min-height: 36px;
  position: sticky;
  top: 0;
  /* One below .cal-day-headers (30) so the day strip stays on top,
     but still above any .cal-block lane stack underneath. */
  z-index: 29;
}
/* Headers actually render at ~44px (6 + 11 + 2 + 13 + 6 + 1 with default
   line-height), not the 32px the previous offset assumed. The mismatch
   meant the DUE row stuck up under the headers and chips' top halves
   got covered when the user scrolled. */
.cal-day-headers + .cal-all-day { top: 44px; }
.cal-all-day-label {
  font-size: 9px; color: var(--ink-faint);
  padding-block-start: 8px; padding-inline-end: 6px;
  text-align: end; letter-spacing: 0.06em; text-transform: uppercase;
}
.cal-all-day-cell {
  border-inline-start: 1px solid var(--line-strong); padding: 4px;
  display: flex; flex-wrap: wrap; gap: 4px;
}
.due-only-chip {
  display: inline-flex; align-items: center; gap: 3px;
  border: 1px dashed var(--amber); color: var(--amber-text);
  padding: 1px 5px; border-radius: 2px; font-size: 10px;
  cursor: grab;
  user-select: none;
}
.due-only-chip:active { cursor: grabbing; }
.due-only-chip.dragging { opacity: 0.4; }
.allday-external-chip {
  display: inline-flex; gap: 3px; align-items: center;
  color: var(--blue, #7ec7ff);
  border: 1px dashed color-mix(in oklab, var(--blue, #7ec7ff) 60%, var(--line-strong));
  padding: 1px 5px; border-radius: 2px; font-size: 10px;
}

.cal-body {
  display: grid;
  grid-template-columns: 50px repeat(var(--day-count), 1fr);
  position: relative;
  /* Lock to exactly 24h — height (not min-height) so a task scheduled past
     midnight (e.g. 23:30 + 60 min) doesn't push cal-body taller than its
     hour grid. Without this, cal-body's natural extent grew with overflowing
     blocks, max-scroll repositioned the bottom on the latest block, and
     the 11 PM hour cell ended up half-clipped by the cal-grid edge. The
     overflowing block visually extends past cal-body bounds but cal-grid's
     overflow:auto clips it — we lose the bottom of one block, not an
     entire hour of the day grid. */
  height: calc(var(--hour-count) * var(--px-per-hour));
}
.cal-hour-col { border-inline-end: 1px solid var(--line-strong); }
.cal-hour-label {
  height: var(--px-per-hour);
  font-size: 10px;
  color: var(--ink-faint);
  padding-block-start: 2px; padding-inline-end: 6px;
  text-align: end;
  line-height: 1;
}
.cal-day-col {
  border-inline-start: 1px solid var(--line-strong);
  background-image: linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(var(--px-per-hour) - 1px),
    color-mix(in oklab, var(--ink-faint) 30%, transparent) calc(var(--px-per-hour) - 1px),
    color-mix(in oklab, var(--ink-faint) 30%, transparent) var(--px-per-hour)
  );
  background-size: 100% var(--px-per-hour);
  position: relative;
}
.cal-day-col.today { background-color: color-mix(in oklab, var(--accent) 3%, transparent); }

/* Business-hours marker — subtle accent tint inside the active window
   plus dashed top/bottom borders for a hard edge. The settings label
   reads "tinted band in calendar" so the fill matches the promise.
   Tint is 4% accent — visible but doesn't wash the whole column. */
.cal-biz-band {
  position: absolute;
  inset-inline: 0;
  background: color-mix(in oklab, var(--accent) 4%, transparent);
  border-top: 1px dashed color-mix(in oklab, var(--ink-faint) 40%, transparent);
  border-bottom: 1px dashed color-mix(in oklab, var(--ink-faint) 40%, transparent);
  pointer-events: none;
  z-index: 0;
}
/* Today column already has a 3% accent wash; biz band stays the same
   tint there — the column wash + band tint compound naturally so the
   band reads slightly stronger inside today, which is the intent. */

.cal-block {
  position: absolute;
  /* Hybrid overlap:
     - 1 or 2 events: cascade (each later offsets 12% right, stays readable)
     - 3+ events:   lane-split via the JS-supplied --lane-count so no event
                    becomes unreadable. Hover always promotes to full width
                    with the highest z-index, so any event is reachable. */
  inset-inline-start: calc(var(--lane-index, 0) / var(--lane-count, 1) * 100% + 2px);
  width: calc(100% / var(--lane-count, 1) - 4px);
  z-index: calc(1 + var(--lane-index, 0));
  background: color-mix(in oklab, var(--accent) 10%, var(--bg));
  /* Subtle outer border, slightly stronger leading rail for the
     accent stripe. Was 1px solid `var(--accent)` on the start edge,
     which read as a thick green slab against the dark grid. A 2px
     softened version is enough to communicate the "this is a task
     block" affordance without dominating the row. */
  border: 1px solid color-mix(in oklab, var(--accent) 22%, var(--line-strong));
  border-inline-start: 2px solid color-mix(in oklab, var(--accent) 60%, transparent);
  border-radius: 3px;
  padding: 4px 6px;
  font-size: 11px;
  color: var(--ink);
  overflow: hidden;
  cursor: pointer;
  transition: left 120ms, width 120ms, background 120ms, box-shadow 120ms;
}
.cal-block:hover {
  z-index: 99;
  background: color-mix(in oklab, var(--accent) 22%, var(--bg));
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.cal-block.external:hover { background: color-mix(in oklab, var(--blue, #7ec7ff) 22%, var(--bg)); }
.cal-block.has-due { border-inline-end: 3px solid var(--amber); }
.cal-block.done { opacity: 0.55; text-decoration: line-through; }
.cal-block.past { opacity: 0.5; }
.cal-block.external {
  background: color-mix(in oklab, var(--blue, #7ec7ff) 10%, var(--bg));
  border-color: color-mix(in oklab, var(--blue, #7ec7ff) 30%, var(--line-strong));
  border-inline-start-color: var(--blue, #7ec7ff);
}
.cal-block.external:hover { background: color-mix(in oklab, var(--blue, #7ec7ff) 20%, var(--bg)); }

/* Google Calendar attendance status — three palettes:
   accepted (blue, default), tentative (amber, "maybe"),
   needsAction (muted/dashed, "no reply"), declined (red, struck-through,
   only visible when the user toggles `showDeclinedEvents`). */
.cal-block.external.rs-tentative {
  background: color-mix(in oklab, var(--amber) 10%, var(--bg));
  border-color: color-mix(in oklab, var(--amber) 30%, var(--line-strong));
  border-inline-start-color: var(--amber);
}
.cal-block.external.rs-tentative:hover { background: color-mix(in oklab, var(--amber) 22%, var(--bg)); }
.cal-block.external.rs-needsAction {
  background: color-mix(in oklab, var(--ink-faint) 8%, var(--bg));
  border-style: dashed;
  border-color: color-mix(in oklab, var(--ink-faint) 50%, var(--line-strong));
  border-inline-start-color: var(--ink-faint);
  color: var(--ink-dim);
}
.cal-block.external.rs-needsAction:hover { background: color-mix(in oklab, var(--ink-faint) 18%, var(--bg)); }
.cal-block.external.rs-declined {
  background: color-mix(in oklab, var(--red) 8%, var(--bg));
  border-color: color-mix(in oklab, var(--red) 30%, var(--line-strong));
  border-inline-start-color: var(--red);
  opacity: 0.65;
  text-decoration: line-through;
}
.cal-block.external.rs-declined:hover { background: color-mix(in oklab, var(--red) 18%, var(--bg)); }

/* All-day chips and today-strip cards mirror the same palette. */
.allday-external-chip.rs-tentative {
  color: var(--amber-text);
  border-color: color-mix(in oklab, var(--amber) 60%, var(--line-strong));
}
.allday-external-chip.rs-needsAction {
  color: var(--ink-dim);
  border-color: color-mix(in oklab, var(--ink-faint) 60%, var(--line-strong));
}
.allday-external-chip.rs-declined {
  color: var(--red-text);
  border-color: color-mix(in oklab, var(--red) 60%, var(--line-strong));
  text-decoration: line-through;
  opacity: 0.7;
}
.today-card.rs-tentative { --card-accent: var(--amber); }
.today-card.rs-needsAction { --card-accent: var(--ink-faint); opacity: 0.85; }
.today-card.rs-declined {
  --card-accent: var(--red);
  opacity: 0.65;
  text-decoration: line-through;
}
.cal-block-title {
  font-weight: 500;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.cal-block-time { font-size: 10px; color: var(--ink-dim); margin-top: 2px; white-space: nowrap; }
/* Short blocks (< ~40px) can't fit a second line — hide the time label so
   it doesn't visually bleed into the block below. The title's ellipsis
   still works. JS doesn't need to know; pure CSS based on block height via
   container queries isn't supported everywhere yet, so we gate on a class. */
.cal-block.short .cal-block-time { display: none; }
.cal-block.short .cal-block-title { -webkit-line-clamp: 1; }
.cal-block-due-marker { position: absolute; top: 2px; inset-inline-end: 4px; color: var(--amber-text); font-size: 10px; }

.cal-block.external .cal-block-prep {
  position: absolute;
  top: 2px;
  inset-inline-end: 4px;
  font-family: inherit;
  font-size: 10px;
  line-height: 1;
  padding: 2px 6px;
  border: 1px solid color-mix(in oklab, var(--blue, #7ec7ff) 50%, var(--line-strong));
  background: color-mix(in oklab, var(--blue, #7ec7ff) 18%, var(--bg));
  color: var(--ink);
  border-radius: 3px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 100ms, background 100ms;
  z-index: 1;
}
.cal-block.external:hover .cal-block-prep,
.cal-block.external:focus-within .cal-block-prep {
  opacity: 1;
}
.cal-block.external .cal-block-prep:hover {
  background: color-mix(in oklab, var(--blue, #7ec7ff) 35%, var(--bg));
}
.cal-block.external.short .cal-block-prep { display: none; }

.cal-now-line {
  position: absolute; inset-inline: 0;
  height: 1px; background: var(--accent);
  z-index: 2; pointer-events: none;
}
.cal-now-dot {
  position: absolute; inset-inline-start: -4px; top: -3px;
  width: 7px; height: 7px;
  background: var(--accent);
  border-radius: 50%;
  box-shadow: 0 0 4px var(--accent-glow);
}

/* ---- Calendar side rail ---- */
.cal-layout {
  display: grid;
  grid-template-columns: 1fr 220px;
  flex: 1;
  min-height: 0;
}
/* Desktop-only rail-width overrides. The :not(.mobile) guard is required:
   without it, these rules outranked the mobile `.cal-layout { 1fr }` rule
   on specificity and reserved an invisible 180/200px rail track on phones,
   squishing the calendar content into the left half of the screen. */
main.calendar.week:not(.mobile) .cal-layout { grid-template-columns: 1fr 180px; }
main.calendar.threeday:not(.mobile) .cal-layout { grid-template-columns: 1fr 200px; }
/* Constrain the grid row to its parent's height. Without this, .cal-layout
   defaults to grid-template-rows: auto and the row sizes to its content
   (cal-grid's full 1632px scrollable area), which makes cal-left taller
   than main.calendar by ~40px (cal-header). main.calendar's overflow:hidden
   clips the bottom 40px — exactly the height of one hour cell — so the
   24:00 line ends up below the visible viewport and behind the footer.
   minmax(0, 1fr) is the canonical "fill parent, allow shrinking past
   content" pattern for grid rows. */
.cal-layout { grid-template-rows: minmax(0, 1fr); }
.cal-left { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.cal-rail {
  border-inline-start: 1px solid var(--line-strong);
  background: var(--bg-card);
  display: flex; flex-direction: column;
  overflow: hidden;
  min-height: 0;
}
.cal-rail-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 10px; border-bottom: 1px solid var(--line-strong);
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint);
}
.cal-rail-title { color: var(--ink-faint); }
.cal-rail-actions {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-bottom: 1px solid var(--line-strong);
}
.cal-rail-actions input {
  flex: 1; min-width: 0;
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: 2px; padding: 4px 6px;
  font: inherit; font-size: 11px;
}
/* Tap-to-schedule hint — mobile-only sibling of the drag-hint. The
   sheet shows it instead of the drag-hint because tapping a card on
   mobile opens the schedule picker (drag is desktop-only). */
.cal-rail-tap-hint {
  font-size: 11px;
  color: color-mix(in oklab, var(--accent, var(--green)) 75%, var(--ink-dim));
  opacity: 1;
  letter-spacing: 0.04em;
  padding: 4px 10px 6px;
  user-select: none;
  font-weight: 500;
  display: none;
}
/* Drag-to-schedule hint at the top of the unscheduled rail. Subtle but
   always visible so users know cards in this list are draggable onto
   the calendar grid. */
.cal-rail-drag-hint {
  /* Bumped from font-size 10px / italic / ink-faint @ 0.7 — was reading
     as throwaway whisper text. Keeping it dim enough to feel like a hint
     (not a header), but bumped to 11px, accent-tinted, full opacity, and
     no italic so the affordance actually catches a glance. The leading
     ↔ glyph is rendered in JS and now picks up the accent color too. */
  font-size: 11px;
  color: color-mix(in oklab, var(--accent, var(--green)) 75%, var(--ink-dim));
  opacity: 1;
  letter-spacing: 0.04em;
  padding: 4px 10px 6px;
  user-select: none;
  font-weight: 500;
}
.cal-rail-drag-hint::first-letter {
  /* The ↔ glyph at the start gets a brighter accent to draw the eye. */
  color: var(--accent, var(--green));
  font-weight: 600;
}
.cal-rail-list { overflow-y: auto; padding: 8px; flex: 1; overscroll-behavior: contain; }
.cal-rail-project {
  border: 1px solid color-mix(in oklab, var(--project-color, var(--line-strong)) 28%, var(--line));
  background: color-mix(in oklab, var(--project-color, var(--bg-card)) 5%, transparent);
  border-radius: 6px;
  overflow: hidden;
  margin-block-end: 8px;
}
.cal-rail-project-head {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 30px;
  padding: 6px 8px;
  border-block-end: 1px solid color-mix(in oklab, var(--project-color, var(--line-strong)) 20%, var(--line));
  background: color-mix(in oklab, var(--project-color, var(--bg-card)) 9%, var(--bg-card));
}
.cal-rail-project-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1px solid color-mix(in oklab, var(--project-color, var(--ink-faint)) 60%, var(--line-strong));
  background: var(--project-color, var(--ink-faint));
  flex: 0 0 auto;
}
.cal-rail-project-name {
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
  font-size: 11px;
  font-weight: 600;
}
.cal-rail-project-count {
  flex: 0 0 auto;
  min-width: 22px;
  padding: 1px 7px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--project-color, var(--accent)) 15%, var(--bg));
  color: color-mix(in oklab, var(--project-color, var(--accent)) 70%, var(--ink));
  font-size: 10px;
  text-align: center;
}
.cal-rail-project-list {
  padding: 6px;
}
.cal-rail-card {
  border: 1px dashed var(--accent-dim);
  color: var(--ink-dim);
  background: transparent;
  padding: 6px 8px;
  border-radius: 2px;
  font-size: 11px;
  margin-bottom: 5px;
  cursor: grab;
}
.cal-rail-project-list .cal-rail-card:last-child { margin-bottom: 0; }
.cal-rail-card:hover { background: color-mix(in oklab, var(--accent) 5%, transparent); color: var(--ink); }
.cal-rail-card.has-due { border-color: var(--amber); color: var(--amber-text); }
.cal-rail-card.has-due:hover { background: color-mix(in oklab, var(--amber) 5%, transparent); }
.cal-rail-card-due-marker { color: var(--amber-text); }
.cal-rail-card-title { line-height: 1.3; word-break: break-word; }
.cal-rail-card-meta { font-size: 10px; color: var(--ink-faint); margin-top: 2px; }
.cal-rail-card.no-est .no-est-label { font-style: italic; }
.cal-rail-more { text-align: center; color: var(--ink-faint); font-size: 10px; padding: 8px; }

/* ---- Calendar quick-create popover ---- */
.cal-quick-pop {
  position: fixed;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  z-index: 30;
  display: flex; flex-direction: column; gap: 8px;
  min-width: 260px;
  color: var(--ink);
}
.cal-quick-head { font-size: 11px; color: var(--ink-dim); }
.cal-quick-pop input,
.cal-quick-pop select {
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: 2px; padding: 6px 8px;
  font: inherit;
  width: 100%;
}
.cal-quick-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.cal-quick-row label { color: var(--ink-faint); min-width: 60px; }
.cal-quick-row input,
.cal-quick-row select { flex: 1; min-width: 0; }
.cal-quick-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 4px; }
.cal-quick-actions button {
  background: transparent; border: 1px solid var(--line-strong); color: var(--ink);
  padding: 4px 10px; border-radius: 2px; cursor: pointer; font: inherit; font-size: 11px;
}
.cal-quick-actions .cal-quick-save { border-color: var(--accent); color: var(--accent-text); }
.cal-quick-actions .cal-quick-save:hover { background: color-mix(in oklab, var(--accent) 15%, transparent); }

/* ---- Calendar drag/drop ---- */
.cal-rail-card.dragging { opacity: 0.4; }
.cal-day-col.drop-target { background-color: color-mix(in oklab, var(--accent) 10%, transparent); }
.cal-block.dragging { opacity: 0.4; }
.cal-rail.drop-target { background: color-mix(in oklab, var(--amber) 8%, var(--bg-card)); }

/* ---- Calendar block resize handles ----
   Slim grab strips at the top + bottom edges of each block. Were 6px
   tall and lit up to 50% accent on hover — the green slab dominated
   the block. Now 4px tall and 28% accent on hover — still grabbable
   without overwhelming the block content. */
.cal-block-resize {
  position: absolute; inset-inline: 0;
  height: 4px;
  cursor: ns-resize;
  z-index: 2;
}
.cal-block-resize.top { top: 0; }
.cal-block-resize.bot { bottom: 0; }
.cal-block:hover .cal-block-resize {
  background: color-mix(in oklab, var(--accent) 28%, transparent);
}
/* Touch devices don't get resize via drag — drop the hover-revealed
   handle entirely so it doesn't show on tap-and-hold. */
@media (hover: none) {
  .cal-block-resize { display: none; }
}

/* ---- Calendar drag ghost + drop preview ---- */
.cal-block.cal-block-source {
  opacity: 0.35;
  outline: 1px dashed var(--accent-dim);
  outline-offset: -2px;
}
.cal-block.cal-block-ghost {
  pointer-events: none;
  z-index: 100;
  background: color-mix(in oklab, var(--accent) 30%, var(--bg-card));
  border-inline-start: 3px solid var(--accent);
  border-radius: 2px;
  padding: 3px 5px;
  font-size: 11px;
  color: var(--ink);
  box-shadow: 0 6px 18px rgba(0,0,0,0.45);
  /* Dashed (not solid) so the ghost reads as "in transit" — distinguishes
     visually from a parked block. Same idiom as `.cal-day-col.drop-target`. */
  outline: 1px dashed var(--accent);
  outline-offset: -1px;
  transition: none; /* preview must be instant — no easing lag */
}
.cal-block.cal-block-ghost.would-unschedule {
  background: color-mix(in oklab, var(--amber) 25%, var(--bg-card));
  border-left-color: var(--amber);
  outline-color: var(--amber);
}
/* Snapped state — quieter than the previous 2px solid + glow. The dashed
   outline + slightly brighter fill is enough to read as "this is the
   landing spot" without dominating the slot. */
.cal-block.cal-block-ghost.snapped {
  background: color-mix(in oklab, var(--accent) 42%, var(--bg-card));
  outline: 1px dashed var(--accent);
  box-shadow: 0 6px 18px rgba(0,0,0,0.45);
}
.cal-day-col.drop-target {
  background-color: color-mix(in oklab, var(--accent) 12%, transparent);
  outline: 1px dashed var(--accent);
  outline-offset: -2px;
}
.cal-rail.drop-target {
  background: color-mix(in oklab, var(--amber) 14%, var(--bg-card));
  outline: 1px dashed var(--amber);
  outline-offset: -2px;
}

/* ---- Today calendar strip (board view) ---- */
.today-cal-strip {
  background: var(--bg-card);
  border-bottom: 1px solid var(--line-strong);
  padding: 4px 12px 6px;
  display: flex; flex-direction: column; gap: 4px;
  font-size: 10px; color: var(--ink-faint);
}
.today-cal-strip[hidden] { display: none; }
.today-strip-row {
  display: grid; grid-template-columns: 60px 1fr; gap: 8px;
  align-items: center;
  min-height: 18px;
}
/* Header row for the gcal + cards toggle pills. Pulled out of the Due row
   so that Due/Tasks/GCal/Axis rows all share an identical 2-column layout,
   which keeps their 1fr track the same width → chips and ticks align. */
.today-strip-badges-row {
  display: flex;
  justify-content: flex-end;
  gap: 4px;
  min-height: 0;
}
.today-strip-row.today-strip-row-external .today-strip-grid {
  height: 14px;
  background: color-mix(in oklab, var(--blue, #7ec7ff) 4%, var(--bg));
}
.today-strip-label {
  font-size: 9px; letter-spacing: 0.06em;
  text-transform: uppercase; text-align: end;
  color: var(--ink-faint);
}
.today-strip-label.gcal { color: var(--blue, #7ec7ff); }

/* Hour axis — thin tick-labeled ruler beneath the two tracks. */
.today-strip-hour-axis {
  /* Hour-range chip moved from start to end of the row in v2 onboarding —
     it was visually competing with the today-strip-label slot on the
     start side. End-side keeps the timeline label-led on the start and
     puts the chip with the badges/controls cluster mental-model. */
  display: grid; grid-template-columns: 1fr auto; gap: 8px;
  align-items: center;
  min-height: 10px;
}
.today-strip-hour-ticks {
  position: relative; height: 9px;
  font-size: 8px; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.today-strip-hour-tick {
  position: absolute; top: 0;
  transform: translateX(-50%);
  line-height: 1;
}
.today-strip-label.axis { visibility: hidden; } /* spacer column, keeps grid alignment */

/* Visible-hours pill that replaces the hidden axis spacer. Mirrors the
   .today-strip-hour-tick typography so it sits flush with the ruler, but
   adds an icon + border to read as a button. Clicking opens the calendar
   config popover (visible window + business-hours band toggle). */
/* Dimmed inline hint at the bottom of the strip — tells the user the
   bars/dues are draggable to reschedule. Always visible (so it works as
   ongoing affordance, not just a nudge), but heavily faded so it doesn't
   compete with the bars themselves. Hidden via the JS conditional when
   there are zero items in either row. */
/* Unified strip header — one row with three clusters:
     [drag hint]   [‹ Today ›  today]   [📇 cards  ⚠ overdue  📅 hide]
   Uses CSS grid with three 1fr-or-auto tracks so the agenda nav stays
   centered regardless of left/right cluster widths, and start/end
   clusters never push it around. */
.today-strip-header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  padding-block-end: 4px;
}
.today-strip-header .today-strip-drag-hint {
  font-size: 10px;
  color: var(--ink-faint);
  text-align: start;
  opacity: 0.6;
  letter-spacing: 0.04em;
  font-style: italic;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.today-strip-header .today-strip-drag-hint-spacer { /* empty — keeps the grid */ }
.today-strip-header .today-strip-agenda-nav {
  /* Override the prior row-level padding/margin since the header now
     handles spacing; nav was bleeding into adjacent clusters before. */
  padding-block-end: 0;
}
.today-strip-header .today-strip-badges {
  display: flex;
  justify-content: flex-end;
  gap: 4px;
  flex-wrap: wrap;
}

/* Gcal show/hide toggle lives in the top row on the right edge. */
.today-strip-gcal-toggle {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-secondary);
  padding: 2px 10px;
  border-radius: 10px;
  font: inherit;
  font-size: var(--fs-caption);
  letter-spacing: 0.02em;
  cursor: pointer;
  line-height: 1.3;
  transition: border-color 80ms ease, color 80ms ease, background 80ms ease;
}
.today-strip-gcal-toggle:hover {
  color: var(--blue, #7ec7ff);
  background: var(--bg-raised);
}
.today-strip-gcal-toggle:focus-visible { border-color: var(--blue, #7ec7ff); outline: none; }
.today-strip-gcal-toggle.on {
  color: var(--blue, #7ec7ff);
  border-color: color-mix(in oklab, var(--blue, #7ec7ff) 40%, transparent);
}

/* Overdue carry-over toggle — same shape as the gcal pill, red accent.
   `.is-elsewhere` greys it out when the user has navigated away from Today
   (the toggle's effect is Today-only, but we render the pill so the user
   keeps the visual cue that it's on). */
.today-strip-overdue-toggle {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-faint);
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 9px;
  letter-spacing: 0.04em;
  cursor: pointer;
  font: inherit;
  line-height: 1.3;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: border-color 80ms ease, color 80ms ease, background 80ms ease;
}
.today-strip-overdue-toggle:hover {
  border-color: var(--red);
  color: var(--red-text);
  background: color-mix(in oklab, var(--red) 8%, transparent);
}
.today-strip-overdue-toggle.on {
  color: var(--red-text);
  border-color: color-mix(in oklab, var(--red) 50%, var(--line-strong));
  background: color-mix(in oklab, var(--red) 10%, transparent);
}
.today-strip-overdue-toggle.off { opacity: 0.7; }
.today-strip-overdue-toggle.is-elsewhere {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}
/* Mobile-only inline "× Hide" CTA on the day-overview strip — same hide
   action as the topbar `▬ Day overview` button and the … menu entry, but
   reachable from the visible section. Visually muted (transparent bg,
   ink-faint outline) so it doesn't compete with the carry-over / gcal
   toggles for attention; the user opts to dismiss only when they're
   done with the section. */
.today-strip-hide-cta {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  border-radius: 999px;
  padding: 4px 10px;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.today-strip-hide-cta:hover {
  border-color: var(--ink-dim);
  color: var(--ink);
}
.today-strip-gcal-toggle .gcal-icon {
  width: 12px; height: 12px;
  flex: 0 0 12px;
  display: block;
}
.today-strip-gcal-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

/* Today-cards toggle — same pill-style as the gcal toggle, green accent. */
/* Design pass — readable agenda toggle. Was 9px / --ink-faint with extra
   .off opacity (which compounded to barely-visible). Now caption-size with
   --ink-secondary so the label reads clearly at rest, regardless of state.
   `font: inherit` is intentionally placed BEFORE font-size so the explicit
   font-size wins (font shorthand otherwise resets all font properties). */
.today-strip-cards-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-secondary);
  padding: 2px 10px;
  border-radius: 10px;
  font: inherit;
  font-size: var(--fs-caption);
  letter-spacing: 0.02em;
  cursor: pointer;
  line-height: 1.3;
  margin-inline-end: 6px;
  transition: border-color 80ms ease, color 80ms ease, background 80ms ease;
}
.today-strip-cards-toggle:hover {
  color: var(--accent-text);
  background: var(--bg-raised);
}
.today-strip-cards-toggle:focus-visible { border-color: var(--accent); outline: none; }
.today-strip-cards-toggle.on {
  color: var(--accent-text);
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, transparent);
}
/* (Strip dismiss is gone — replaced by the topbar `▬ day plan` toggle.) */
/* Legacy track class kept for back-compat in case any skin references it. */
.today-strip-due-track { display: flex; flex-wrap: wrap; gap: 4px; min-height: 14px; }
/* Due chips now live in a time-aligned grid (.today-strip-due-grid), so each
   chip sits at its actual due time horizontally — the row reads against the
   hour axis below just like the tasks + gcal rows. */
.today-strip-due-grid {
  height: 20px;
  background: transparent;
  border: none;
  overflow: visible;
}
.today-strip-due {
  position: absolute;
  top: 0;
  /* Right-align: chip's right edge sits on the due-time tick. The ◆ marker
     lives at the end of the label so it visually pins to the tick. */
  transform: translateX(-100%);
  display: inline-flex; align-items: center;
  border: 1px dashed var(--amber); color: var(--amber-text);
  padding: 1px 6px; border-radius: 2px; font-size: 10px;
  line-height: 1.4;
  cursor: ew-resize;          /* hint at horizontal-drag affordance */
  background: var(--bg);
  white-space: nowrap;
  z-index: 1;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  touch-action: none;
}
.today-strip-due.dragging {
  z-index: 5;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  background: color-mix(in oklab, var(--amber) 22%, var(--bg));
}
/* Off-screen (clamped) dues pin to the container edge and don't translate,
   otherwise they'd be pushed fully outside by the right-align transform. */
.today-strip-due.offscreen { opacity: 0.45; transform: none; }
.today-strip-due:hover {
  background: color-mix(in oklab, var(--amber) 15%, var(--bg));
  z-index: 2;
  max-width: none;
}
.today-strip-grid {
  position: relative; height: 16px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  overflow: hidden;
}
.today-strip-grid.drop-target {
  background-color: color-mix(in oklab, var(--accent) 12%, var(--bg));
  outline: 1px dashed var(--accent);
  outline-offset: -1px;
}
.today-strip-block {
  position: absolute;
  /* Task row: inline styles may omit top/height → fall back to full row. */
  top: 0; height: 100%;
  background: color-mix(in oklab, var(--accent) 50%, transparent);
  border-inline-start: 2px solid var(--accent);
  cursor: pointer;
  min-width: 2px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 4px;
  overflow: hidden;
  font-size: 10px;
  line-height: 1;
  color: var(--ink);
}
/* Sub-lanes on the gcal row need 1px inner gap so adjacent lanes read as
   separate bands, not one merged block. */
/* Height is set inline per-render based on max overlap lane count in JS —
   each lane stays ~22px regardless of how many events overlap. 28px min
   covers the no-overlap case. */
.today-strip-row-external .today-strip-block {
  border-radius: 2px;
  margin: 0; /* top/height come from inline style */
  box-shadow: 0 0 0 1px var(--bg-card);
}
.today-strip-block-label {
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1 1 auto;
  min-width: 0;
  /* Title wins the truncation battle — it stays, time drops first. */
  order: 0;
}
.today-strip-block-time {
  color: color-mix(in oklab, var(--ink) 65%, transparent);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  /* flex: 1 1 0 with a tiny basis means time surrenders all its space to the
     label as the block narrows — once label needs every pixel, time shrinks
     to 0 (overflow: hidden inherited via .today-strip-block). */
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  text-align: end;
  order: 1;
}
/* On very narrow blocks, hide the time entirely so the label can breathe. */
.today-strip-block[style*="width:0."] .today-strip-block-time,
.today-strip-block[style*="width:1."] .today-strip-block-time,
.today-strip-block[style*="width:2."] .today-strip-block-time,
.today-strip-block[style*="width:3."] .today-strip-block-time,
.today-strip-block[style*="width:4."] .today-strip-block-time {
  display: none;
}
.today-strip-block.external {
  background: color-mix(in oklab, var(--blue, #7ec7ff) 50%, transparent);
  border-left-color: var(--blue, #7ec7ff);
}
.today-strip-block:hover { filter: brightness(1.3); }
.today-strip-block { cursor: grab; touch-action: none; }
.today-strip-block.dragging { cursor: grabbing; opacity: 0.85; outline: 1px solid var(--accent); }
.today-strip-block.external { cursor: pointer; }
.today-strip-now {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--accent); pointer-events: none;
}

/* ---- Google event popover ---- */
.cal-ev-pop {
  position: fixed;
  background: var(--bg-card);
  border: 1px solid var(--blue, #7ec7ff);
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  z-index: 30;
  min-width: 260px; max-width: 360px;
  color: var(--ink);
  font-size: 12px;
}
.cal-ev-title { font-weight: 600; margin-bottom: 4px; }
.cal-ev-when { font-size: 11px; color: var(--ink-dim); margin-bottom: 8px; }
.cal-ev-row { font-size: 11px; color: var(--ink-dim); margin: 2px 0; }
.cal-ev-row a { color: var(--blue, #7ec7ff); text-decoration: none; }
.cal-ev-row a:hover { text-decoration: underline; }
.cal-ev-actions { margin-top: 10px; display: flex; justify-content: flex-end; }
.cal-ev-prep {
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent-text);
  padding: 4px 10px; border-radius: 2px; cursor: pointer; font: inherit; font-size: 11px;
}
.cal-ev-prep:hover { background: color-mix(in oklab, var(--accent) 12%, transparent); }
.cal-ev-linked { margin-top: 10px; border-top: 1px dashed var(--line-strong); padding-top: 8px; }
.cal-ev-linked-label {
  font-size: 10px; color: var(--ink-faint);
  letter-spacing: 0.06em; text-transform: uppercase;
  margin-bottom: 4px;
}
.cal-ev-linked-item { font-size: 11px; padding: 2px 0; cursor: pointer; }
.cal-ev-linked-item:hover { color: var(--accent-text); }
.cal-ev-linked-time { color: var(--ink-faint); font-size: 10px; }
/* ---------- Pinnable widget (PiP / popup) ----------
   The widget is a 280px-wide compact card showing running tasks, the next
   agenda item, and a break toggle. Lives in either:
     - a Document Picture-in-Picture window (always-on-top), or
     - a popup window loaded with `?widget=1`.
   In the popup case, body.widget-mode hides everything except .widget-root
   so the rest of the app's chrome doesn't paint. */
body.widget-mode {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  overflow: hidden;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
body.widget-mode > :not(.widget-root) { display: none !important; }

.widget-root {
  --widget-row-h: 40px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 16px;
  font-size: 16px;
  line-height: 1.4;
  background: var(--bg);
  color: var(--ink);
  height: 100vh;
  box-sizing: border-box;
  /* Whole-widget scroll — when running stack + upcoming + footer + hint
     exceed the window height, the user gets a scrollbar instead of
     overlapping sections. */
  overflow-y: auto;
}
.widget-root .widget-section {
  display: flex;
  align-items: stretch;        /* full-width children — no horizontal centering */
  gap: 10px;
  min-height: 32px;
}
.widget-root .widget-running-stack {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
  flex-shrink: 0;
}
.widget-root .widget-running-row {
  display: grid;
  grid-template-columns: 22px auto 1fr 32px 32px;
  align-items: center;
  gap: 8px;
  height: var(--widget-row-h);
  padding: 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: color-mix(in oklab, var(--accent, var(--green)) 6%, transparent);
}
.widget-root .widget-timer-icon { color: var(--accent, var(--green)); font-size: 16px; }
.widget-root .widget-timer-time {
  color: var(--accent, var(--green));
  font-variant-numeric: tabular-nums;
  font-size: 16px;
  font-weight: 600;
}
.widget-root .widget-timer-title {
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 15px;
}
.widget-root .widget-stop {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--red-text);
  width: 32px; height: 32px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.widget-root .widget-stop:hover { background: color-mix(in oklab, var(--red) 12%, transparent); }
.widget-root .widget-overflow {
  font-size: 14px;
  color: var(--ink-faint);
  text-align: center;
  padding: 4px;
}
/* Prominent up-next row. Same shape as widget-upcoming-row but slightly
   bigger button + accent border to mark it as "the next one to start".
   Grid: ▶ button | time | title (1fr stretches) | ✓ done.
   Was previously `32px auto auto 1fr` which left the title without a
   stretching column and pushed the ✓ to the wrong place. */
.widget-root .widget-up-next-big {
  display: grid;
  grid-template-columns: 32px auto 1fr 32px;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid var(--accent-dim);
  border-radius: 5px;
  background: color-mix(in oklab, var(--accent, var(--green)) 6%, transparent);
}
.widget-root .widget-play-next {
  background: transparent;
  border: 1px solid var(--accent-dim);
  color: var(--accent-text);
  width: 32px; height: 32px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.widget-root .widget-play-next:hover { background: var(--accent-glow); }
.widget-root .widget-up-next-label {
  color: var(--ink-faint);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.widget-root .widget-up-next-time {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  font-size: 16px;
}
.widget-root .widget-up-next-title {
  color: var(--ink);
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.widget-root .widget-up-next-line,
.widget-root .widget-agenda-line {
  color: var(--ink-faint);
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.widget-root .widget-idle {
  color: var(--ink-faint);
  font-style: italic;
  font-size: 15px;
}
.widget-root .widget-footer {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
  /* Stick to the bottom of the visible widget area so the footer's
     `break` / `open` buttons never get clipped below the fold when the
     widget content (running stack + upcoming) is taller than the PiP
     window's actual height. The PiP API enforces minimum / maximum
     heights that don't always match our `widgetWindow.resizeTo()`
     request — sticky positioning side-steps that mismatch entirely.
     `bottom: 0` pins to the viewport bottom; `margin-block-start: auto`
     ensures the footer is pushed below the scroll content within the
     flex column. Background + small top shadow indicates scrollable
     content beneath when the user has scrolled up. */
  position: sticky;
  bottom: 0;
  margin-block-start: auto;
  padding-block: 8px;
  background: var(--bg);
  box-shadow: 0 -8px 12px -8px var(--bg);
  z-index: 1;
}
.widget-root .widget-btn {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 6px 14px;
  border-radius: 4px;
  cursor: pointer;
  font: inherit;
  font-size: 14px;
}
.widget-root .widget-btn:hover {
  border-color: var(--accent-dim);
  color: var(--accent-text);
}
.widget-root .widget-btn-end-break {
  border-color: var(--amber, var(--green));
  color: var(--amber, var(--green));
}
.widget-root .widget-break {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 12px;
  border: 1px solid var(--amber, var(--green));
  border-radius: 5px;
  background: color-mix(in oklab, var(--amber, var(--green)) 8%, transparent);
}
.widget-root .widget-break-icon { font-size: 20px; }
.widget-root .widget-break-label {
  color: var(--amber, var(--green));
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.widget-root .widget-break-time {
  color: var(--amber, var(--green));
  font-variant-numeric: tabular-nums;
  font-size: 20px;
  font-weight: 600;
}
.widget-root[data-state="break"] .widget-break-next {
  opacity: 0.5;
  font-style: italic;
}

/* Upcoming-tasks stack — shown below the running stack so the user can
   start the next task without leaving the widget. Each row is a peer of
   widget-running-row in shape but with a play button instead of pause,
   muted accent so running rows still read as "what you're doing now". */
.widget-root .widget-upcoming-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  padding-block-start: 4px;
  border-block-start: 1px dashed var(--line-strong);
  min-height: 18px;
}
.widget-root .widget-upcoming-stack {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}
/* Compact scrollable list of additional upcoming items (after the
   prominent up-next row). Capped so the widget doesn't grow tall just
   because the user has 8 things scheduled today. */
.widget-root .widget-upcoming-scroll {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  max-height: 160px;
  overflow-y: auto;
  flex-shrink: 0;
  padding-block-start: 2px;
  border-block-start: 1px dashed var(--line-strong);
}
/* Upcoming-section header with toggle. Visually prominent so the toggle
   is findable — bigger font, lighter accent on hover, dashed top border
   separating from the running stack above. The ▾/▸ chevron sits on the
   end of the row and rotates as the section opens/closes. */
.widget-root .widget-upcoming-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
  padding: 8px 4px 6px;
  margin-block-start: 4px;
  border: 0;
  border-block-start: 1px dashed var(--line-strong);
  background: transparent;
  user-select: none;
  font: inherit;
  font-size: 12px;
  text-align: start;
}
.widget-root .widget-upcoming-section-header:hover { color: var(--accent, var(--green)); }
.widget-root .widget-upcoming-section-header .chev {
  font-size: 14px;
  color: var(--ink-dim);
}
.widget-root .widget-upcoming-section-header:hover .chev { color: var(--accent, var(--green)); }
.widget-root .widget-upcoming-scroll::-webkit-scrollbar { width: 6px; }
.widget-root .widget-upcoming-scroll::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 3px;
}
.widget-root .widget-upcoming-row {
  display: grid;
  grid-template-columns: 28px auto 1fr 28px;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  background: transparent;
}
.widget-root .widget-upcoming-row .widget-play-row {
  width: 28px; height: 28px;
  border: 1px solid var(--accent-dim);
  border-radius: 4px;
  background: transparent;
  color: var(--accent-text);
  font-size: 13px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.widget-root .widget-upcoming-row .widget-play-row:hover {
  background: var(--accent-glow);
}
.widget-root .widget-upcoming-time {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  font-size: 14px;
}
.widget-root .widget-upcoming-time.overdue,
.widget-root .widget-up-next-time.overdue {
  color: var(--red);
}
.widget-root .widget-upcoming-title {
  color: var(--ink-dim);
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* macOS Spaces hint — small dismissible note at the bottom of the widget
   telling the user the one manual step that puts the floating window on
   every macOS Space. Only rendered on Mac platforms and only until the ×
   button is clicked (localStorage flag, persists across reloads). */
.widget-root .widget-mac-hint {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 8px;
  border: 1px dashed var(--line-strong);
  border-radius: 4px;
  background: color-mix(in oklab, var(--ink-faint) 5%, transparent);
}
.widget-root .widget-mac-hint-text {
  flex: 1;
  font-size: 11px;
  line-height: 1.4;
  color: var(--ink-faint);
  font-style: italic;
}
.widget-root .widget-mac-hint-dismiss {
  background: transparent;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 4px;
  flex: 0 0 auto;
}
.widget-root .widget-mac-hint-dismiss:hover { color: var(--ink); }

/* ✓ Done button — peer of the ⏸ stop button on running rows and the ▶
   start button on upcoming rows. Click marks the task done (toggleDone),
   which also stops the timer if it was running. */
.widget-root .widget-done {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--accent, var(--green));
  width: 32px; height: 32px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.widget-root .widget-done:hover {
  background: color-mix(in oklab, var(--accent, var(--green)) 12%, transparent);
  border-color: var(--accent, var(--green));
}
.widget-root .widget-upcoming-row .widget-done {
  width: 28px; height: 28px;
  font-size: 13px;
}
/* ====================================================================
   Feedback feature — multi-step modal (desktop) / bottom-sheet (mobile)
   ==================================================================== */

.feedback-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.62);
  z-index: var(--z-drawer-backdrop, 250);
  animation: feedback-bg-in 160ms ease-out;
}
@keyframes feedback-bg-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.feedback-sheet {
  position: fixed;
  z-index: calc(var(--z-drawer, 260) + 1);
  background: var(--bg-raised);
  color: var(--ink);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 24px 80px rgba(0,0,0,0.55);
  display: flex;
  flex-direction: column;
}

.feedback-sheet.feedback-desktop {
  top: 50%;
  inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  width: min(520px, 92vw);
  max-height: min(720px, 90vh);
  border-radius: 12px;
  animation: feedback-modal-in 200ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
[dir="rtl"] .feedback-sheet.feedback-desktop {
  /* translate(-50%, -50%) reads physically; mirror so the centring is correct
     when the page direction is RTL. */
  transform: translate(50%, -50%);
}
@keyframes feedback-modal-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 12px)); }
  to   { opacity: 1; transform: translate(-50%, -50%); }
}

.feedback-sheet.feedback-mobile {
  inset-inline: 0;
  bottom: 0;
  width: 100%;
  max-height: 88vh;
  border-radius: 16px 16px 0 0;
  border-block-end: 0;
  animation: feedback-sheet-in 240ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
@keyframes feedback-sheet-in {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

.feedback-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px 8px;
  border-block-end: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
}
.feedback-headline {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.feedback-eyebrow {
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--accent-text);
  font-weight: 600;
}
.feedback-cobuild {
  font-size: 12px;
  line-height: 1.3;
  color: var(--ink-faint);
  font-style: italic;
}
.feedback-close {
  background: transparent;
  border: 0;
  color: var(--ink-dim);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.feedback-close:hover {
  background: var(--bg-card);
  color: var(--ink);
}

.feedback-progress {
  display: flex;
  gap: 6px;
  padding: 6px 18px 0;
  min-height: 14px;
  align-items: center;
}
.feedback-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink) 18%, transparent);
  transition: background-color 160ms ease;
}
.feedback-dot.filled {
  background: var(--accent);
}

.feedback-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 18px 22px 8px;
}
.feedback-title {
  font-size: 20px;
  line-height: 1.3;
  margin: 0 0 6px;
  color: var(--ink);
}
.feedback-lede {
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink-faint);
  margin: 0;
}
.feedback-sub {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-dim);
  margin: 0 0 14px;
}
.feedback-input {
  width: 100%;
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  border-radius: 8px;
  padding: 10px 12px;
  font: inherit;
  font-size: 14px;
  line-height: 1.5;
  resize: vertical;
}
textarea.feedback-input {
  min-height: 76px;
  max-height: 240px;
}
.feedback-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent);
}
.feedback-hint {
  font-size: 11px;
  color: var(--ink-faint);
  margin: 6px 0 0;
}
.feedback-error {
  font-size: 12px;
  color: var(--red, #ff6b6b);
  margin: 8px 0 0;
}

.feedback-stars {
  display: flex;
  gap: 6px;
  margin: 4px 0 6px;
}
.feedback-star {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: color-mix(in srgb, var(--ink) 28%, transparent);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease, transform 120ms ease;
}
.feedback-star:hover {
  color: var(--amber, #ffc857);
  border-color: var(--amber, #ffc857);
  transform: translateY(-1px);
}
.feedback-star.active {
  color: var(--amber, #ffc857);
  border-color: var(--amber, #ffc857);
  background: color-mix(in srgb, var(--amber, #ffc857) 12%, transparent);
}

.feedback-footer {
  padding: 12px 18px 18px;
  border-block-start: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
}
.feedback-footer-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}
.feedback-btn {
  font: inherit;
  font-size: 13px;
  padding: 9px 14px;
  border-radius: 8px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.feedback-btn-primary {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
  font-weight: 600;
}
.feedback-btn-primary:hover { background: var(--accent); border-color: var(--accent); filter: brightness(0.88); }
.feedback-btn-primary:disabled {
  opacity: 0.6;
  cursor: progress;
}
.feedback-btn-text {
  background: transparent;
  color: var(--ink-dim);
  border: 1px solid transparent;
}
.feedback-btn-text:hover {
  color: var(--ink);
  border-color: var(--line-strong);
  background: var(--bg-card);
}

@media (max-width: 720px) {
  .feedback-footer-row { justify-content: stretch; }
  .feedback-btn-primary { flex: 1 1 auto; min-height: 44px; }
  .feedback-btn-text { flex: 0 0 auto; min-height: 44px; }
  .feedback-stars { flex-wrap: wrap; }
  .feedback-star { padding: 10px 14px; font-size: 26px; }
}

/* ===================================================================
   Focused project view — rail (desktop), bottom sheet (mobile),
   focus-view header, kanban-filter-bar toggle + NEW badge,
   move-to submenu dot.
   ================================================================ */

/* When the board has a rail: full-width filter bar at the top, then four
   explicit rows below for the rail (left column, spans all three right
   rows) and the right-column trio: focus-view-header → col-headers →
   project-row cells. Every child has an explicit grid-area so nothing
   gets auto-placed into a stretched `1fr` cell with empty space below.
   The fourth row gets `1fr` so the project-row fills any remaining
   vertical space (the cells row stays anchored to the col-headers; the
   slack lives inside the row, not as a gap above it). */
.board.has-project-rail {
  display: grid;
  /* --rail-width is set on <html> at boot from localStorage["todo.railWidth"]
     and updated live during drag of .project-rail-resize-handle. Default
     240px matches the legacy fixed width. */
  grid-template-columns: var(--rail-width, 240px) 1fr;
  grid-template-rows: auto auto auto 1fr;
  grid-template-areas:
    "filter   filter"
    "rail     header"
    "rail     colhead"
    "rail     rows";
  gap: 0;
  /* Keep .board at least viewport-tall (minus top chrome and bottom
     statusbar) so the sticky-positioned rail below has range to stay
     pinned for the full visible scroll. Without this, when the cards
     column is shorter than the viewport (e.g. small projects, "show
     all" off, narrow filter), .board's bottom comes into view early
     and the rail "unsticks" + scrolls with the page — the user-
     reported "rail scrolls at end of main scroll" symptom.
     Divide 100dvh by --ui-zoom (set by applyUiScale via body { zoom: X }).
     100dvh resolves to the unscaled viewport in CSS px, but body content
     renders at CSS × zoom — plain 100dvh overflows by (zoom − 1). Same
     lesson the calendar grid learned (calendar-extras.css:366).
     See .claude/rules/10-body-zoom-viewport-units.md. */
  min-height: calc((100dvh / var(--ui-zoom, 1)) - var(--sticky-top-filter-bar, 52px) - var(--statusbar-h, 34px));
}
.board.has-project-rail .kanban-filter-bar { grid-area: filter; }
.board.has-project-rail .focus-view-header { grid-area: header; }
.board.has-project-rail .col-headers       { grid-area: colhead; }
.board.has-project-rail .project-row       { grid-area: rows; align-self: start; }
.board.has-project-rail .project-rail      { grid-area: rail; }

/* Focus-mode toggle button — sits inside .kfb-layout-seg next to multi/single. */
.btn-focus-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px 8px;
  border-inline-start: 1px solid var(--line);
  margin-inline-start: 2px;
  cursor: pointer;
  color: var(--ink-faint);
  background: transparent;
}
.btn-focus-toggle.on {
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.btn-focus-toggle svg {
  width: 16px;
  height: 16px;
  display: block;
}
/* Inline-span wrappers that hold the SVG need explicit flex centering —
   without it, the block-display SVG inside an inline-display span sits
   at the span's top-left baseline instead of visually centered.
   Symptom (per user screenshot): the icon looks shifted down/right
   inside the button bounds. */
.btn-focus-toggle .kfb-focus-glyph,
.btn-focus-toggle .kfb-layout-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  line-height: 0;
}
.ac-compact-toggle .ac-compact-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  line-height: 0;
}
/* NEW pill — floats ABOVE the toggle button, not covering it. Bottom edge
   sits 6px above the button's top edge, horizontally centered. A small
   downward triangular tail anchors the pill to its trigger.
   The kanban-filter-bar bumps its z-index to 25 (one level above the
   topbar's 20) so the pill, which pokes past the filter bar's top into
   the topbar's visual space, paints ON TOP rather than being clipped by
   the topbar's background. */
.kanban-filter-bar { z-index: 25; overflow: visible; }
.focus-new-badge {
  position: absolute;
  bottom: calc(100% + 6px);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  font-weight: 600;
  padding: 4px 9px;
  border-radius: 5px;
  background: var(--accent);
  color: var(--bg);
  pointer-events: none;
  white-space: nowrap;
  box-shadow: 0 2px 6px color-mix(in srgb, var(--accent) 20%, transparent);
  z-index: 30;
}
/* Pointer tail — triangle below the pill, lined up with the button. */
.focus-new-badge::after {
  content: "";
  position: absolute;
  top: 100%;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--accent);
}
/* RTL: translateX swap to keep centering symmetric. */
[dir="rtl"] .focus-new-badge { transform: translateX(50%); }
[dir="rtl"] .focus-new-badge::after { transform: translateX(50%); }

/* Edge-anchored variant: when the host button sits near a viewport edge
   (e.g. .ac-compact-toggle is at margin-inline-start: auto = right edge),
   the centered badge would overflow off-screen. Anchor the badge to the
   button's inline-end and extend INWARD; shift the tail so it still
   points at the button's center.
   See: .claude/rules/03-floating-ui-viewport-safety.md */
.ac-compact-toggle .focus-new-badge {
  inset-inline-start: auto;
  inset-inline-end: 0;
  transform: none;
  /* Cap width to viewport-minus-padding so even narrow viewports keep
     the badge fully visible; truncation kicks in via white-space: nowrap
     + max-width + overflow:hidden + text-overflow if the label is too
     long (rare — the en label is ~17 chars). */
  max-width: min(calc(100vw - 16px), 220px);
  overflow: hidden;
  text-overflow: ellipsis;
}
.ac-compact-toggle .focus-new-badge::after {
  /* Tail anchored to the button's center (11px = button's 22px width / 2)
     instead of the badge's center, so it still visually points at the
     button after the badge slid leftward. */
  inset-inline-start: auto;
  inset-inline-end: 6px;
  transform: none;
}
[dir="rtl"] .ac-compact-toggle .focus-new-badge { transform: none; }
[dir="rtl"] .ac-compact-toggle .focus-new-badge::after { transform: none; }

/* Mobile-only inline cluster that hosts the focus toggle + project pill.
   No visible divider: the kanban-filter-bar's natural flex-wrap places
   items on a second row when needed; a hard <hr>-style border made the
   focus row read as a separate strip rather than part of the same control
   cluster. */
.kfb-focus-row {
  display: none;
}
@media (max-width: 720px) {
  /* The focus row claims its own full-width line (.kfb-focus-row's
     flex: 1 0 100% below); the filter chip + controls wrap onto the line
     beneath it. wrap (not nowrap + horizontal scroll) keeps every
     control reachable — see .claude/rules/06-stateful-ui-safety.md §3. */
  .kanban-filter-bar {
    flex-wrap: wrap;
    justify-content: flex-start;
    /* Tighter padding to recover horizontal pixels for the controls. */
    padding-inline: 8px;
    /* row-gap 8 separates the focus row from the controls row;
       column-gap 6 packs items within a row. */
    gap: 8px 6px;
  }
  /* multi/single is meaningless on mobile (the responsive board is
     stacked anyway). Hide to reclaim horizontal space. */
  .kanban-filter-bar .kfb-layout-seg { display: none; }
  /* Mobile filter chip: drop the text label and the chevron, keep just
     the funnel icon + count. User direction: small icons only on mobile. */
  .kfb-mobile-chip .kfb-mobile-chip-label,
  .kfb-mobile-chip .kfb-mobile-chip-chev { display: none; }
  .kfb-mobile-chip {
    padding: 8px 10px;
    min-height: 38px;
    gap: 6px;
    flex: 0 0 auto;
  }
  .kfb-mobile-chip .kfb-mobile-chip-icon svg {
    width: 16px;
    height: 16px;
    display: block;
  }
  /* In its own full-width row the pill fills the space left of the
     toggle; the name ellipsis-truncates, the chevron pins to the end.
     min-width: 0 lets the flex item shrink so the ellipsis engages. */
  .kfb-project-pill {
    flex: 1 1 auto;
    min-width: 0;
  }
  .kfb-project-pill .kpp-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .kfb-focus-row {
    /* Dedicated full-width row, above the wrapped filter controls. */
    display: flex;
    flex: 1 0 100%;
    align-items: center;
    gap: 6px;
    padding: 0;
    border-block-end: none;
  }
  .kfb-focus-row .btn-focus-toggle {
    border-inline-start: none;
    margin-inline-start: 0;
    border: 1px solid var(--line);
    border-radius: 6px;
  }
}

.kfb-project-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Bigger tap target + accent treatment so the focused-project control
     reads as the prominent, tappable anchor of the mobile focus row —
     not a faint chip lost among the filter buttons. */
  min-height: 38px;
  padding: 6px 12px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-card));
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.kfb-project-pill .kpp-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink-faint);
}
.kfb-project-pill .kpp-chev {
  font-size: 10px;
  color: var(--ink-faint);
}

/* Project rail (desktop inline + mobile bottom-sheet shared). */
.project-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 4px;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
  border-inline-end: 1px solid var(--line);
  background: var(--bg-raised);
}
.project-rail.in-sheet {
  border-inline-end: none;
  max-height: none;
  padding: 0 4px 12px;
}

/* Resize handle — sits on the inline-end edge of the rail (desktop
   only). 8px wide hit area; the visible indicator is a thin pseudo-
   element line that's ALWAYS visible (1px, themed) and thickens to
   3px accent on hover/drag. Always-on indicator makes the handle
   discoverable instead of relying on cursor hover to find it. */
.project-rail-resize-handle {
  position: absolute;
  top: 0;
  inset-inline-end: -4px;        /* center the 8px hit area on the rail's edge */
  width: 8px;
  height: 100%;
  cursor: col-resize;
  z-index: 5;
  background: transparent;
}
/* Always-visible indicator — 1px line in the brand's neutral grey,
   overlaid on the rail's existing border-inline-end. Renders smoothly
   in both light + dark themes because it uses --line. */
.project-rail-resize-handle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  inset-inline-end: 4px;         /* sits exactly on the rail's edge */
  width: 1px;
  background: var(--line);
  pointer-events: none;
  transition: width 120ms ease, background 120ms ease, inset-inline-end 120ms ease;
}
.project-rail-resize-handle:hover::before,
.project-rail-resize-handle.is-dragging::before {
  width: 3px;
  inset-inline-end: 3px;         /* re-center the thicker line on the rail's edge */
  background: var(--accent, var(--green));
}
/* While dragging, lock the user-select on the body so text doesn't get
   selected as the cursor moves. JS toggles `body.is-rail-resizing`. */
body.is-rail-resizing { user-select: none; cursor: col-resize; }
body.is-rail-resizing * { cursor: col-resize !important; }
/* Project rail — position: sticky inside the .board grid item. Tracks
   the filter-bar naturally at top-of-page (no overlap) and stays
   pinned as the user scrolls through cards.
   - top = --sticky-top-focus-header (filter-bar bottom). The rail's
     TOP aligns with the focus-view-header's TOP.
   - height = viewport minus top chrome minus --statusbar-h, so the
     rail extends from below the filter-bar to just above the bottom
     statusbar.
   - max-height: none overrides the base `.project-rail`'s
     calc(100vh - 200px) cap so the explicit height wins.
   - overflow-y: auto gives the rail its own scrollbar; overflow-x:
     hidden suppresses the never-used horizontal scrollbar.
   - overscroll-behavior: contain stops the page from picking up scroll
     once the rail bottoms out.
   - align-self: start anchors the rail to the top of its grid area
     instead of stretching to fill row 2-4.
   The matching `min-height` on .board above ensures sticky's range
   (which is bounded by the parent's height) always extends past the
   viewport, so the rail stays pinned even when the cards column is
   shorter than the viewport. */
.board.has-project-rail .project-rail {
  /* Same shape as main: max-height + overflow-y: auto. max-height
     (not explicit height) lets the flex container size to its content
     up to the cap, which avoids the flex-shrink squash that explicit
     height caused. Sticky position keeps the rail pinned to the
     viewport while the main column scrolls. */
  position: sticky;
  top: var(--sticky-top-focus-header, var(--sticky-top-filter-bar, 52px));
  align-self: start;
  /* Divide 100dvh by --ui-zoom — body { zoom: X } makes 100dvh resolve to
     the unscaled viewport in CSS px, but body content renders at CSS ×
     zoom, so plain 100dvh overflows by (zoom − 1) visual pixels. The
     calendar hit this and fixed it the same way (calendar-extras.css:366).
     See .claude/rules/10-body-zoom-viewport-units.md.
     --statusbar-h is written dynamically by updateStickyTops() so it
     reflects the statusbar's real offsetHeight (the legal-links row
     wraps it to ~50px on desktop, not the 34px min-height fallback).
     --chrome-above-rail is the WORST-CASE distance from viewport top to
     the rail's natural top (page scrolled to 0, quote-bar visible).
     Using --sticky-top-focus-header instead (the stuck-position offset)
     made the max-height too generous when the page was scrolled up: the
     rail's natural top sat 80-150 CSS px lower than the stuck top, so
     the rail extended that much past the viewport bottom and hid the
     "+ Add project" button behind the statusbar. Subtracting the
     worst-case chrome means at the stuck position the rail is slightly
     shorter than it could be, but it always fits — and the user's bottom
     button is always reachable. The extra 24px buffer keeps a visible gap
     between the "+ Add project" button and the statusbar at every UI scale
     and scroll position (Chrome rounds fractional CSS px at higher zoom
     levels, and --chrome-above-rail can under-measure by a few CSS px
     when measured mid-render before the layout settles). */
  max-height: calc((100dvh / var(--ui-zoom, 1)) - var(--chrome-above-rail, var(--sticky-top-focus-header, 244px)) - var(--statusbar-h, 34px) - 24px);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
}

/* Count, or elapsed time on hover. min-width: 0 so the slot sizes to content and never steals the name's width. */
.project-rail-item .pri-metric {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
  color: var(--ink-faint);
  font-size: 11px;
  white-space: nowrap;
}
.project-rail-item .pri-elapsed {
  display: none;
  /* Theme-aware accent — the metric switches from neutral count to
     a saturated "this is tracked time" cue on hover. Falls back to
     the brand green if a theme doesn't define --accent. */
  color: var(--accent, var(--green));
}
.project-rail-item:hover .pri-count,
.project-rail-item:focus-within .pri-count {
  display: none;
}
.project-rail-item:hover .pri-elapsed,
.project-rail-item:focus-within .pri-elapsed {
  display: inline;
}

/* Overflow ⋯ menu (Rename / Colour / Delete). display: none until hover so it never reserves the project name's width. */
.project-rail-item .pri-actions {
  display: none;
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  margin-inline-start: 4px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  align-items: center;
  justify-content: center;
  transition: color 120ms ease;
}
.project-rail-item:hover .pri-actions,
.project-rail-item:focus-within .pri-actions {
  display: inline-flex;
}
.project-rail-item .pri-actions:hover {
  color: var(--ink);
}
/* All Projects row has no actions — stays hidden even on hover. */
.project-rail-item .pri-actions[aria-hidden="true"] {
  display: none;
}

.project-rail-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  /* 22px ⋯ button + 8px padding — fixed so the hover-only button doesn't shake the row. */
  min-height: 30px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink);
  font-size: 13px;
}
.project-rail-item:hover {
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
/* Active rail item — significantly more visible than the previous 16%
   tint + 3px stripe. The active project is the primary thing the user is
   looking at; the rail row reads as "selected", not just "hovered+".
   Three layers of cue: tinted background, tinted border, and a 4px left
   stripe with a soft glow. Project name picks up the project color so
   the chrome itself signals which project is in focus. */
.project-rail-item.is-active {
  background: color-mix(in srgb, var(--project-color, var(--accent)) 22%, transparent);
  border-color: color-mix(in srgb, var(--project-color, var(--accent)) 45%, transparent);
  /* No font-weight bump — bold glyphs are wider, so the active row's name
     would truncate earlier than its regular-weight siblings within the
     identical .pri-name column. The four cues above (tinted background,
     border, stripe, project-colored name) already mark it as selected. */
}
.project-rail-item.is-active .pri-name {
  color: var(--project-color, var(--accent));
}
.project-rail-item.is-active::before {
  content: "";
  position: absolute;
  inset-block: 3px;
  inset-inline-start: 0;
  width: 4px;
  border-radius: 2px;
  background: var(--project-color, var(--accent));
  box-shadow: 0 0 8px color-mix(in srgb, var(--project-color, var(--accent)) 60%, transparent);
}
.project-rail-item.is-drop-target {
  outline: 2px dashed var(--accent);
  outline-offset: -3px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* Drag-to-reorder: faint source + insertion-line indicators.
   Both use ::after so they don't conflict with is-active::before (the left
   accent stripe). The before/after class names refer to insertion position,
   not the CSS pseudo-element. */
.project-rail-item[draggable="true"] { cursor: grab; }
.project-rail-item.pri-dragging-source { opacity: 0.35; }
.project-rail-item.is-reorder-before::after,
.project-rail-item.is-reorder-after::after {
  content: "";
  position: absolute;
  inset-inline: 4px;
  height: 2px;
  border-radius: 1px;
  background: var(--accent);
  pointer-events: none;
  z-index: 1;
}
.project-rail-item.is-reorder-before::after { top: -1px; bottom: auto; }
.project-rail-item.is-reorder-after::after  { bottom: -1px; top: auto; }
/* Empty-filter state shown when all projects are hidden by the active filter. */
.project-rail-empty-filter {
  padding: 12px 10px;
  font-size: 12px;
  color: var(--ink-faint);
  text-align: center;
}
.project-rail-item .pri-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink-faint);
  flex: 0 0 auto;
}
.project-rail-item .pri-name {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.project-rail-item .pri-count {
  flex: 0 0 auto;
  color: var(--ink-faint);
  font-size: 11px;
}

.project-rail-new-project {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  margin-block-start: 6px;
  border: 1px solid color-mix(in oklab, var(--accent) 34%, var(--line));
  border-radius: 6px;
  background: color-mix(in oklab, var(--accent) 7%, transparent);
  color: var(--accent-text);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.project-rail-new-project:hover {
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 11%, transparent);
}
.project-rail-new-project .prnp-icon svg {
  width: 12px;
  height: 12px;
  display: block;
}

/* Inline new-project input row (replaces the + button while typing). */
.project-rail-new-inline {
  margin-block-start: 6px;
}
.project-rail-new-inline .prni-input {
  display: block;
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  font-size: 12px;
  font-family: inherit;
  outline: none;
  box-sizing: border-box;
}
.project-rail-new-inline .prni-input::placeholder {
  color: var(--ink-faint);
}

/* Mobile sheet header for the project rail. */
.project-rail-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  border-block-end: 1px solid var(--line);
}
.project-rail-sheet-header .prsh-title {
  font-weight: 600;
  font-size: 14px;
}
.project-rail-sheet-header .prsh-close {
  background: transparent;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 4px;
}
.project-rail-sheet-header .prsh-close svg {
  width: 18px;
  height: 18px;
  display: block;
}

/* Focus-view header — replaces .project-label when focused mode is ON. */
.focus-view-header {
  padding: 8px 14px 10px;
  border-block-end: 1px solid var(--line);
}
.focus-view-header .fvh-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.focus-view-header .fvh-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink-faint);
  flex: 0 0 auto;
}
.focus-view-header .fvh-name {
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
  cursor: text;
}
.focus-view-header .fvh-actions {
  margin-inline-start: auto;
  padding: 4px 6px;
  border: none;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
}
.focus-view-header .fvh-actions svg {
  width: 16px;
  height: 16px;
  display: block;
}
.focus-view-header .fvh-stats {
  margin-block-start: 4px;
  font-size: 12px;
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.focus-view-header .fvh-stat.open {
  color: var(--ink);
  font-weight: 500;
}

/* Project-actions popover (rename / color / clear done / delete). */
.project-actions-popover {
  z-index: 9000;
}
.project-actions-popover .is-destructive {
  color: var(--red-text);
}

/* Move-to submenu — colored dot per project. */
.card-action-item .move-to-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink-faint);
}

/* RTL: nothing extra — all positioning above uses logical properties. */

/* Project picker chip used by the .new-task row in All Projects view.
   Idle state: a subtle terminal-aesthetic button with a project color
   dot, name, and chevron. Hover/focus: tinted border + active ink.
   The popover is mounted on document.body (portal) and positioned by
   JS via measure-and-clamp (rule 03). */

.proj-picker-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 1px 8px;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 10px;
  line-height: 1.4;
  color: var(--ink-faint);
  background: color-mix(in oklab, var(--bg-card) 60%, transparent);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
  white-space: nowrap;
  /* Absorb the available row-leading space so the chip sits flush against
     the right edge (idle) or the editing-state save hint (editing). The
     sibling rule below cancels the .new-task-kbd's own auto-margin so
     the kbd sticks to the chip instead of splitting the free space, which
     would otherwise float the chip into the middle of the row. */
  margin-inline-start: auto;
}
/* When both the picker chip AND the kbd hint are present in the idle row
   (All Projects view), the kbd's own margin-inline-start: auto (defined
   in board.css:843) would steal a share of the free space and push the
   chip leftward into the middle of the row. Cancel that here so the chip
   stays flush right and the kbd sits beside it. */
.new-task .proj-picker-chip + .new-task-kbd {
  margin-inline-start: 6px;
}
.proj-picker-chip:hover,
.proj-picker-chip:focus-visible {
  color: var(--ink-dim);
  border-color: color-mix(in oklab, var(--accent) 30%, var(--line-strong));
  outline: none;
}
.proj-picker-chip .ppc-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--proj-color, var(--ink-faint));
  border: 1px solid color-mix(in oklab, var(--proj-color, var(--line)) 70%, transparent);
  flex: 0 0 auto;
}
.proj-picker-chip .ppc-chev {
  font-size: 9px;
  opacity: 0.6;
  margin-inline-start: 1px;
}

/* In the editing-state row the input has flex:1 and absorbs the leading
   space on its own, so the chip's auto-margin would be a no-op — but
   keeping it consistent here is cheaper than special-casing the
   editing variant. The end-margin gives the chip a small breathing
   gap before the save hint. */
.new-task.editing .proj-picker-chip {
  margin-inline-end: 4px;
}

/* Popover — mounted on document.body, positioned via JS (rule 03). */
.proj-picker-popover {
  position: fixed;
  z-index: 1200;
  background: var(--bg-raised, var(--bg-card));
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  min-width: 180px;
  max-width: min(280px, calc(100vw - 16px));
  max-height: min(320px, calc(100vh - 80px));
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.proj-picker-popover .ppp-search {
  padding: 6px 8px;
  border-block-end: 1px solid var(--line);
  flex: 0 0 auto;
}
.proj-picker-popover .ppp-search input {
  width: 100%;
  padding: 4px 6px;
  font-family: inherit;
  font-size: 12px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--ink);
}
.proj-picker-popover .ppp-search input:focus {
  outline: none;
  border-color: var(--accent);
}
.proj-picker-popover .ppp-list {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.proj-picker-popover .ppp-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  font-size: 13px;
  cursor: pointer;
  color: var(--ink);
  outline: none;
}
.proj-picker-popover .ppp-item .ppp-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--proj-color, var(--ink-faint));
  flex: 0 0 auto;
}
.proj-picker-popover .ppp-item:hover,
.proj-picker-popover .ppp-item:focus {
  background: var(--bg-card-hov, color-mix(in oklab, var(--accent) 6%, var(--bg-card)));
}
.proj-picker-popover .ppp-item.is-current {
  color: var(--accent-text, var(--accent));
}
.proj-picker-popover .ppp-item.is-current .ppp-name::after {
  content: " ✓";
  opacity: 0.7;
  font-size: 11px;
}
/* ---- Notification bell + dropdown ---- */
.notif-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 14px; height: 14px;
  padding: 0 3px;
  margin-inline-start: 4px;
  /* Soft red — blended 60% with the surface so the badge reads as a
     calm tag instead of a saturated alarm. Loud reds (Monokai's
     #f92672 magenta especially) were eye-fatiguing because the bell
     sits in the topbar's static chrome — the user looks at it
     every render. The mix lands around 4.5:1 contrast on dark
     themes against the white digit, still scannable. */
  background: color-mix(in oklab, var(--red, #e55) 60%, var(--bg, #1a1a1a));
  color: var(--ink, #fff);
  font-size: 9px;
  font-weight: 700;
  border-radius: 8px;
  line-height: 1;
  vertical-align: middle;
}
.notif-badge[hidden] { display: none; }
#btn-notifs.pulse {
  animation: notif-pulse 500ms ease-out;
}
@keyframes notif-pulse {
  0%   { box-shadow: 0 0 0 0 var(--red, #e55); }
  60%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.notif-dropdown {
  position: fixed;
  z-index: var(--z-popover);
  width: 320px;
  max-height: 420px;
  display: flex;
  flex-direction: column;
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 0 0 3px var(--accent-glow);
  border-radius: 4px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink);
  animation: palette-in 120ms ease-out;
  overflow: hidden;
}
.notif-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  gap: 8px;
}
.notif-title-bar {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.notif-actions { display: flex; gap: 6px; }
.notif-action {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  border-radius: 2px;
  font: inherit;
  font-size: 10px;
  padding: 2px 6px;
  cursor: pointer;
  transition: color 120ms, border-color 120ms;
}
.notif-action:hover { color: var(--accent-text); border-color: var(--accent-dim); }
.notif-list {
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.notif-item {
  display: block;
  width: 100%;
  text-align: start;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line);
  padding: 8px 12px;
  cursor: pointer;
  color: var(--ink);
  font: inherit;
  transition: background 120ms;
}
.notif-item:hover { background: var(--bg-card-hov); }
.notif-item.unread { border-inline-start: 2px solid var(--accent); padding-inline-start: 10px; }
.notif-item.read { opacity: 0.62; }
.notif-title { font-weight: 500; line-height: 1.3; }
.notif-body {
  color: var(--ink-dim);
  margin-top: 2px;
  font-size: 11px;
  line-height: 1.3;
  white-space: normal;
}
.notif-meta {
  color: var(--ink-faint);
  font-size: 10px;
  margin-top: 4px;
  letter-spacing: 0.04em;
}
.notif-meta .notif-open { color: var(--accent-text); }
.notif-empty {
  /* Minimal empty state — one short line, centered, with enough vertical
     room that the dropdown still feels like a panel and not a tooltip.
     The retention disclaimer ("keeps 50 / 7 days / persists across reloads")
     and the "what would appear here" hint were dropped — both were chrome
     the user doesn't need before any notification has fired. They re-emerge
     organically as soon as the list has items (the footer renders then). */
  padding: 28px 16px;
  text-align: center;
  color: var(--ink-dim);
  font-size: 12px;
  line-height: 1.4;
}
.notif-footer {
  padding: 6px 12px;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: 10px;
  text-align: center;
}
.legend-section { padding: 6px 0; border-top: 1px solid var(--line); }
.legend-section:first-child { border-top: none; padding-top: 0; }
.legend-section h4 {
  margin: 0 0 6px;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 600;
}
/* ===================================================================
   Break banner
   =================================================================== */

.break-banner {
  border-bottom: 1px solid var(--amber);
  background: linear-gradient(180deg, var(--amber-glow) 0%, transparent 120%), var(--bg-raised);
  padding: 10px 20px 0;
}
.break-banner .bb-row {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 12px;
  align-items: center;
}
.bb-icon { font-size: 18px; }
.bb-text .bb-title {
  color: var(--amber-text);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-size: 11px;
}
.bb-text .bb-sub { color: var(--ink-dim); font-size: 11px; }
/* Rest-themed quote rendered alongside the timer. Italic for "voice" feel,
   muted color so the timer + clock stay the focal point, and an
   inline-end ellipsis cap so a long quote on a narrow viewport doesn't
   stretch the banner past the clock. */
.bb-text .bb-quote {
  color: var(--ink-dim);
  font-size: 11px;
  font-style: italic;
  margin-block-start: 2px;
  max-width: 64ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bb-text .bb-quote-author { color: var(--ink-faint); font-style: normal; }
.bb-clock {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 22px;
  font-weight: 600;
  color: var(--amber-text);
  letter-spacing: 0.02em;
  padding: 0 8px;
}
.bb-end {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 5px 12px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}
.bb-end:hover { color: var(--amber-text); border-color: var(--amber); background: var(--amber-glow); }
/* Extend +5m and settings-toggle buttons share the same base style as bb-end */
.bb-extend,
.bb-settings-toggle {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 5px 10px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}
.bb-extend:hover { color: var(--amber-text); border-color: var(--amber); background: var(--amber-glow); }
.bb-settings-toggle:hover { color: var(--ink); border-color: var(--line-strong); background: var(--bg-hover); }
.bb-settings-toggle.active { color: var(--amber-text); border-color: var(--amber); background: var(--amber-glow); }
/* Inline settings panel — slides in below the main banner row */
.bb-settings-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  align-items: center;
  padding: 8px 0 4px;
  border-top: 1px solid var(--line);
  margin-top: 6px;
}
.bbs-row {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--ink-dim);
}
.bbs-label { color: var(--ink-dim); white-space: nowrap; }
.bbs-unit  { color: var(--ink-faint); }
.bbs-input {
  width: 46px;
  padding: 2px 5px;
  background: var(--bg-input, var(--bg-raised));
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  text-align: center;
}
.bbs-toggle { font-size: 10px; padding: 3px 8px; }
.bb-progress {
  height: 3px;
  margin-top: 8px;
  background: var(--line);
  overflow: hidden;
}
.bb-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--amber), var(--red));
  transition: width 1s linear;
}

/* ===================================================================
   Break overlays — 6 variants beyond the inline banner.
   Switched at runtime by `state.settings.breakStyle`. Each variant uses
   the same .bo-* inner content blocks (title, clock, quote, end btn,
   progress) so JS only emits the markup once. Variants differ in
   container layout, backdrop, and entrance animation.
   =================================================================== */
.break-overlay {
  position: fixed;
  inset: 0;
  z-index: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;     /* default; variants opt-in to interaction */
  --bo-amber: var(--amber);
}
/* The inner card / content block. Shared shell. */
.break-overlay .bo-card {
  pointer-events: auto;
  background: var(--bg-raised);
  border: 1px solid var(--bo-amber);
  border-radius: 4px;
  padding: 28px 36px;
  min-width: 360px;
  max-width: min(560px, 90vw);
  text-align: center;
  box-shadow: 0 0 60px rgba(0,0,0,0.6), 0 0 0 1px var(--amber-glow) inset;
}
.break-overlay .bo-eyebrow {
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bo-amber);
  font-weight: 600;
  margin-bottom: 12px;
}
.break-overlay .bo-clock {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 64px;
  font-weight: 600;
  color: var(--bo-amber);
  letter-spacing: 0.04em;
  line-height: 1;
  margin: 4px 0 18px;
}
.break-overlay .bo-quote {
  color: var(--ink-dim);
  font-size: 13px;
  line-height: 1.6;
  font-style: italic;
  max-width: 46ch;
  margin: 0 auto 20px;
  min-height: 3em;
}
.break-overlay .bo-quote .bo-quote-author {
  display: block;
  margin-top: 6px;
  font-size: 11px;
  font-style: normal;
  color: var(--ink-faint);
  letter-spacing: 0.05em;
}
.break-overlay .bo-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  align-items: center;
}
.break-overlay .bo-end {
  pointer-events: auto;
  background: transparent;
  border: 1px solid var(--bo-amber);
  color: var(--bo-amber);
  padding: 8px 16px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.break-overlay .bo-end:hover { background: var(--amber-glow); }
/* Extend + settings-gear buttons — same shape as bo-end but lower-emphasis.
   Use the theme's ink/line tokens so the buttons render visibly in BOTH
   light and dark themes (the previous rgba(255,255,255,…) was invisible
   on light backgrounds). Hover/active escalate to the amber accent so the
   user can tell which control they're on. */
.break-overlay .bo-extend,
.break-overlay .bo-settings-toggle {
  pointer-events: auto;
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 7px 14px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.06em;
}
.break-overlay .bo-settings-toggle { min-width: 38px; padding: 7px 10px; }
.break-overlay .bo-extend:hover,
.break-overlay .bo-settings-toggle:hover { border-color: var(--bo-amber); color: var(--bo-amber); background: var(--amber-glow); }
.break-overlay .bo-settings-toggle.active { border-color: var(--bo-amber); color: var(--bo-amber); background: var(--amber-glow); }
/* Settings panel inside the overlay card — sits inside the .bo-card so it
   inherits the card's surface; uses theme tokens so it adapts to light/dark. */
.break-overlay .bo-settings-panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--bg-hover, var(--bg));
  border: 1px solid var(--line);
  border-radius: 4px;
  text-align: start;
}
.break-overlay .bo-settings-panel .bbs-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  color: var(--ink-dim);
}
.break-overlay .bo-settings-panel .bbs-label { color: var(--ink-dim); }
.break-overlay .bo-settings-panel .bbs-unit  { color: var(--ink-faint); }
.break-overlay .bo-settings-panel .bbs-input {
  background: var(--bg-raised);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  width: 56px;
}
.break-overlay .bo-settings-panel .bbs-toggle { font-size: 10px; padding: 4px 10px; }
.break-overlay .bo-progress {
  height: 2px;
  margin-top: 16px;
  background: var(--line);
  overflow: hidden;
  border-radius: 1px;
}
.break-overlay .bo-progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--bo-amber), var(--red));
  transition: width 1s linear;
}
.break-overlay .bo-sub {
  margin-top: 10px;
  font-size: 10px;
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ---------- 1. Spotlight (centered card, blur+dim backdrop) ---------- */
.break-overlay.style-spotlight {
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);   /* iOS Safari */
  pointer-events: auto;
  animation: bo-fade 220ms ease-out;
}
/* Glassmorphism is intentional here. The break overlay's purpose is to make
   work mentally fade — softening the bg with blur enacts that visually so
   the user's eye lets go of the kanban they were focused on. Flat dim is
   not the same. Audited and re-restored 2026-05-17 after a brief over-zealous
   removal. See DESIGN.md "Glassmorphism: Purposeful Only" rule. */
@keyframes bo-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---------- 2. Curtain (top 60% drops down, board peeks below) ------- */
.break-overlay.style-curtain {
  align-items: stretch;
  justify-content: center;
  background: transparent;
}
.break-overlay.style-curtain::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  block-size: 60vh;
  background:
    radial-gradient(ellipse at 50% 0%, color-mix(in oklab, var(--bo-amber) 10%, transparent), transparent 70%),
    linear-gradient(180deg, var(--bg-raised) 0%, var(--bg-raised) 70%, transparent 100%);
  border-block-end: 1px solid var(--bo-amber);
  pointer-events: auto;
  animation: bo-curtain-drop 360ms cubic-bezier(0.2, 0.8, 0.2, 1);
  transform-origin: top;
}
.break-overlay.style-curtain .bo-card {
  position: relative;
  z-index: 1;
  margin-top: 12vh;
  align-self: flex-start;
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
}
.break-overlay.style-curtain .bo-clock { font-size: 88px; }
@keyframes bo-curtain-drop {
  from { transform: translateY(-100%); }
  to   { transform: translateY(0); }
}

/* ---------- 3. Vignette (color-only halo, board fully usable) -------- */
.break-overlay.style-vignette {
  background: radial-gradient(
    ellipse at center,
    transparent 50%,
    color-mix(in oklab, var(--bo-amber) 6%, transparent) 75%,
    color-mix(in oklab, var(--bo-amber) 20%, transparent) 100%
  );
  pointer-events: none;             /* board stays clickable */
  animation: bo-fade 600ms ease-out;
}
.break-overlay.style-vignette .bo-card {
  position: absolute;
  inset-block-start: 16px;
  inset-inline-end: 16px;
  margin: 0;
  padding: 14px 18px;
  min-width: 220px;
  text-align: start;
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.break-overlay.style-vignette .bo-clock { font-size: 28px; margin: 0 0 6px; }
.break-overlay.style-vignette .bo-quote { display: none; }
.break-overlay.style-vignette .bo-progress { margin-top: 8px; }
.break-overlay.style-vignette .bo-actions { justify-content: flex-start; }

/* ---------- 4. Breathing orb (centered pulsing circle) --------------- */
.break-overlay.style-orb {
  background: rgba(0,0,0,0.7);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);   /* iOS Safari */
  pointer-events: auto;
  flex-direction: column;
  animation: bo-fade 300ms ease-out;
}
/* Strongest blur in the app — the orb variant is the most immersive break
   style; the heavy blur dissolves the work-context entirely so the orb +
   timer have the user's full attention. Intentional glassmorphism. */
.break-overlay.style-orb .bo-orb {
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--bo-amber) 0%, color-mix(in oklab, var(--bo-amber) 25%, transparent) 50%, transparent 70%);
  margin-bottom: 24px;
  animation: bo-breathe 6s ease-in-out infinite;
  filter: blur(2px);
}
@keyframes bo-breathe {
  0%, 100% { transform: scale(0.7); opacity: 0.55; }
  50%      { transform: scale(1.05); opacity: 1.0; }
}
.break-overlay.style-orb .bo-card {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  min-width: 0;
}
.break-overlay.style-orb .bo-clock { font-size: 48px; }

/* ---------- 5. Side panel (40% slides from end-edge) ----------------- */
.break-overlay.style-side {
  background: transparent;
  pointer-events: none;
  justify-content: flex-end;
}
.break-overlay.style-side .bo-card {
  pointer-events: auto;
  inline-size: min(420px, 40vw);
  max-width: none;
  block-size: 100vh;
  margin: 0;
  border-radius: 0;
  border: none;
  border-inline-start: 1px solid var(--bo-amber);
  background: linear-gradient(180deg, var(--bg-raised) 0%, var(--bg) 100%);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 32px 28px;
  animation: bo-side-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes bo-side-in {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
[dir="rtl"] .break-overlay.style-side .bo-card {
  animation-name: bo-side-in-rtl;
}
@keyframes bo-side-in-rtl {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}
.break-overlay.style-side .bo-clock { font-size: 56px; }

/* ---------- 6. Cinema (full blackout, ambient amber rays) ------------ */
.break-overlay.style-cinema {
  background: #000;
  pointer-events: auto;
  flex-direction: column;
  animation: bo-fade 500ms ease-out;
}
.break-overlay.style-cinema::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse at 30% 30%, color-mix(in oklab, var(--bo-amber) 8%, transparent), transparent 50%),
    radial-gradient(ellipse at 70% 70%, color-mix(in oklab, var(--bo-amber) 5%, transparent), transparent 50%);
  pointer-events: none;
  animation: bo-cinema-drift 24s ease-in-out infinite alternate;
}
@keyframes bo-cinema-drift {
  from { transform: translate(0, 0); }
  to   { transform: translate(8%, -6%); }
}
.break-overlay.style-cinema .bo-card {
  background: transparent;
  border: none;
  box-shadow: none;
  z-index: 1;
}
.break-overlay.style-cinema .bo-clock { font-size: 96px; color: var(--bo-amber); }
.break-overlay.style-cinema .bo-eyebrow { font-size: 12px; letter-spacing: 0.32em; }

/* Preview tag — pinned to the top of the overlay so users know they're
   looking at a preview, not a real break. Also doubles as a close
   button (the × glyph in the label says "click to exit"). The earlier
   pointer-events: none was DROPPED so clicks register; cursor + hover
   styles make the affordance obvious. */
.break-overlay .bo-preview-tag {
  position: absolute;
  inset-block-start: 12px;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  background: var(--bo-amber);
  color: var(--bg);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 4px 12px;
  border: none;
  border-radius: 2px;
  font-weight: 700;
  font-family: inherit;
  z-index: 2;
  cursor: pointer;
  transition: filter 120ms, transform 120ms;
}
.break-overlay .bo-preview-tag:hover {
  filter: brightness(1.1);
  transform: translateX(-50%) translateY(-1px);
}
.break-overlay .bo-preview-tag:focus-visible {
  outline: 2px solid var(--bg);
  outline-offset: 2px;
}
[dir="rtl"] .break-overlay .bo-preview-tag {
  transform: translateX(50%);
}
[dir="rtl"] .break-overlay .bo-preview-tag:hover {
  transform: translateX(50%) translateY(-1px);
}

/* ---------- Settings picker for break style (mini cards) ------------- */
.break-style-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 8px;
  margin-block-start: 4px;
}
.break-style-tile {
  position: relative;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 12px 10px;
  cursor: pointer;
  font: inherit;
  color: var(--ink-dim);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transition: border-color 120ms, color 120ms;
}
.break-style-tile:hover { border-color: var(--accent-dim); color: var(--ink); }
.break-style-tile.on {
  border-color: var(--amber);
  color: var(--amber-text);
  background: var(--amber-glow);
}
.break-style-tile .bst-thumb {
  /* Mini visual preview: 80x44 box with the variant's signature look. */
  inline-size: 80px;
  block-size: 44px;
  border-radius: 2px;
  background: var(--bg);
  border: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
.break-style-tile .bst-name {
  font-size: 11px;
  letter-spacing: 0.04em;
}
.break-style-tile .bst-preview {
  position: absolute;
  inset-block-start: 4px;
  inset-inline-end: 4px;
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: transparent;
  border: 1px solid var(--line);
  padding: 2px 6px;
  border-radius: 2px;
  cursor: pointer;
}
.break-style-tile .bst-preview:hover { color: var(--amber-text); border-color: var(--amber); }

/* Mini-thumb art per variant — pure CSS, no images, ~600 bytes total. */
.bst-thumb-banner::before {
  content: ""; position: absolute; inset-inline: 0; inset-block-start: 0;
  block-size: 8px; background: var(--amber);
}
.bst-thumb-spotlight::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 50%, var(--amber-glow) 0%, rgba(0,0,0,0.4) 60%);
}
.bst-thumb-spotlight::after {
  content: ""; position: absolute; inset-inline-start: 30%; inset-block-start: 30%;
  inline-size: 40%; block-size: 40%; background: var(--bg-raised); border: 1px solid var(--amber);
}
.bst-thumb-curtain::before {
  content: ""; position: absolute; inset-inline: 0; inset-block-start: 0;
  block-size: 70%; background: linear-gradient(180deg, var(--amber-glow), var(--bg-raised));
  border-block-end: 1px solid var(--amber);
}
.bst-thumb-vignette::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(ellipse at center, transparent 40%, var(--amber-glow) 100%);
}
.bst-thumb-orb::before {
  content: ""; position: absolute; inset: 0; background: rgba(0,0,0,0.4);
}
.bst-thumb-orb::after {
  content: ""; position: absolute; inset-inline-start: 50%; inset-block-start: 50%;
  transform: translate(-50%, -50%);
  inline-size: 18px; block-size: 18px; border-radius: 50%;
  background: radial-gradient(circle, var(--amber), transparent 70%);
}
.bst-thumb-side::before {
  content: ""; position: absolute; inset-block: 0; inset-inline-end: 0;
  inline-size: 40%; background: var(--amber-glow); border-inline-start: 1px solid var(--amber);
}
.bst-thumb-cinema::before {
  content: ""; position: absolute; inset: 0; background: #000;
}
.bst-thumb-cinema::after {
  content: ""; position: absolute; inset-inline-start: 25%; inset-block-start: 35%;
  inline-size: 50%; block-size: 30%;
  background: radial-gradient(ellipse at center, var(--amber-glow), transparent 70%);
}

/* ===================================================================
   Topbar action buttons — the .icon-btn cluster + its SVG glyphs
   =================================================================== */
/* Utility buttons drop the resting border. Hover lifts them onto
   --bg-raised so the affordance is unambiguous; focus-visible adds an
   accent border for keyboard nav. */
.topbar-actions .icon-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid transparent;
  background: transparent;
  padding: 5px 9px;
  border-radius: 3px;
  cursor: pointer;
  color: var(--ink-secondary);
  font-size: var(--fs-caption);
  flex-shrink: 0;
  white-space: nowrap;
  transition: background 100ms, color 100ms, border-color 100ms;
}
.topbar-actions .icon-btn:hover { color: var(--accent-text); background: var(--bg-raised); }
.topbar-actions .icon-btn:focus-visible { border-color: var(--accent); outline: none; }
/* Toggle state — the day-overview button gets an accent border + icon
   colour while the day-overview strip is showing. */
.topbar-actions .icon-btn.on { color: var(--accent-text); border-color: var(--accent); }

/* Inline-SVG glyph wrapper. Scoped to .topbar (not .topbar-actions) so
   it also covers the mobile search-toggle / close buttons, which sit
   outside the actions cluster. 15px reads balanced beside the 11px
   caption labels — slightly above the text cap-height so the icon
   anchors the button without dominating it. currentColor flows from
   .icon-btn. */
.topbar .tb-glyph { display: inline-flex; align-items: center; }
.topbar .tb-glyph svg { width: 15px; height: 15px; display: block; }

/* Icon-only utilities (notifications, help, settings, overflow): square
   padding so the glyph sits centered with no label. */
.topbar-actions .tb-icon-only { padding: 5px; }
/* The notif count sits inline after the bell — the flex gap already
   spaces it, so drop the badge's own inline margin (avoids a double gap). */
.topbar-actions .tb-icon-only .notif-badge { margin-inline-start: 0; }

/* Primary nav (board ↔ calendar). A resting border marks it as a
   control — distinct from the borderless utility icons — without the
   accent weight the command-palette button carries. */
.topbar-actions .tb-view-btn { border-color: var(--line-strong); }
.topbar-actions .tb-view-btn:hover { border-color: var(--accent-dim); }

/* The ⌘K palette button is a plain icon button like the rest — strip
   the native <kbd> chrome so the cap reads as text in the button's own
   colour (grey at rest, accent on hover, inherited from .icon-btn). */
.topbar-actions #btn-palette .palette-kbd {
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.5px;
  color: inherit;
  background: transparent;
  border: none;
  padding: 0;
  margin: 0;
}
/* ===================================================================
   Trash drawer
   =================================================================== */

/* Banner directly under the trash drawer header. Surfaces the analytics-
   retention contract: trashed items still count toward insights until they
   are auto-purged after 30 days or removed manually. Logical properties so
   RTL mirrors automatically. */
.trash-analytics-notice {
  color: var(--ink-faint);
  font-size: 12px;
  font-style: italic;
  padding-block: 8px 12px;
  padding-inline: 4px;
  line-height: 1.5;
  border-block-end: 1px solid var(--line);
  margin-block-end: 8px;
}

.trash-list { display: flex; flex-direction: column; gap: 6px; }
.trash-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--bg-cell);
}
.trash-row:hover { border-color: var(--line-strong); }
.trash-main { flex: 1; min-width: 0; }
.trash-title {
  color: var(--ink);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.trash-meta {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.04em;
  margin-top: 2px;
}
.trash-actions { display: flex; gap: 4px; }
.trash-btn {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 3px 10px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}
.trash-btn.trash-restore:hover { color: var(--accent-text); border-color: var(--accent); }
.trash-btn.trash-purge:hover { color: var(--red-text); border-color: var(--red); }

/* ---------- Multi-timer popover ----------
   Anchored above the running-state timer chip. Header shows the focus
   session elapsed + how many tasks are tracking against it. One row per
   running timer with elapsed clock, task title, and an inline ⏸ stop.
   Footer: total live time, "pause all", and a contextual "take break". */
.timer-pop {
  min-width: 280px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.timer-pop-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 4px 6px 6px;
  border-bottom: 1px dashed var(--line);
}
.timer-pop-session { color: var(--accent-text); font-weight: 600; font-size: 12px; }
.timer-pop-count { color: var(--ink-faint); font-size: 11px; }
.timer-pop-rows { display: flex; flex-direction: column; gap: 2px; }
.timer-row {
  display: grid;
  grid-template-columns: 14px auto 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  border-radius: 3px;
  font-size: 12px;
}
.timer-row:hover { background: var(--bg-card-hov, var(--bg-raised)); }
.timer-row-play { color: var(--accent-text); font-size: 10px; line-height: 1; }
.timer-row-time {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.timer-row-title {
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.timer-row-stop {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  border-radius: 3px;
  padding: 2px 6px;
  font: inherit;
  cursor: pointer;
  transition: color 100ms, border-color 100ms, background 100ms;
}
.timer-row-stop:hover {
  color: var(--red-text);
  border-color: color-mix(in oklab, var(--red) 50%, var(--line-strong));
  background: color-mix(in oklab, var(--red) 8%, transparent);
}
.timer-pop-foot {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 2px;
  border-top: 1px dashed var(--line);
  font-size: 11px;
}
.timer-pop-total { color: var(--ink-faint); margin-inline-end: auto; }
.timer-pop-action {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-secondary);
  border-radius: 3px;
  padding: 3px 8px;
  font: inherit;
  cursor: pointer;
  transition: color 100ms, border-color 100ms, background 100ms;
}
.timer-pop-action:hover {
  color: var(--accent-text);
  border-color: var(--accent-dim);
  background: var(--bg-card-hov, var(--bg-raised));
}
.timer-pop-break-btn:hover { color: var(--accent-text); }

/* ---------- Today strip ---------- */

/* Namespaced from `.today` (2026-05-17). The bare selector also matched
   the day-strip's `.cal-week-strip-day.today` cell and the date-picker's
   `.dp-day.today` cell, silently stamping its 14/20/18 padding + amber
   gradient onto them (visible as a tall dark "today" cell in the
   day-view strip). The namespaced form prevents the cross-surface bleed
   without changing this surface's appearance. */
.today-strip {
  border-bottom: 1px solid var(--line);
  background:
    linear-gradient(180deg, var(--amber-glow) 0%, transparent 100%),
    var(--bg);
  padding: 14px 20px 18px;
}
.today-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin-bottom: 10px;
}
.today-head .label {
  color: var(--amber-text);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 11px;
}
.today-head .date {
  color: var(--ink-dim);
  font-size: 11px;
}
.today-head .count {
  color: var(--ink-faint);
  font-size: 11px;
  margin-inline-start: auto;
}
.today-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(260px, 1fr);
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 4px;
}
.today-row::-webkit-scrollbar { height: 6px; }
.today-row::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }

.today-card {
  background: var(--bg-card);
  border-inline-start: 2px solid var(--amber);
  border-radius: 2px var(--radius) var(--radius) 2px;
  padding: 12px 14px;
  display: grid;
  gap: 4px;
  cursor: pointer;
}
.today-card.overdue { border-inline-start-color: var(--red); }
.today-card:hover { background: var(--bg-card-hov); }
.today-card .due {
  font-size: var(--fs-caption);
  font-weight: var(--fw-medium);
  color: var(--amber-text);
  letter-spacing: 0.02em;
}
.today-card.overdue .due { color: var(--red-text); }
.today-card .ttl {
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  color: var(--ink-primary);
  line-height: 1.4;
}
.today-card .grp {
  font-size: var(--fs-caption);
  color: var(--ink-tertiary);
}

.today-empty {
  color: var(--ink-faint);
  font-style: italic;
  padding: 4px 0;
}

/* ---------- PWA install nudge banner ---------- */
/* Slim banner above the topbar — only renders when shouldShowInstallNudge()
   agrees (sessions >= 3, beforeinstallprompt fired, not yet dismissed,
   not already installed). Logical CSS so RTL flips automatically. */
.install-nudge {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background: color-mix(in oklab, var(--accent) 8%, var(--bg-raised));
  border-block-end: 1px solid var(--accent-dim);
  font-size: 12px;
}
.install-nudge-icon {
  font-size: 16px;
  color: var(--accent-text);
  flex-shrink: 0;
}
.install-nudge-text {
  flex: 1;
  color: var(--ink);
  line-height: 1.4;
}
.install-nudge-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}
.install-nudge-btn {
  background: transparent;
  border: 1px solid var(--ink-faint);
  color: var(--ink-dim);
  padding: 4px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  border-radius: var(--radius-sm, 4px);
  transition: background 80ms ease-out, color 80ms ease-out, border-color 80ms ease-out;
}
.install-nudge-btn:hover { color: var(--ink); border-color: var(--ink-dim); }
.install-nudge-btn.primary {
  border-color: var(--accent);
  color: var(--accent-text);
  background: var(--accent-glow);
  font-weight: 600;
}
.install-nudge-btn.primary:hover {
  background: color-mix(in oklab, var(--accent) 22%, transparent);
}
@media (max-width: 540px) {
  .install-nudge { flex-direction: column; align-items: stretch; gap: 8px; }
  .install-nudge-actions { justify-content: stretch; }
  .install-nudge-btn { flex: 1; }
}

/* Install status chip in Settings → About pane. Two states show as chips
   (installed / ios). The "available" state uses the standard .toggle
   button so it matches the other settings affordances; "unsupported"
   renders as plain text. */
.install-status {
  display: inline-block;
  padding: 4px 10px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-radius: var(--radius-sm, 4px);
}
.install-status.installed {
  color: var(--accent-text);
  background: var(--accent-glow);
  border: 1px solid var(--accent);
}
.install-status.ios {
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--ink-faint);
}

/* ---------- Toast ---------- */

.toast {
  position: fixed;
  bottom: 40px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 8px 30px rgba(0,0,0,0.6), 0 0 0 3px var(--accent-glow);
  border-radius: var(--radius);
  padding: 10px 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  z-index: 200;
  font-size: 12px;
  animation: toast-in 160ms ease-out;
}
@keyframes toast-in {
  from { opacity: 0; transform: translate(-50%, 6px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.toast-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}
.toast-actions[hidden] { display: none; }
.toast-action {
  border: 1px solid var(--accent);
  color: var(--accent-text);
  background: transparent;
  padding: 2px 10px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
  white-space: nowrap;
  transition: background 80ms ease-out;
}
.toast-action:hover { background: var(--accent-glow); }
.toast-action.primary {
  background: var(--accent-glow);
  border-color: var(--accent);
  font-weight: 600;
}
.toast-action.primary:hover { background: color-mix(in oklab, var(--accent) 22%, transparent); }
/* Rich mode (≥2 buttons) — stack the actions row beneath the message so two
   localized labels never wrap awkwardly on narrow viewports. Single-button
   mode keeps the inline layout. */
.toast.rich {
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding-block-end: 12px;
}
.toast.rich .toast-actions {
  justify-content: flex-end;
}
@keyframes toast-in-mobile {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (max-width: 600px) {
  /* On mobile the statusbar is fixed at the bottom (min-height 34px +
     safe-area-inset-bottom). Raise the toast above it so it isn't hidden
     behind the chip bar or the home-indicator strip. */
  .toast {
    bottom: calc(var(--statusbar-h, 34px) + env(safe-area-inset-bottom) + 16px);
    left: 12px;
    right: 12px;
    transform: none;
    max-width: calc(100% - 24px);
    animation-name: toast-in-mobile;
  }
}
@media (max-width: 540px) {
  .toast.rich .toast-actions { justify-content: stretch; }
  .toast.rich .toast-action { flex: 1; }
}

/* ===================================================================
   THEME PICKER POPOVER — dropdown list shown when clicking the
   topbar theme button. Each row has a bg-chip with 4 mini accent
   swatches (green/amber/red/blue) so you preview the theme before
   applying. The currently-active theme gets a ✓ and a highlight.
   =================================================================== */

.theme-pop {
  position: fixed;
  /* 900 so the appearance popover sits above .break-overlay (800) — that
     way the live break-style preview behind the popover stays visible
     while the user is still picking. */
  z-index: 900;
  background: var(--bg-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 4px;
  min-width: 180px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.theme-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  font: inherit;
  width: 100%;
}
.theme-item:hover {
  background: var(--bg-card-hov);
  border-color: var(--line-strong);
}
.theme-item.selected {
  background: color-mix(in oklab, var(--accent) 10%, transparent);
  border-color: color-mix(in oklab, var(--accent) 45%, var(--line-strong));
}
.theme-item .theme-swatches {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 5px;
  border-radius: 3px;
  border: 1px solid var(--line-strong);
}
.theme-item .theme-sw {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.theme-item .theme-name {
  flex: 1 1 auto;
  font-size: 12px;
  letter-spacing: 0.02em;
}
.theme-item .theme-check {
  flex: 0 0 auto;
  color: var(--accent-text);
  font-size: 12px;
  width: 12px;
  text-align: center;
}

/* Zoom popover reuses .theme-pop layout — percentage chip replaces the
   color swatches that the theme picker uses in the same slot. */
.zoom-pop .theme-item .zoom-pct {
  flex: 0 0 auto;
  min-width: 44px;
  padding: 3px 6px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--bg-card);
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-size: 12px;
}
.zoom-pop .theme-item.selected .zoom-pct {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--line-strong));
  background: color-mix(in oklab, var(--accent) 12%, var(--bg-card));
  color: var(--accent-text);
}

/* Consolidated Appearance popover — theme + size + language + sound in one
   panel. Sized to content (with a sensible min) so the theme row dictates
   width without leaving empty trailing space. */
.appearance-pop {
  width: max-content;
  min-width: 220px;
  max-width: 360px;
  padding: 8px;
  gap: 10px;
}
.appearance-section { display: flex; flex-direction: column; gap: 5px; }
.appearance-label {
  color: var(--ink-faint);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-inline-start: 2px;
}
.appearance-sublabel {
  margin-inline-start: 6px;
  color: var(--ink-dim);
  font-size: 10px;
  letter-spacing: 0.02em;
  text-transform: none;
  font-variant-numeric: tabular-nums;
}
.appearance-row { display: flex; flex-wrap: wrap; gap: 4px; }
.appearance-row-themes {
  display: grid;
  /* `minmax(0, 1fr)` (not `1fr` — which is `minmax(auto, 1fr)`) so a
     long-label chip like "catppuccin-latte" can't push its column past
     the parent's actual width. Without this, the grid blows out of
     narrow containers (e.g. the settings drawer's content pane at
     ~540px) and chips spill outside the modal. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
  /* Hard guard against any descendant that still tries to overflow. */
  min-width: 0;
  max-width: 100%;
}

/* One theme chip: the tile IS the preview — its background uses the theme's
   own bg color, its label uses the theme's accent. No 4-dot mini-swatch any
   more (that "0000" was visually noisy and forced the label into a narrow
   column where long names like "catppuccin-latte" wrapped per character).
   Each chip forwards two custom properties from inline style — set in
   renderThemeChip() — and CSS uses them via color-mix so hover/selected
   look right for every theme without per-theme rules. */
.appearance-swatch {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 32px;
  padding: 6px 10px;
  background: var(--swatch-bg, var(--bg-raised));
  color: var(--swatch-accent, var(--ink-dim));
  border: 1px solid color-mix(in oklab, var(--swatch-accent, var(--line-strong)) 35%, transparent);
  border-radius: 4px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  line-height: 1.2;
  letter-spacing: 0.02em;
  transition: border-color 120ms, box-shadow 120ms, transform 120ms;
  /* Required so the tile can shrink inside the
     grid-template-columns: minmax(0, 1fr) parent without overflowing. */
  min-width: 0;
}
.appearance-swatch-name {
  /* Long names ("catppuccin-latte", "solarized-light") must wrap if the
     column is narrow. break-word breaks at sensible boundaries (hyphens
     first, then word edges) — unlike `anywhere` which would split mid-
     letter cluster. */
  overflow-wrap: break-word;
  word-break: break-word;
  min-width: 0;
  text-align: center;
}
.appearance-swatch:hover {
  border-color: var(--swatch-accent, var(--accent));
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--swatch-accent, var(--accent)) 40%, transparent);
}
.appearance-swatch.selected {
  border-color: var(--swatch-accent, var(--accent));
  box-shadow:
    0 0 0 2px color-mix(in oklab, var(--swatch-accent, var(--accent)) 60%, transparent),
    0 0 14px color-mix(in oklab, var(--swatch-accent, var(--accent)) 22%, transparent);
}
/* Subtle separation between the two theme rows; applied to whichever row
   is rendered second. */
.appearance-row-themes-gap { margin-top: 2px; }

/* Native-script sample appended to language-specific font chips. The chip
   is already styled in the picked font, so the sample renders in that
   font — turning the chip into a real script preview. Dimmed slightly
   so the Latin label reads first. */
.mono-font-sample {
  color: var(--ink-faint);
  margin-inline-start: 4px;
}
.appearance-pill.selected .mono-font-sample {
  color: inherit;
  opacity: 0.85;
}

/* Break visual tiles inside the appearance popover. Reuses the same
   .bst-thumb-* mini-thumbnail art defined in the break-style picker
   section. Selecting a tile here both sets the persistent style AND
   triggers a live preview overlay behind the popover. */
.appearance-row-break {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
  width: 100%;
}
.appearance-break-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px 4px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  color: var(--ink-dim);
  transition: border-color 120ms, color 120ms, background 120ms;
}
/* `:hover` only on devices with a real pointer. iOS Safari treats the
   first tap on a `:hover`-styled element as "activate hover"; the click
   handler fires only on the SECOND tap. The previous broad nullifier in
   the @media (hover: none) block reverts the visual but the selector
   itself still exists, which is enough to trigger the double-tap trap.
   Wrapping the rule in @media (hover: hover) means the selector is
   absent on touch — first tap fires click, the tile is selected, and
   the preview opens without a stuck hover-only visual state. */
@media (hover: hover) {
  .appearance-break-tile:hover {
    border-color: color-mix(in oklab, var(--accent) 35%, var(--line-strong));
    background: var(--bg-card-hov);
    color: var(--ink);
  }
}
.appearance-break-tile.selected {
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  color: var(--accent-text);
}
.appearance-break-tile .bst-thumb {
  inline-size: 56px;
  block-size: 30px;
  /* Positioning context + clip for the bst-thumb-* pseudo-elements.
     Without these, .bst-thumb-cinema::before (inset:0; background:#000)
     escapes to the popover's containing block and paints the whole
     popover black, while .bst-thumb-orb::after parks its glow at the
     popover's center — making the menu look empty. */
  position: relative;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 2px;
}
.appearance-break-name {
  font-size: 10px;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.appearance-pill {
  flex: 0 0 auto;
  padding: 3px 9px;
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  color: var(--ink-dim);
  font: inherit;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 100ms, color 100ms, border-color 100ms;
}
.appearance-pill:hover { background: var(--bg-card-hov); color: var(--ink); }
.appearance-pill.selected {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  border-color: var(--accent);
  color: var(--accent-text);
}

/* Settings-drawer UI-zoom picker — row of 4 preset toggles, tabular
   digits so widths don't jitter as percentages change. */
.ui-zoom-picker {
  display: inline-flex;
  gap: 4px;
  flex-wrap: wrap;
}
.ui-zoom-picker .toggle {
  font-variant-numeric: tabular-nums;
  min-width: 48px;
}

/* Topbar zoom button — tabular digits + chevron hint that it opens a
   dropdown (matches the theme button layout one slot over). */
.topbar .zoom-btn { font-variant-numeric: tabular-nums; }
.topbar .zoom-btn .chev { font-size: 10px; opacity: 0.7; margin-inline-start: 2px; }

/* Topbar theme button: current-theme accent dot + chevron hint.
   The dot uses the theme's primary --accent accent so you can tell
   at a glance which theme is active without reading the name. */
.topbar .theme-btn .theme-btn-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 4px color-mix(in oklab, var(--accent) 60%, transparent);
  margin-inline-end: 4px;
  vertical-align: middle;
}
.topbar .theme-btn .chev {
  font-size: 10px;
  opacity: 0.7;
  margin-inline-start: 2px;
}

/* ===================================================================
   PRIORITY — meta-row pill + quiet inline-edge rail
   =================================================================== */

/* Priority is carried primarily by the `.priority-pill` text badge in
   the card meta row (self-describing "P0" / "P1" / "P3"). The colored
   inline-edge rail below is now only peripheral reinforcement for the
   two attention levels — it reads on a right-handed or RTL scan
   without the user having to learn a colour legend.
   - P0 / P1: a quiet 2px rail on both inline edges.
   - P2 (default): no rail, no pill. A clean card is the off-state, so
     any colour on a card edge actually means something.
   - P3 (low): no rail; the card dims via opacity instead. An attention
     rail would contradict "low priority". */
.card[data-priority="0"] { border-inline-start: 2px solid var(--red);   border-inline-end: 2px solid var(--red); }
.card[data-priority="1"] { border-inline-start: 2px solid var(--amber); border-inline-end: 2px solid var(--amber); }
.card[data-priority="3"] { opacity: 0.85; }

/* Priority pill — the primary, self-describing cue. Text-only and
   colour-coded, matching the boxless filled-pill treatment
   (board.css §6). Rendered only for non-default priorities; default
   P2 cards show none. Click cycles the priority (board-cards.tsx). */
.card .pill.priority-pill {
  cursor: pointer;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.card .pill.priority-pill.p0 { color: var(--red-text); }
.card .pill.priority-pill.p1 { color: var(--amber-text); }
.card .pill.priority-pill.p3 { color: var(--ink-faint); }
.card .pill.priority-pill:hover { color: var(--accent-text); }

/* Task colour as a soft background tint — applied only when task.color is
   set AND differs from the group colour (see card render in app.js). The
   priority left-rail keeps its own colour; we only tint the body + adjust
   the outer border so the card reads as a member of its colour family. */
.card.has-task-bg {
  background: color-mix(in oklab, var(--task-bg-color) 8%, var(--bg-card));
  border-color: color-mix(in oklab, var(--task-bg-color) 30%, var(--line-strong));
}
.card.has-task-bg:hover {
  background: color-mix(in oklab, var(--task-bg-color) 14%, var(--bg-card-hov));
  border-color: color-mix(in oklab, var(--task-bg-color) 45%, var(--accent-dim));
}

/* Task color in card view lives ONLY on the clickable swatch — we don't
   paint the card frame, so it never fights the priority left-border stripe.
   (The color does show on calendar blocks & today-strip — different views.) */

.card .card-color-sq {
  width: 12px;
  height: 12px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: transparent;
  cursor: pointer;
  padding: 0;
  margin-top: 3px;
  flex-shrink: 0;
  color: var(--ink-faint);
  font-size: 9px;
  line-height: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 120ms, opacity 120ms, border-color 120ms;
}
.card .card-color-sq.has-color { border-width: 0; }
.card .card-color-sq.no-color {
  opacity: 0;
}
.card:hover .card-color-sq.no-color,
.card.focused .card-color-sq.no-color { opacity: 0.5; }
.card .card-color-sq:hover { transform: scale(1.15); opacity: 1 !important; }

/* Clickable due-date pill. Empty state styling lives in the unified
   .card .pill.empty rule so +schedule / +est / +due / ⊘block all match. */
.card .pill.due-pill { cursor: pointer; }
.card .pill.due-pill:hover { color: var(--accent-text); }

/* Drawer autosave hint */
.autosave-hint {
  color: var(--ink-faint);
  font-size: 11px;
  font-style: italic;
  letter-spacing: 0.02em;
}

/* ===================================================================
   Legend popover
   =================================================================== */

.legend-popover {
  position: fixed;
  z-index: var(--z-popover);
  /* Wider so the 2-col kbd grid's longer labels ("new task in focused
     group", "set priority on focused card") sit on a single line
     instead of wrapping to two. 540px gives each column ~225px of
     label room — enough for the longest English label at the project's
     body font. max-width caps on narrow viewports so the popover never
     overflows the screen. */
  width: 540px;
  max-width: calc(100vw - 24px);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 0 0 3px var(--accent-glow);
  border-radius: 4px;
  padding: 0;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink);
  animation: palette-in 120ms ease-out;
}
/* Single keyboard-shortcut pane — the Colors tab (and its tab strip +
   footnote) was dropped; the popover is now one reference list. */
.legend-popover .legend-body { padding: 10px 12px; }
/* Two-column kbd grid — packs the shortcuts in rows of 2 instead of one
   long stacked column, halving the popover's height. Each column is its
   own kbd-key + label sub-grid so the keys align vertically across rows
   instead of jittering with each shortcut's text width. */
.legend-popover .legend-kbd-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 18px;
}
.legend-popover .legend-kbd-grid .legend-kbd-row {
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: baseline;
  gap: 8px;
  padding: 1px 0;
}
.legend-popover .legend-kbd-grid .legend-kbd-row kbd {
  margin-inline-end: 0;
  justify-self: start;
}
.legend-popover .legend-kbd-row {
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-dim);
  padding: 1px 0;
}
.legend-popover .legend-kbd-row kbd {
  display: inline-block;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  padding: 0 4px;
  font-size: 10px;
  font-family: var(--mono);
  color: var(--ink);
  min-width: 12px;
  text-align: center;
  margin-inline-end: 4px;
}

/* ===================================================================
   TIMER chip
   =================================================================== */

/* Running timer — the only meta chip that ALWAYS reads as "live". The pulsing
   dot + accent ink carry the signal; no chrome needed. */
.pill.running {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}
.pill.running .dot {
  animation: timer-pulse 1.6s ease-in-out infinite;
  box-shadow: 0 0 6px var(--accent-glow);
}
@keyframes timer-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.pill.idle {
  color: var(--ink-tertiary);
  font-variant-numeric: tabular-nums;
}

/* Primary action on the card. Reads as a button at rest — soft accent
   background, accent-colored glyph — so people don't miss the start
   affordance the way a faint icon-only span lets them. Sized noticeably
   larger than the checkbox (24x24 vs 14x14) so the start affordance is
   the visually heaviest thing on the row, matching its frequency-of-use.
   margin-top compensates for the parent's align-items:flex-start so the
   button stays centered against the title's first line of text. */
.card .play,
.today-card .play {
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--accent-text);
  background: var(--accent-glow);
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 10px;
  cursor: pointer;
  margin-top: -3px;
  user-select: none;
  transition: transform 80ms cubic-bezier(0.2, 0.9, 0.3, 1),
              background 120ms, border-color 120ms, color 120ms,
              box-shadow 120ms;
  transform-origin: center;
}
.card .play .play-glyph svg,
.today-card .play .play-glyph svg { width: 14px; height: 14px; }
/* Anticipation cue — hovering anywhere on the card brightens the play
   button so it reads as the row's primary affordance before the cursor
   arrives at it. Pairs with the direct :hover state below for a graceful
   escalation: rest (20%, --accent-glow) → card-hover (32%) → play-hover
   (44% + outer ring). Each step ~12% denser than the last. */
.card:hover .play:not(.running),
.today-card:hover .play:not(.running) {
  background: color-mix(in oklab, var(--accent) 32%, transparent);
}
.card .play:hover,
.today-card .play:hover {
  background: color-mix(in oklab, var(--accent) 44%, transparent);
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.card .play:focus-visible,
.today-card .play:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
}
.card .play.running,
.today-card .play.running {
  /* Filled state — already running. Inverted from the resting "invitation"
     state so the running task reads as a state, not as an action prompt. */
  color: var(--bg-card);
  background: var(--accent);
  border-color: var(--accent);
}
.card .play:active,
.today-card .play:active { transform: scale(0.85); }
.card .play.pressed,
.today-card .play.pressed { animation: play-pulse 320ms cubic-bezier(0.2, 0.9, 0.3, 1); }

@keyframes play-pulse {
  0%   { transform: scale(1); }
  25%  { transform: scale(0.78); }
  60%  { transform: scale(1.35); color: var(--accent-text); }
  100% { transform: scale(1); }
}

/* Start / stop visual feedback on the whole card. The card already moves
   between lanes via the FLIP animation in render(), but moving cards
   without a "what just happened" cue feels chaotic — both the START and
   STOP gestures get their own keyframe so the user reads "timer fired"
   vs "timer wrapped" without ambiguity. Both keyframes run on the card
   itself so the cue is visible from across the column. */
.card.timer-started { animation: card-start-glow 600ms cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes card-start-glow {
  0%   { box-shadow: 0 0 0 0 transparent; }
  40%  { box-shadow: 0 0 0 3px var(--accent-glow), 0 0 18px var(--accent-glow); border-color: var(--accent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Stop animation — symmetric to start but uses --amber instead of
   --accent (semantic split: green = run, amber = wrap/finish/hold).
   The "shrink" curve (3px → 0) reads as wind-down; the start curve goes
   the opposite way (0 → 3px → 0). Keeps the same 600ms duration so
   start and stop feel like sibling cues. */
.card.timer-stopped { animation: card-stop-glow 600ms cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes card-stop-glow {
  0%   { box-shadow: 0 0 0 3px var(--amber-glow), 0 0 16px var(--amber-glow); border-color: var(--amber); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Glyph cross-fade on the play button — the ▶ ↔ ■ swap was an instant
   replaceContent before, which felt like the button "blinked" between
   two unrelated states. Now the swap reads as a transformation: the old
   glyph fades + scales out, new glyph fades + scales in, total ~180ms. */
.card .play .play-glyph,
.today-card .play .play-glyph {
  display: inline-block;
  transition: transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1), opacity 180ms;
}
.card .play.glyph-swap .play-glyph,
.today-card .play.glyph-swap .play-glyph {
  animation: play-glyph-swap 180ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
@keyframes play-glyph-swap {
  0%   { transform: scale(1)   rotate(0deg); opacity: 1; }
  50%  { transform: scale(0.4) rotate(-30deg); opacity: 0; }
  100% { transform: scale(1)   rotate(0deg); opacity: 1; }
}

/* Stop-press pulse — symmetric to .play.pressed but ends inverted (the
   running button is filled accent → returns to outlined accent on stop).
   Color flashes to amber at peak so the "stopping" gesture is visually
   distinguishable from "starting". */
.card .play.stop-pressed {
  animation: play-stop-pulse 320ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
@keyframes play-stop-pulse {
  0%   { transform: scale(1); }
  25%  { transform: scale(0.78); }
  60%  { transform: scale(1.35); background: var(--amber); border-color: var(--amber); }
  100% { transform: scale(1); }
}

/* Reduced-motion respects user preference — all the start/stop celebratory
   feedback collapses to a 1-frame nothing. The state change still happens
   (button toggles, lanes swap), just without the cue animation. */
@media (prefers-reduced-motion: reduce) {
  .card.timer-started,
  .card.timer-stopped,
  .card .play.pressed,
  .card .play.stop-pressed,
  .card .play.glyph-swap .play-glyph {
    animation: none;
  }
  .card .play .play-glyph { transition: none; }
}

.card .thumbs {
  display: flex;
  gap: 4px;
  padding-inline-start: 22px;
  flex-wrap: wrap;
}
.card .thumbs img {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: 2px;
  border: 1px solid var(--line-strong);
  cursor: pointer;
}

/* ===================================================================
   Side drawer — pins to the inline-end edge (right in LTR, left in RTL).
   Used by task detail, time log, settings, reports and insights.
   =================================================================== */

.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  z-index: var(--z-drawer-backdrop);
  animation: palette-bg 120ms ease-out;
}

.drawer {
  position: fixed;
  /* inset-inline-end (not physical `right`) so the drawer pins to the
     trailing edge in both directions — right in LTR, left in RTL. */
  top: 0; inset-inline-end: 0; bottom: 0;
  width: min(560px, 92vw);
  background: var(--bg-raised);
  border-inline-start: 1px solid var(--accent-dim);
  box-shadow: -20px 0 60px rgba(0,0,0,0.5);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
  animation: drawer-in 160ms ease-out;
}
/* RTL: the drawer sits on the left edge, so the depth shadow must cast
   toward content (now to its right) and the slide-in must arrive from
   the left. translateX / box-shadow x-offsets don't auto-mirror. */
[dir="rtl"] .drawer {
  box-shadow: 20px 0 60px rgba(0,0,0,0.5);
  animation-name: drawer-in-rtl;
}
@keyframes drawer-in {
  from { transform: translateX(30px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}
@keyframes drawer-in-rtl {
  from { transform: translateX(-30px); opacity: 0; }
  to   { transform: translateX(0);      opacity: 1; }
}
/* When a drawer is rebuilt while already open (e.g. settings drawer after a
   toggle), the new DOM node would replay drawer-in / palette-bg from scratch,
   producing a visible slide+fade flicker on every interaction. The .no-anim
   marker — set by renderSettingsMount when isRebuild is true — opts out. */
.drawer.no-anim,
.drawer-backdrop.no-anim { animation: none; }

.drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* `env(safe-area-inset-top)` keeps the title + close button below the
     iOS notch / dynamic island when the drawer is full-screen on phones
     (viewport-fit=cover). Desktop / non-notched browsers report the
     env() as 0, so the 12px floor wins and the header is unchanged. */
  padding: max(12px, env(safe-area-inset-top)) 16px 12px;
  border-bottom: 1px solid var(--line);
  gap: 10px;
}
.drawer-header .title-line { color: var(--accent-text); font-weight: 600; letter-spacing: 0.04em; }
.drawer-header .close {
  border: 1px solid var(--line-strong);
  padding: 2px 8px;
  border-radius: 3px;
  cursor: pointer;
  color: var(--ink-dim);
  font-size: 11px;
}
.drawer-header .close:hover { color: var(--accent-text); border-color: var(--accent-dim); }

/* Sticky bottom close bar — phone-only affordance, hidden by default and
   shown via the mobile @media block. The bar element is appended to every
   drawer at runtime by setupMobileDrawerExtras() in app.js. */
.drawer-mobile-close { display: none; }

/* Prominent timer control at the top of the task drawer — always visible,
   no scroll needed. Green outline when idle, filled green when running. */
.drawer-timer-top {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px;
  border: 1px solid var(--accent-dim);
  border-radius: 4px;
  background: transparent;
  color: var(--accent-text);
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background 120ms, color 120ms, border-color 120ms, box-shadow 120ms;
}
.drawer-timer-top:hover { background: var(--accent-glow); }
.drawer-timer-top.running {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow);
}
.drawer-timer-top .tt-icon { font-size: 10px; }
.drawer-timer-top .tt-label { text-transform: uppercase; font-weight: 600; }
.drawer-timer-top .tt-elapsed {
  padding-inline-start: 8px;
  border-inline-start: 1px solid color-mix(in oklab, currentColor 30%, transparent);
  font-variant-numeric: tabular-nums;
}

.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.drawer-field { display: flex; flex-direction: column; gap: 4px; }
.drawer-field label {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.drawer-field input[type="text"],
.drawer-field input[type="datetime-local"],
.drawer-field textarea,
.drawer-field select {
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  padding: 6px 10px;
  border-radius: 3px;
  font: inherit;
  color: var(--ink);
}
.drawer-field textarea {
  min-height: 120px;
  resize: vertical;
  font-family: var(--mono);
}
.drawer-field textarea:focus,
.drawer-field input:focus,
.drawer-field select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

.drawer-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.notes-preview {
  font-size: 12px;
  color: var(--ink-dim);
  padding: 10px 12px;
  background: var(--bg-cell);
  border: 1px dashed var(--line-strong);
  border-radius: 3px;
  white-space: pre-wrap;
  line-height: 1.55;
}
.notes-preview img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 6px 0;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
}
.notes-preview.attachments { padding: 8px 10px; }
.notes-preview.attachments[hidden] { display: none; }
.notes-preview .attachments-header {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 6px;
}
.notes-preview .attachments-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 6px;
}
.notes-preview .attachments-grid a { display: block; line-height: 0; }
.notes-preview .attachments-grid img {
  width: 100%;
  height: 96px;
  object-fit: cover;
  margin: 0;
  cursor: zoom-in;
}

.notes-droptarget textarea.over {
  border-color: var(--accent);
  background: var(--accent-glow);
}

.sessions-list {
  display: none;          /* Collapsed by default; .log-group.expanded reveals */
  flex-direction: column;
  gap: 4px;
  font-variant-numeric: tabular-nums;
  border-block-start: 1px solid var(--line);
}
.log-group.expanded .sessions-list { display: flex; }
.sessions-list .session {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  gap: 12px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  color: var(--ink-dim);
  font-size: 11px;
  align-items: center;
}
.sessions-list .session .t { color: var(--ink); }
.session-edit-btn {
  background: color-mix(in oklab, var(--accent) 6%, transparent);
  color: var(--accent-text);
  border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
  border-radius: 3px;
  padding: 2px 7px;
  font: inherit;
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.9;
  transition: opacity 120ms, color 120ms, border-color 120ms, background 120ms;
}
.session-edit-btn:hover,
.session-edit-btn:focus-visible {
  background: color-mix(in oklab, var(--accent) 18%, transparent);
  border-color: var(--accent);
  opacity: 1;
  outline: none;
}

/* Inline session editor — replaces the row's children when data-mode="edit".
   Lives inside .session (log + reports tab) AND .session-row (sessions
   sheet). The grid layout from the row's parent is bypassed by switching
   .session / .session-row to block when in edit mode; the inline editor
   then arranges itself as a 1-column flex stack. */
.session[data-mode="edit"],
.sessions-sheet .session-row[data-mode="edit"] {
  display: block;
  padding: 10px 8px;
  background: color-mix(in oklab, var(--accent) 6%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
  border-radius: 4px;
  color: var(--ink);
  font-size: 12px;
}
.session-inline-editor {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sie-row {
  display: grid;
  grid-template-columns: 78px auto 1fr;
  align-items: center;
  gap: 8px;
}
.sie-row .sie-label {
  color: var(--ink-dim);
  font-size: 12px;
}
.sie-date {
  background: var(--bg-card, var(--bg));
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 5px 9px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  text-align: start;
  min-width: 84px;
}
.sie-date:hover,
.sie-date:focus-visible {
  border-color: var(--accent);
  outline: none;
}
.sie-time,
.sie-input {
  background: var(--bg-card, var(--bg));
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 5px 8px;
  font: inherit;
  font-size: 12px;
  outline: none;
  min-width: 0;
}
.sie-time:focus,
.sie-input:focus {
  border-color: var(--accent);
}
.sie-row .sie-dur {
  max-width: 90px;
}
.sie-row .sie-unit {
  color: var(--ink-dim);
  font-size: 12px;
}
.sie-err {
  color: var(--red-text);
  font-size: 11px;
  padding-block-start: 2px;
}
.sie-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  margin-block-start: 2px;
}
.sie-btn {
  all: unset;
  cursor: pointer;
  padding: 4px 12px;
  border: 1px solid var(--ink-dim);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  border-radius: 3px;
}
.sie-btn:hover {
  background: var(--ink-faint, rgba(255, 255, 255, 0.06));
}
.sie-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.sie-btn.sie-save {
  border-color: var(--accent);
  color: var(--accent-text);
}
.sie-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.sessions-list .totals {
  display: flex;
  justify-content: space-between;
  padding: 6px 8px;
  color: var(--accent-text);
  font-size: 12px;
}

/* Time log drawer — reuse .drawer with a dedicated body */
.log-grand-total {
  display: flex;
  justify-content: space-between;
  color: var(--accent-text);
  font-size: 13px;
  padding: 4px 0;
  border-block-end: 1px solid var(--line);
  margin-block-end: 8px;
  cursor: help;
}
/* Sub-row shown only when parallel timers overlapped (focused > wall-clock).
   Muted so the wall-clock total above remains the primary number; the row's
   role is to explain why per-task sums below add to more than the total. */
.log-grand-total-focused {
  color: var(--ink-faint);
  font-size: 12px;
  margin-block-start: -4px;
  border-block-end-style: dashed;
}
.log-empty {
  color: var(--ink-faint);
  font-style: italic;
}
.log-group {
  border: 1px solid var(--line);
  border-radius: 3px;
  margin-bottom: 8px;
  overflow: hidden;
}
.log-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 8px 12px;
  background: var(--bg-cell);
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 11px;
  text-align: start;
  border: 0;
  cursor: pointer;
  transition: background 100ms;
}
.log-group-head:hover { background: color-mix(in oklab, var(--accent) 8%, var(--bg-cell)); }
.log-group-head .gname { color: var(--ink); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-group-head .ggroup { color: var(--ink-faint); }
.log-group-head .ghead-end { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.log-group-head .gcount { color: var(--ink-faint); font-size: 10px; }
.log-group-head .gtotal { color: var(--accent-text); font-variant-numeric: tabular-nums; }
.log-group-head .gchev {
  color: var(--ink-faint);
  font-size: 10px;
  display: inline-block;
  transition: transform 120ms;
}
.log-group.expanded .log-group-head .gchev { transform: rotate(90deg); }
[dir="rtl"] .log-group-head .gchev { transform: scaleX(-1); }
[dir="rtl"] .log-group.expanded .log-group-head .gchev { transform: rotate(90deg); }

/* ===================================================================
   Active-now strip — running timers across all tasks
   =================================================================== */

.active-now {
  /* Sticks below the topbar so running timers stay visible while scrolling
     through tasks. Reads --sticky-top-topbar (the topbar's measured
     offsetHeight, written by updateStickyTops()). The previous hardcoded
     `top: 52px` only matched the topbar on a single-row desktop layout —
     when the topbar wraps (mobile, denser locales, more nav items), the
     topbar's real bottom sits 30-60 CSS px below 52, so active-now stuck
     ABOVE the topbar's bottom and left an uncovered band between its own
     stuck bottom and the filter-bar's stuck top (--sticky-top-filter-bar
     is computed from the topbar's measured height, so the chain only
     stays gap-free if active-now follows the same measurement).
     See .claude/rules/10-body-zoom-viewport-units.md (sticky-stack section).
     z-index bumped to 25 (above topbar's 20) so the floating NEW badge
     on the .ac-compact-toggle can paint over the topbar instead of
     being clipped by it — same trick as the kanban-filter-bar uses for
     the focus-mode badge. */
  position: sticky;
  top: var(--sticky-top-topbar, 52px);
  z-index: 25;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--accent-glow) 0%, transparent 100%), var(--bg);
  padding: 12px 20px 14px;
  overflow: hidden;
  transform-origin: top;
}
.active-now.animate-in {
  animation: active-slide-in 500ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

@keyframes active-slide-in {
  /* Was animating max-height + padding to slide the element open. That
     conflicts with .active-now becoming position: sticky — the sticky
     compositor resets the layout-affecting keyframes mid-flight, leaving
     max-height stuck at 0. Animate only compositor-friendly properties
     (opacity + translateY) so layout stays stable while the element
     fades + nudges in. */
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.active-now.animate-in .active-card {
  animation: active-card-in 420ms cubic-bezier(0.25, 0.8, 0.25, 1) 100ms backwards;
}
/* Card slides in from the trailing edge: right-to-left in LTR (positive X
   start, settles to 0), and left-to-right under [dir="rtl"] via the mirrored
   keyframe. translateX doesn't auto-flip with writing direction, so the RTL
   counterpart is required (see .claude/rules/02-i18n-rtl.md). */
@keyframes active-card-in {
  from { opacity: 0; transform: translateX(12px) scale(0.96); }
  to   { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes active-card-in-rtl {
  from { opacity: 0; transform: translateX(-12px) scale(0.96); }
  to   { opacity: 1; transform: translateX(0) scale(1); }
}
[dir="rtl"] .active-now.animate-in .active-card {
  animation-name: active-card-in-rtl;
}
.active-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-bottom: 8px;
}
.active-head .label {
  color: var(--accent-text);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 11px;
}
.active-head .count { color: var(--ink-faint); font-size: 11px; }
/* Dominant session-elapsed clock — sits at the head of the strip and
   reads as the "main" time. Per-task .ac-elapsed numbers below are
   intentionally demoted (smaller, dim color) so the eye lands on this
   clock first. Live-ticked in place via [data-active-session-clock]. */
.active-head .active-session-clock {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  font-size: 22px;
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-shadow: 0 0 10px var(--accent-glow);
  margin-inline-end: 4px;
}
.active-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(220px, 280px);
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 4px;
}
.active-card {
  background: var(--bg-card);
  border: 1px solid var(--accent-dim);
  border-inline-start: 2px solid var(--accent);
  border-radius: var(--radius);
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  cursor: pointer;
  min-width: 0;
}
.active-card:hover { background: var(--bg-card-hov); border-color: var(--accent); }
/* When the task's group has an assigned color, mirror the kanban accent on
   the active-now card: 2px start rail + faint background tint + matching
   border. The strip lives outside .project-row so --project-color is set inline
   on the card itself (see renderActiveNow). */
.active-card.has-color {
  border-inline-start-color: var(--project-color);
  border-color: color-mix(in oklab, var(--project-color) 45%, var(--line-strong));
  background: color-mix(in oklab, var(--project-color) 6%, var(--bg-card));
}
.active-card.has-color:hover {
  border-color: var(--project-color);
  background: color-mix(in oklab, var(--project-color) 10%, var(--bg-card-hov));
}
.active-card.has-color .ac-grp { color: var(--project-color); }
/* Tracked meeting card — blue identity, mirroring the gcal today-card so
   the two surfaces read as the same thing. */
.active-card-event {
  border-color: color-mix(in oklab, var(--blue, #7ec7ff) 45%, var(--line-strong));
  border-inline-start-color: var(--blue, #7ec7ff);
  background: color-mix(in oklab, var(--blue, #7ec7ff) 6%, var(--bg-card));
}
.active-card-event:hover {
  border-color: var(--blue, #7ec7ff);
  background: color-mix(in oklab, var(--blue, #7ec7ff) 10%, var(--bg-card-hov));
}
.active-card-event .ac-grp { color: var(--blue, #7ec7ff); }
.ac-top { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
.ac-grp {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  flex-shrink: 0;
}
.ac-title {
  color: var(--ink);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}
.ac-bottom { display: flex; align-items: center; gap: 6px; }
.ac-bottom .ac-elapsed { margin-inline-end: auto; }
.ac-elapsed {
  /* Demoted from a bold accent number to a quiet dim readout. The
     dominant clock now lives in .active-head .active-session-clock — the
     per-task elapsed here is supporting detail, not the main signal. */
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  font-weight: 400;
  opacity: 0.85;
}
.ac-stop, .ac-done {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 2px 8px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.06em;
}
.ac-stop:hover { color: var(--red-text); border-color: var(--red); }
.ac-done:hover { color: var(--accent-text); border-color: var(--accent); }
.ac-end-session {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 2px 10px;
  border-radius: 6px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.03em;
}
.ac-end-session:hover { color: var(--red-text); border-color: var(--red); }

/* Overdue chip on the active-now card. Lives in the top row alongside the
   group + title so the urgency is visible at a glance, even before the user
   reads the task name. */
.ac-chip {
  font-size: 10px;
  padding: 1px 6px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  white-space: nowrap;
  margin-inline-start: auto;
  letter-spacing: 0.04em;
}
.ac-chip.overdue {
  color: var(--red-text);
  border-color: var(--red);
  background: color-mix(in oklab, var(--red) 10%, transparent);
}

/* ---- Today cards (horizontal row below the compact strip) ---- */
.today-cards {
  padding: 8px 20px 4px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.today-cards[hidden] { display: none; }
/* Agenda nav lives at the top of the today-cal-strip and rebinds both the
   bars and the cards row at once. Buttons cluster around the label (no
   flex: 1 spreader) so the "today" chip sits adjacent to ›, not flung to
   the far edge of the row. */
.today-strip-agenda-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding-block-end: 4px;
  font-size: 11px;
  color: var(--ink-faint);
}
.today-strip-agenda-nav .agenda-nav-label {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  min-width: 80px;
  text-align: center;
}
.today-strip-agenda-nav .agenda-nav-btn,
.today-strip-agenda-nav .agenda-nav-today {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 1px 8px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  line-height: 1.2;
}
.today-strip-agenda-nav .agenda-nav-btn:hover { border-color: var(--accent-dim); color: var(--accent-text); }
.today-strip-agenda-nav .agenda-nav-today { color: var(--accent-text); border-color: var(--accent-dim); }
.today-strip-agenda-nav .agenda-nav-today:hover { background: var(--accent-glow); }
/* On the "Today" day, the button is reserved (visibility:hidden) instead of
   removed — that keeps the centered flex row the same width so ‹ and › stay
   in fixed screen positions across day changes (click-spam navigation). */
.today-strip-agenda-nav .agenda-nav-today.is-hidden {
  visibility: hidden;
  pointer-events: none;
}
/* Chevrons reverse in RTL so "previous day" still points back. */
[dir="rtl"] .today-strip-agenda-nav .agenda-nav-btn { transform: scaleX(-1); }
.today-cards-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(180px, 240px);
  gap: 8px;
  overflow-x: auto;
  overflow-y: hidden;
  padding-block-end: 4px;
  /* Right-edge fade hints at horizontal overflow. Uses mask-image so the
     gradient is invisible when there's no overflow (scroll 0). */
  mask-image: linear-gradient(to right, black 0, black calc(100% - 40px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 40px), transparent 100%);
}
[dir="rtl"] .today-cards-row {
  mask-image: linear-gradient(to left, black 0, black calc(100% - 40px), transparent 100%);
  -webkit-mask-image: linear-gradient(to left, black 0, black calc(100% - 40px), transparent 100%);
}
/* Enter animation for freshly-added today-cards. Tagged by renderTodayCards
   only on cards whose id wasn't in the previous render, so it fires once
   per added item — not on every rerender. The card slides in from the
   inline-end edge: right→left in LTR, left→right in RTL. translateX is
   physical, so we explicitly mirror the sign under [dir="rtl"]. */
/* Card intro slide-in dropped per user request — cards now appear instantly
   on render. The .today-card--enter class is still added by the agenda render
   for backwards-compat (in case any per-card hook reads it), but is a no-op. */
.today-card--enter { animation: none; }
.today-cards-empty {
  color: var(--ink-faint);
  font-size: 11px;
  font-style: italic;
  padding: 12px 0;
}
.today-card {
  --card-accent: var(--accent);
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-inline-start: 3px solid var(--card-accent);
  border-radius: var(--radius);
  padding: 6px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  cursor: pointer;
  min-width: 0;
  transition: background 120ms, border-color 120ms, transform 120ms;
}
.today-card:hover {
  background: var(--bg-card-hov);
  border-color: var(--card-accent);
  transform: translateY(-1px);
}
.today-card.past { opacity: 0.5; }
.today-card-task { --card-accent: var(--accent); }
.today-card-gcal { --card-accent: var(--blue, #7ec7ff); }
.today-card-due { --card-accent: var(--amber); }
/* A meeting whose timer is running — steady (no pulse: calm by design,
   a meaningful share of users have ADHD) blue emphasis on the card. */
.today-card-gcal.tracking {
  border-color: var(--blue, #7ec7ff);
  background: color-mix(in oklab, var(--blue, #7ec7ff) 8%, var(--bg-card));
}
.today-card-label {
  color: var(--ink-faint);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.today-card-time {
  color: var(--ink-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* When the time slot is a reschedule trigger (task and due-only cards),
   render it as a discreet button so the click affordance is visible without
   shouting. Hover lifts it to the accent color. */
button.today-card-time.sched-pill {
  display: block;
  width: 100%;
  background: transparent;
  border: 1px dashed transparent;
  border-radius: 3px;
  padding: 1px 4px;
  margin-inline-start: -4px;
  font: inherit;
  color: var(--ink-dim);
  text-align: start;
  cursor: pointer;
}
button.today-card-time.sched-pill:hover {
  border-color: var(--accent-dim);
  color: var(--accent-text);
}
.today-card-title-row {
  display: flex;
  align-items: flex-start;
  gap: 6px;
}
.today-card-title-row .today-card-title { flex: 1; min-width: 0; }
.today-card-title {
  color: var(--ink);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-word;
  /* Reserve two lines of vertical space even when the title is short, so
     every card has the same content height and the meta row below lines up
     across the agenda. With the line-clamp above, longer titles still
     truncate at exactly 2 lines, so the box never grows past this. */
  min-height: calc(1.3em * 2);
}
.today-card-title-row {
  /* The title row stretches to fill the card so the meta row gets pushed
     to the bottom by `margin-block-start: auto` below. Combined with
     grid stretch on the cards row, every card's footer aligns horizontally
     regardless of how many chips it carries. */
  flex: 1 1 auto;
}
.today-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  /* `auto` (not 2px) so the meta hugs the bottom of the card. With
     `.today-card { display: flex; flex-direction: column }` and the
     title-row stretching, this is what makes the play buttons + chips
     align across cards even when titles wrap differently. */
  margin-block-start: auto;
  min-height: 0;
}
.today-card-chip {
  font-size: 10px;
  color: var(--ink-faint);
  padding: 1px 5px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  white-space: nowrap;
}
.today-card-chip.running { color: var(--accent-text); border-color: var(--accent-dim); }
.today-card-chip.blocked { color: var(--red-text); border-color: var(--red); }
.today-card-chip.overdue {
  color: var(--red-text);
  border-color: var(--red);
  background: color-mix(in oklab, var(--red) 10%, transparent);
}
.today-card-chip.loc,
.today-card-chip.atn,
.today-card-chip.tracked { color: var(--ink-dim); }

/* Inline "+ Plan" chip on gcal today-cards — spawns a prep task tied to the
   meeting without leaving the agenda. Same visual idiom as .today-card-chip
   but uses the gcal blue accent so it reads as actionable, not a passive label. */
.today-card-prep {
  font-family: inherit;
  font-size: 10px;
  line-height: 1;
  padding: 1px 6px;
  border: 1px solid color-mix(in oklab, var(--blue, #7ec7ff) 50%, var(--line-strong));
  background: color-mix(in oklab, var(--blue, #7ec7ff) 12%, transparent);
  color: var(--ink);
  border-radius: 2px;
  cursor: pointer;
  margin-inline-start: auto;   /* push to end of meta row */
  white-space: nowrap;
  transition: background 100ms, border-color 100ms;
}
.today-card-prep:hover {
  background: color-mix(in oklab, var(--blue, #7ec7ff) 28%, transparent);
  border-color: var(--blue, #7ec7ff);
}
/* Today-card actions row — mirrors the .active-card pattern: text-labeled
   buttons sitting side-by-side at the bottom of the card. The user can
   stop a running timer or mark done from the agenda without opening the
   drawer. */
/* Title row carries the kanban-card affordances inline: a checkbox toggles
   done, a small `.play` icon-only span toggles the timer. Reuses the
   kanban-card `.checkbox` and `.play` styling so the two surfaces look
   identical and the user's mental model stays consistent. */
.today-card-title-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.today-card-title-row .today-card-title { flex: 1; min-width: 0; }
.today-card.done .today-card-title { text-decoration: line-through; opacity: 0.7; }

/* Toggle chevron on the today-strip 'due' label cell. Replaces the text-only
   label with a clickable button showing the chevron + label. */
.today-strip-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 10px;
  transition: color 120ms;
}
.today-strip-toggle:hover { color: var(--accent-text); }
.today-strip-toggle.on { color: var(--accent-text); }
.today-strip-toggle .chev { font-size: 9px; }
[dir="rtl"] .today-strip-toggle .chev { transform: none; /* ▸/▾ are directionless enough */ }

/* ===================================================================
   See-more button (Today strip)
   =================================================================== */

.see-more {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 2px 10px;
  border-radius: 999px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  margin-inline-start: auto;
}
.see-more:hover { color: var(--amber-text); border-color: var(--amber); }

/* ===================================================================
   MOBILE (phones only — tablets get the desktop layout).
   Breakpoint: 600px. Tablets >= 600px keep desktop mode.

   Strategy: shrink desktop, stack columns, hide / simplify controls
   that require hover or fine-grained mouse input. Heavy interactions
   (drag-schedule, drag-between-columns) are guarded in JS via
   matchMedia.
   =================================================================== */
@media (max-width: 600px) {
  /* ---------- Topbar ----------
     The desktop topbar is now a single-row flex layout. On mobile we
     let it wrap to a second row (search bar full-width above buttons)
     and drop the brand to save space. */
  /* Mobile topbar: a single row of [brand] [search-icon] [view-toggle] [⋯].
     Filter input is collapsed by default, expands when the user taps the
     search icon (the topbar gets `is-search-open` and the input replaces
     the row). Brand is shown but compact — drops the `$` and blinking
     cursor; just the `~/todo` mark. */
  .topbar {
    flex-wrap: nowrap;
    gap: 8px;
    /* env(safe-area-inset-top) covers the notch / dynamic island so the
       topbar's --bg-raised fill paints behind iOS Safari's top chrome
       (also acts as the scrolling header backdrop). */
    padding: max(8px, env(safe-area-inset-top)) 10px 8px;
    align-items: center;
  }
  .topbar-actions {
    flex-wrap: nowrap;
    row-gap: 0;
    column-gap: 6px;
    margin-inline-start: auto;
    width: auto;
    flex: 0 0 auto;
  }
  .topbar-actions .icon-btn {
    padding: 6px 10px;
    font-size: 13px;
    min-height: 36px;
    min-width: 36px;
    /* Same iOS-safe tap hardening as .kanban-add-fab — without these
       the first tap on the view-toggle / overflow button would fire a
       focus/select gesture and the actual click only registered on the
       second tap. */
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  /* Mobile topbar icons run a touch larger than the 15px desktop size
     so they stay legible in the 36px tap targets. */
  .topbar .tb-glyph svg { width: 18px; height: 18px; }
  /* Brand: compact `~/todo` only — drops `$` glyph and the blinking
     cursor on mobile. */
  .brand {
    display: inline-flex;
    flex: 0 0 auto;
    font-size: 12px;
  }
  .brand .name { display: none; }
  .brand .cursor { display: none; }
  /* Filter input collapses to a single-icon button on mobile. The actual
     `<input>` and the `/` glyph are hidden until the topbar gains the
     `is-search-open` class. The new search-icon button sits in their
     place — tapping it sets the open state in JS. */
  .topbar .filter-wrap {
    max-width: none;
    width: auto;
    min-width: 0;
    flex: 0 0 auto;
    padding: 0;
    border: 0;
    background: transparent;
  }
  .topbar .filter-wrap .slash,
  .topbar .filter-wrap input,
  .topbar .filter-wrap .hint { display: none; }
  .topbar .filter-wrap .filter-hidden-chip { display: none; }
  .topbar #btn-search-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    min-height: 36px;
    padding: 6px;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 6px;
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
  }
  /* When search opens, swap: hide brand + search-icon + view-toggle,
     show the full-width input and a close button. */
  .topbar.is-search-open .brand,
  .topbar.is-search-open #btn-search-toggle,
  .topbar.is-search-open #btn-view-toggle,
  .topbar.is-search-open #btn-tb-overflow { display: none; }
  .topbar.is-search-open .filter-wrap {
    flex: 1 1 auto;
    width: 100%;
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 6px 10px;
    background: var(--bg-raised);
  }
  .topbar.is-search-open .filter-wrap .slash,
  .topbar.is-search-open .filter-wrap input { display: inline-flex; }
  .topbar.is-search-open .filter-wrap input { display: inline; flex: 1; }
  .topbar.is-search-open #btn-search-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    min-height: 36px;
    padding: 6px;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 6px;
    color: var(--ink-faint);
    font: inherit;
    cursor: pointer;
  }
  /* The `⋯` overflow trigger — square 36×36, outlined so it reads as a
     menu affordance, with a chunky SVG glyph matching native system bars. */
  .topbar #btn-tb-overflow {
    width: 36px;
    height: 36px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-raised);
    border: 1px solid var(--accent-dim);
    color: var(--accent-text);
  }
  .topbar #btn-tb-overflow .tb-glyph svg { width: 22px; height: 22px; }
  /* View-toggle: keep the label visible on mobile ("calendar" / "board")
     — the icon alone is ambiguous for this primary nav control. */
  .topbar #btn-view-toggle {
    padding: 6px 10px;
    font-size: 13px;
    white-space: nowrap;
  }

  /* ---------- Day overview ----------
     Mobile drops the timeline "stripes" (due / scheduled / now-line rows)
     in favor of a clean vertical agenda card list. The strip header (date
     nav + carry-over + gcal toggles) stays so the user can switch days,
     toggle overdue carry-over, and toggle gcal events. */
  .today-cal-strip {
    overflow-x: visible;
    padding: 8px 12px 6px;
  }
  .today-strip-row,
  .today-strip-now,
  .today-strip-drag-hint,
  .today-strip-drag-hint-spacer,
  .today-strip-hour-axis,
  #today-strip-hours-btn { display: none !important; }
  /* Stack header rows on mobile so the day-nav can span full width.
     Otherwise the desktop 1fr/auto/1fr grid puts the nav in a center
     slot whose width depends on neighbouring drag-hint + badges, and
     "Today" ends up offset by a few px. Full-width row + absolute
     chevrons = mathematically centered label. */
  .today-strip-header {
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: stretch;
  }
  .today-strip-header .today-strip-agenda-nav { align-self: stretch; }
  .today-strip-header .today-strip-badges {
    align-self: stretch;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
  }
  /* Center the "Today" label between the chevrons. The agenda-nav row is
     a flex container; absolute-position the prev/next buttons at the
     inline edges so the label centers via justify-content: center.
     Logical properties keep RTL correct. */
  .today-strip-agenda-nav {
    position: relative;
    justify-content: center;
    /* Reserve room for the absolutely-positioned chevrons (≈44px each)
       so the centered label can't slide underneath them. The label
       truncates with ellipsis if the day text gets unusually long
       (long weekday names in some locales). */
    padding-inline: 48px;
    min-height: 36px;
  }
  .today-strip-agenda-nav .agenda-nav-btn[data-act="prev"] {
    position: absolute; inset-inline-start: 0; top: 50%; transform: translateY(-50%);
  }
  .today-strip-agenda-nav .agenda-nav-btn[data-act="next"] {
    position: absolute; inset-inline-end: 0; top: 50%; transform: translateY(-50%);
  }
  [dir="rtl"] .today-strip-agenda-nav .agenda-nav-btn[data-act="prev"],
  [dir="rtl"] .today-strip-agenda-nav .agenda-nav-btn[data-act="next"] {
    transform: scaleX(-1) translateY(-50%);
  }
  .today-strip-agenda-nav .agenda-nav-today { display: none; }
  /* Bump the centered "Today" label a notch on mobile — it's the day-overview
     anchor and benefits from being the visual hero of the strip header. */
  .today-strip-agenda-nav .agenda-nav-label {
    font-size: 16px;
    font-weight: 600;
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .today-cards-row {
    display: flex;
    flex-direction: column;
    grid-auto-flow: unset;
    grid-auto-columns: unset;
    gap: 8px;
    overflow-x: hidden;
    mask-image: none;
    -webkit-mask-image: none;
  }
  .today-card { width: 100%; min-width: 0; }
  .today-card.today-card-after-carryover {
    border-block-start: 1px dashed var(--line-strong);
    padding-block-start: 10px;
    margin-block-start: 6px;
    border-inline-start-color: var(--card-accent);
  }

  /* ---------- Board ---------- */
  /* The bottom padding only needs to clear the fixed statusbar — body
     already reserves `--statusbar-h + safe-area-inset-bottom` via its
     padding-block-end, so .board's own bottom padding is just visual
     breathing room below the last card. The previous 100px was a
     placeholder for a .mobile-fab that the app no longer renders, and
     it produced a ~96px black gap between the last card and the
     statusbar (visible on the kanban tail). 16px reads as "end of
     content" without orphan dead space. */
  .board { padding: 12px 10px 16px; overflow-x: visible; }
  /* Mobile topbar wraps and gets tall; sticky-with-fixed-offset would hide
     these elements behind it. Stack normally instead. */
  .active-now,
  .kanban-filter-bar {
    position: static;
    top: auto;
  }
  .col-headers {
    /* Mobile: keep the status summary on ONE row (QUEUED / IN PROGRESS /
       DONE side-by-side). Inherits desktop's `grid-template-columns:
       1fr 1fr 1fr`; hide-done collapses it to `1fr 1fr` via the
       desktop rule at .board.hide-done .col-headers. The headers still
       don't map 1-to-1 to mobile cells (cells stack vertically below),
       but as a top-of-board count strip the row is more useful than a
       vertical list of two labels. */
    position: static;
    top: auto;
    gap: 6px;
  }
  .col-headers .col-header {
    /* Tight padding + smaller caps so 3 columns fit a 320px viewport
       without wrapping "IN PROGRESS" onto two lines. */
    padding: 6px 8px;
    font-size: 10px;
    gap: 6px;
    min-width: 0;
  }
  .col-headers .col-header > span:not(.dot):not(.n) {
    /* Truncate the label rather than wrap when the column is narrow. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }
  .project-row > .cells-row,
  .board.hide-done .project-row > .cells-row {
    grid-template-columns: 1fr;
    /* Mirror the desktop `layout-single` ordering so on mobile the
       in-progress cell sits ABOVE queued (and done at the bottom). The
       cells are flex items via the grid's auto-flow, so `order` is
       honored. Without this, mobile gets the DOM order
       (queued → in-progress → done), which buries the running tasks
       below the queue. */
    display: flex;
    flex-direction: column;
    /* Override the desktop `align-items: start` (intended for grid
       block-axis alignment). Once the row becomes a flex column,
       `align-items` controls the CROSS axis (width), and `start`
       collapses each cell to its content width — done cells with
       short titles ended up half-width on the inline-start side
       while the in-progress cell stayed full-width because its
       cards pushed the cell's intrinsic width to viewport. Stretch
       restores full-width cells regardless of card length. */
    align-items: stretch;
  }
  .project-row > .cells-row > .cell[data-status="in-progress"] { order: 1; }
  .project-row > .cells-row > .cell[data-status="queued"]      { order: 2; }
  .project-row > .cells-row > .cell[data-status="done"]        { order: 3; }

  /* Each group becomes a vertical stack of status sections; a card stays
     full-width within its cell. Group label sits on top with its stats. */
  .project-row {
    padding: 14px 0 16px 0;
    /* Project-color accent on the inline-END edge — mirrors desktop
       (board.css) so the stripe is on the right in LTR everywhere. */
    border-inline-end: 3px solid var(--project-color, var(--line));
    padding-inline-end: 10px;
  }
  .project-row > .cells-row { gap: 6px; }
  /* Pin the group header below the cumulative mobile sticky stack
     (topbar + filter-bar). --sticky-top-col-headers is set by
     updateStickyTops() to the sum of those heights — on mobile the topbar
     wraps to ~135px and the filter chips wrap to ~195px, so a hardcoded
     offset would drift. col-headers is static on mobile (see rule above),
     so we don't need the +28 col-header allowance the desktop rule has.
     z-index 4 keeps the header above cards (z 1) and below the filter-bar
     (z 10) so labels can scroll under filter-bar without occluding it. */
  .project-label {
    position: sticky;
    top: var(--sticky-top-col-headers, 140px);
    z-index: 4;
    padding: 6px 0;
    /* Explicit opaque fill so cards scrolling under the pinned label
       don't show through. The desktop rule sets `background: var(--bg)`
       and it cascades, but mobile widens the touch target with extra
       padding — making any seam more visible. has-color groups still
       layer the colored gradient on top via the rule above. */
    background: var(--bg);
  }
  .project-label .bar { height: 4px; }

  /* Each cell grows to fit its cards; add a small section header per
     status so stacking is parseable. */
  .cell {
    min-height: 0;
    padding: 2px 0;
    gap: 6px;
  }
  .cell::before {
    content: attr(data-status-label);
    display: block;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink-faint);
    padding: 4px 4px 2px;
  }
  .cell:empty::after {
    content: "—";
    color: var(--ink-faint);
    opacity: 0.5;
    font-size: 10px;
    padding: 2px 4px;
  }
  .cell:empty { outline: none; }

  /* Slightly smaller body font on mobile so cards/labels don't dominate
     the viewport. The desktop default is 14-15px; on mobile a 13px base
     reads more like a native task app and lets two short cards fit
     where one used to. Inputs stay >= 16px to prevent iOS zoom-on-focus
     (rule below in the polish block). */
  body { font-size: 13px; }
  /* Mobile card type scale — title 15 / desc + placeholder 13. Previous
     values (title 13 / desc 12 / desc-edit 14) inverted the hierarchy
     because notes-edit was bigger than the title. Numbers match what
     iOS Reminders uses for the same row geometry. */
  .card .title { font-size: 15px; }
  .card .meta,
  .card .card-due-pill,
  .card .pill,
  .card .chip { font-size: 11px; }
  .project-label .gname { font-size: 14px; }
  .col-headers .col-header { font-size: 11px; }
  .kanban-filter-bar .kfb-chip { font-size: 12px; }
  .kanban-filter-bar .kfb-chip-count { font-size: 11px; }

  /* Tap targets: min 44px height on primary interactive elements. */
  .card {
    padding: 10px 12px;
    min-height: 48px;
    transform: none !important; /* kill the -1px hover lift on touch */
  }
  .card:hover { transform: none; box-shadow: none; }
  /* On touch there's no hover and tap-to-focus is disabled (see app.js
     click handler — focusedTaskId is gated on !isMobile()), so the empty
     pill placeholders ("+ schedule", "+ est", "+ due", "+ block") and
     the color swatch must be visible at rest. Without this they stay
     hidden forever on mobile and the user can't reach those popovers
     unless they long-press → action menu. */
  .card .pill.schedule-pill.empty,
  .card .pill.due-pill.empty,
  .card .pill.est-pill.empty,
  .card .pill.block-chip.empty,
  .card .card-color-sq.no-color {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transition: none;
  }
  /* Same for inline card-actions on mobile — show always. */
  .card .card-actions { display: flex; }
  /* Drop the focused-card ring on mobile entirely — tap no longer marks
     a card as "selected" (no .focused class is added on touch), so the
     accent border + outer glow have no source. Keep the rule as a
     guard against any latent .focused that might survive a re-render. */
  .card.focused {
    border-color: var(--line);
    box-shadow: none;
  }
  .card .checkbox,
  .today-card .checkbox { width: 20px; height: 20px; }
  /* Expand the checkbox touch target to ~44px tall without overlapping the
     adjacent title. The ::before grows upward, downward, and toward
     inline-start (the card edge); inline-end stays flush so a finger
     reaching for the title never fires "mark done" instead. */
  .card .checkbox::before,
  .today-card .checkbox::before {
    content: '';
    position: absolute;
    inset-block: -12px;
    inset-inline-start: -12px;
    inset-inline-end: 0;
  }
  /* Mobile play/stop: enlarge from 22px → 36px so a thumb tap doesn't fall
     between checkbox and ⋮. The button now lives at the inline-end of the
     card top row (DOM order in app.js), giving it the prominent corner
     instead of sharing the inline-start with the checkbox. Today-cards
     adopt the same affordance so the agenda + kanban share one
     vocabulary on mobile. */
  .card .play,
  .today-card .play {
    width: 36px; height: 36px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 14px;
    margin-top: 0;
  }
  .card .play .play-glyph svg,
  .today-card .play .play-glyph svg { width: 16px; height: 16px; }
  .card .card-color-sq { width: 16px; height: 16px; opacity: 0.6 !important; }

  /* ---------- Calendar ---------- */
  /* Desktop's `main.calendar { position: sticky; top: 0; overflow:
     hidden; max-height: calc(100vh - …); }` (line ~7175) keeps the
     unscheduled rail glued to the viewport while the page scrolls.
     Mobile has no rail (hidden below) and the calendar IS the page,
     so the sticky+clip combo just collapses the grid into a 0-height
     strip behind the topbar — the user sees an empty pink area where
     hour markers, day column, and now-line should be. Drop the
     sticky positioning + overflow clip on mobile so the grid lays
     out in normal flow and fills the remaining vertical space below
     the chrome. */
  main.calendar {
    max-height: none;
    position: static;
    overflow: visible;
  }
  /* Mobile: cal-grid flows with the page instead of being its own scroll
     container. Without this the user has TWO scroll layers (page +
     cal-grid) competing — and cal-grid's overflow:auto clips its content
     to its own height, so only ~¼ of the last hour fit in the visible
     window before the user had to chain another scroll inside it.
     overflow:visible lets the entire 24h grid lay out as normal page
     content; the user just scrolls the page to see all hours. */
  .cal-grid {
    overflow: visible;
    padding-block-end: 0;
    overscroll-behavior: auto;
  }
  /* Sticky day-headers + all-day row + day-strip stick to the PAGE'S
     scroll on mobile (cal-grid is no longer a scroll container, so
     sticky-inside cascades to the next ancestor that is — html). Anchor
     below the topbar (--sticky-top-topbar is set by updateStickyTops to
     the topbar's measured offsetHeight) so they don't slide under the
     topbar when the user scrolls.

     Day mode renders no .cal-day-headers row, but it DOES render
     .cal-week-strip (the horizontal day-pill row). Make it sticky to
     the topbar bottom too. */
  .cal-day-headers { top: var(--sticky-top-topbar, 52px); }
  .cal-all-day { top: var(--sticky-top-topbar, 52px); }
  .cal-day-headers + .cal-all-day {
    top: calc(var(--sticky-top-topbar, 52px) + 44px);
  }
  .cal-week-strip {
    position: sticky;
    top: var(--sticky-top-topbar, 52px);
    z-index: 3;
  }
  /* When the day-strip is present (day mode), the all-day row sticks
     below it. cal-week-strip rendered height is ~70px on mobile. */
  .cal-week-strip + .cal-grid .cal-all-day {
    top: calc(var(--sticky-top-topbar, 52px) + 70px);
  }
  /* Mobile cal-header — replaced by a streamlined two-cluster layout:
     row 1 = "May 2026" + ‹ › chevrons; row 2 = day/week toggle, GCal,
     ⋯ menu. Hour-range selects are hidden on mobile (they live in the
     ⋯ popover via openCalendarConfigPopover). The desktop .cal-header-*
     classes still work but the mobile-specific .cal-header-mobile-*
     blocks below override layout. */
  .cal-header.cal-header-mobile {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    padding: 10px 12px 8px;
    gap: 8px;
    row-gap: 8px;
    border-bottom: none;            /* the strip below has its own border */
  }
  .cal-header-mobile-title {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    width: 100%;
  }
  .cal-header-mobile-title .cal-title {
    font-size: 16px;
    font-weight: 600;
    margin-inline: 8px;
    color: var(--ink);
    letter-spacing: 0.01em;
  }
  .cal-header-mobile-title .cal-nav {
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    line-height: 1;
    border: none;
    background: transparent;
    color: var(--ink-dim);
    border-radius: 50%;
  }
  .cal-header-mobile-title .cal-nav:hover { color: var(--accent-text); background: var(--bg-raised); }
  .cal-header-mobile-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
  }
  .cal-header-mobile-actions .cal-mode-btn { padding: 5px 12px; }
  .cal-header-mobile-actions .cal-config-btn {
    width: 32px;
    padding: 5px 0;
    font-size: 16px;
    line-height: 1;
  }
    .cal-grid { max-height: none; }

  /* Chrome-collapse — once the user scrolls into the calendar grid, the
     upper chrome (topbar + cal-header) shrinks so the day claims more of
     the viewport. Re-expands on scroll-back-to-top. Navigation stays
     visible — only secondary rows + non-essential bars are hidden. */
  .topbar,
  main.calendar .cal-header,
  main.calendar .cal-header-actions {
    transition: padding 160ms ease-out, max-height 160ms ease-out, opacity 160ms ease-out;
  }
  body.chrome-collapsed .topbar {
    padding-block: 2px;
  }
  body.chrome-collapsed .topbar .brand .prompt,
  body.chrome-collapsed .topbar .brand .cursor { display: none; }
  body.chrome-collapsed .topbar .icon-btn { padding-block: 4px; font-size: 11px; }
  body.chrome-collapsed .quote-bar,
  body.chrome-collapsed .install-nudge { display: none; }
  body.chrome-collapsed main.calendar .cal-header-actions {
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    margin: 0;
  }
  body.chrome-collapsed main.calendar .cal-header {
    padding-block: 2px;
    row-gap: 0;
  }
  body.chrome-collapsed main.calendar .cal-header .cal-title { font-size: 12px; }
  .cal-day-headers { grid-template-columns: 40px 1fr; }
  .cal-all-day { grid-template-columns: 40px 1fr; }
  .cal-body { grid-template-columns: 40px 1fr; }
  .cal-hour-label { font-size: 9px; padding-block-start: 2px; padding-inline-end: 4px; }

  /* Mobile week-mode: 7 narrow columns that FIT in viewport without
     horizontal scroll — earlier version used minmax(72px, 1fr) +
     overflow-x: auto on cal-grid, but that made cal-grid a scroll
     container and broke sticky-to-page for cal-day-headers /
     cal-all-day (sticky-inside cascades to nearest scroll container,
     which became cal-grid instead of html). Dropping the min-width
     lets columns shrink to ~47px each on a 375px viewport (40px hour-
     col + 7 × 47px ≈ 369px), which is tight but readable for the
     short weekday abbreviation + day number. cal-grid stays
     overflow:visible (per the rule above), so sticky-inside cascades
     all the way up to html and headers/all-day pin below the topbar
     like in 3day mode. */
  main.calendar.week.mobile .cal-day-headers,
  main.calendar.week.mobile .cal-all-day,
  main.calendar.week.mobile .cal-body {
    grid-template-columns: 40px repeat(7, 1fr);
  }

  /* Mobile 3-day mode: 3 equal columns fit a 360px screen comfortably
     (~107px each after the hour col). No horizontal scroll needed —
     unlike week mode, all 3 columns stay readable at full width. */
  main.calendar.threeday.mobile .cal-day-headers,
  main.calendar.threeday.mobile .cal-all-day,
  main.calendar.threeday.mobile .cal-body {
    grid-template-columns: 40px repeat(3, 1fr);
  }

  /* Mobile: cal-layout becomes single-column (rail isn't rendered inline
     on mobile anymore — it lives in a bottom sheet, see below). The
     grid takes the full width. */
  .cal-layout { grid-template-columns: 1fr; }
  /* Hide the inline rail on mobile entirely. The bottom-sheet wrapper
     mounts the same rail as a modal when the user taps the FAB chip. */
  main.calendar.mobile .cal-layout > .cal-rail { display: none; }

  /* Floating "Unscheduled (N)" chip — the entry point to the bottom
     sheet. Sits centered above the statusbar so it's reachable without
     scrolling, regardless of where the user is in the grid. */
  .cal-rail-fab {
    position: fixed;
    /* Center via auto-margin instead of `left:50% + translateX(-50%)`.
       The FAB's innerHTML is rewritten on every render() (count + label
       update), which makes its measured width fluctuate briefly. The
       transform-based centering depended on that measured width, so on
       Chromium the chip would land slightly off-center after a render
       tick. inset-inline:0 + margin:auto + width:max-content resolves
       to a stable center regardless of when width is read. */
    inset-inline: 0;
    margin-inline: auto;
    width: max-content;
    max-width: calc(100% - 32px);
    bottom: calc(var(--statusbar-h, 34px) + 14px + env(safe-area-inset-bottom));
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 18px;
    background: var(--bg-raised);
    border: 1px solid var(--accent-dim);
    color: var(--accent-text);
    border-radius: 999px;
    font: inherit;
    font-size: 12px;
    box-shadow: 0 6px 16px rgba(0,0,0,0.45);
    cursor: pointer;
    z-index: 28;
    font-variant-numeric: tabular-nums;
  }
  .cal-rail-fab .cal-rail-fab-icon { font-size: 14px; }
  .cal-rail-fab .cal-rail-fab-count {
    background: var(--accent);
    color: var(--bg);
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
  }
  /* Hide the FAB on desktop and on calendar views without it. */
  @media (min-width: 601px) { .cal-rail-fab { display: none; } }

  /* Mobile-only kanban "+" FAB. Bottom-end (bottom-right LTR /
     bottom-left RTL) above the statusbar — the primary entry point for
     task creation since the per-project inline `+ new task` rows are
     hidden on phones (see .new-task hide rule below). Cloned visual
     vocabulary from .cal-rail-fab so the two floating affordances feel
     consistent. */
  .kanban-add-fab {
    position: fixed;
    inset-inline-end: 16px;
    bottom: calc(var(--statusbar-h, 34px) + 14px + env(safe-area-inset-bottom));
    width: 52px; height: 52px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    border-radius: 999px;
    font-size: 28px;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 6px 16px rgba(0,0,0,0.45);
    z-index: 28;
    /* Tap-target hardening — kills the iOS double-tap zoom + selection
       gesture that fired on the first tap before the click registered. */
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
  }
  .kanban-add-fab:active { transform: scale(0.96); }
  .kanban-add-fab-glyph { pointer-events: none; }

  /* Hide the per-project "+ new task" inline rows on mobile — the FAB
     handles task creation. Settings drawer / palette / keyboard shortcut
     paths still work for power users. */
  .new-task { display: none; }
}

/* Hard hide on desktop — JS gates renderKanbanAddFab() behind isMobile(),
   but if the viewport crosses the boundary while a mobile-mounted FAB is
   still in the DOM (PWA fullscreen toggle, dev-tools resize, browser
   fullscreen entry/exit) we don't want a stray oversized FAB or its
   surrounding statusbar to overflow the desktop layout. Belt-and-braces
   with the JS gate. */
@media (min-width: 601px) {
  .kanban-add-fab { display: none !important; }
}

/* Reopen the mobile-only block so subsequent rules below stay scoped
   correctly (the original block continued past .new-task before this
   patch — see the cal-rail-sheet styles that follow). */
@media (max-width: 600px) {

  /* Bottom sheet — slides up from below to reveal the rail. Backdrop
     dims the calendar; tapping it (or the close button or any rail card)
     dismisses the sheet. */
  .cal-rail-sheet-wrap {
    position: fixed;
    inset: 0;
    z-index: 60;
  }
  .cal-rail-sheet-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.55);
    animation: cal-rail-sheet-fade 180ms ease;
  }
  .cal-rail-sheet {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    max-height: 75vh;
    background: var(--bg);
    border-block-start: 1px solid var(--accent-dim);
    border-start-start-radius: 14px;
    border-start-end-radius: 14px;
    box-shadow: 0 -8px 28px rgba(0,0,0,0.5);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: cal-rail-sheet-slide 220ms cubic-bezier(0.2, 0.85, 0.25, 1);
  }
  .cal-rail-sheet-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    border-block-end: 1px solid var(--line);
  }
  .cal-rail-sheet-title {
    font-size: 14px;
    color: var(--ink);
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }
  .cal-rail-sheet-close {
    background: transparent;
    border: 0;
    color: var(--ink-faint);
    font-size: 18px;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    cursor: pointer;
  }
  /* Inside the sheet, the rail's own border / chrome should be flat —
     the sheet itself provides the panel chrome. */
  .cal-rail-sheet .cal-rail.in-sheet {
    border: 0;
    background: transparent;
    flex: 1;
    overflow-y: auto;
    max-height: none;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-drag-hint { display: none; }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-head { display: none; }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-actions {
    padding: 10px 16px 8px;
    gap: 10px;
    border-block-end: 0;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-actions input {
    font-size: 14px; padding: 8px 12px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-tap-hint {
    display: block;
    padding: 0 16px 8px;
    font-size: 12px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-list {
    padding: 6px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-project {
    margin-block-end: 0;
    border-radius: 10px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-project-head {
    min-height: 38px;
    padding: 9px 12px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-project-name {
    font-size: 13px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-project-count {
    font-size: 11px;
    min-width: 28px;
  }
  /* Mobile-sheet-only typography lift — the iOS-zoom-prevention rule
     forces text inputs to 16px (see the @media (hover:none) block
     further down). At desktop sizes the surrounding cards are 11px,
     which leaves the input towering over its own content. Lifting
     card body to 13px / title 14px / meta 11px gives the sheet a
     normal mobile hierarchy (input 16 > title 14 > body 13 > meta 11)
     instead of the jarring "input bigger than its rows" feel. Desktop
     rail keeps the dense 11px look — these overrides are scoped to
     `.cal-rail-sheet .cal-rail.in-sheet`. */
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-card {
    padding: 12px 14px;
    min-height: 48px;
    font-size: 13px;
    margin-bottom: 6px;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-project-list .cal-rail-card:last-child { margin-bottom: 0; }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-card-title {
    font-size: 14px;
    font-weight: 500;
  }
  .cal-rail-sheet .cal-rail.in-sheet .cal-rail-card-meta {
    font-size: 11px;
    margin-top: 4px;
  }
  @keyframes cal-rail-sheet-slide {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
  }
  @keyframes cal-rail-sheet-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  /* Reduced-motion users skip the slide. */
  @media (prefers-reduced-motion: reduce) {
    .cal-rail-sheet,
    .cal-rail-sheet-backdrop { animation: none; }
  }

  /* ---------- Neutral bottom-sheet primitive (mountBottomSheet) ----------
     Generic chrome for slide-up sheets. Mirrors the cal-rail-sheet look
     so the two visually match while migration is in flight. New sheets
     should use these classes; legacy cal-rail-sheet keeps its own. */
  .bottom-sheet-wrap {
    position: fixed;
    inset: 0;
    z-index: 60;
  }
  .bottom-sheet-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.55);
    animation: bottom-sheet-fade 180ms ease;
  }
  .bottom-sheet-panel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    max-height: 75vh;
    background: var(--bg);
    border-block-start: 1px solid var(--accent-dim);
    border-start-start-radius: 14px;
    border-start-end-radius: 14px;
    box-shadow: 0 -8px 28px rgba(0,0,0,0.5);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: bottom-sheet-slide 220ms cubic-bezier(0.2, 0.85, 0.25, 1);
  }
  @keyframes bottom-sheet-slide {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
  }
  @keyframes bottom-sheet-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    .bottom-sheet-panel,
    .bottom-sheet-backdrop { animation: none; }
  }

  /* ---------- Drawer — full-screen overlay ----------
     `width: 100%` (not `100vw`) so the drawer fills the body's content box
     instead of the unzoomed viewport. With state.settings.uiScale applying
     `zoom: 1.1` on <body>, `100vw` resolves to 375 CSS px which paints as
     412.5px — overflowing the actual viewport on the inline-start side and
     clipping the close button (visually left in RTL). `100%` is the body's
     zoomed content width, which is exactly the visible viewport. */
  .drawer {
    width: 100% !important;
    max-width: 100% !important;
    left: 0 !important;
    right: 0 !important;
  }
  .drawer-backdrop { background: var(--bg); }

  /* Mobile-only: collapse the header close to a "✕" glyph. The original
     "Close" / "إغلاق" text stays in the DOM as the button's accessible
     name (font-size:0 hides it visually); the ::before paints the icon.
     Frees horizontal room next to the title — important in narrow RTL
     viewports where Arabic header titles are wider than English. */
  .drawer-header .close {
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 50%;
    font-size: 0;
    line-height: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .drawer-header .close::before {
    content: "✕";
    font-size: 16px;
    line-height: 1;
  }

  /* Sticky bottom close bar — long settings panes used to force a scroll
     back to the top to dismiss. Appended to every drawer by
     setupMobileDrawerExtras() (one MutationObserver hooks all drawer
     types). Lives below the scrollable .drawer-body in the drawer's
     flex column. */
  .drawer-mobile-close {
    display: flex;
    flex: 0 0 auto;
    padding: 10px 16px max(10px, env(safe-area-inset-bottom));
    border-block-start: 1px solid var(--line);
    background: var(--bg-raised);
  }
  .drawer-mobile-close-btn {
    flex: 1;
    padding: 12px;
    border: 1px solid var(--line-strong);
    border-radius: 4px;
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: 13px;
    letter-spacing: 0.04em;
    cursor: pointer;
  }
  .drawer-mobile-close-btn:active {
    background: var(--bg-card-hov, var(--bg-raised));
    color: var(--accent-text);
    border-color: var(--accent-dim);
  }

  /* ---------- Settings ---------- */
  .settings-row { grid-template-columns: 1fr; gap: 4px; }
  .settings-row input[type="number"],
  .settings-row input[type="text"],
  .settings-row select {
    width: 100%;
  }

  /* ---------- Hide hover-only affordances ---------- */
  .task-hover-preview { display: none !important; }
  .quote-bar .quote-next { display: none; }

  /* ---------- Mobile chrome trims ----------
     The legal Privacy/Terms strip stays in source HTML for SEO crawlers
     but is hidden on mobile so it doesn't fight the FAB / statusbar. */
  .static-legal { display: none; }

  /* Topbar: keyboard-shortcut helpers (palette, legend), reports,
     settings, and the day-overview toggle collapse into the `⋯`
     overflow on mobile. day-overview has a desktop toolbar button, but
     a 375px topbar can't spare a 6th control's width. NOTE notifs
     (`#btn-notifs`) stays visible — the badge is glanceable and
     frequently tapped, so it lives in the always-visible topbar, not
     buried in `⋯`. (Widget / drive / trash have no standalone topbar
     button at all — they live only in `⋯`.) */
  #btn-palette,
  #btn-legend,
  #btn-reports,
  #btn-settings,
  #btn-day-plan,
  .topbar-actions .sep { display: none; }
  /* Notifs: stays visible on mobile, compact icon-only style. */
  .topbar #btn-notifs {
    padding: 6px 10px;
  }
  /* Notif dropdown: desktop is a 320px popover anchored to the bell via
     JS-set `inset-inline-end`. On a ~390px viewport that pushes the
     inline-start edge off-screen. Stretch it to a full-width sheet by
     pinning the start edge and letting width fill the gap. We deliberately
     do NOT touch `inset-inline-end` — that's an inline style set by
     openNotificationDropdown() and inline styles outrank CSS anyway. */
  .notif-dropdown {
    inset-inline-start: 8px;
    width: auto;
    max-height: 60vh;
  }
  .notif-dropdown .notif-item { padding-block: 10px; }
  .notif-dropdown .notif-title { font-size: 13px; }
  .notif-dropdown .notif-body { font-size: 12px; }

  /* Board: hide the multi/single column toggle — mobile always
     collapses to a single stacked column via the col-headers / cells-row
     rules above, so the segmented control has nothing to switch.
     `!important` because the desktop base rule `.kanban-filter-bar
     .kfb-layout-seg { display: inline-flex }` comes later in source
     order and would otherwise win the cascade. */
  .kanban-filter-bar .kfb-layout-seg { display: none !important; }
  /* Two visible children on mobile (mobile-chip + controls-cluster) —
     the 7-chip cluster is hidden below. Allow wrap so the controls
     cluster falls onto a second row instead of overflowing the bar's
     right edge on narrow viewports. `margin-inline-start: auto` on the
     cluster keeps it right-aligned on its own row. */
  .kanban-filter-bar {
    flex-wrap: wrap;
    gap: 6px;
    padding: 6px 10px;
  }
  /* Hide the full chip cluster on mobile — it's replaced by the single
     `.kfb-mobile-chip` button below. `!important` because the desktop
     base rule `.kanban-filter-bar .kfb-chip-cluster { display: flex }`
     comes later in source order and would otherwise win the cascade
     at equal specificity. */
  .kanban-filter-bar .kfb-chip-cluster { display: none !important; }
  .kanban-filter-bar .kfb-controls-cluster {
    flex-wrap: nowrap;
    flex: 0 0 auto;
    margin-inline-start: auto;
    gap: 6px;
  }
  /* Drop the leading glyph + the desktop min-width floor on the two
     control chips so [project picker | hide done | compact] all fit
     a 375px viewport without horizontal scroll. The button label
     itself ("hide done" / "compact") is enough signal — the glyph
     was visual reinforcement only. */
  .kanban-filter-bar .kfb-icon-glyph { display: none; }
  .kanban-filter-bar #btn-done,
  .kanban-filter-bar #btn-compact { min-width: 0; }
  /* Mobile compact filter chip — replaces the 7-chip cluster with a
     single button that shows the active filter and opens a popover
     listing all 7. Saves a row of vertical space. */
  .kanban-filter-bar .kfb-mobile-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--bg-raised);
    border: 1px solid var(--accent-dim);
    border-radius: 999px;
    color: var(--accent-text);
    padding: 6px 12px;
    font: inherit;
    font-size: 12px;
    cursor: pointer;
    flex: 0 0 auto;
  }
  .kanban-filter-bar .kfb-mobile-chip-icon { font-size: 14px; }
  .kanban-filter-bar .kfb-mobile-chip-label { color: var(--ink); }
  .kanban-filter-bar .kfb-mobile-chip-chev { color: var(--ink-faint); font-size: 10px; }
  .kanban-filter-bar .kfb-mobile-chip .kfb-chip-count {
    background: color-mix(in oklab, var(--accent) 18%, transparent);
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11px;
  }
  /* Active-state highlight in the popover. */
  .tb-overflow-popover .tb-overflow-item.is-active {
    background: color-mix(in oklab, var(--accent) 12%, transparent);
    color: var(--accent-text);
  }
  /* `.project-label` keeps `position: sticky` on mobile (configured in
     the earlier mobile block ~line 7582) so the group name pins under
     the topbar while the user scrolls through that group's cards. An
     earlier revision flipped it to `static !important` to "un-sticky
     everything on mobile" but that defeated the only sticky strip the
     phone layout actually wants — the label is the user's only cue
     for which group they're scrolling inside.
     `.active-now` is the other inline strip; it's hidden entirely
     below (replaced by .mobile-timer-pill). `.kanban-filter-bar` and
     `.col-headers` are already taken to in-flow in the first mobile
     block, so no extra rule is needed here. */
  /* Hide the bulky inline `.active-now` strip on mobile — its compact
     replacement is `.mobile-timer-pill` (rendered into <body> by
     renderMobileFloaters), which already shows the running task title +
     elapsed + stop button as a floating pill. The inline section
     duplicates that info in a much taller, screen-stealing form. */
  .active-now { display: none; }

  /* Group label: drop the drag-grip (group reorder is desktop-only DnD)
     and the stats chip row (the segmented progress bar already encodes
     the same proportions, more compactly). */
  .project-label .grip { display: none; }
  .project-label .stats { display: none; }

  /* Statusbar: right cluster (theme / break-visual / hours / sound /
     locale / legal) duplicates Settings. On mobile they're all reachable
     via Settings -> Appearance / Calendar / Legal sections, so the
     chips don't earn the horizontal real estate. Left and center
     clusters (stats, today-worked, timer, break CTA) stay. */
  .statusbar-theme,
  .statusbar-breakstyle,
  .statusbar-sound,
  .statusbar-locale,
  .statusbar-legal,
  .statusbar-section.statusbar-right .statusbar-sep { display: none; }

  /* Statusbar timer + break CTA — bumped prominence on mobile so the
     "what am I working on / time to break" channel is unmissable.
     Replaces the inline `.active-now` strip that's hidden on mobile. */
  .statusbar { padding: 4px 8px; gap: 6px; }
  /* Equal box height across all footer chips so the stats pill ("● 6 ◆ 1"),
     the running-timer pill, and the take-break CTA line up on a single
     baseline. The running-timer rule below sets `padding: 6px 12px` for
     prominence (~32px tall with a 14px+12px stack and 1px border); the
     plain stats chip uses the base `padding: 3px 6px` from the desktop
     rule and sits ~22px tall, which read as "the stats chip got smaller"
     against its taller neighbours. min-height pins everyone to the same
     box; box-sizing: border-box keeps the border + padding inside that
     box so the height is honoured even when only the timer adds a thicker
     accent border. */
  .statusbar-chip {
    min-height: 28px;
    box-sizing: border-box;
  }
  .statusbar-timer.is-running,
  .statusbar-timer.is-onbreak {
    padding: 3px 8px;
    background: color-mix(in oklab, var(--accent) 14%, var(--bg-raised));
    border-color: var(--accent-dim);
  }
  .statusbar-timer.is-running .chip-timer-time,
  .statusbar-timer.is-onbreak .chip-timer-time {
    font-size: 12px;
    font-weight: 700;
  }
  .statusbar-timer.is-running .chip-tasks,
  .statusbar-timer.is-onbreak .chip-tasks {
    font-size: 11px;
  }
  .statusbar-timer.is-running .chip-tasks { display: none; }
  /* Overdue: pulse the running-timer chip red so the user notices even
     mid-scroll. Same animation as the legacy active-now overdue chip. */
  .statusbar-timer.is-running.is-overdue {
    background: color-mix(in oklab, var(--red) 18%, var(--bg-raised));
    border-color: color-mix(in oklab, var(--red) 55%, transparent);
    animation: statusbar-overdue-pulse 1.6s ease-in-out infinite;
  }
  .statusbar-timer.is-running.is-overdue .chip-timer-time,
  .statusbar-timer.is-running.is-overdue .chip-play { color: var(--red-text); }
  /* Break CTA prominent when overdue — communicates "take break NOW"
     without the user having to read the label. Compact padding so the
     chip fits inside the 34px statusbar without clipping the label. */
  .statusbar-break-cta {
    padding: 3px 8px;
    background: color-mix(in oklab, var(--amber) 22%, var(--bg-raised));
    border: 1px solid color-mix(in oklab, var(--amber) 55%, transparent);
    color: var(--amber-text);
    font-weight: 600;
    font-size: 11px;
  }
  .statusbar-timer.is-running.is-overdue + .statusbar-break-cta {
    background: var(--red);
    color: var(--bg);
    border-color: var(--red);
    animation: statusbar-overdue-pulse 1.6s ease-in-out infinite;
  }
  @keyframes statusbar-overdue-pulse {
    0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--red) 60%, transparent); }
    50%      { box-shadow: 0 0 0 6px color-mix(in oklab, var(--red) 8%, transparent); }
  }
  /* Ignite cue — fires once when a new focus session starts (first render
     after sessionStartedAt flips to a new ISO; see _animatedSessionId in
     app.js). Reads as the chip "lighting up" from below: rises a few px,
     blooms an accent ring, settles. ~720ms feels deliberate without
     blocking the next tap. */
  .statusbar-timer.is-running.is-just-started {
    animation: statusbar-timer-ignite 720ms cubic-bezier(0.2, 0.9, 0.3, 1);
  }
  @keyframes statusbar-timer-ignite {
    0%   { transform: translateY(6px) scale(0.94);
           box-shadow: 0 0 0 0 transparent; }
    45%  { transform: translateY(-1px) scale(1.06);
           box-shadow: 0 -2px 14px var(--accent-glow),
                       0 0 0 4px color-mix(in oklab, var(--accent) 18%, transparent); }
    100% { transform: translateY(0) scale(1);
           box-shadow: 0 0 0 0 transparent; }
  }
  @media (prefers-reduced-motion: reduce) {
    .statusbar-timer.is-running.is-overdue,
    .statusbar-timer.is-running.is-overdue + .statusbar-break-cta,
    .statusbar-timer.is-running.is-just-started { animation: none; }
  }
}

/* ===================================================================
   MOBILE POLISH — safe areas, FAB, timer pill, keyboard avoidance.
   =================================================================== */
@media (max-width: 600px) {
  /* iOS safe-area insets — respect the notch + home-indicator. The
     topbar avoids the top notch; the FAB + timer pill sit above the
     home bar. */
  /* The 96px was for a .mobile-fab + .mobile-timer-pill pair that the
     app no longer renders. Body's padding-block-end already reserves
     `--statusbar-h + safe-area-inset-bottom` for the fixed statusbar,
     so the calendar only needs a small extra cushion above its own
     scroll container. .board overrides this above with 16px. */
  main.calendar { padding-bottom: calc(16px + env(safe-area-inset-bottom)); }

  /* Floating action button — quick-add a task. Sits above the home
     bar via safe-area inset. */
  .mobile-fab {
    position: fixed;
    right: max(16px, env(safe-area-inset-right));
    bottom: calc(16px + env(safe-area-inset-bottom));
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--bg);
    border: 0;
    font-size: 28px;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 4px 16px rgba(0,0,0,0.5), 0 0 0 1px var(--accent-dim);
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .mobile-fab:active { transform: scale(0.94); }

  /* Persistent timer pill — shows at the bottom when any task is
     running. Floats above the FAB, left-aligned. Tap to jump to the
     running task's drawer. */
  .mobile-timer-pill {
    position: fixed;
    left: max(12px, env(safe-area-inset-left));
    bottom: calc(24px + env(safe-area-inset-bottom));
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 14px;
    background: var(--accent);
    color: var(--bg);
    border-radius: 32px;
    font-size: 12px;
    font-weight: 600;
    box-shadow: 0 4px 14px rgba(0,0,0,0.5);
    z-index: 29;
    max-width: calc(100vw - 96px - env(safe-area-inset-right) - env(safe-area-inset-left));
    cursor: pointer;
    font-variant-numeric: tabular-nums;
    border: 0;
  }
  .mobile-timer-pill .mtp-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1 1 auto;
    min-width: 0;
  }
  .mobile-timer-pill .mtp-elapsed {
    opacity: 0.85;
    white-space: nowrap;
    flex-shrink: 0;
  }
  .mobile-timer-pill .mtp-stop {
    margin-inline-start: 4px;
    background: rgba(0,0,0,0.25);
    color: inherit;
    border: 0;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 10px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Make text inputs / textareas scroll into view when the virtual
     keyboard opens — otherwise iOS hides them under the keyboard. */
  input, textarea, select { scroll-margin-bottom: 40vh; }
}

/* Prevent iOS Safari's auto-zoom on focus, which fires whenever a
   text-like input/select has a computed font-size below 16px. The
   app's base size is 13px and inputs inherit it, so every focus on
   phone OR tablet was triggering the zoom.
   Scoped to touch-only devices (`hover: none` + `pointer: coarse`)
   so desktop keeps the denser 13px UI; iPad is covered too because
   it can be wider than 600px in portrait.
   `!important` because several inputs have class-scoped overrides
   (`.palette-input { font-size: 14px }`, `.cal-rail-search input
   { font-size: 14px }`, etc) at higher specificity than the
   `input[type=...]` attribute selector. Preventing the browser-level
   zoom is a hard requirement, not a stylistic choice — without
   `!important` those inputs still trigger the bug, and once iOS has
   zoomed in it doesn't auto-zoom back out on blur. */
@media (hover: none) and (pointer: coarse) {
  input[type="text"],
  input[type="number"],
  input[type="email"],
  input[type="tel"],
  input[type="url"],
  input[type="search"],
  input[type="password"],
  input[type="date"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="week"],
  input[type="time"],
  input:not([type]),
  textarea,
  select,
  [contenteditable="true"] {
    font-size: 16px !important;
  }
}

/* Hide FAB + timer pill by default; mobile media query doesn't need to
   hide them since they only exist in mobile-rendered DOM. But belt +
   suspenders in case JS leaks them to desktop somehow. */
@media (min-width: 601px) {
  .mobile-fab, .mobile-timer-pill { display: none; }
}

/* === Mobile topbar declutter ===
   The desktop topbar has 13+ buttons; on mobile this wraps to 4+ rows and
   overwhelms the screen. Every hidden button here is reachable via ⌘K, which
   is the "everything lives here" entry point we teach in the tour. Keep only
   the stats summary + indicators that surface useful state, plus palette and
   settings so they're one tap away. */
@media (max-width: 600px) {
  /* Legacy hide list. Most of these buttons no longer live in the topbar
     (theme/locale/mute moved to the statusbar; done/compact moved to the
     kanban filter bar; insights/log/legend moved into the `⋯` overflow
     menu). The current hide list (palette / legend / insights / widget /
     notifs / trash / day-plan / settings) is in the main mobile media
     block above — search "Topbar: keyboard-shortcut helpers". The
     view-toggle (`#btn-view-toggle`) MUST stay visible — it's the
     primary navigation between board ↔ calendar. */

  /* Compact the stats row — smaller font, tighter gaps, wrap allowed */
  .topbar-actions {
    gap: 6px;
    font-size: 11px;
    row-gap: 4px;
  }
  .topbar-actions .sep { opacity: 0.4; }

  /* Palette button is the front door — give it emphasis */
  .topbar-actions #btn-palette {
    border-color: var(--accent, var(--green));
    color: var(--accent, var(--green));
  }
}

/* ===================================================================
   Running-card orbit — layout-agnostic
   ===================================================================
   Every card with a live timer (.card.running, not paused/blocked/done)
   gets a single bright accent arc that travels clockwise around its
   border. Replaces the previous ambient "breathe" pulse — a moving
   indicator reads as "live process" more clearly than a fading ring,
   and on desktop where multiple cards may run at once, motion is the
   signal that draws the eye without saturating it.

   Implementation:
     1. @property --orbit-angle exposes the conic-gradient `from`
        parameter as a tween-able typed custom property. Without
        @property, animating the angle would jump-cut.
     2. ::after pseudo fills the card with `inset: -1px` so the ring
        sits on the border line.
     3. conic-gradient draws a transparent disc with one bright accent
        arc (~80deg of brightness with soft edges).
     4. mask trick (xor of two layers, one clipped to content-box, one
        to border-box) erases the inner area, leaving only the padding
        ring around the perimeter.
     5. animation rotates --orbit-angle 0→360 every 2.4s, sweeping
        the bright arc around the border.

   Stacks naturally with .card.timer-started (the 600ms green start
   glow on tap) since both use different layers — that's a one-shot
   box-shadow on the card itself, this is a continuous mask on the
   pseudo-element. */
@property --orbit-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.card.running:not(.paused):not(.blocked):not(.done) {
  isolation: isolate;
}
.card.running:not(.paused):not(.blocked):not(.done)::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from var(--orbit-angle),
    transparent 0deg,
    transparent 240deg,
    color-mix(in oklab, var(--accent) 30%, transparent) 260deg,
    var(--accent) 300deg,
    color-mix(in oklab, var(--accent) 30%, transparent) 340deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
  filter: drop-shadow(0 0 4px color-mix(in oklab, var(--accent) 50%, transparent));
  animation: card-running-orbit 2.4s linear infinite;
}
@keyframes card-running-orbit {
  to { --orbit-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  /* Stop the orbit but keep a static accent ring so the running state
     is still legible without motion. */
  .card.running:not(.paused):not(.blocked):not(.done)::after {
    animation: none;
    background: color-mix(in oklab, var(--accent) 35%, transparent);
    filter: none;
  }
}

/* Touch-device guard — applies to any device without a fine pointer,
   not just narrow viewports. Prevents the hover tooltip from double-
   firing on iPad + keyboard setups. */
@media (hover: none) {
  .task-hover-preview { display: none !important; }
  .card .card-color-sq.no-color { opacity: 0.5; }
  /* Touch devices treat the FIRST tap on a `:hover` rule as "activate
     hover state" and only fire `click` on the SECOND tap. Buttons that
     toggle state (statusbar chips, kanban filter chips, icon buttons,
     overflow popover items, theme chips) need single-tap-to-activate,
     so we null out their hover styles on touch — they keep their
     press/active state via :active, which fires once per tap. */
  /* Whack-a-mole approach (one selector at a time) doesn't scale — we'd
     hit this trap every time someone adds a hovered button. Instead,
     broadly nullify hover-state changes on the most common interactive
     element families. The `revert` keyword resets the property to what
     it would be without ANY rule, so the resting style is preserved
     while the :hover override is dropped. */
  button:hover,
  [role="button"]:hover,
  .icon-btn:hover,
  .pill:hover,
  .chip:hover,
  [class*="-chip"]:hover,
  [class*="-btn"]:hover,
  [class*="-pill"]:hover,
  [class*="-item"]:hover,
  [class*="-tab"]:hover,
  .card:hover,
  .card-action-item:hover,
  .project-label .chev:hover,
  .project-label .project-delete:hover,
  .project-label .project-rename:hover,
  .project-label .name .chev:hover,
  .drawer-header .close:hover,
  .toast-action:hover,
  .install-nudge-btn:hover,
  .trash-btn:hover,
  .today-strip-agenda-nav .agenda-nav-btn:hover,
  .today-strip-agenda-nav .agenda-nav-today:hover,
  .due-quick-popover .dqp-pick-day:hover,
  .due-quick-popover .dqp-pick-set:hover,
  .asg-quick-popover .aqp-me-save:hover,
  .asg-pill .asg-remove:hover,
  .dp-actions button:hover,
  .dp-actions .dp-set:hover,
  .quote-bar .quote-toggle:hover,
  .break-overlay .bo-end:hover,
  .break-overlay .bo-preview-tag:hover,
  .break-style-tile .bst-preview:hover,
  .cal-quick-actions .cal-quick-save:hover,
  .cal-block.external:hover,
  .cal-block.external.rs-tentative:hover,
  .cal-block.external.rs-needsAction:hover,
  .cal-block.external.rs-declined:hover,
  .filter-wrap .filter-hidden-chip:hover,
  .due-summary .due-clear:hover,
  .card-color-sq:hover,
  .tour-tip button:hover,
  .topbar-actions .icon-btn:hover,
  .topbar-actions .palette-btn:hover {
    background: revert !important;
    background-color: revert !important;
    background-image: revert !important;
    border-color: revert !important;
    color: revert !important;
    transform: none !important;
    box-shadow: revert !important;
    text-shadow: revert !important;
    opacity: revert !important;
  }
  /* Theme picker on touch — drop the outer-glow box-shadow on the
     active chip so it doesn't overflow the popover container. The
     1px ring stays as the selected indicator; the 16px radial blur
     gets dropped (saves ~32px of overflow per chip). */
  .tour-tp-chip[data-theme="terminal"].tour-tp-chip-highlight,
  .tour-tp-chip[data-theme="monokai"].tour-tp-chip-highlight,
  .tour-tp-chip[data-theme="gruvbox"].tour-tp-chip-highlight,
  .tour-tp-chip[data-theme="solarized-light"].tour-tp-chip-highlight,
  .tour-tp-chip[data-theme="pearl"].tour-tp-chip-highlight,
  .tour-tp-chip[data-theme="blossom"].tour-tp-chip-highlight {
    box-shadow: 0 0 0 1px currentColor;
    transform: none;
  }
}

/* ---------- Kanban filter bar ----------
   Sits between the today-cal-strip and the column headers. Left cluster:
   single-select time chips (`All Overdue Today This week Unscheduled
   Blocked`) with counts post-text-filter. Right cluster: relocated +done
   and compact toggles, removed from the topbar. */
.kanban-filter-bar {
  /* Sticks below the topbar (and below .active-now when running timers exist)
     so the active filter chip stays visible while scrolling. Top offset is
     set dynamically via --sticky-top-filter-bar so the value tracks active-now
     visibility — see updateStickyTops() in app.js. */
  position: sticky;
  top: var(--sticky-top-filter-bar, 52px);
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 12px;
  border-block-end: 1px solid var(--line-strong);
  background: var(--bg-card);
}
.kanban-filter-bar .kfb-chip-cluster {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.kanban-filter-bar .kfb-controls-cluster {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
/* Design pass §6 — chips drop the resting outline. Hover lifts onto a
   subtle bg tint; the active filter still carries the accent border so
   "what filter is on" reads at a glance. */
.kanban-filter-bar .kfb-chip {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-secondary);
  padding: 2px 10px;
  border-radius: 10px;
  font: inherit;
  font-size: var(--fs-caption);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  line-height: 1.3;
  transition: border-color 80ms ease, color 80ms ease, background 80ms ease;
}
.kanban-filter-bar .kfb-chip:hover {
  background: var(--bg-raised);
  color: var(--accent-text);
}
.kanban-filter-bar .kfb-chip:focus-visible { border-color: var(--accent); outline: none; }
.kanban-filter-bar .kfb-chip.active {
  color: var(--accent-text);
  border-color: var(--accent);
  background: var(--accent-glow);
}
.kanban-filter-bar .kfb-chip-count {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.kanban-filter-bar .kfb-chip.active .kfb-chip-count {
  color: var(--accent-text);
}

/* `view done` / `full` toggles inherit the .icon-btn class but live
   inside the kanban filter bar — the .topbar-actions-scoped .on rule
   never reached them, so toggling did nothing visually. Mirror the
   .kfb-chip vocabulary here so on/off state reads like the rest of the
   bar (off = dim text + no border; on = accent text + accent border +
   accent glow). */
.kanban-filter-bar .icon-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-secondary);
  padding: 2px 10px;
  border-radius: 10px;
  font: inherit;
  font-size: var(--fs-caption);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  line-height: 1.3;
  white-space: nowrap;
  transition: border-color 80ms ease, color 80ms ease, background 80ms ease;
}
.kanban-filter-bar .icon-btn:hover {
  background: var(--bg-raised);
  color: var(--accent-text);
}
.kanban-filter-bar .icon-btn:focus-visible { border-color: var(--accent); outline: none; }
.kanban-filter-bar .icon-btn.on {
  color: var(--accent-text);
  border-color: var(--accent);
  background: var(--accent-glow);
}
/* Each toggle swaps its LABEL between two states (`view done` ↔ `hide
   done`; `full` ↔ `compact`). Different widths reflow neighbors on every
   click — visible jitter. Locking to the wider of the two labels keeps
   the cluster still through state changes. */
.kanban-filter-bar #btn-done { min-width: 92px; justify-content: flex-start; }
.kanban-filter-bar #btn-compact { min-width: 78px; justify-content: flex-start; }

.done-count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--accent);
  color: var(--bg);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  margin-inline-start: 2px;
}
@keyframes badge-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.45); }
  65%  { transform: scale(0.88); }
  85%  { transform: scale(1.1); }
  100% { transform: scale(1); }
}
.done-count-badge.badge-pop {
  animation: badge-pop 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ===================================================================
   Board layout segmented toggle (multi-column / single-column)
   ===================================================================
   Two icon buttons sitting side-by-side in the kanban filter bar.
   The active side carries the accent fill the same way kfb-chip.active
   does, so it reads as part of the same control language. The glyphs
   are pure CSS (no SVG / no font-icon) so they stay crisp at any zoom
   and don't add a network round-trip. */
.kanban-filter-bar .kfb-layout-seg {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg);
}
.kanban-filter-bar .kfb-layout-btn {
  background: transparent;
  border: none;
  padding: 4px 8px;
  cursor: pointer;
  color: var(--ink-dim);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 80ms ease, background 80ms ease;
}
.kanban-filter-bar .kfb-layout-btn + .kfb-layout-btn {
  border-inline-start: 1px solid var(--line);
}
.kanban-filter-bar .kfb-layout-btn:hover {
  background: var(--bg-raised);
  color: var(--accent-text);
}
.kanban-filter-bar .kfb-layout-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.kanban-filter-bar .kfb-layout-btn.on {
  color: var(--accent-text);
  background: var(--accent-glow);
}
/* Glyph rendering — three vertical bars (multi) and three stacked
   horizontal bars (single). Same pixel grid (10×10) so the two buttons
   feel like a true segmented pair. currentColor inherits the button's
   color, so hover/active states tint the icon without extra rules. */
.kanban-filter-bar .kfb-layout-glyph {
  display: inline-block;
  width: 14px;
  height: 12px;
  background-repeat: no-repeat;
  background-position: center;
}
.kanban-filter-bar .kfb-layout-glyph-multi {
  background-image: linear-gradient(currentColor, currentColor),
                    linear-gradient(currentColor, currentColor),
                    linear-gradient(currentColor, currentColor);
  background-size: 3px 12px, 3px 12px, 3px 12px;
  background-position: left center, center center, right center;
}
.kanban-filter-bar .kfb-layout-glyph-single {
  background-image: linear-gradient(currentColor, currentColor),
                    linear-gradient(currentColor, currentColor),
                    linear-gradient(currentColor, currentColor);
  background-size: 14px 2px, 14px 2px, 14px 2px;
  background-position: top, center, bottom;
}

/* === tour === */

/* The v1 banner CSS (.tour-banner / -prompt / -skip / @keyframes
   tour-banner-pulse) was removed when the tour switched to auto-start
   on first run (schemaVersion 6). See tour.js header for context. */

/* Tour tooltip */
.tour-tip {
  position: fixed;
  z-index: var(--z-tour-tip);
  /* Width caps respect the viewport. The `min()` floor prevents the tip
     from being wider than viewport-minus-margins even before positionTip's
     JS clamp runs. `box-sizing: border-box` is set globally but stated
     here too so the 320px cap is GUARANTEED to include border + padding —
     a content-box interpretation would push the rendered width past it
     and overflow narrow viewports. The 320px max (was 360px) leaves more
     breathing room beside topbar-anchored steps on Chromebook-class
     widths where 360px-anchored-far-right was still clipping the
     "next →" button. */
  box-sizing: border-box;
  min-width: min(260px, calc(100vw - 24px));
  max-width: min(320px, calc(100vw - 24px));
  padding: 12px 14px;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  border: 1px solid var(--ink-dim);
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.tour-tip::before, .tour-tip::after {
  content: "";
  position: absolute;
  inset-inline-start: -1px;
  inset-inline-end: -1px;
  height: 1px;
  background: var(--accent, var(--green));
}
.tour-tip::before { inset-block-start: -1px; }
.tour-tip::after  { inset-block-end: -1px; }
.tour-tip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-block-end: 6px;
  gap: 12px;
}
.tour-tip-title { color: var(--accent, var(--green)); font-weight: bold; }
.tour-tip-count { color: var(--ink-dim); font-size: 11px; white-space: nowrap; }
.tour-tip-body  {
  color: var(--ink);
  opacity: 0.92;
  /* Long unbreakable tokens (URLs, ID-like runs, ⌘K-style keystrokes) used
     to push the tip wider than its max-width when the layout engine
     couldn't find a wrap point inside them. `overflow-wrap: anywhere`
     allows breaks inside any character; `word-break: break-word` is the
     fallback for older Chromium that doesn't honour `anywhere`. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* The v1 .tour-tip-input-row / -input / -input-save CSS was removed when
   the greet step (which collected the user's name) was dropped from the
   tour. The redesigned tour has no input-collecting steps. */
.tour-tip-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-block-start: 10px;
  /* Wrap to a second row on narrow viewports rather than pushing the
     "next →" button past the tip's right edge — three buttons + two
     8px gaps + button padding can exceed even a 320px max-width tip on
     locales whose translations of [esc] skip / back / next come out
     longer. */
  flex-wrap: wrap;
  max-width: 100%;
}
.tour-tip button {
  all: unset;
  cursor: pointer;
  padding: 4px 10px;
  border: 1px solid var(--ink-dim);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
}
.tour-tip .tour-next {
  border-color: var(--accent, var(--green));
  color: var(--accent, var(--green));
}
.tour-tip .tour-skip { opacity: 0.7; }
.tour-tip button:hover { background: var(--ink-faint, rgba(255,255,255,0.06)); }

/* Tooltip arrow — small chevron that always points at the spotlit
   anchor regardless of which side the tip lands on. Placement is set
   on `.tour-tip[data-placement="..."]` by positionTip() in tour.js
   (right | left | below | above | center). The four placements move
   the arrow to the matching edge of the tip and rotate the chevron
   so its corner points outward (toward the anchor).

   The shared base sets neutral defaults; each placement-specific block
   below positions + colors the two adjacent borders that form the
   chevron's outer corner.

   Anchor was on the START side with rotate(-45deg) before the
   `data-placement` system landed; the right + default cases below
   preserve that exact rendering. */
.tour-tip-arrow {
  position: absolute;
  width: 10px;
  height: 10px;
  background: var(--bg);
  border: 1px solid transparent;
}

/* The arrow's offset along the tip's edge (--tour-arrow-x for top/bottom
   placements, --tour-arrow-y for left/right) is set inline by
   positionTip() in tour.js — it computes where the anchor's CENTER lies
   relative to the tip's bounding box and pins the arrow there, so the
   chevron always physically points AT the anchor regardless of how far
   the tip slid horizontally/vertically to fit in viewport. */

/* Default + tip-on-right of anchor: arrow on the start side, chevron
   points toward start (= toward anchor on the left in LTR). */
.tour-tip[data-placement="right"] .tour-tip-arrow,
.tour-tip:not([data-placement]) .tour-tip-arrow {
  inset-inline-start: -6px;
  inset-block-start: var(--tour-arrow-y, 18px);
  border-inline-start-color: var(--ink-dim);
  border-block-start-color: var(--ink-dim);
  transform: rotate(-45deg);
}
[dir="rtl"] .tour-tip[data-placement="right"] .tour-tip-arrow,
[dir="rtl"] .tour-tip:not([data-placement]) .tour-tip-arrow {
  transform: rotate(135deg);
}

/* Tip-on-left: arrow on the end side, chevron points toward end. */
.tour-tip[data-placement="left"] .tour-tip-arrow {
  inset-inline-end: -6px;
  inset-block-start: var(--tour-arrow-y, 18px);
  border-inline-end-color: var(--ink-dim);
  border-block-end-color: var(--ink-dim);
  transform: rotate(-45deg);
}

/* Tip-below anchor: arrow on top edge, chevron points up. */
.tour-tip[data-placement="below"] .tour-tip-arrow {
  inset-inline-start: var(--tour-arrow-x, 18px);
  inset-block-start: -6px;
  border-inline-start-color: var(--ink-dim);
  border-block-start-color: var(--ink-dim);
  transform: rotate(45deg);
}

/* Tip-above anchor: arrow on bottom edge, chevron points down. */
.tour-tip[data-placement="above"] .tour-tip-arrow {
  inset-inline-start: var(--tour-arrow-x, 18px);
  inset-block-end: -6px;
  border-inline-end-color: var(--ink-dim);
  border-block-end-color: var(--ink-dim);
  transform: rotate(45deg);
}

/* Centered tip (no anchor or fallback) — no arrow to point with. */
.tour-tip[data-placement="center"] .tour-tip-arrow {
  display: none;
}

/* Spotlight on anchor — visually marks the action button as the FOCAL
   POINT of the step. The page-dim is NOT painted here as a 9999px
   box-shadow — that approach broke multi-spotlight (each element's dim
   shadow paints over the others' bright areas, ending up with the whole
   page dim). Instead, .tour-dim-overlay (a single full-viewport element)
   handles dimming, and spotlit elements sit above it via z-index. */
.tour-spotlight {
  position: relative;
  z-index: var(--z-tour-spotlight);
  box-shadow:
    0 0 0 4px var(--accent, var(--green)),
    0 0 26px 8px color-mix(in oklab, var(--accent, #5ef07a) 60%, transparent);
  transition: box-shadow 200ms ease-in-out;
  border-radius: 4px;
  /* Belt-and-suspenders: also paint a 2px outline so even if a parent
     stacking context clips the box-shadow ring, the spotlit element
     still has an unmistakable accent border. */
  outline: 2px solid var(--accent, var(--green));
  outline-offset: 1px;
}

/* "Lift" — used by step.spotlightExtra() callers that want extras
   to sit above the dim overlay (so they're visible) without the FULL
   bright ring + outer glow that the main anchor wears. A thin accent
   outline is still drawn so the user can see WHICH extra is part of
   the highlighted region — without it, the lifted element is just
   "not dimmed" which reads as ordinary, not actively lit.
   For continuous-region cases (focusHold's column cells) the thin
   outline aligns flush with the cell edge, tracing the column shape
   as one cohesive band rather than as a flashing per-cell ring. */
.tour-lift {
  position: relative;
  z-index: var(--z-tour-spotlight);
  outline: 1px solid color-mix(in oklab, var(--accent, var(--green)) 70%, transparent);
  outline-offset: 0;
}

/* Single full-viewport dim layer. Inserted by mountTipFor when a step
   has any anchor/extras to highlight, removed by unmountCurrentStep.
   pointer-events:none so the user can still click on lifted/spotlit
   elements through it. z-index sits one below --z-tour-spotlight so
   spotlit + lifted elements paint on top. */
.tour-dim-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.62);
  z-index: calc(var(--z-tour-spotlight) - 1);
  pointer-events: none;
  animation: tour-dim-in 160ms ease-out;
}
@keyframes tour-dim-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Pulse on mount AND on advance. mountTipFor adds + auto-removes the
   class. Same shadow shape as .tour-spotlight but with a soft outward
   pulse on the outermost layer. */
@keyframes tour-pulse {
  0% {
    box-shadow:
      0 0 0 4px var(--accent, var(--green)),
      0 0 0 0 var(--accent, var(--green));
  }
  100% {
    box-shadow:
      0 0 0 4px var(--accent, var(--green)),
      0 0 0 26px transparent;
  }
}
.tour-pulse { animation: tour-pulse 300ms ease-out; }

@media (prefers-reduced-motion: reduce) {
  .tour-pulse { animation: none; }
  .tour-spotlight { transition: none; }
  .tour-dim-overlay { animation: none; }
}

/* Stacking-context escape — applies to any sticky/positioned ancestor
   that contains a tour spotlight/lift descendant. Multiple ancestors
   in this app create stacking contexts that trap z-index of descendants:
     - main.calendar (sticky + overflow:hidden) — calendar steps
     - .col-headers (sticky) — focusHold step's anchor lives here
     - .topbar (sticky) — most icon-button steps live here
   Without this lift, the descendant's bright ring stays trapped inside
   the local stacking context and the body-level dim overlay (z 9997)
   covers it. We lift the ancestor itself above the dim and turn off
   any overflow:hidden so the box-shadow ring isn't clipped. Dim then
   darkens only siblings outside the lifted ancestor — visually the
   highlighted region is still the one bright zone. */
main.calendar:has(.tour-spotlight),
main.calendar:has(.tour-lift),
.col-headers:has(.tour-spotlight),
.col-headers:has(.tour-lift),
.topbar:has(.tour-spotlight),
.topbar:has(.tour-lift) {
  z-index: calc(var(--z-tour-spotlight) + 1);
  overflow: visible;
}

/* Drag-preview / drop-target visibility while a tour is active. Drag
   previews (created by the kanban + calendar drag systems on dragstart)
   render as fixed-position elements with their own z-index — typically
   below the dim overlay. Without this, drags initiated mid-tour show
   no preview ghost and no drop-target outline, breaking dragSchedule's
   actual gesture demo. body.tour-running is added by startTour and
   removed by teardownEngine, so this only applies during active tour. */
body.tour-running .drag-preview,
body.tour-running .drag-ghost,
body.tour-running .cal-drag-preview,
body.tour-running [data-drag-preview],
body.tour-running .drop-target,
body.tour-running .drop-target-active {
  z-index: calc(var(--z-tour-spotlight) + 2);
}

/* end card — wider than a tooltip step */
.tour-end {
  min-width: 380px;
  max-width: 460px;
}
.tour-end pre {
  margin: 0;
  /* MUST be a true monospace stack — the end card draws an ASCII box
     (┌─┐│└─┘) padded with regular spaces to align the right border; in
     a proportional font the box-drawing chars and padded gaps diverge
     and the right border zig-zags. `--font-mono` is the monospace stack
     widget.css and hover-preview.css already use. */
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
  color: var(--ink-dim);
  white-space: pre;
  font-size: 12px;
  line-height: 1.4;
}

/* nudge variant */
.tour-nudge {
  min-width: 240px;
  max-width: 320px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.45);
}
.tour-nudge .tour-tip-body { font-size: 12.5px; }

/* Back button on tip-style steps (engineIdx > 0). Same dim/quiet styling
   as .tour-skip — visually a "secondary" affordance compared to the accent
   .tour-next. Lives on the start-side of .tour-tip-actions because the row
   is `justify-content: flex-end` (so it sits left of skip/next on LTR,
   right of them on RTL — natural for a "back" gesture). */
.tour-tip .tour-back {
  opacity: 0.7;
  margin-inline-end: auto; /* pin to the opposite end from skip+next */
}

/* ===== Full-screen onboarding overlays (boot · theme picker · ready) =====

   These mount as full-viewport overlays on top of the running app. The
   user reads on-screen typewriter copy, picks a theme, then advances into
   the regular anchor-tip tour steps. Default theme is monokai so the
   accent green / dim grays already match the terminal aesthetic. */

.tour-fullscreen {
  position: fixed;
  inset: 0;
  z-index: var(--z-tour-tip, 9999);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  /* Two layers, both theme-driven so the backdrop tints with whichever
     theme is currently active (live-updates as the picker arrows
     through):
       1. radial accent-tinted glow at top — gives the screen a hint of
          the selected theme's "personality" color (green for monokai,
          purple for pearl, pink for blossom, etc.) without overwhelming.
       2. flat theme-bg at low opacity — keeps the underlying app
          subtly visible (the running app behind earns its keep) while
          adding just enough contrast to read the picker copy.
     CRT scanlines from body::before continue to bleed through. */
  background:
    radial-gradient(ellipse at 50% 30%,
      color-mix(in oklab, var(--accent, var(--green)) 12%, transparent) 0%,
      transparent 60%),
    color-mix(in oklab, var(--bg) 8%, transparent);
  /* Light blur — cards and columns behind are RECOGNIZABLE (you can see
     the kanban shape, you can read accent colors), just softened so
     they don't compete with the picker copy. Earlier 16px blur made
     the underlying app a featureless mush — the user wanted to "see
     the cards behind." 8px keeps content readable as context.
     RESTORED 2026-05-17 after a brief over-zealous removal — this is
     a purposeful use of glassmorphism, per DESIGN.md rule. */
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-family: var(--mono);
  color: var(--ink);
  animation: tour-fullscreen-fadein 240ms ease-out;
}

@keyframes tour-fullscreen-fadein {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.tour-fullscreen-card {
  width: min(620px, 92vw);
  padding: 32px 36px;
  background: var(--bg);
  border: 1px solid var(--accent, var(--green));
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--accent, var(--green)) 35%, transparent),
    0 12px 48px rgba(0,0,0,0.55);
  animation: tour-card-in 320ms cubic-bezier(.2,.7,.3,1.1);
}

@keyframes tour-card-in {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}

/* ----- boot screen ----- */

.tour-boot-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 18px;
}
.tour-boot-title {
  color: var(--accent, var(--green));
  font-weight: 600;
  letter-spacing: 0.02em;
}
.tour-boot-divider {
  color: var(--ink-dim);
  margin-block: 6px 18px;
  letter-spacing: 0.05em;
  font-size: 13px;
}
.tour-boot-body {
  font-size: 16px;
  line-height: 1.55;
  min-height: 120px; /* reserve space so the layout doesn't jump as letters appear */
  margin-block-end: 24px;
}
.tour-boot-line { display: block; }
.tour-boot-line-cont { padding-inline-start: 18px; }
.tour-boot-line-spacer { height: 12px; }
.tour-boot-prompt {
  color: var(--accent, var(--green));
  margin-inline-end: 6px;
  font-weight: 600;
}
.tour-boot-typeA, .tour-boot-typeB, .tour-boot-typeC,
.tour-ready-typeA, .tour-ready-typeB { white-space: pre-wrap; }

.tour-boot-hints {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--ink-dim);
}
.tour-boot-hint {
  flex: 1;
  padding: 6px 14px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  text-align: center;
  cursor: default;
  user-select: none;
  transition: border-color 120ms, color 120ms;
}
.tour-boot-caret {
  color: var(--accent, var(--green));
  font-size: 13px;
  /* Shares the .brand .cursor keyframes (defined above) so the boot
     screen's blink and the topbar shell-cursor's blink stay in sync —
     1s step-end keeps both ticks crisp and on the same beat. */
  animation: terminal-cursor-blink 1s step-end infinite;
}

/* ----- theme picker ----- */

.tour-tp-title {
  font-size: 18px;
  color: var(--accent, var(--green));
  font-weight: 600;
  margin-block-end: 4px;
}
.tour-tp-divider {
  color: var(--ink-dim);
  font-size: 13px;
  margin-block-end: 14px;
  letter-spacing: 0.05em;
}
.tour-tp-body {
  color: var(--ink-dim);
  font-size: 13px;
  margin-block-end: 22px;
}
.tour-tp-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-block-end: 22px;
}
.tour-tp-chip {
  all: unset;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--line-strong);
  background: var(--bg-cell, var(--bg));
  font-family: var(--mono);
  font-size: 14px;
  color: var(--ink);
  cursor: pointer;
  transition: border-color 120ms, background 120ms, transform 120ms;
}

/* Apply button — sits below the theme grid in the picker, centered
   so it reads as a single deliberate "commit my choice" CTA, not as
   yet another theme chip. Pill shape + accent fill differentiates it
   from the rectangular chips above. Always visible; on mobile it's
   the only path to advance, on desktop the chip-click already
   advances so this acts as a pointer-friendly fallback. */
.tour-tp-apply {
  all: unset;
  display: block;
  margin: 20px auto 0;
  padding: 12px 28px;
  min-width: 180px;
  text-align: center;
  background: var(--accent);
  color: var(--bg);
  font-family: var(--mono);
  font-size: 14px;
  font-weight: 700;
  border-radius: 999px;
  cursor: pointer;
  transition: background 120ms, transform 120ms;
}
.tour-tp-apply:hover { background: color-mix(in oklab, var(--accent) 85%, white); }
.tour-tp-apply:active { transform: translateY(1px); }
.tour-tp-apply:focus-visible { outline: 2px solid var(--bg); outline-offset: -4px; }
/* Hide the keyboard-driven body text + hint on mobile — both
   reference "arrow keys to preview · enter to confirm" (the desktop
   keyboard flow), which makes no sense on touch. The Apply button +
   tappable chips are self-explanatory. */
@media (max-width: 600px) {
  .tour-tp-body,
  .tour-tp-hint { display: none; }
}
.tour-tp-chip:hover { border-color: var(--ink-dim); }
.tour-tp-chip:focus-visible { outline: 2px solid var(--accent, var(--green)); outline-offset: 2px; }
.tour-tp-chip-highlight {
  border-color: var(--accent, var(--green));
  box-shadow: 0 0 0 1px var(--accent, var(--green));
  transform: translateY(-1px);
}
.tour-tp-chip-name { font-weight: 500; }
.tour-tp-chip-swatch {
  display: inline-block;
  width: 32px;
  height: 18px;
  border-radius: 2px;
  border: 1px solid var(--line-strong);
}
/* Per-theme swatch backgrounds — show what each theme looks like at a glance.
   These are independent of <html data-theme> so all 4 chips look right
   regardless of which theme is currently active. */
.tour-tp-chip-swatch[data-theme-swatch="terminal"]        { background: linear-gradient(135deg, #0a0e0a 0%, #0a0e0a 50%, #00ff7f 50%, #00ff7f 100%); }
.tour-tp-chip-swatch[data-theme-swatch="monokai"]         { background: linear-gradient(135deg, #272822 0%, #272822 50%, #a6e22e 50%, #a6e22e 100%); }
.tour-tp-chip-swatch[data-theme-swatch="gruvbox"]         { background: linear-gradient(135deg, #282828 0%, #282828 50%, #fe8019 50%, #fe8019 100%); }
.tour-tp-chip-swatch[data-theme-swatch="solarized-light"] { background: linear-gradient(135deg, #fdf6e3 0%, #fdf6e3 50%, #859900 50%, #859900 100%); }
.tour-tp-chip-swatch[data-theme-swatch="pearl"]           { background: linear-gradient(135deg, #faf8f5 0%, #faf8f5 50%, #6b5b95 50%, #6b5b95 100%); }
.tour-tp-chip-swatch[data-theme-swatch="blossom"]         { background: linear-gradient(135deg, #fff0f3 0%, #fff0f3 50%, #c2185b 50%, #c2185b 100%); }

/* Theme-picker chips paint each option in ITS OWN theme palette, not the
   currently-active theme. Without these overrides the four chips render
   identically (same bg, same border, same text colour) and only the small
   right-edge swatch hints at the difference. By forcing every chip to its
   target theme's actual bg + ink + accent, the picker becomes a real
   side-by-side preview — the user sees what they're choosing instead of
   four monochrome boxes with tiny corner stripes. The :hover/highlight
   border still uses the chip's own accent so the focus cue stays
   internally consistent within each chip. */
.tour-tp-chip[data-theme="terminal"] {
  background: #0a0b0a;
  color: #d7e0d4;
  border-color: #3a423a;
}
.tour-tp-chip[data-theme="terminal"] .tour-tp-chip-name { color: #5ef07a; }
.tour-tp-chip[data-theme="terminal"]:hover { border-color: #5ef07a; }
.tour-tp-chip[data-theme="terminal"].tour-tp-chip-highlight {
  border-color: #5ef07a;
  box-shadow: 0 0 0 1px #5ef07a, 0 0 16px rgba(94, 240, 122, 0.18);
}

.tour-tp-chip[data-theme="monokai"] {
  background: #272822;
  color: #f8f8f2;
  border-color: #49483e;
}
.tour-tp-chip[data-theme="monokai"] .tour-tp-chip-name { color: #a6e22e; }
.tour-tp-chip[data-theme="monokai"]:hover { border-color: #a6e22e; }
.tour-tp-chip[data-theme="monokai"].tour-tp-chip-highlight {
  border-color: #a6e22e;
  box-shadow: 0 0 0 1px #a6e22e, 0 0 16px rgba(166, 226, 46, 0.18);
}

.tour-tp-chip[data-theme="gruvbox"] {
  background: #282828;
  color: #ebdbb2;
  border-color: #3c3836;
}
.tour-tp-chip[data-theme="gruvbox"] .tour-tp-chip-name { color: #fe8019; }
.tour-tp-chip[data-theme="gruvbox"]:hover { border-color: #fe8019; }
.tour-tp-chip[data-theme="gruvbox"].tour-tp-chip-highlight {
  border-color: #fe8019;
  box-shadow: 0 0 0 1px #fe8019, 0 0 16px rgba(254, 128, 25, 0.20);
}

.tour-tp-chip[data-theme="solarized-light"] {
  background: #fdf6e3;
  color: #586e75;
  border-color: #e1dbc4;
}
.tour-tp-chip[data-theme="solarized-light"] .tour-tp-chip-name { color: #859900; }
.tour-tp-chip[data-theme="solarized-light"]:hover { border-color: #859900; }
.tour-tp-chip[data-theme="solarized-light"].tour-tp-chip-highlight {
  border-color: #859900;
  box-shadow: 0 0 0 1px #859900, 0 0 16px rgba(133, 153, 0, 0.22);
}

.tour-tp-chip[data-theme="pearl"] {
  background: #faf8f5;
  color: #2c2c2c;
  border-color: #e3e0d7;
}
.tour-tp-chip[data-theme="pearl"] .tour-tp-chip-name { color: #2f8a3f; }
.tour-tp-chip[data-theme="pearl"]:hover { border-color: #2f8a3f; }
.tour-tp-chip[data-theme="pearl"].tour-tp-chip-highlight {
  border-color: #2f8a3f;
  box-shadow: 0 0 0 1px #2f8a3f, 0 0 16px rgba(47, 138, 63, 0.22);
}

.tour-tp-chip[data-theme="blossom"] {
  background: #fff0f3;
  color: #3d2730;
  border-color: #f5c6d8;
}
.tour-tp-chip[data-theme="blossom"] .tour-tp-chip-name { color: #e8509c; }
.tour-tp-chip[data-theme="blossom"]:hover { border-color: #e8509c; }
.tour-tp-chip[data-theme="blossom"].tour-tp-chip-highlight {
  border-color: #e8509c;
  box-shadow: 0 0 0 1px #e8509c, 0 0 16px rgba(232, 80, 156, 0.22);
}

.tour-tp-hint {
  color: var(--ink-dim);
  font-size: 13px;
  text-align: center;
}

/* ----- locale picker ----- */

.tour-lp-icon {
  font-size: 38px;
  text-align: center;
  margin-block-end: 6px;
}
.tour-lp-divider {
  color: var(--ink-dim);
  font-size: 13px;
  text-align: center;
  letter-spacing: 0.05em;
  margin-block-end: 18px;
}
.tour-lp-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-block-end: 22px;
  /* 18 locale chips = 6 rows. Cap the grid to the visible viewport and let
     it scroll, so the card's hint + advance affordance never get pushed
     off-screen on short viewports. 100dvh ÷ --ui-zoom per rule 10 (body
     zoom); the ~250px subtracts the icon, divider, hint and card padding
     above/below the grid. Languages are never dropped to fit. */
  max-height: calc((100dvh / var(--ui-zoom, 1)) - 250px);
  overflow-y: auto;
}
.tour-lp-chip {
  all: unset;
  padding: 12px 10px;
  border: 1px solid var(--line-strong);
  background: var(--bg-cell, var(--bg));
  font-family: var(--mono);
  font-size: 14px;
  color: var(--ink);
  text-align: center;
  cursor: pointer;
  transition: border-color 120ms, transform 120ms, background 120ms;
  /* The native names are mostly short, but a few — পর্তুগিজ-style
     scripts — render taller. min-width: 0 keeps the grid honest. */
  min-width: 0;
  overflow-wrap: anywhere;
}
.tour-lp-chip:hover { border-color: var(--ink-dim); }
.tour-lp-chip:focus-visible { outline: 2px solid var(--accent, var(--green)); outline-offset: 2px; }
.tour-lp-chip-highlight {
  border-color: var(--accent, var(--green));
  box-shadow: 0 0 0 1px var(--accent, var(--green));
  transform: translateY(-1px);
}
.tour-lp-hint {
  color: var(--ink-dim);
  font-size: 13px;
  text-align: center;
}

/* ----- ready screen ----- */

.tour-ready-body {
  font-size: 16px;
  line-height: 1.6;
  min-height: 80px;
  margin-block-end: 24px;
}
.tour-ready-line { display: block; }
.tour-ready-line-spacer { height: 10px; }
/* Tagline points forward — slightly larger + accent-tinted so it
   reads as the "what's next" call-out, separating the title beat
   ("workspace ready") from the housekeeping body ("[esc] to skip"). */
.tour-ready-tagline {
  color: color-mix(in oklab, var(--accent, var(--green)) 70%, var(--ink));
  font-size: 17px;
}
.tour-ready-sub {
  color: var(--ink-dim);
  font-size: 14px;
}

/* ----- smooth theme crossfade (only while theme picker is mounted) -----

   The theme picker live-applies <html data-theme="..."> as the user
   arrows through chips. By default that swap is instant — every CSS
   variable resolves to a new value at once and surfaces snap. The
   .tour-theming class added to <html> while the picker is mounted
   adds a 240ms transition on the major palette properties for the
   visible app surfaces, so the swap feels like a fade rather than a
   cut. Removed when the picker unmounts so the rest of the app
   doesn't pay the transition cost on theme changes from settings. */

html.tour-theming,
html.tour-theming body,
html.tour-theming .topbar,
html.tour-theming .col,
html.tour-theming .col-header,
html.tour-theming .card,
html.tour-theming .today-cal-strip,
html.tour-theming .quote-bar,
html.tour-theming .tour-fullscreen,
html.tour-theming .tour-fullscreen-card,
html.tour-theming .tour-tp-chip {
  transition:
    background-color 240ms ease,
    color            240ms ease,
    border-color     240ms ease,
    box-shadow       240ms ease;
}

/* ----- reduced-motion guard ----- */

@media (prefers-reduced-motion: reduce) {
  .tour-fullscreen        { animation: none; }
  .tour-fullscreen-card   { animation: none; }
  .tour-boot-caret        { animation: none; opacity: 1; }
  .brand .cursor          { animation: none; opacity: 1; }
  .tour-tp-chip           { transition: none; }
  .tour-tp-chip-highlight { transform: none; }
  /* Skip the theme-fade entirely — instant swap is preferred under
     reduced motion, even though the cross-fade is short. */
  html.tour-theming, html.tour-theming * { transition: none !important; }
}

/* === app modal (in-app confirm/alert) === */

.app-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay-top);
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  animation: app-modal-fade 120ms ease-out;
}
@keyframes app-modal-fade { from { opacity: 0; } to { opacity: 1; } }

.app-modal {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--ink-dim);
  box-shadow: 0 12px 32px rgba(0,0,0,0.55);
  padding: 16px 18px;
  min-width: 280px;
  max-width: 440px;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
}
.app-modal-title {
  color: var(--accent, var(--green));
  font-weight: bold;
  margin-block-end: 8px;
}
.app-modal-body { color: var(--ink); opacity: 0.92; }
/* Single-line text input for showPromptDialog (e.g., the "Block with
   reason..." right-click action). Sits between body text and the action
   buttons; sized to the modal's full width. */
.app-modal-input-wrap {
  margin-block-start: 12px;
}
.app-modal-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-card, var(--bg));
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 8px 10px;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
}
.app-modal-input:focus {
  border-color: var(--accent, var(--green));
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent, var(--green)) 25%, transparent);
}
.app-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-block-start: 14px;
}
.app-modal-btn {
  all: unset;
  cursor: pointer;
  padding: 5px 12px;
  border: 1px solid var(--ink-dim);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
}
.app-modal-btn:hover   { background: var(--ink-faint, rgba(255,255,255,0.06)); }
.app-modal-btn:focus-visible {
  outline: 2px solid var(--accent, var(--green));
  outline-offset: 1px;
}
.app-modal-btn.confirm     { border-color: var(--accent, var(--green)); color: var(--accent, var(--green)); }
.app-modal-btn.destructive { border-color: var(--red); color: var(--red-text); }
.app-modal-btn.destructive:hover { background: color-mix(in oklab, var(--red) 12%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .app-modal-backdrop { animation: none; }
}

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

/* Sessions sheet — per-task timer history. Right-side sidebar on desktop,
   bottom sheet on mobile. Triggered from the card action menu's "Sessions"
   item. Reuses the drawer-backdrop z-index so it composites correctly above
   the rest of the app. */
.sessions-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  z-index: var(--z-drawer-backdrop, 250);
  display: flex;
  justify-content: flex-end;
  align-items: stretch;
  animation: sessions-sheet-fade 140ms ease-out;
}
@keyframes sessions-sheet-fade { from { opacity: 0; } to { opacity: 1; } }

.sessions-sheet {
  width: min(420px, 92vw);
  background: var(--bg-raised, var(--bg));
  border-inline-start: 1px solid var(--accent-dim);
  box-shadow: -16px 0 40px rgba(0,0,0,0.45);
  display: flex;
  flex-direction: column;
  padding: 14px 16px 18px;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  animation: sessions-sheet-slide 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes sessions-sheet-slide {
  from { transform: translateX(20px); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}
[dir="rtl"] .sessions-sheet {
  animation-name: sessions-sheet-slide-rtl;
}
@keyframes sessions-sheet-slide-rtl {
  from { transform: translateX(-20px); opacity: 0; }
  to   { transform: translateX(0);      opacity: 1; }
}

.sessions-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-end: 10px;
}
.sessions-sheet-title {
  color: var(--accent-text);
  font-weight: bold;
  font-size: 14px;
}
.sessions-sheet-close {
  all: unset;
  cursor: pointer;
  color: var(--ink-dim);
  font-size: 18px;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 4px;
  transition: background 120ms, color 120ms;
}
.sessions-sheet-close:hover {
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: var(--ink);
}
.sessions-sheet-task {
  color: var(--ink-secondary);
  font-size: 12px;
  margin-block-end: 12px;
  border-block-end: 1px solid var(--line);
  padding-block-end: 10px;
  word-break: break-word;
}
.sessions-sheet-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
}
.sessions-sheet .session-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  border-block-end: 1px dashed color-mix(in oklab, var(--ink) 8%, transparent);
  font-size: 12px;
}
.sessions-sheet .session-row .t { color: var(--ink); }
.sessions-sheet .session-row .arrow { color: var(--ink-faint); }
.sessions-sheet .session-row .dur {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.sessions-sheet-empty {
  color: var(--ink-faint);
  font-size: 12px;
  padding: 24px 0;
  text-align: center;
}
.sessions-sheet-total {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-block-start: 10px;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--accent-dim);
  font-weight: bold;
  color: var(--ink);
}
.sessions-sheet-total .ltr-num {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 600px) {
  .sessions-sheet-backdrop {
    align-items: flex-end;
    justify-content: stretch;
  }
  .sessions-sheet {
    width: 100%;
    max-width: 100%;
    border-inline: none;
    border-block-start: 1px solid var(--accent-dim);
    border-start-start-radius: 14px;
    border-start-end-radius: 14px;
    box-shadow: 0 -16px 40px rgba(0,0,0,0.45);
    max-height: 85vh;
    padding-block-end: calc(18px + env(safe-area-inset-bottom, 0px));
    animation: sessions-sheet-slide-up 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  [dir="rtl"] .sessions-sheet { animation-name: sessions-sheet-slide-up; }
}
@keyframes sessions-sheet-slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .sessions-sheet-backdrop, .sessions-sheet { animation: none; }
}

/* Estimation popover — typed-duration custom row. Sits below the chip row
   so users can either pick a preset or type any duration (Reminders/ATask
   style). Validation surfaces inline via .est-custom-err. */
.est-quick-popover .est-custom-row {
  display: flex;
  gap: 6px;
  margin-block-start: 8px;
}
.est-quick-popover .est-custom-input {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  padding: 6px 8px;
  outline: none;
  min-width: 0;
}
.est-quick-popover .est-custom-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 25%, transparent);
}
.est-quick-popover .est-custom-input.invalid {
  border-color: var(--red);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--red) 25%, transparent);
}
.est-quick-popover .est-custom-set {
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent-text);
  border-radius: 4px;
  padding: 5px 12px;
  font: inherit;
  font-size: 12px;
}
.est-quick-popover .est-custom-set:hover {
  background: color-mix(in oklab, var(--accent) 12%, transparent);
}
.est-quick-popover .est-custom-err {
  color: var(--red-text);
  font-size: 11px;
  margin-block-start: 6px;
}
.est-quick-popover .est-clear-btn {
  margin-block-start: 8px;
  align-self: flex-start;
}

/* Mobile: confirm/prompt dialogs render as a bottom sheet so the buttons
   are easy to thumb-reach and the body text stays clearly visible above
   the touch target. The backdrop pins content to the bottom edge; the
   modal itself fills the inline axis and slides up from the bottom. */
@media (max-width: 600px) {
  .app-modal-backdrop {
    align-items: flex-end;
    padding: 0;
    animation: app-modal-sheet-fade 160ms ease-out;
  }
  .app-modal {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    border-inline: none;
    border-block-end: none;
    border-start-start-radius: 14px;
    border-start-end-radius: 14px;
    padding: 18px 18px calc(22px + env(safe-area-inset-bottom, 0px));
    font-size: 14px;
    animation: app-modal-sheet-slide 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .app-modal-actions {
    flex-direction: column-reverse;
    gap: 10px;
    margin-block-start: 18px;
  }
  .app-modal-actions .app-modal-btn {
    box-sizing: border-box;
    width: 100%;
    padding: 12px 14px;
    text-align: center;
    font-size: 14px;
    border-radius: 8px;
  }
}
@keyframes app-modal-sheet-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes app-modal-sheet-slide {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  @media (max-width: 600px) {
    .app-modal-backdrop, .app-modal { animation: none; }
  }
}

/* ===================================================================
   Blocked flag — pill on card, red-dashed card outline
   =================================================================== */

.card.blocked {
  border-color: var(--red);
  border-style: dashed;
  background: linear-gradient(90deg, var(--red-glow), transparent 40%), var(--bg-card);
}
.card.blocked:hover { background: linear-gradient(90deg, var(--red-glow), transparent 40%), var(--bg-card-hov); }

/* Paused state — added in v9 alongside the doing/hold collapse. Same
   visual pattern as .card.blocked (dashed border + glow gradient) but
   amber instead of red; pause is a less-critical interrupt than block.
   When BOTH are set, .card.blocked wins via cascade order — blocked is
   the more important condition to surface, the paused-pill in the chips
   row still tells the user the timer is also paused. */
/* Paused styling MUST NOT touch the inline-start border — that's where
   the priority stripe lives (`.card[data-priority] { border-inline-start:
   3px solid var(--red/amber/accent) }`). Earlier this rule used
   `border-color: var(--amber); border-style: dashed` shorthand, which
   overrode the priority color and the user lost their priority cue.
   Only paint the block + inline-end borders amber/dashed; the inline-
   start stripe stays whatever priority assigned it. */
.card.paused {
  border-block-color: var(--amber);
  border-inline-end-color: var(--amber);
  border-block-style: dashed;
  border-inline-end-style: dashed;
  background: linear-gradient(90deg, var(--amber-glow), transparent 40%), var(--bg-card);
}
.card.paused:hover { background: linear-gradient(90deg, var(--amber-glow), transparent 40%), var(--bg-card-hov); }

/* Design pass §3 — color tone alone signals state. The leading glyph (⊘ for
   blocked, ⏸ for paused) plus the colored ink carry the signal without the
   background tint that fights the calm card. */
.pill.blocked-pill { color: var(--red-text); }
.pill.paused-pill  { color: var(--amber-text); }

/* Block CTA — the empty/dashed counterpart of blocked-pill. Always
   present on non-blocked, non-done cards as a "click to block" target
   on the chips row. Same shape as the +schedule / +est / +due empty
   chips (dashed grey, "+ block" label). On click, blocks the task and
   the next render replaces this chip with .pill.blocked-pill. */
/* Block chip — empty state inherits the unified .card .pill.empty look
   (dashed --ink-faint border, ink-tertiary color). Only the hover color
   diverges (red, since this CTA blocks the task). */
.pill.block-chip.empty { cursor: pointer; }
.card .pill.block-chip.empty:hover { color: var(--red-text); border-color: var(--red); }

.blocked-row { display: flex; flex-direction: column; gap: 6px; }
.blocked-toggle {
  text-align: start;
  color: var(--ink);
  border-color: var(--line-strong);
  font-family: var(--mono);
}
.blocked-toggle.on {
  color: var(--red-text);
  border-color: var(--red);
  background: var(--red-glow);
}
#d-block-reason[disabled] { opacity: 0.4; }

/* Per-card block CTAs (visible only on hover) */
.card-actions {
  display: flex;
  gap: 4px;
  padding-inline-start: 22px;
  margin-top: 4px;
}

/* Block CTAs share the same dashed-pill idiom as the empty `+ due` /
   `+ schedule` placeholders so the card's "things you can set" affordances
   read as one coherent group rather than four different shapes. */
.block-cta {
  background: transparent;
  border: 1px dashed var(--line-strong);
  color: var(--ink-faint);
  padding: 2px 8px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.04em;
}
.block-cta:hover { color: var(--red-text); border-color: var(--red); background: var(--red-glow); }
.block-cta.reason { font-style: italic; }

.block-reason-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  margin-top: 4px;
  margin-inline-start: 22px;
  border: 1px solid var(--red);
  border-radius: 3px;
  background: var(--red-glow);
}
.block-reason-row .block-reason-input {
  flex: 1;
  min-height: 16px;
  max-height: 200px;
  background: transparent;
  border: none;
  outline: none;
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  line-height: 1.4;
  resize: none;
  overflow-y: auto;
  padding: 0;
  display: block;
}
.block-reason-row .hint {
  color: var(--ink-faint);
  font-size: 10px;
  white-space: nowrap;
  align-self: flex-end;
  padding-block-end: 1px;
}

/* Clickable blocked pill — click toggles unblock; native title attribute
   shows the full untruncated reason on hover. */
.pill.blocked-pill { cursor: pointer; }
.pill.blocked-pill:hover { background: var(--red); color: var(--bg); }

/* ===================================================================
   Hide-done mode — 3-column grid instead of 4
   =================================================================== */

.board.hide-done .col-headers {
  /* Done hidden → 2 visible status columns. */
  grid-template-columns: 1fr 1fr;
}
/* hide-done: cells-row drops to 2 columns. Group-row itself remains a
   block (label on top, cells-row below) — no grid-template-areas needed. */

/* ===================================================================
   Group name inline edit
   =================================================================== */

.gname { cursor: text; }
/* Edit-in-place on the span itself — keeps the same width, font, and wrap
   behavior as the displayed name so the multi-line layout (e.g. an "Axiom
   tasks" name that breaks across two lines) stays identical while editing.
   The accent ring + bg tint signal the editing state without resizing. */
.gname-editing {
  outline: none;
  background: color-mix(in oklab, var(--accent) 8%, var(--bg));
  border-radius: 2px;
  padding: 0 4px;
  margin: 0 -4px;
  box-shadow: 0 0 0 1px var(--accent), 0 0 0 3px var(--accent-glow);
  white-space: normal;
  cursor: text;
}
/* Legacy single-line input editor — preserved for any path that still
   instantiates it (palette commands, drawer actions). */
.gname-edit {
  font: inherit;
  font-weight: 600;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--accent);
  outline: none;
  border-radius: 2px;
  padding: 1px 4px;
  width: 140px;
  box-shadow: 0 0 0 3px var(--accent-glow);
}

/* Persistent "+ new group" row at the bottom of the board */
.new-project-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  margin-top: 12px;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  background: var(--bg-cell);
  color: var(--ink-faint);
  font-size: 12px;
  cursor: text;
  max-width: 260px;
}
.new-project-row:hover { border-color: var(--accent-dim); color: var(--ink-dim); }
.new-project-row .plus { color: var(--accent-text); }
.new-project-row input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--ink);
  font: inherit;
}
.new-project-row .hint { color: var(--ink-faint); font-size: 11px; }

/* ---- Drawer Schedule section ---- */
.schedule-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
  align-items: start;
}
.schedule-col { display: flex; flex-direction: column; gap: 4px; }
.schedule-label {
  font-size: 10px;
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.schedule-col input[type="text"],
.schedule-col input[type="datetime-local"] {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 6px 8px;
  font: inherit;
}
.schedule-col input.invalid { border-color: var(--red); }
.schedule-error { font-size: 10px; color: var(--red-text); }
.schedule-clear {
  align-self: flex-start;
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-faint);
  border-radius: 2px;
  padding: 2px 6px;
  font-size: 10px;
  cursor: pointer;
}
.schedule-clear:disabled { opacity: 0.4; cursor: not-allowed; }
.schedule-end { color: var(--ink-dim); font-size: 12px; padding-top: 6px; }
/* Design pass §3 — filled pills lose their box. Color carries the role. */
.pill.est-pill { color: var(--ink-tertiary); cursor: pointer; }
.pill.est-pill:hover { color: var(--accent-text); }
.pill.sched-pill {
  color: var(--blue, #6aa3d6);
  cursor: pointer;
}
.pill.sched-pill:hover { color: var(--accent-text); }

/* Past-scheduled — start time is in the past, click to re-schedule. */
.pill.sched-pill.overdue-sched { color: var(--red-text); }
.pill.sched-pill.overdue-sched:hover { color: var(--red-text); }

/* Empty schedule chip — inherits the unified .card .pill.empty look so
   +schedule / +est / +due / ⊘block all read the same. Hover handled by
   the shared rule. */
.pill.schedule-pill.empty { cursor: pointer; }

/* Schedule popover (re-uses .due-quick-popover positioning + chip styles). */
.sched-quick-popover { width: 280px; }
/* Summary header is shared between schedule and due popovers — same
   shape, same affordances. Scoping on .due-quick-popover catches both
   (the schedule popover sets that class too at construction time). */
.due-quick-popover .sqp-summary {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--ink-dim);
  padding-block-end: 4px;
  border-block-end: 1px dashed var(--line);
}
.due-quick-popover .sqp-summary .sqp-text.overdue { color: var(--red-text); }
.due-quick-popover .sqp-summary .sqp-clear {
  margin-inline-start: auto;
  background: transparent; border: 0; color: var(--ink-faint);
  cursor: pointer; font-size: 14px; line-height: 1; padding: 0 4px;
}
.due-quick-popover .sqp-summary .sqp-clear:hover { color: var(--red-text); }
/* Inline past-time warning. Lives in the schedule popover only — the
   user is already inside the modal context and seeing this means they
   know before committing. After they dismiss (outside click / Escape)
   no further nag fires; closing IS the accept signal. */
.sched-quick-popover .sqp-past-warn {
  font-size: 11px;
  color: var(--red-text);
  background: var(--red-glow);
  border: 1px solid color-mix(in oklab, var(--red) 35%, transparent);
  border-radius: var(--radius);
  padding: 4px 8px;
  margin-block-start: 4px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.sched-quick-popover .sqp-section-label {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-block-start: 4px;
}
.sched-quick-popover .dqp-chip.active {
  color: var(--accent-text);
  border-color: var(--accent);
  background: var(--accent-glow);
}
.sched-quick-popover .sqp-empty {
  font-size: 11px;
  color: var(--ink-faint);
  font-style: italic;
  padding-block: 2px;
}


/* ---- Settings: Google Calendar list ----
   Grid columns: [checkbox: auto] [dot: auto] [name: minmax(0, 1fr)].
   The `minmax(0, 1fr)` is the key — unlike `1fr` alone, it explicitly
   allows the track to shrink below its content min-width, which lets
   the name column truncate with ellipsis. Previous flex attempts hit
   min-content gotchas that collapsed the name to zero or broke it
   character-by-character.
   ----------------------------------------------------------------- */
#s-google-calendars {
  display: block;
  max-height: 280px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 4px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: color-mix(in oklab, var(--bg-card) 60%, transparent);
}
.cal-check {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  column-gap: 10px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 12px;
  color: var(--ink);
  border-radius: 2px;
  transition: background 120ms;
}
.cal-check:hover { background: var(--bg-card-hov); color: var(--accent-text); }
.cal-check input[type="checkbox"] { margin: 0; }
.cal-check .cal-dot {
  width: 10px; height: 10px; border-radius: 50%;
  display: inline-block;
}
.cal-check .cal-name {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cal-check .cal-primary {
  color: var(--ink-faint);
  font-size: 10px;
  margin-inline-start: 6px;
}

/* ---- Drawer Color picker ---- */
.color-row {
  display: flex; flex-wrap: wrap; gap: 6px;
  align-items: center;
}
.color-swatch {
  width: 26px; height: 26px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--bg);
  cursor: pointer;
  padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  transition: transform 80ms;
}
.color-swatch:hover { transform: scale(1.12); border-color: var(--ink-dim); }
.color-swatch .color-swatch-dot {
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--swatch-color, var(--ink-faint));
  display: inline-block;
}
/* Outer accent ring with a 2px gap, so a selected swatch reads as
   "ringed for selection" rather than "filled" — the previous
   inset-shadow + dark-border combo collided visually with the
   filled-circle done checkbox (.card.done .checkbox). */
/* Outer accent ring with a real transparent gap, so a selected swatch
   reads as "ringed for selection" rather than "filled" — the previous
   inset-shadow + dark-border combo collided visually with the
   filled-circle done checkbox (.card.done .checkbox).
   `outline-offset` rather than a layered box-shadow because the gap
   needs to show whatever surface the swatch sits on (--bg-card in the
   `.project-color-pop` popover, --bg-raised in the task drawer). A solid
   bg-card layer would leave a visible seam on the drawer surface in
   themes where bg-card ≠ bg-raised. */
.color-swatch.selected {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.color-swatch.color-clear {
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1;
}
.color-preview-row {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px;
}
.color-preview-label {
  font-size: 10px; color: var(--ink-faint);
  letter-spacing: 0.06em; text-transform: uppercase;
  min-width: 60px;
}
.color-preview-card {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 6px 10px;
  flex: 1;
  font-size: 12px;
  color: var(--ink);
  /* Instant — no transition lag — so hover preview snaps immediately. */
  transition: none;
}

/* ---- Calendar inline config popover ---- */
.cal-config-btn { letter-spacing: 0; }

.cal-config-pop {
  position: fixed;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  z-index: 30;
  display: flex; flex-direction: column; gap: 6px;
  min-width: 240px;
  color: var(--ink);
  font-size: 12px;
}
.cal-config-row {
  display: flex; align-items: center; gap: 8px;
}
.cal-config-row label {
  color: var(--ink-faint);
  font-size: 11px;
  min-width: 110px;
}
.cal-config-row input[type="number"] {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  padding: 3px 6px;
  font: inherit;
}
.cal-config-quick {
  flex: 1;
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 4px 8px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit; font-size: 11px;
  text-align: start;
}
.cal-config-quick:hover { border-color: var(--accent); color: var(--accent-text); }

/* ---- Sticky rail: keep unscheduled visible while page scrolls ----
   main.calendar sticks to the top of the viewport once the user scrolls past
   the topbar/quotebar above it. The max-height ceiling keeps its bottom
   edge above the fixed .statusbar so the calendar never extends behind it.
   100dvh follows the iOS Safari address-bar dynamic viewport so on phones
   the calendar fits whatever the *current* visible viewport actually is.
   Cal-grid and cal-rail-list inherit the same ceiling so their overflow
   clips cleanly at the bottom edge, not somewhere behind the footer. */
/* Chrome above main: topbar (sticky, min-height 52px) + quote-bar (~34px:
   padding 8+8 + ~17px line + 1px border). Hardcoded as a CSS var here
   instead of the sticky/sub calc so the calendar fits between top chrome
   and bottom statusbar at scrollY=0 — the previous calc only accounted
   for the bottom statusbar, so main.calendar overflowed by chrome-above-
   height and covered the footer until the user scrolled past the chrome. */
:root { --cal-chrome-above: 86px; }
/* Desktop only — sticky-top + max-height + overflow:hidden is the
   side-rail-friendly layout where the calendar sticks above the
   statusbar while the page scrolls behind it. On mobile the calendar
   IS the page (mobile.css resets main.calendar to position:static +
   overflow:visible inside its @media block); leaving this rule
   unconditional means it wins the source-order tie against mobile.css
   (this file loads later in index.html), clipping main.calendar to one
   viewport-minus-chrome and freezing page scroll on the calendar view.
   The @media gate restores mobile's normal page-scroll behaviour. */
@media (min-width: 601px) {
  main.calendar {
    position: sticky;
    top: 0;
    max-height: calc(100dvh - var(--cal-chrome-above) - var(--statusbar-h) - env(safe-area-inset-bottom));
    overflow: hidden;
    width: 100%;
    border-bottom: 1px solid var(--line);
  }
}
/* When the calendar view is rendered, drop body's statusbar-reservation
   padding AND #app's min-height: 100vh — both are needed for board view
   (kanban tail breathes above the fixed statusbar; #app fills the viewport
   so footers / drawers anchor cleanly), but on calendar view they double-
   reserve the same 34px statusbar space that main.calendar's max-height
   already accounted for, producing a 34px page scroll on every render.
   :has() lets us key off "this view contains main.calendar" without a JS
   class toggle — modern Chrome / Firefox / Safari all support it. */
/* Desktop only. On mobile the calendar uses normal page scroll, so the
   statusbar reservation (body padding) and #app's min-height: 100vh stay
   in place — same kanban-like vertical scroll behaviour as the board. */
@media (min-width: 601px) {
  body:has(main.calendar),
  body.cal-active { padding-block-end: 0; }
  body:has(main.calendar) #app,
  body.cal-active #app { min-height: 0; }
}
/* Calendar view: switch statusbar from fixed-overlay to in-flow sticky.
   Body becomes a flex column so #app fills the area above the statusbar
   and the statusbar takes its natural height as the last flex item.
   This is what fixes the long-standing "calendar grid extending behind
   the footer" bug — with statusbar in flow, the grid physically cannot
   extend past it (CSS box model pushes it up). Calc-based heights up
   the chain (#app, main.calendar) no longer need to subtract
   --statusbar-h because the flex layout reserves the space directly. */
@media (min-width: 601px) {
  body.cal-active {
    display: flex;
    flex-direction: column;
  }
  body.cal-active > #app { flex: 1 1 auto; min-height: 0; }
  body.cal-active .statusbar {
    position: sticky;
    bottom: 0;
  }
}
/* Lock BOTH <html> and <body> to viewport on calendar view as the FINAL
   backstop. body alone was not enough — live diagnostics showed html's
   scrollHeight exceeded its clientHeight by ~96px (a fixed-positioned
   descendant whose computed bounds extend past viewport contributes to
   the document's scrollable area). overflow:hidden on html makes it a
   scroll container with scrollbars suppressed (so scrollbar-gutter:
   stable still reserves the gutter, no board↔calendar horizontal
   jitter), while height:100dvh + the locked body+#app prevents any
   actual scrollable content. body uses overflow:clip (not hidden) so it
   doesn't become a scroll container and inner overflow:auto containers
   like .cal-grid keep their normal scroll behaviour. */
/* Desktop only — on mobile the calendar lays out as a normal page-
   scrolling element (the @media (max-width: 600px) block below sets
   main.calendar { max-height: none; position: static; overflow: visible }
   so kanban-style page scroll behaviour applies, since mobile has no
   side rail and the calendar IS the page). Locking html / body on
   mobile froze the page entirely and the user couldn't scroll the
   calendar at all. */
@media (min-width: 601px) {
  /* Force explicit heights down the chain instead of relying on max-height
     to cap. Live diagnostics showed main.calendar's offsetHeight = 944 even
     when its computed max-height was 858 — flex-item shrink semantics +
     grid auto rows let content push the box past max-height in some cases.
     `height` is unambiguous: the box IS this tall, period. overflow:hidden
     clips children to that exact box. */
  /* Divide every dvh-based height by --ui-zoom (set by applyUiScale).
     `body { zoom: 1.1 }` makes 100dvh resolve to viewport-rendered px
     in body's CSS coord system, but rendered output = CSS × zoom — so
     plain 100dvh overflows viewport by (zoom − 1). Dividing by zoom
     brings rendered height back to viewport exactly. */
  body.cal-active #app {
    height: calc(100dvh / var(--ui-zoom, 1));
    min-height: 0;
  }
  body.cal-active main.calendar {
    /* Divide first (gives CSS-px viewport), then subtract chrome heights
     in CSS px directly. Distributing the divide over chrome/statusbar
     would scale them too, double-shrinking. */
    height: calc((100dvh / var(--ui-zoom, 1)) - var(--cal-chrome-above) - var(--statusbar-h) - env(safe-area-inset-bottom));
    max-height: none;
    flex: none;
  }
  html.cal-active {
    /* html is OUTSIDE body's zoom context, so 100dvh here is unscaled. */
    height: 100dvh;
    overflow: hidden;
  }
  body.cal-active {
    height: calc(100dvh / var(--ui-zoom, 1));
    overflow: clip;
  }
}
/* Removed cal-grid + cal-rail-list explicit max-height calcs — the
   .cal-layout { grid-template-rows: minmax(0, 1fr) } above propagates
   main.calendar's max-height down to cal-left, and cal-grid's flex:1 +
   min-height:0 then fills cal-left minus the cal-header's height. Same
   for cal-rail-list. The duplicate max-height was off-by-cal-header,
   making the row taller than main.calendar's overflow:hidden ceiling
   and clipping the last hour. */

/* ---- Google Sign-In button (official dark variant, Material Design) ---- */
/* https://developers.google.com/identity/branding-guidelines — "Sign in with Google" */
.btn-gsi {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 14px;
  background: #131314;
  border: 1px solid #8e918f;
  border-radius: 20px;
  color: #e3e3e3;
  font-family: "Roboto", "Helvetica Neue", Arial, sans-serif;
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.25px;
  cursor: pointer;
  white-space: nowrap;
  transition: box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
}
.btn-gsi > span { white-space: nowrap; }
.btn-gsi:hover {
  background: #1e1f20;
  border-color: #a8abaa;
  box-shadow: 0 1px 3px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.2);
}
.btn-gsi:active { background: #2a2b2c; }
.btn-gsi:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-gsi .gsi-logo {
  width: 18px; height: 18px; flex: 0 0 18px;
  display: block;
}
.btn-gsi-compact {
  height: 32px; padding: 0 10px; border-radius: 16px;
  font-size: 13px; gap: 8px;
}
.btn-gsi-compact .gsi-logo { width: 16px; height: 16px; flex-basis: 16px; }

.cal-gcal-status {
  font-size: 11px;
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: 2px;
  letter-spacing: 0.04em;
  background: transparent;
  cursor: pointer;
  font: inherit;
  transition: color 80ms ease, border-color 80ms ease, background 80ms ease;
}
.cal-gcal-status.on {
  color: var(--accent-text);
  border-color: var(--accent-dim);
}
.cal-gcal-status.off {
  color: var(--ink-faint);
  border-color: var(--line-strong);
}
.cal-gcal-status:hover { background: color-mix(in oklab, var(--accent) 6%, transparent); }

.project-color-btn {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 1px solid var(--ink-faint);
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0;
  margin: 0 6px 0 2px;
  flex: 0 0 14px;
  font-size: 11px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0.5; /* unset state — discoverable but not loud */
  transition: transform 80ms ease, opacity 80ms ease, color 80ms ease;
}
.project-color-btn.has-color { opacity: 1; }
.project-color-btn:hover {
  transform: scale(1.15);
  opacity: 1;
  color: var(--ink);
}

.project-color-pop {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 6px;
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  z-index: 200;
  box-shadow: 0 6px 18px rgba(0,0,0,0.5);
}
.project-color-pop .color-swatch { width: 20px; height: 20px; }

/* Custom-colour trigger — a native <input type="color"> dressed as one
   of the popover's round swatches. The conic-gradient ring marks it as
   the "any colour" picker; the input itself shows the current colour. */
.project-color-pop .color-native {
  width: 20px;
  height: 20px;
  padding: 2px;
  border-radius: 50%;
  display: inline-flex;
  flex: 0 0 auto;
  cursor: pointer;
  background: conic-gradient(from 0deg,
    #ff5a5a, #ffd34d, #5ee06b, #5ec7e0, #6f8bff, #c38bff, #ff5a5a);
}
.color-native-input {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}
.color-native-input::-webkit-color-swatch-wrapper { padding: 0; }
.color-native-input::-webkit-color-swatch { border: none; border-radius: 50%; }
.color-native-input::-moz-color-swatch { border: none; border-radius: 50%; }

/* ---- Fast task hover preview (tooltip card) ---- */
.task-hover-preview {
  position: fixed;
  z-index: var(--z-overlay-top);
  max-width: 320px;
  min-width: 200px;
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  padding: 10px 12px;
  font-size: 12px;
  color: var(--ink);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  opacity: 0;
  transform: translateY(2px);
  transition: opacity 90ms ease, transform 90ms ease;
  line-height: 1.4;
  display: flex; flex-direction: column; gap: 4px;
}
.task-hover-preview.visible {
  opacity: 1;
  transform: translateY(0);
}
.thp-title-row {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 2px;
}
.thp-color {
  width: 10px; height: 10px; border-radius: 50%;
  border: 1px solid var(--ink-faint);
  flex: 0 0 10px;
}
.thp-title {
  font-weight: 600; color: var(--ink);
  word-break: break-word;
}
.thp-meta {
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  font-size: 11px; color: var(--ink-dim);
}
.thp-dot { color: var(--ink-faint); }
.thp-when { color: var(--ink); font-variant-numeric: tabular-nums; }
.thp-dur { color: var(--ink-faint); }
.thp-group { color: var(--ink); }
.thp-status { text-transform: uppercase; letter-spacing: 0.04em; font-size: 10px; }
.thp-status.thp-doing { color: var(--accent-text); }
.thp-status.thp-hold { color: var(--amber-text); }
.thp-status.thp-backlog { color: var(--ink-faint); }
.thp-status.thp-done { color: var(--ink-faint); text-decoration: line-through; }
.thp-blocked { color: var(--red-text); }
.thp-pri { text-transform: uppercase; font-size: 10px; letter-spacing: 0.04em; }
.thp-pri-urgent { color: var(--red-text); }
.thp-pri-high { color: var(--amber-text); }
.thp-pri-med { color: var(--ink-dim); }
.thp-pri-low { color: var(--ink-faint); }
.thp-due { color: var(--amber-text); }
.thp-assignees { font-size: 11px; color: var(--ink-dim); }
.thp-notes {
  font-size: 11px; color: var(--ink-dim);
  border-top: 1px solid var(--line-strong);
  padding-top: 6px; margin-top: 2px;
  white-space: pre-wrap; word-break: break-word;
}

/* Card action popover menu. Opened by right-click on desktop and
   long-press on touch. The previous inline `⋮` trigger was retired
   because (a) it competed visually with the play/stop button at the
   card's inline-end, and (b) it duplicated affordances every user
   already knows from native context menus. */
.card-action-menu {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 4px;
  min-width: 180px;
  max-width: calc(100vw - 24px);
  box-shadow: 0 8px 24px rgba(0,0,0,0.55);
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.card-action-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  text-align: start;
  cursor: pointer;
  border-radius: 3px;
  width: 100%;
}
.card-action-item:hover { background: var(--bg-card-hov); color: var(--accent-text); }
.card-action-item.destructive { color: var(--red-text); }
.card-action-item.destructive:hover { background: color-mix(in oklab, var(--red) 12%, var(--bg-card-hov)); color: var(--red-text); }
.card-action-icon {
  width: 18px;
  text-align: center;
  color: var(--ink-faint);
  flex-shrink: 0;
}
.card-action-item:hover .card-action-icon { color: inherit; }
.card-action-sep { height: 1px; background: var(--line); margin: 3px 4px; }

/* Items that open a submenu show a chevron after the label. The label
   takes the remaining space so the chevron always sits flush at the end. */
.card-action-item .card-action-label { flex: 1; }
.card-action-item.has-submenu .card-action-chev {
  color: var(--ink-faint);
  font-size: 14px;
  line-height: 1;
  flex-shrink: 0;
}
[dir="rtl"] .card-action-item.has-submenu .card-action-chev { transform: scaleX(-1); }
.card-action-item.has-submenu:hover .card-action-chev { color: inherit; }
.card-action-submenu { min-width: 180px; }
.card-action-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  padding: 1px 5px;
  font: 10px/1.4 var(--font-mono, ui-monospace, monospace);
  background: var(--bg-card-hov);
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 3px;
  color: var(--ink-faint);
  opacity: 0.55;
  flex-shrink: 0;
  pointer-events: none;
  user-select: none;
}
@media (max-width: 600px) { .card-action-kbd { display: none; } }

/* Faded <kbd> badge to the right of the action label that mirrors the
   per-card keyboard shortcut (see public/js/lib/keyboard.js). margin-inline-
   start: auto pushes it to the row's trailing edge so the chev (if any)
   sits flush against it. Hidden on mobile — touch users have no keyboard. */
.card-action-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  padding: 1px 5px;
  margin-inline-start: auto;
  font: 10px/1.4 var(--font-mono, ui-monospace, monospace);
  background: var(--bg-card-hov);
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 3px;
  color: var(--ink-faint);
  opacity: 0.55;
  flex-shrink: 0;
  pointer-events: none;
  user-select: none;
}
/* When both a badge and a submenu chev exist, drop margin-auto on the chev
   so the badge claims the trailing space and the chev tucks beside it. */
.card-action-item .card-action-kbd + .card-action-chev { margin-inline-start: 4px; }
@media (max-width: 600px) { .card-action-kbd { display: none; } }

/* ===================================================================
   Group label — grip handle, delete button, drag-over indicators
   =================================================================== */

.project-label .grip {
  color: var(--ink-faint);
  font-size: 10px;
  cursor: grab;
  margin-inline-end: 4px;
  opacity: 0.5;
  user-select: none;
}
.project-label:hover .grip { opacity: 1; color: var(--ink-dim); }
.project-label[draggable="true"] { cursor: grab; }
.project-label[draggable="true"]:active { cursor: grabbing; }

/* Trash button at the trailing edge of the project-label.
   Pushed to the row's end (logical inline-end) by margin-inline-start: auto
   on the flex item — that mirrors automatically under RTL so the trash
   sits on the far LEFT in Arabic and the far RIGHT in English, keeping
   the destructive action well separated from the title and stats. */
.project-label .project-delete {
  margin-inline-start: auto;
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-faint);
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  opacity: 0;
  transition: opacity 120ms, color 120ms, background 120ms;
}
.project-label:hover .project-delete,
.project-label:focus-within .project-delete { opacity: 0.7; }
.project-label .project-delete:hover,
.project-label .project-delete:focus-visible {
  color: var(--red-text);
  background: color-mix(in oklab, var(--red) 12%, transparent);
  opacity: 1;
  outline: none;
}
/* On mobile the trash is always visible — there's no hover state to
   reveal it, and the row is wide enough that the icon doesn't crowd. */
@media (max-width: 600px) {
  .project-label .project-delete { opacity: 0.6; }
}

/* "Clear done · N" button at the top of each group's done cell — symmetric
   with the "+ new task" row at the bottom of queued. Styled with a visible
   solid border + leading trash icon so it reads as an actionable button at
   rest, not muted body text. Hover escalates to red ("armed destructive").
   Icon is delivered via ::before so the i18n strings stay icon-free and RTL
   mirrors via the logical-property padding without extra rules. */
.cell-clear-done {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  margin-block-end: 6px;
  padding-block: 5px;
  padding-inline: 10px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--fs-caption);
  text-align: start;
  cursor: pointer;
  transition: background 100ms, color 100ms, border-color 100ms;
}
.cell-clear-done::before {
  content: "🗑";
  font-size: 12px;
  line-height: 1;
  opacity: 0.7;
  flex: 0 0 auto;
}
.cell-clear-done:hover {
  background: var(--bg-card-hov);
  color: var(--red-text);
  border-color: var(--red);
}
.cell-clear-done:hover::before { opacity: 1; }

/* Inline rename pencil — sits next to the group name. Hover-revealed on
   desktop so the affordance is discoverable without crowding the row;
   always visible on mobile (no hover state). */
.project-label .project-rename {
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink-faint);
  padding: 2px 5px;
  margin-inline-start: 2px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  line-height: 1;
  opacity: 0;
  transition: opacity 120ms, color 120ms, background 120ms;
}
.project-label:hover .project-rename,
.project-label:focus-within .project-rename { opacity: 0.7; }
.project-label .project-rename:hover,
.project-label .project-rename:focus-visible {
  color: var(--accent-text);
  background: color-mix(in oklab, var(--accent) 10%, transparent);
  opacity: 1;
  outline: none;
}
@media (max-width: 600px) {
  .project-label .project-rename { opacity: 0.6; }
}
/* Keep the cursor explicitly text-like on the name span so users grok
   it's editable on dblclick even before they hover the rename button. */
.project-label .gname { cursor: text; }

.project-row.project-dragging { opacity: 0.35; }
.project-row.project-drop-before { box-shadow: inset 0 2px 0 var(--accent); }
.project-row.project-drop-after  { box-shadow: inset 0 -2px 0 var(--accent); }

/* ===================================================================
   Assignees — card avatars + drawer picker
   =================================================================== */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  font-variant-caps: all-small-caps;
}
.avatar.sm { width: 16px; height: 16px; font-size: 8px; }
.avatar.more { background: var(--bg-card); color: var(--ink-dim); font-size: 9px; }

.card .assignees {
  display: inline-flex;
  gap: -4px;
  flex-shrink: 0;
  margin-inline-start: 6px;
  cursor: pointer;
}
.card .assignees .avatar {
  margin-inline-start: -4px;
  border-color: var(--bg-card);
  transition: transform 120ms;
}
.card .assignees:hover .avatar { transform: translateY(-1px); }
.card .assignees.empty {
  opacity: 0;
  transition: opacity 120ms;
}
.card:hover .assignees.empty,
.card.focused .assignees.empty { opacity: 0.75; }
.card .assignees.empty:hover { opacity: 1; }
.card .assignees.empty .avatar {
  border-style: dashed;
  background: transparent;
  color: var(--ink-faint);
}

.asg-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.asg-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 6px;
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--ink);
  font-size: 11px;
}
.asg-pill .asg-name { padding: 0 2px; }
.asg-pill .me-tag {
  color: var(--accent-text);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-inline-start: 4px;
}
.asg-pill .asg-remove {
  background: transparent;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  padding: 0 4px;
}
.asg-pill .asg-remove:hover { color: var(--red-text); }

.asg-input-row { display: flex; gap: 6px; align-items: center; }
.asg-input {
  flex: 1;
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 4px 8px;
  border-radius: 3px;
  font: inherit;
  font-size: 12px;
}
.asg-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
.asg-me {
  background: transparent;
  border: 1px dashed var(--accent-dim);
  color: var(--accent-text);
  padding: 3px 10px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}
.asg-me:hover { background: var(--accent-glow); border-style: solid; }
.asg-suggest {
  background: var(--bg-raised);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  margin-top: 4px;
  max-height: 200px;
  overflow-y: auto;
  padding: 2px 0;
}
.asg-item {
  width: 100%;
  background: transparent;
  border: none;
  color: var(--ink);
  text-align: start;
  padding: 5px 10px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.asg-item:hover { background: var(--bg-card-hov); color: var(--accent-text); }
.asg-item.new { color: var(--ink-dim); font-style: italic; }
.asg-item.new em { color: var(--accent-text); font-style: normal; }

/* ===================================================================
   Settings drawer
   =================================================================== */

.settings-section {
  border-top: 1px solid var(--line);
  padding-top: 14px;
  margin-top: 4px;
}
.settings-section:first-child { border-top: none; margin-top: 0; padding-top: 0; }
.settings-section h3 {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 10px;
  font-weight: 600;
}
.settings-row {
  display: grid;
  grid-template-columns: 1fr 160px;
  gap: 12px;
  align-items: center;
  padding: 6px 0;
}
/* Block-layout variant — flips the parent to a single full-width column
   (label on top, content below as a block). Used by rows that render
   multi-row grids like the theme picker and the font picker, where the
   default `1fr 160px` template would shove the grid into the narrow
   right column and force a second sibling grid onto a new wider row,
   leaving the two grids visually misaligned. */
.settings-row.settings-row-block {
  grid-template-columns: 1fr;
  align-items: start;
  gap: 6px;
}
.settings-row label { color: var(--ink-dim); font-size: 12px; }
.settings-row input[type="number"],
.settings-row input[type="text"],
.settings-row select {
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 5px 8px;
  border-radius: 3px;
  font: inherit;
  font-size: 12px;
}
.settings-row input:focus, .settings-row select:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow);
}
.settings-row .toggle {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 5px 10px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  justify-self: end;
}
.settings-row .toggle.on { color: var(--accent-text); border-color: var(--accent); background: var(--accent-glow); }

.sound-preview {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 3px 8px;
  border-radius: 2px;
  cursor: pointer;
  font: inherit;
  font-size: 10px;
  margin-inline-start: 6px;
}
.sound-preview:hover { color: var(--accent-text); border-color: var(--accent); }

/* ===================================================================
   Settings two-pane drawer
   Left rail = category nav (Profile, Look & feel, Timers, Alerts,
   Calendar, Help, About). Right pane = the active category's controls.
   Wider than the default drawer so the right pane breathes. On narrow
   viewports the rail collapses into a horizontal tab strip above the
   content (single drawer, no drilldown).
   =================================================================== */
.drawer.settings { width: min(720px, 96vw); }

.drawer.settings .drawer-body.settings-twopane {
  flex-direction: row;
  padding: 0;
  gap: 0;
  overflow: hidden;
}

.settings-rail {
  flex: 0 0 180px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 0;
  background: color-mix(in oklab, var(--bg-cell) 60%, transparent);
  border-inline-end: 1px solid var(--line);
  overflow-y: auto;
}
.settings-rail-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  background: transparent;
  border: none;
  border-inline-start: 2px solid transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: 12px;
  text-align: start;
  cursor: pointer;
  transition: color 120ms, background 120ms, border-color 120ms;
}
.settings-rail-item:hover {
  color: var(--ink);
  background: var(--accent-glow);
}
.settings-rail-item.active {
  color: var(--accent-text);
  background: var(--accent-glow);
  border-inline-start-color: var(--accent);
  font-weight: 600;
}
.settings-rail-item .rail-icon {
  font-size: 14px;
  width: 18px;
  text-align: center;
  flex: 0 0 auto;
}
.settings-rail-item .rail-label {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-content {
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  position: relative;
}
.settings-pane {
  display: none;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
}
.settings-pane.active { display: flex; }

.sound-preview-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.sound-preview-grid .sound-preview { margin-inline-start: 0; }

/* Narrow viewport: rail folds into a horizontal scrollable tab strip
   above the content. Keeps the same single-drawer footprint. */
@media (max-width: 700px) {
  .drawer.settings { width: 100%; }
  .drawer.settings .drawer-body.settings-twopane { flex-direction: column; }
  .settings-rail {
    flex: 0 0 auto;
    flex-direction: row;
    gap: 0;
    padding: 4px 6px;
    border-inline-end: none;
    border-bottom: 1px solid var(--line);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
  }
  .settings-rail-item {
    flex: 0 0 auto;
    padding: 8px 12px;
    border-inline-start: none;
    border-bottom: 2px solid transparent;
  }
  .settings-rail-item.active {
    border-inline-start: none;
    border-bottom-color: var(--accent);
  }
  .settings-rail-item .rail-label { font-size: 11px; }
}

/* ===================================================================
   Next-break indicator (in Active-now strip)
   Shown as a chip in three states:
     default — neutral countdown to next break ("· next break in 5:23")
     soon    — within 2 minutes of break time, switches to amber
     overdue — past break threshold, switches to red and shows how long
   The color shift is the user's at-a-glance signal to take a break;
   the chip-style border makes it feel like a real status pill, not just
   trailing text.
   =================================================================== */
.active-head .next-break {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  margin-inline-start: 4px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: color-mix(in oklab, var(--ink-faint) 12%, transparent);
  color: var(--ink-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  transition: background 200ms, border-color 200ms, color 200ms;
}
.active-head .next-break.soon {
  border-color: var(--amber);
  background: color-mix(in oklab, var(--amber) 14%, transparent);
  color: var(--amber-text);
}
/* Overdue chip — calm warm pill, NOT an alarm. Soft tinted bg + warm
   border, ink-primary text for legibility. No 3D shelf, no pulsing
   halo — those compounded into "alarm bell" intensity even with the
   warmer --break color. The pill itself is enough to draw the eye;
   color carries the meaning, no animation needed.
   The breathe keyframe is a gentle bg fade kept at low contrast — it
   moves about 4% bg opacity over 3.2s, which the eye registers as
   "this thing is alive" without becoming a flicker. */
.active-head .next-break.overdue {
  border-color: color-mix(in oklab, var(--break) 70%, transparent);
  background: color-mix(in oklab, var(--break) 10%, transparent);
  color: var(--break-text);
  font-weight: 600;
  padding: 2px 10px;
  animation: next-break-breathe 3.2s ease-in-out infinite;
  transition: transform 80ms ease, background 120ms;
}
@keyframes next-break-breathe {
  0%, 100% { background: color-mix(in oklab, var(--break) 10%, transparent); }
  50%      { background: color-mix(in oklab, var(--break) 14%, transparent); }
}

/* Inline italic quote next to the overdue chip. Sits on the same flex
   row as the chip, eats remaining width with ellipsis. */
.active-now .nudge-inline-quote {
  flex: 1 1 200px;
  min-width: 0;
  font-style: italic;
  font-size: 11.5px;
  color: var(--ink-dim);
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* When .next-break is rendered as a <button> (overdue state), give it
   real button affordances. The 3D shelf (constant box-shadow at +2y) is
   defined in the .next-break.overdue block. Here: cursor, brighter
   hover, and the press behavior — translateY(2px) on :active so the
   button visually meets the shadow underneath, then the shadow halo
   from the pulse animation continues uninterrupted. */
button.next-break {
  font: inherit;
  cursor: pointer;
}
/* Flip animation — countdown face rotates up, "Break now" face rotates in.
   .nb-back is absolutely positioned so the chip width stays locked to the
   .nb-front content; overflow: hidden clips the mid-flip rotation. */
button.next-break:not(.overdue) {
  position: relative;
  overflow: hidden;
  perspective: 400px;
}
.nb-front {
  display: block;
  transition: transform 240ms ease, opacity 240ms ease;
}
.nb-back {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  transform: rotateX(-90deg);
  opacity: 0;
  transition: transform 240ms ease, opacity 240ms ease;
}
button.next-break:not(.overdue):hover {
  border-color: color-mix(in oklab, var(--break) 70%, transparent);
  background: color-mix(in oklab, var(--break) 10%, transparent);
  color: var(--break-text);
  transition: background 150ms, border-color 150ms, color 150ms;
}
button.next-break:not(.overdue):hover .nb-front {
  transform: rotateX(90deg);
  opacity: 0;
}
button.next-break:not(.overdue):hover .nb-back {
  transform: rotateX(0deg);
  opacity: 1;
}
button.next-break.overdue:hover {
  background: color-mix(in oklab, var(--break) 18%, transparent);
  border-color: color-mix(in oklab, var(--break) 85%, transparent);
}
button.next-break.overdue:active { transform: translateY(1px); }
button.next-break.overdue:focus-visible {
  outline: 1px solid var(--break);
  outline-offset: 2px;
}

/* ===================================================================
   Insights drawer — KPIs, ASCII bars
   =================================================================== */

.drawer.insights { width: min(720px, 96vw); }
.insights-body { gap: 18px; }

.ins-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}
.ins-kpi {
  border: 1px solid var(--line);
  border-inline-start: 2px solid var(--accent);
  background: var(--bg-cell);
  border-radius: 3px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ins-kpi-label {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.ins-kpi-val {
  color: var(--accent-text);
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.ins-kpi-val-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
}
.ins-kpi-sub { color: var(--ink-dim); font-size: 11px; }

.ins-panel {
  border-top: 1px solid var(--line);
  padding-top: 12px;
}
.ins-panel h3 {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 10px;
  font-weight: 600;
}

.ins-daybars, .ins-rows { display: flex; flex-direction: column; gap: 4px; }
.ins-daybar, .ins-row {
  display: grid;
  grid-template-columns: minmax(90px, 180px) minmax(18ch, 34ch) minmax(64px, max-content);
  justify-content: start;
  column-gap: 18px;
  align-items: center;
  font-variant-numeric: tabular-nums;
  font-size: 12px;
}
.ins-daylabel, .ins-row-label {
  color: var(--ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: 6px;
}
.ins-bar {
  color: var(--accent-text);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0;
  white-space: nowrap;
}
.ins-daytime, .ins-row-val {
  text-align: end;
  color: var(--ink);
  font-size: 11px;
}
.ins-empty { color: var(--ink-faint); font-style: italic; padding: 8px 0; }

@media (max-width: 600px) {
  .ins-daybar,
  .ins-row {
    grid-template-columns: minmax(74px, 0.8fr) minmax(12ch, 1fr) minmax(54px, max-content);
    column-gap: 8px;
  }
}

/* ===================================================================
   Reports panel — Daily report + review tabs
   =================================================================== */

.drawer.reports { width: min(860px, 88%); }
.reports-header { flex-shrink: 0; }
.reports-tabs {
  display: flex;
  flex-wrap: wrap;
  row-gap: 6px;
  gap: 8px;
  padding: 10px 16px;
  border-block-end: 1px solid var(--line);
  background: var(--bg-raised);
}
.reports-tab {
  border: 1px solid var(--line-strong);
  background: var(--bg-cell);
  color: var(--ink-dim);
  border-radius: 3px;
  padding: 6px 10px;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.reports-tab.is-active {
  color: var(--accent-text);
  border-color: var(--accent-dim);
  background: var(--accent-glow);
}
.reports-body { gap: 14px; }
.reports-controls {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-cell);
}
.reports-control-block {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.reports-label {
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.reports-day-chips,
.reports-project-chips,
.report-actions {
  display: flex;
  flex-wrap: wrap;
  row-gap: 6px;
  gap: 8px;
}
.report-day-chip,
.report-project-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--ink-dim);
  border-radius: 999px;
  padding: 5px 9px;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.report-day-chip.has-activity { color: var(--ink); }
.report-day-chip.is-muted { color: var(--ink-faint); border-style: dashed; }
.report-day-chip.is-selected,
.report-project-chip.is-selected {
  color: var(--accent-text);
  border-color: var(--accent-dim);
  background: var(--accent-glow);
}
.report-project-chip {
  --project-color: var(--ink-faint);
  border-radius: 4px;
}
.report-project-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  border: 1px solid var(--project-color);
  background: var(--project-color);
  flex: 0 0 auto;
}
.report-project-all .report-project-dot { background: transparent; }
.reports-custom-range {
  display: flex;
  flex-wrap: wrap;
  row-gap: 8px;
  gap: 10px;
}
.reports-custom-range label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-dim);
  font-size: 11px;
}
.reports-custom-range input {
  min-width: 150px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  color: var(--ink);
  font: inherit;
  padding: 5px 7px;
}
.report-output {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.report-output-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 10px;
  gap: 12px;
  padding-block-end: 12px;
  border-block-end: 1px solid var(--line);
}
.report-output-head h2 {
  margin: 0 0 6px;
  font-size: 18px;
  color: var(--accent-text);
}
.report-subtitle {
  display: flex;
  flex-wrap: wrap;
  row-gap: 4px;
  gap: 10px;
  color: var(--ink-dim);
  font-size: 11px;
}
.report-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 8px;
}
.report-kpi {
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--accent-dim);
  background: var(--bg-cell);
  border-radius: 4px;
  padding: 9px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.report-kpi span {
  color: var(--ink-faint);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.report-kpi strong {
  color: var(--ink);
  font-size: 16px;
  font-weight: 600;
}
.report-section {
  border-block-start: 1px solid var(--line);
  padding-block-start: 12px;
}
.report-section h3 {
  margin: 0 0 9px;
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.report-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.report-summary-line,
.report-task-row,
.report-empty {
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-cell);
  padding: 8px 10px;
  font-size: 12px;
}
.report-task-row {
  --project-color: var(--ink-faint);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  border-inline-start: 3px solid var(--project-color);
}
.report-task-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}
.report-task-project {
  color: var(--ink-faint);
  margin-inline-end: 6px;
}
.report-task-meta {
  color: var(--ink-dim);
  font-size: 11px;
  text-align: end;
}
.report-empty {
  color: var(--ink-faint);
  font-style: italic;
}
.reports-embedded {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.reports-log-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 8px;
  gap: 10px;
  padding-block-end: 10px;
  border-block-end: 1px solid var(--line);
}
.reports-log-title {
  color: var(--accent-text);
  font-size: 12px;
  font-weight: 600;
}

@media (max-width: 600px) {
  .reports-tabs,
  .reports-controls,
  .report-output-head {
    padding-inline: 12px;
  }
  .report-output-head {
    align-items: stretch;
  }
  .report-actions,
  .report-actions .icon-btn {
    width: 100%;
  }
  .report-actions .icon-btn {
    justify-content: center;
  }
  .report-task-row {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .report-task-meta {
    grid-column: 2;
    text-align: start;
  }
}

/* ---------- Command palette ---------- */

.palette-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 15vh;
  z-index: var(--z-palette-backdrop);
  animation: palette-bg 220ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* Cmd+K is a fast-context-switch tool. Light blur on the bg helps the
   palette feel like a floating tool rather than a hard modal — same
   feel as Raycast / Superhuman / Linear's command-K. Purposeful blur. */
@keyframes palette-bg {
  from { opacity: 0; backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); }
  to   { opacity: 1; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
}

.palette {
  width: min(620px, 92vw);
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 20px 60px rgba(0,0,0,0.8), 0 0 0 4px var(--accent-glow);
  border-radius: var(--radius);
  overflow: hidden;
  animation: palette-in 340ms cubic-bezier(0.2, 1.1, 0.35, 1);
  transform-origin: top center;
}
@keyframes palette-in {
  0%   { opacity: 0; transform: translateY(-24px) scale(0.94); filter: blur(2px); }
  60%  { opacity: 1; transform: translateY(2px) scale(1.015); filter: blur(0); }
  100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}

.palette-item { animation: palette-item-in 260ms cubic-bezier(0.25, 0.8, 0.25, 1) backwards; }
.palette-item:nth-child(1)  { animation-delay: 40ms; }
.palette-item:nth-child(2)  { animation-delay: 60ms; }
.palette-item:nth-child(3)  { animation-delay: 80ms; }
.palette-item:nth-child(4)  { animation-delay: 100ms; }
.palette-item:nth-child(5)  { animation-delay: 115ms; }
.palette-item:nth-child(6)  { animation-delay: 130ms; }
.palette-item:nth-child(7)  { animation-delay: 140ms; }
.palette-item:nth-child(8)  { animation-delay: 150ms; }
.palette-item:nth-child(n+9) { animation-delay: 160ms; }
@keyframes palette-item-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

.palette-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
}
.palette-input {
  flex: 1;
  color: var(--ink);
  caret-color: var(--accent);
  font-size: 14px;
}
.palette-input::placeholder { color: var(--ink-faint); }
.palette-esc {
  font-size: 10px;
}

.palette-list {
  max-height: 52vh;
  overflow-y: auto;
  padding: 6px 0;
}
.palette-project-label {
  padding: 6px 14px 4px;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.palette-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  cursor: pointer;
  color: var(--ink-dim);
}
.palette-item .icon {
  color: var(--ink-faint);
  width: 14px;
  flex-shrink: 0;
}
.palette-item .label { flex: 1; color: var(--ink); }
.palette-item .sub { color: var(--ink-faint); font-size: 11px; }
.palette-item .match { color: var(--accent-text); }
.palette-item.selected {
  background: var(--accent-glow);
  color: var(--ink);
}
.palette-item.selected .icon,
.palette-item.selected .sub { color: var(--accent-text); }
.palette-item:hover { background: var(--bg-card-hov); }
.palette-empty {
  padding: 20px 14px;
  color: var(--ink-faint);
  font-style: italic;
  text-align: center;
}

.palette-footer {
  display: flex;
  gap: 18px;
  padding: 8px 14px;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: 11px;
}

/* scroll */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }


/* ===================================================================
   Time picker dropdown — Google-Calendar-style 15-min slot list.
   Replaces native `<input type="time">` chrome which CSS can't reach.
   The owning input remains a free-text field; the menu is appended to
   <body> at position:fixed and torn down on commit / outside click. */
.time-picker-menu {
  position: fixed;
  z-index: var(--z-popover);
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  max-height: 240px;
  overflow-y: auto;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  padding: 4px 0;
  font-family: inherit;
}
.time-picker-option {
  font: inherit;
  font-size: 12px;
  padding: 6px 14px;
  border: 0;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  text-align: start;
  white-space: nowrap;
  font-feature-settings: "tnum";
}
.time-picker-option:hover {
  background: var(--bg-card-hov);
}
.time-picker-option.selected {
  background: var(--accent-glow);
  color: var(--accent-text);
  font-weight: 600;
}
/* Keyboard-nav active slot — set by mountTimePicker's arrow-key handler.
   Mirrors `.ts-menu button.highlighted`; logical-property-free so RTL
   needs no counterpart. */
.time-picker-option.highlighted {
  background: var(--bg-card-hov);
  color: var(--accent-text);
}

/* ===================================================================
   Date picker dropdown — Google-Calendar-style mini month grid.
   Anchored to a `.dqp-pick-day` button inside the schedule/due popover.
   Reuses .dp-head / .dp-dow / .dp-grid markup so the existing custom-
   calendar styles (defined further down) cover the day cells. */
.date-picker-menu {
  position: fixed;
  z-index: var(--z-popover);
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 8px;
  min-width: 240px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  font-family: inherit;
}

/* ===================================================================
   Due-quick popover (from card's + due pill)
   =================================================================== */
.due-quick-popover {
  position: fixed;
  z-index: var(--z-popover);
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 12px 40px rgba(0,0,0,0.7), 0 0 0 3px var(--accent-glow);
  border-radius: 4px;
  padding: 10px 12px;
  width: 240px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  animation: palette-in 220ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
.due-quick-popover .dqp-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.due-quick-popover .dqp-chip {
  font: inherit;
  font-size: 11px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  background: var(--bg-cell);
  color: var(--ink-dim);
  border-radius: 999px;
  cursor: pointer;
}
.due-quick-popover .dqp-chip:hover { color: var(--accent-text); border-color: var(--accent); }
.due-quick-popover .dqp-chip.clear {
  color: var(--red-text);
  border-style: dashed;
}
.due-quick-popover .dqp-chip.custom { font-style: italic; color: var(--ink-faint); }

/* "[Day ▾] at [HH:MM] [Set]" inline row — let users pick "next Monday at
   3pm" without opening the custom modal. The day select covers 14 days;
   beyond that the .dqp-chip.custom path stays correct. */
.due-quick-popover .dqp-pick-label {
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-block-start: 2px;
}
.due-quick-popover .dqp-pick-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
}
/* Day button + time input share a row — same height, font, and
   border so they read as a single inline pick row. The day field is
   a button (not a select); clicking opens the .date-picker-menu
   month grid above, mirroring the time picker's UX. */
.due-quick-popover .dqp-pick-day,
.due-quick-popover .dqp-pick-time {
  font: inherit;
  font-size: 12px;
  padding: 4px 8px;
  height: 28px;
  box-sizing: border-box;
  border: 1px solid var(--line-strong);
  background: var(--bg-cell);
  color: var(--ink);
  border-radius: var(--radius);
  -webkit-appearance: none;
  appearance: none;
}
/* Day button + time input share the row equally — both grow to fill
   spare width with `flex: 1 1 0`. Gives a balanced "[date] at [time]"
   reading, instead of the day shrinking to fit short content like
   "5/15" while the time stretched to take the rest of the row. */
.due-quick-popover .dqp-pick-day {
  flex: 1 1 0;
  min-width: 0;
  cursor: pointer;
  text-align: start;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 📅 prefix matches the card's schedule pill so the same surface
   reads with the same visual grammar. Pseudo-element rather than text
   content so setDate's textContent assignment doesn't overwrite it. */
.due-quick-popover .dqp-pick-day::before {
  content: "📅 ";
  margin-inline-end: 2px;
}
.due-quick-popover .dqp-pick-day:hover {
  border-color: var(--accent-dim);
}
.due-quick-popover .dqp-pick-time {
  flex: 1 1 0;
  min-width: 0;
  font-feature-settings: "tnum";
  text-align: start;
}
.due-quick-popover .dqp-pick-day:focus,
.due-quick-popover .dqp-pick-time:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
}
.due-quick-popover .dqp-pick-at {
  font-size: 11px;
  color: var(--ink-faint);
}
.due-quick-popover .dqp-pick-set {
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  padding: 4px 12px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--bg-card);
  border-radius: 999px;
  cursor: pointer;
  flex: 0 0 auto;
}
.due-quick-popover .dqp-pick-set:hover {
  background: color-mix(in oklab, var(--accent) 85%, var(--bg-card));
}
.due-quick-popover .dqp-pick-set:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-glow);
}

/* ===================================================================
   Assignee quick popover on card
   =================================================================== */
.asg-quick-popover {
  position: fixed;
  z-index: var(--z-popover);
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 12px 40px rgba(0,0,0,0.7), 0 0 0 3px var(--accent-glow);
  border-radius: 4px;
  padding: 8px;
  width: 260px;
  animation: palette-in 220ms cubic-bezier(0.2, 0.9, 0.3, 1);
}
.asg-quick-popover .aqp-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
/* "you are …" identity row at the top of the popover — lets a user declare
   themselves once, so future tasks auto-assign to them via addTask(). */
.asg-quick-popover .aqp-me-row {
  display: flex; align-items: center; gap: 6px;
  margin-bottom: 6px;
  padding-bottom: 6px;
  border-bottom: 1px dashed var(--line-strong);
  font-size: 11px;
}
.asg-quick-popover .aqp-me-label {
  color: var(--ink-faint); font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.asg-quick-popover .aqp-me-name { color: var(--ink); font-weight: 600; flex: 1; }
.asg-quick-popover .aqp-me-row input { flex: 1; width: auto; }
.asg-quick-popover .aqp-me-change,
.asg-quick-popover .aqp-me-save {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-faint);
  padding: 2px 8px;
  border-radius: 3px;
  font-size: 10px;
  cursor: pointer;
}
.asg-quick-popover .aqp-me-save {
  color: var(--accent-text);
  border-color: var(--accent);
}
.asg-quick-popover .aqp-me-change:hover,
.asg-quick-popover .aqp-me-save:hover { background: var(--accent-glow); }
.asg-quick-popover input {
  width: 100%;
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 4px 8px;
  border-radius: 3px;
  font: inherit;
  font-size: 12px;
}
.asg-quick-popover input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
.asg-quick-popover .aqp-list {
  margin-top: 4px;
  max-height: 200px;
  overflow-y: auto;
}

/* ===================================================================
   Due-date picker — terminal-native, preset chips + optional custom input
   =================================================================== */

.due-picker { display: flex; flex-direction: column; gap: 8px; }

.due-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--bg-cell);
  color: var(--ink);
  font-size: 12px;
}
.due-summary .due-icon { color: var(--accent-text); font-size: 14px; }
.due-summary .due-text { flex: 1; }
.due-summary .due-clear {
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  color: var(--ink-dim);
  padding: 0 8px;
  font-size: 14px;
  cursor: pointer;
  line-height: 1;
}
.due-summary .due-clear:hover { color: var(--red-text); border-color: var(--red); }

.due-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.due-chip {
  font: inherit;
  font-size: 11px;
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--bg-cell);
  color: var(--ink-dim);
  cursor: pointer;
  letter-spacing: 0.02em;
  transition: border-color 120ms, color 120ms, background 120ms;
}
.due-chip:hover { border-color: var(--accent-dim); color: var(--ink); }
.due-chip.active {
  border-color: var(--accent);
  color: var(--accent-text);
  background: var(--accent-glow);
}
.due-chip#d-due-custom { font-style: italic; color: var(--ink-faint); }

#d-due { display: none !important; }

/* ===================================================================
   Custom date-picker popover (replaces native datetime-local)
   =================================================================== */

.date-popover {
  position: fixed;
  z-index: var(--z-popover);
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 20px 60px rgba(0,0,0,0.7), 0 0 0 3px var(--accent-glow);
  border-radius: 4px;
  padding: 12px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink);
  width: 280px;
  animation: palette-in 120ms ease-out;
}
.dp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 2px 10px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 8px;
}
.dp-title { color: var(--ink); font-weight: 600; letter-spacing: 0.04em; }
.dp-nav {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  font-size: 14px;
  line-height: 1;
  padding: 2px 10px;
  border-radius: 3px;
  cursor: pointer;
}
.dp-nav:hover { color: var(--accent-text); border-color: var(--accent-dim); }

.dp-dow {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  color: var(--ink-faint);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  padding-bottom: 4px;
}

.dp-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  margin-bottom: 10px;
}
.dp-day {
  padding: 6px 0;
  background: transparent;
  border: 1px solid transparent;
  color: var(--ink);
  cursor: pointer;
  font: inherit;
  font-variant-numeric: tabular-nums;
  border-radius: 2px;
}
.dp-day:hover { background: var(--bg-card-hov); border-color: var(--accent-dim); }
.dp-day.other { color: var(--ink-faint); opacity: 0.45; }
.dp-day.today { border-color: var(--amber); color: var(--amber-text); }
.dp-day.sel {
  background: var(--accent-glow);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 600;
}

.dp-time {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 2px;
  border-top: 1px solid var(--line);
  color: var(--ink-dim);
}
.dp-time label {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-inline-end: auto;
}
.dp-time input {
  width: 42px;
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  padding: 3px 6px;
  border-radius: 2px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
  appearance: textfield;
}
.dp-time input::-webkit-outer-spin-button,
.dp-time input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.dp-time input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }

.dp-actions {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}
.dp-actions button {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink-dim);
  padding: 4px 12px;
  border-radius: 3px;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}
.dp-actions button:hover { color: var(--accent-text); border-color: var(--accent-dim); }
.dp-actions .dp-set {
  color: var(--accent-text);
  border-color: var(--accent);
  background: var(--accent-glow);
  margin-inline-start: auto;
}
.dp-actions .dp-set:hover { background: var(--accent-dim); color: var(--bg); }

/* ===================================================================
   Themed select (replaces native <select>) — button + dropdown menu
   =================================================================== */

.themed-select {
  position: relative;
  font-family: var(--mono);
}
.ts-btn {
  width: 100%;
  text-align: start;
  background: var(--bg-cell);
  border: 1px solid var(--line-strong);
  color: var(--ink);
  padding: 6px 10px;
  border-radius: 3px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  font: inherit;
}
.ts-btn:hover { border-color: var(--accent-dim); }
.ts-btn:focus,
.themed-select.open .ts-btn {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-glow);
}
.ts-label { flex: 1; }
.ts-chev { color: var(--ink-faint); font-size: 10px; transition: transform 150ms; }
.themed-select.open .ts-chev { transform: rotate(180deg); color: var(--accent-text); }

.ts-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--bg-raised);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 12px 40px rgba(0,0,0,0.6), 0 0 0 3px var(--accent-glow);
  border-radius: 3px;
  padding: 4px 0;
  z-index: 50;
  animation: palette-in 120ms ease-out;
  max-height: 240px;
  overflow-y: auto;
}
.ts-menu[hidden] { display: none !important; }
.ts-menu button {
  width: 100%;
  text-align: start;
  background: transparent;
  border: none;
  color: var(--ink);
  padding: 6px 12px;
  cursor: pointer;
  font: inherit;
  display: flex;
  align-items: center;
  gap: 8px;
}
.ts-menu button:hover,
.ts-menu button.highlighted { background: var(--bg-card-hov); color: var(--accent-text); }
.ts-menu button.selected { color: var(--accent-text); }
.ts-menu button.selected::before { content: "✓"; color: var(--accent-text); }
.ts-menu button:not(.selected)::before { content: " "; width: 10px; }

/* ===================================================================
   THEMES — swap the palette via <html data-theme="…">.
   Default (no attr) stays as the original terminal-green.
   Loaded AFTER styles.css so data-theme overrides win.
   ===================================================================
   In production build.sh concatenates this file into dist/styles.css;
   in dev (bun server.ts) the browser fetches it as a separate <link>.
   See docs/superpowers/specs/2026-05-15-architecture-split-design.md
   (batch 12) for the planned per-theme split into themes/<name>.css.
   =================================================================== */

/* ---- Semantic slot reference (every theme must fill these) ----
   --bg / --bg-raised / --bg-cell / --bg-card / --bg-card-hov : surface stack
   --line / --line-strong                                    : dividers
   --ink / --ink-dim / --ink-faint                           : text-priority
   --accent  (+ --accent-glow, --accent-dim)  : "running / doing / success"
   --amber  (+ --amber-glow)               : "due today / hold"
   --red    (+ --red-glow)                 : "overdue / blocked / destructive"
   --blue                                   : "due soon (≤3d) / external events"
   --violet                                 : reserved accent (unused today)
   Each theme MUST keep the four accent hues visually distinct from
   each other, otherwise pills collapse (see .pill.due-soon etc.). */

/* Dracula — the iconic purple-and-neon dark theme. Uses the canonical
   Dracula palette (draculatheme.com) with the project's semantic slot
   mapping: the signature purple is the primary "running/success" accent,
   yellow is "due today", red is "overdue", cyan is "due soon", and pink
   fills the --violet reserved slot. */
html[data-theme="dracula"] {
  --bg:          #282a36;
  --bg-raised:   #343746;
  --bg-cell:     #2d2f3e;
  --bg-card:     #3d4053;
  --bg-card-hov: #44475a;
  --line:        #44475a;
  --line-strong: #6272a4;
  --ink:         #f8f8f2;
  --ink-dim:     #d0d0c8;
  --ink-faint:   #a4afd1;   /* WCAG-AA on bg-raised (5.4:1); was dracula's #6272a4 = 2.5:1 — fails AA. Hue preserved. */
  --accent:       #bd93f9;   /* dracula purple — the iconic signature now leads */
  --accent-glow:  #bd93f933;
  --accent-dim:   #8a6bc7;
  --amber:       #f1fa8c;   /* dracula yellow */
  --amber-glow:  #f1fa8c22;
  --red:         #ff5555;   /* dracula red */
  --red-glow:    #ff555522;
  --break:       #ffb86c;   /* dracula orange — softer than the alarm red */
  --break-glow:  #ffb86c22;
  --blue:        #8be9fd;   /* dracula cyan */
  --violet:      #ff79c6;   /* dracula pink — was holding the purple slot which moved to --accent */
  /* AA-compliant text variants (audit 2026-05-17). Hue preserved, lightness lifted. */
  --accent-text: #c79dff;   /* was --accent #bd93f9 = 4.23:1 on bg-card; now 4.71:1 */
  --red-text:    #ff9191;   /* was --red #ff5555 = 3.25:1 on bg-card; now 4.72:1 */
}

/* ---- Monokai — iconic Sublime/VSCode palette. Lime-yellow accent with
       hot-pink as the signature contrast. Wider color spread than dracula. */
html[data-theme="monokai"] {
  --bg:          #272822;
  --bg-raised:   #2f3129;
  --bg-cell:     #2c2d25;
  --bg-card:     #3a3b31;
  --bg-card-hov: #44463a;
  --line:        #49483e;   /* SOFT monokai-warm line (1.23:1 on bg). Reverted from #8a897f WCAG-AA lift 2026-05-17 v3: monokai's warm palette doesn't want grey-spreadsheet borders. Cards separate via bg-card tonal layering. See DESIGN.md "Soft-Border Theme Identity". */
  --line-strong: #75715e;   /* SOFT monokai default-button border (2.31:1 on bg). Same Soft-Border tradeoff. */
  --ink:         #f8f8f2;
  --ink-dim:     #cfd0ca;
  --ink-faint:   #bcb6a1;   /* WCAG-AA on worst-case bg-card-hov (4.74:1); was #a59f8a = 3.63 — borderline on hover. Audit 2026-05-17. */
  --accent:       #a6e22e;   /* monokai lime */
  --accent-glow:  #a6e22e33;
  --accent-dim:   #6f9e1a;
  --amber:       #e6db74;   /* monokai yellow */
  --amber-glow:  #e6db7422;
  --red:         #f92672;   /* monokai magenta/red */
  --red-glow:    #f9267222;
  --break:       #fd971f;   /* monokai orange */
  --break-glow:  #fd971f22;
  --blue:        #66d9ef;   /* monokai cyan */
  --violet:      #ae81ff;   /* monokai purple */
  /* AA-compliant text variants (audit 2026-05-17). Monokai's --red is magenta; lifted toward pink. */
  --red-text:    #ff78c4;   /* was --red #f92672 = 3.00:1 on bg-card; now 4.72:1 */
  --violet-text: #bf92ff;   /* was --violet #ae81ff = 3.99:1 on bg-card; now 4.74:1 */
}

/* ---- Solarized Dark (Ethan Schoonover, 2011) — precision-tuned teal base
       with ochre and cyan accents. Exceptionally easy on the eyes. */
html[data-theme="solarized-dark"] {
  --bg:          #002b36;   /* base03 */
  --bg-raised:   #073642;   /* base02 */
  --bg-cell:     #04303d;
  --bg-card:     #0d4050;
  --bg-card-hov: #1a4f5f;
  --line:        #073642;
  --line-strong: #586e75;   /* base01 */
  --ink:         #93a1a1;   /* base1 */
  --ink-dim:     #b8c4c4;   /* WCAG-AA on bg-raised (7.3:1); was solarized base0 #839496 = 4.1:1 — borderline + faint had no headroom. */
  --ink-faint:   #93a1a1;   /* WCAG-AA on bg-raised (4.9:1); was solarized base01 #586e75 = 2.4:1 — fails AA. Reuses base1 (the original --ink) since base0 is now too dim for the faint slot under the new tier. */
  --accent:       #859900;   /* solarized green */
  --accent-glow:  #85990033;
  --accent-dim:   #5e6e00;
  --amber:       #b58900;   /* solarized yellow */
  --amber-glow:  #b5890022;
  --red:         #dc322f;   /* solarized red */
  --red-glow:    #dc322f22;
  --break:       #cb4b16;   /* solarized orange */
  --break-glow:  #cb4b1622;
  --blue:        #268bd2;   /* solarized blue */
  --violet:      #6c71c4;   /* solarized violet */
  /* AA-compliant text variants (audit 2026-05-17). Solarized's intentional low-contrast palette needed lifting on dark bg-card. */
  --accent-text: #9eb219;   /* was --accent #859900 = 3.51:1 on bg-card; now 4.73:1 */
  --amber-text:  #cea219;   /* was --amber #b58900 = 3.51:1 on bg-card; now 4.72:1 */
  --red-text:    #ff8380;   /* was --red #dc322f = 2.43:1 on bg-card; now 4.72:1 */
  --blue-text:   #4bb0f7;   /* was --blue #268bd2 = 3.06:1 on bg-card; now 4.74:1 */
  --break-text:  #ff8651;   /* was --break #cb4b16 = 2.44:1 on bg-card; now 4.70:1 */
}

/* ---- Nord (Arctic Ice Studio) — cold, desaturated blue-grey base with
       soft aurora accents. The only palette here that leads with blue. */
html[data-theme="nord"] {
  --bg:          #2e3440;   /* nord0 */
  --bg-raised:   #3b4252;   /* nord1 */
  --bg-cell:     #353b48;
  --bg-card:     #434c5e;   /* nord2 */
  --bg-card-hov: #4c566a;   /* nord3 */
  --line:        #434c5e;
  --line-strong: #4c566a;
  --ink:         #eceff4;   /* nord6 */
  --ink-dim:     #d8dee9;   /* nord4 */
  --ink-faint:   #a8b1c2;   /* WCAG-AA on bg-raised (4.7:1); was Nord's #7b8694 = 2.7:1 — fails AA. Steel-blue hue preserved, lifted toward nord4. */
  --accent:       #88c0d0;   /* frost light blue — Nord's iconic cool lead */
  --accent-glow:  #88c0d033;
  --accent-dim:   #5e8d99;
  --amber:       #ebcb8b;   /* aurora yellow */
  --amber-glow:  #ebcb8b22;
  --red:         #bf616a;   /* aurora red */
  --red-glow:    #bf616a22;
  --break:       #d08770;   /* nord aurora orange */
  --break-glow:  #d0877022;
  --blue:        #5e81ac;   /* nord1 darker frost — distinct from --accent frost */
  --violet:      #b48ead;   /* aurora purple */
  /* AA-compliant text variants (audit 2026-05-17). Nord's desaturated mids needed lifting on bg-card. */
  --accent-text: #91c9d9;   /* was --accent #88c0d0 = 4.31:1 on bg-card; now 4.75:1 */
  --red-text:    #ffa8b1;   /* was --red #bf616a = 2.11:1 on bg-card; now 4.72:1 */
  --blue-text:   #a0c3ee;   /* was --blue #5e81ac = 2.14:1 on bg-card; now 4.74:1 */
  --break-text:  #f7ae97;   /* was --break #d08770 = 3.03:1 on bg-card; now 4.71:1 */
}

/* ---- Gruvbox Dark Medium (Pavel Pertsev) — warm retro earth tones.
       Vintage monitor feel without being as saturated as amber. */
html[data-theme="gruvbox"] {
  --bg:          #282828;
  --bg-raised:   #32302f;
  --bg-cell:     #2b2b2b;
  --bg-card:     #3c3836;
  --bg-card-hov: #504945;
  --line:        #3c3836;   /* GRUVBOX-CANONICAL — intentionally same as --bg-card (1.00:1). Cards are NOT delineated by borders here; they separate from --bg via tonal lightness (bg-card #3c3836 is ~10% lighter than bg #282828). Reverted from #8a8684 WCAG-AA lift 2026-05-17 v3: the grey lift broke gruvbox's warm earth-tone identity. See DESIGN.md "Soft-Border Theme Identity". */
  --line-strong: #665c54;   /* SOFT gruvbox button border (1.78:1 on bg). Same Soft-Border tradeoff. */
  --ink:         #ebdbb2;   /* gruvbox fg */
  --ink-dim:     #cbbca1;   /* WCAG-AA on bg-card-hov (4.73:1); was #bdae93 = 4.05:1 — borderline on hover. Audit 2026-05-17. */
  --ink-faint:   #cbbca7;   /* WCAG-AA on bg-card-hov (4.74:1); was #a89984 = 3.17:1 — fails on hover. Audit 2026-05-17. */
  --accent:       #fe8019;   /* gruvbox bright orange — warmest signature, distinct from olive */
  --accent-glow:  #fe801933;
  --accent-dim:   #cc6e24;   /* WCAG-AA 3:1 on bg-card (3.22:1); was #b85a10 = 2.49:1. Audit 2026-05-17. */
  --amber:       #fabd2f;   /* gruvbox yellow */
  --amber-glow:  #fabd2f22;
  --red:         #fb4934;   /* gruvbox red */
  --red-glow:    #fb493422;
  --break:       #fe8019;   /* gruvbox orange */
  --break-glow:  #fe801922;
  --blue:        #83a598;   /* gruvbox aqua */
  --violet:      #d3869b;   /* gruvbox pink */
  /* AA-compliant text variants (audit 2026-05-17). Gruvbox warm-earth needed lifting on bg-card. */
  --red-text:    #ff806b;   /* was --red #fb4934 = 3.37:1 on bg-card; now 4.72:1 */
  --blue-text:   #8bada0;   /* was --blue #83a598 = 4.31:1 on bg-card; now 4.74:1 */
  --violet-text: #dd90a5;   /* was --violet #d3869b = 4.23:1 on bg-card; now 4.75:1 */
}

html[data-theme="amber"] {
  --bg:          #1a1005;
  --bg-raised:   #221508;
  --bg-cell:     #1f130a;
  --bg-card:     #2a1c0c;
  --bg-card-hov: #3a260f;
  --line:        #3a2610;
  --line-strong: #543818;
  --ink:         #f0c896;
  --ink-dim:     #b2895e;
  --ink-faint:   #a07a52;   /* WCAG-AA on bg-raised (4.6:1); was #8a6d42 = 3.7:1 (large-text only). Earlier #6b5133 also failed. */
  --accent:       #ffb347;   /* amber CRT — the "green" lead is actually amber */
  --accent-glow:  #ffb34733;
  --accent-dim:   #a16a1a;
  --amber:       #ffd580;
  --amber-glow:  #ffd58022;
  --red:         #ff7043;
  --red-glow:    #ff704322;
  --break:       #ff9f43;
  --break-glow:  #ff9f4322;
  /* Previously #f5c16c which collapsed into the amber family and was
     indistinguishable from --amber in pills. Cool-teal-amber breaks strict
     monochrome but preserves the "due soon" vs "due today" semantic split. */
  --blue:        #8bb8a8;
  --violet:      #d49eb5;   /* dusty rose — distinct from --red orange */
}

html[data-theme="light"] {
  --bg:          #f7f6f2;
  --bg-raised:   #fefdf9;   /* warm-tinted near-white (was #ffffff). Audit 2026-05-17: never pure #fff. */
  --bg-cell:     #efeee8;
  --bg-card:     #fefdf9;   /* warm-tinted near-white */
  --bg-card-hov: #f0efe9;
  --line:        #e3e0d7;
  --line-strong: #cfcbbd;
  --ink:         #1a1a1a;
  --ink-dim:     #555555;
  --ink-faint:   #6e6e64;   /* WCAG-AA on --bg #f7f6f2 (4.8:1); was #8a8a82 = 3.5:1 (large-text only). Earlier #9a9a92 also failed. Neutral grey hue preserved. */
  --accent:       #2f8a3f;
  --accent-glow:  #2f8a3f22;
  --accent-dim:   #6aa87a;
  --amber:       #c8881f;
  --amber-glow:  #c8881f18;
  --red:         #c0392b;
  --red-glow:    #c0392b18;
  --break:       #e67e22;
  --break-glow:  #e67e2218;
  --blue:        #2d6cdf;
  --violet:      #8a4fc2;
  /* AA-compliant text variants (audit 2026-05-17). Saturated mids needed darkening on white. */
  --accent-text: #237e33;   /* was --accent #2f8a3f = 4.02:1 on bg; now 4.73:1 */
  --amber-text:  #9f5f00;   /* was --amber #c8881f = 2.78:1 on bg; now 4.72:1 */
  --blue-text:   #2968db;   /* was --blue #2d6cdf = 4.49:1 on bg; now 4.74:1 */
  --break-text:  #b64e00;   /* was --break #e67e22 = 2.63:1 on bg; now 4.76:1 */
}

html[data-theme="light"] body::before {
  /* kill the CRT scanlines on the light theme */
  background: none;
}

/* ---- Blossom — soft pastel-rose palette, light base.
       Replaces the phosphor-green accent with a hot-pink semantic slot so pills,
       pomodoro indicators, and the brand prompt all stay vibrant while keeping
       the interface warm and low-contrast on the eyes. */
html[data-theme="blossom"] {
  --bg:          #fdf2f6;   /* cotton-candy cream */
  --bg-raised:   #fffafc;   /* pink-tinted near-white (was #ffffff). Audit 2026-05-17. */
  --bg-cell:     #fbe7ef;
  --bg-card:     #fffafc;   /* pink-tinted near-white */
  --bg-card-hov: #fdd9e4;
  --line:        #f5c6d8;   /* SOFT cherry-pastel pink line (1.38:1 on cream bg). Reverted from #aa7b8d WCAG-AA lift 2026-05-17 v3: pastel cherry blossom doesn't want mid-rose-grey borders. See DESIGN.md "Soft-Border Theme Identity". */
  --line-strong: #e99cbb;   /* SOFT cherry default-button border (1.93:1 on bg). Same Soft-Border tradeoff. */
  --ink:         #5d2a3d;   /* deep rose-plum — legible on cream bg */
  --ink-dim:     #7d4c5d;   /* WCAG-AA on --bg cream (6.3:1); was #9b6379 = 4.7:1 — left no headroom for faint. Same rose-plum hue, deeper. */
  --ink-faint:   #89516a;   /* WCAG-AA on worst-case bg-card-hov (4.70:1); was #945c75 = 4.01:1 on hov — borderline. Audit 2026-05-17. */
  --accent:       #e8509c;   /* hot pink as the primary "green" semantic slot */
  --accent-glow:  #e8509c22;
  --accent-dim:   #c94184;
  --amber:       #f07d6b;   /* peach-coral for today */
  --amber-glow:  #f07d6b20;
  --red:         #e11d48;   /* rose-red for overdue */
  --red-glow:    #e11d4820;
  --break:       #f56565;   /* soft coral — keeps warmth without rose alarm */
  --break-glow:  #f5656520;
  --blue:        #a78bfa;   /* lavender */
  --violet:      #d946ef;   /* magenta */
  /* AA-compliant text variants (audit 2026-05-17). Pastel hues needed deepening on cream bg. */
  --accent-text: #c62e7a;   /* was --accent #e8509c hot pink = 3.17:1 on bg; now 4.72:1 */
  --amber-text:  #ba4735;   /* was --amber #f07d6b peach = 2.45:1 on bg; now 4.75:1 */
  --red-text:    #d7133e;   /* was --red #e11d48 = 4.30:1 on bg; now 4.74:1 */
  --blue-text:   #765ac9;   /* was --blue #a78bfa lavender = 2.49:1 on bg; now 4.71:1 */
  --break-text:  #c83838;   /* was --break #f56565 coral = 2.77:1 on bg; now 4.71:1 */
  --violet-text: #b522cb;   /* was --violet #d946ef magenta = 3.16:1 on bg; now 4.74:1 */
}

html[data-theme="blossom"] body::before {
  /* kill the CRT scanlines — same reason as the light theme */
  background: none;
}

/* ---- Solarized Light — Ethan Schoonover's parchment-tan palette, the
       canonical companion to solarized-dark. Same accent set as the dark
       variant; just inverts base and content tones. */
html[data-theme="solarized-light"] {
  --bg:          #fdf6e3;   /* base3 */
  --bg-raised:   #eee8d5;   /* base2 */
  --bg-cell:     #f5efd8;
  --bg-card:     #ffffff;   /* Schoonover's canonical Solarized card — kept pure white intentionally per spec. The impeccable "never #fff" rule is overridden for canonical-spec themes. */
  --bg-card-hov: #eee8d5;
  --line:        #e1dbc4;   /* SOFT Solarized canonical parchment line (1.29:1 on bg). Reverted from #908a73 WCAG-AA lift 2026-05-17 v3: Schoonover's canonical Solarized is precision-tuned low-contrast; lifting breaks the canonical reference. See DESIGN.md "Soft-Border Theme Identity". */
  --line-strong: #c9c2a8;   /* SOFT canonical button border (1.65:1 on bg). Same Soft-Border tradeoff. */
  --ink:         #536970;   /* WCAG-AA on bg-raised parchment (4.73:1); was solarized base01 #586e75 = 4.39:1 — borderline on bg-raised+bg-card-hov. Hue preserved. */
  --ink-dim:     #475a60;   /* WCAG-AA on bg-raised base2 (5.9:1); was solarized base00 #657b83 = 3.6:1 — large-text only. */
  --ink-faint:   #52666c;   /* WCAG-AA on bg-raised base2 (4.9:1); was solarized base1 #93a1a1 = 2.2:1 — fails AA. Solarized's intentional low-contrast palette had to be deepened for faint text on light bg. */
  --accent:       #859900;
  --accent-glow:  #85990022;
  --accent-dim:   #6f8a3a;
  --amber:       #b58900;
  --amber-glow:  #b5890018;
  --red:         #dc322f;
  --red-glow:    #dc322f18;
  --break:       #cb4b16;   /* solarized orange */
  --break-glow:  #cb4b1618;
  --blue:        #268bd2;
  --violet:      #6c71c4;
  /* AA-compliant text variants (audit 2026-05-17). Solarized's intentional low contrast needed deepening on bg-raised parchment. */
  --accent-text: #596d00;   /* was --accent #859900 = 2.62:1 on bg-raised; now 4.74:1 */
  --amber-text:  #895d00;   /* was --amber #b58900 = 2.62:1 on bg-raised; now 4.72:1 */
  --red-text:    #c71d1a;   /* was --red #dc322f = 3.77:1 on bg-raised; now 4.74:1 */
  --blue-text:   #0368af;   /* was --blue #268bd2 = 3.00:1 on bg-raised; now 4.76:1 */
  --break-text:  #b83803;   /* was --break #cb4b16 = 3.76:1 on bg-raised; now 4.75:1 */
  --violet-text: #6166b9;   /* was --violet #6c71c4 = 4.06:1 on bg; now 4.74:1 */
}

html[data-theme="solarized-light"] body::before { background: none; }

/* ---- GitHub — clean white with GitHub's brand palette. Dense, professional,
       reads great on a high-DPI display. */
html[data-theme="github"] {
  --bg:          #ffffff;   /* Canonical github.com white — kept pure intentionally. Users pick this theme to MATCH github.com exactly; tinting away from #fff breaks that brand fidelity. */
  --bg-raised:   #f6f8fa;
  --bg-cell:     #f6f8fa;
  --bg-card:     #ffffff;   /* Same — canonical github.com card bg */
  --bg-card-hov: #eaeef2;
  --line:        #d0d7de;
  --line-strong: #afb8c1;
  --ink:         #1f2328;
  --ink-dim:     #3d444d;   /* WCAG-AA on bg-raised #f6f8fa (9.3:1); was github fg.muted #656d76 = 4.9:1 — left no headroom for faint. */
  --ink-faint:   #5d646e;   /* WCAG-AA on bg-raised (5.6:1); was github fg.subtle #8c959f = 2.9:1 — fails AA. */
  --accent:       #0969da;   /* GitHub brand blue — the actual primary chrome on github.com */
  --accent-glow:  #0969da22;
  --accent-dim:   #4a8edf;
  --amber:       #9a6700;
  --amber-glow:  #9a670018;
  --red:         #cf222e;
  --red-glow:    #cf222e18;
  --break:       #bc4c00;   /* github orange */
  --break-glow:  #bc4c0018;
  --blue:        #54aeff;   /* lighter github blue — distinct from --accent brand blue */
  --violet:      #8250df;
  /* AA-compliant text variants (audit 2026-05-17). GitHub's lighter blue needed darkening on white. */
  --blue-text:   #1771c2;   /* was --blue #54aeff = 2.22:1 on bg-raised; now 4.72:1 */
}

html[data-theme="github"] body::before { background: none; }

/* ---- Catppuccin Latte — pastel-on-cream, the popular community palette.
       Soft contrast, easy on the eyes for long sessions. */
html[data-theme="catppuccin-latte"] {
  --bg:          #eff1f5;   /* base */
  --bg-raised:   #ffffff;   /* Canonical Catppuccin Latte spec — kept pure white. Catppuccin is a precisely-specified palette; users pick this theme to MATCH the canonical reference. */
  --bg-cell:     #e6e9ef;   /* mantle */
  --bg-card:     #ffffff;   /* Same — canonical Catppuccin spec */
  --bg-card-hov: #dce0e8;   /* crust */
  --line:        #ccd0da;   /* surface0 */
  --line-strong: #bcc0cc;   /* surface1 */
  --ink:         #4c4f69;   /* text */
  --ink-dim:     #4c4f67;   /* WCAG-AA on --bg base (7.1:1); was catppuccin subtext1 #6c6f85 = 4.9:1 — left no headroom for faint. */
  --ink-faint:   #65687e;   /* WCAG-AA on --bg base (4.8:1); was catppuccin subtext0 #8c8fa1 = 3.2:1 — large-text only. */
  --accent:       #8839ef;   /* mauve — the iconic Catppuccin signature, now leads */
  --accent-glow:  #8839ef22;
  --accent-dim:   #a36ee8;
  --amber:       #df8e1d;   /* yellow */
  --amber-glow:  #df8e1d18;
  --red:         #d20f39;   /* red */
  --red-glow:    #d20f3918;
  --break:       #fe640b;   /* catppuccin peach */
  --break-glow:  #fe640b18;
  --blue:        #1e66f5;
  --violet:      #ea76cb;   /* Catppuccin pink — was holding mauve which moved to --accent */
  /* AA-compliant text variants (audit 2026-05-17). Catppuccin's pastel-mids needed deepening on light bg. */
  --amber-text:  #a65500;   /* was --amber #df8e1d = 2.31:1 on bg; now 4.74:1 */
  --blue-text:   #175fee;   /* was --blue #1e66f5 = 4.34:1 on bg; now 4.75:1 */
  --break-text:  #ca3000;   /* was --break #fe640b = 2.64:1 on bg; now 4.72:1 */
}

html[data-theme="catppuccin-latte"] body::before { background: none; }

/* ---- Pearl — clean, neutral, business-default. Off-white surfaces, slate
       text, business blue for "running / done", cyan for "due soon" and
       external events. Designed to feel like Linear / Notion / Asana
       rather than a coder editor. Default for new installs. */
html[data-theme="pearl"] {
  --bg:          #fafbfc;
  --bg-raised:   #fbfcfe;   /* cool-tinted near-white (was #ffffff). Audit 2026-05-17. */
  --bg-cell:     #f4f6f8;
  --bg-card:     #fbfcfe;   /* cool-tinted near-white */
  --bg-card-hov: #eef1f5;
  --line:        #e3e7ec;   /* SOFT (1.20:1 on bg) — Notion/Linear/Asana feel. Reverted from #898d92 WCAG-AA lift on user feedback 2026-05-17 ("looks very noisy not soft"). Pearl's brand identity overrides strict AA UI 3:1 here. See DESIGN.md "Soft-Border Theme Identity" rule. */
  --line-strong: #c5ccd5;   /* SOFT (1.56:1 on bg) — more present than --line for default-button borders + focused states but still calm. Same Soft-Border tradeoff. */
  --ink:         #1c2330;
  --ink-dim:     #4a5260;
  --ink-faint:   #626b7b;   /* WCAG-AA on the WORST-case bg (--bg-card-hov #eef1f5, 4.74:1); was #666f7f = 4.47:1 — borderline on hover bg. Audit 2026-05-17. */
  --accent:       #2563eb;   /* business blue — Linear/Notion/Asana primary; passes 4.59:1 on white as text */
  --accent-glow:  #2563eb22;
  --accent-dim:   #5b8bef;
  --amber:       #b65400;   /* WCAG-AA on --bg + --bg-card (4.76:1); was #d97706 = 3.07:1 — chip text unreadable. Same warm-amber hue, deeper. */
  --amber-glow:  #b6540018;
  --red:         #dc2626;
  --red-glow:    #dc262618;
  --break:       #ce3c00;   /* WCAG-AA on --bg + --bg-card (4.75:1); was #ea580c = 3.44:1 — chip text unreadable. Same warm-orange hue, deeper. */
  --break-glow:  #ce3c0018;
  --blue:        #007a9b;    /* WCAG-AA on --bg + --bg-card (4.76:1); was #0891b2 cyan-600 = 3.55:1 — chip text unreadable. Same cyan hue, deeper. */
  --violet:      #7c3aed;
}

html[data-theme="pearl"] body::before { background: none; }

/* ---- Break-overlay --bo-amber per light theme (audit 2026-05-17 v2) ------
   The break overlay's breathing orb + clock numerals sit on a fixed dark
   scrim (rgba(0,0,0,0.82)) regardless of theme. Light themes have their
   --amber token tuned DARK for WCAG AA on white card bg — e.g. pearl's
   --amber #b65400 deep orange, catppuccin's #df8e1d mustard, blossom's
   #f07d6b peach-coral. Drawn on the dark scrim, those collapse to near-
   invisible.

   Override --bo-amber inside .break-overlay so the orb + clock use a
   bright warm value that pops on dark, while every OTHER amber usage
   (chip text on light bg, today indicator on light cards, etc.) stays
   AA-compliant via the unchanged --amber + --amber-text tokens.

   Dark themes don't need this override — their --amber is already bright
   for legibility on dark surfaces. */
html[data-theme="pearl"] .break-overlay,
html[data-theme="light"] .break-overlay,
html[data-theme="solarized-light"] .break-overlay,
html[data-theme="github"] .break-overlay {
  --bo-amber: #ffb347;   /* warm classic CRT amber — same hue family as the AA --amber, lifted to glow on dark scrim */
}
html[data-theme="blossom"] .break-overlay {
  --bo-amber: #ff9a82;   /* warm coral lifted for dark scrim — matches blossom's cherry-peach identity */
}
html[data-theme="catppuccin-latte"] .break-overlay {
  --bo-amber: #fab387;   /* canonical Catppuccin "peach" — bright on dark, matches palette */
}

/* Same root cause for the QUOTE text + extend/settings buttons inside the
   break overlay's style-orb and style-cinema variants — those two are the
   only variants where .bo-card is transparent (text renders DIRECTLY on
   the dark scrim, no theme-bg surface behind it). The theme's --ink-dim
   / --ink-faint / --line-strong values are dark for AA on light card bg,
   so they collapse to invisible on the dark scrim.

   Spotlight, curtain, vignette, and side variants all have either a card
   bg, a gradient overlay, or a positioned card with shadow — text in those
   variants sits on a theme-bg surface and the regular tokens work. */
html:is([data-theme="pearl"], [data-theme="light"], [data-theme="blossom"], [data-theme="solarized-light"], [data-theme="github"], [data-theme="catppuccin-latte"]) .break-overlay:is(.style-orb, .style-cinema) {
  --ink:          rgba(255, 255, 255, 0.95);
  --ink-dim:      rgba(255, 255, 255, 0.72);
  --ink-faint:    rgba(255, 255, 255, 0.50);
  --line-strong:  rgba(255, 255, 255, 0.32);
  --line:         rgba(255, 255, 255, 0.16);
}

/* color-scheme overrides for light themes. The :root default is `dark`;
   these themes opt into the OS light dropdown / native control rendering
   so a `<select>` on a white page doesn't pop a black menu. Applied at
   the html root so it covers EVERY native control (selects, time/date
   pickers, scrollbars, autofill backgrounds) without needing per-element
   declarations. */
html[data-theme="light"],
html[data-theme="blossom"],
html[data-theme="solarized-light"],
html[data-theme="github"],
html[data-theme="catppuccin-latte"],
html[data-theme="pearl"] {
  color-scheme: light;
}

/* ---- Light-theme tweaks --------------------------------------------------
   The light/business themes opt into a softer 8px radius so they feel a
   little less code-editor at the corners. The earlier `--mono` override
   to a system sans stack was DROPPED in schemaVersion 6 — light themes
   now keep the same monospace font as dark themes, and any user who
   wants sans can pick the "system" preset from the appearance settings
   font picker (which is now visible on every theme, not just dark). */
html:is([data-theme="pearl"], [data-theme="light"], [data-theme="blossom"], [data-theme="solarized-light"], [data-theme="github"], [data-theme="catppuccin-latte"]) {
  --radius: 8px;
}
