/* ==========================================================================
   BOARDLY - shared design system v2
   Theme: a bright, colorful work desk. Light mode = warm paper + blueprint
   grid; dark mode = deep ink desk lamp glow. One set of CSS variables
   drives both, and Tailwind's color tokens point at those variables (see
   each page's tailwind.config), so `bg-paper`, `text-ink`, etc. repaint
   automatically the instant .dark is toggled on <html> - no dark: prefixes
   needed anywhere in the markup.
   ========================================================================== */

/* ==========================================================================
   BOARDLY DESIGN TOKENS v3 - "Drafting Desk"
   Identity: a real drafter's desk. Paper is the surface you work on
   (blueprint-gridded), ink is what you write with, and the accents are the
   colored pencils/washi tape/highlighter tabs sitting on that desk. Fraunces
   (an inked, editorial serif with real weight) carries anything you'd write
   BY HAND - titles, hero numbers, section names. Geist Sans carries
   anything the INTERFACE says - labels, nav, buttons, body copy. IBM
   Plex Mono carries anything MEASURED - dates, durations, counts,
   technical metadata - the way a blueprint annotates dimensions in a
   monospaced hand. One token set drives light + dark; see TYPE SCALE
   below for hierarchy.

   TYPEFACE HISTORY (Boardly 2.0, Phase 1): body/UI face changed twice
   this phase. First General Sans to Synonym (General Sans is banned by
   the brief's Section 37 for being visually generic). Then, after
   Charles saw Synonym live and asked for something closer to
   claude.ai's own clean style, Synonym to Geist Sans: Google Fonts
   hosted, free, variable, not on Section 37's ban list. Charles chose
   it after a live side by side comparison against Switzer (the closer
   match to claude.ai's actual body face, Styrene B, which is a paid
   Commercial Type license and not something this project can legally
   embed). Both swaps cost nothing structurally, still a one-line
   variable change here plus a CDN link + Tailwind config change on
   every page, because the whole codebase already routes body text
   through var(--font-body) / font-body rather than hardcoding the
   family name anywhere. Fraunces (display) and IBM Plex Mono (data)
   are kept. Neither is on the brief's ban list, and both are
   load-bearing brand identity across 24 shipped pages (marketing site,
   every resume template, every document export).
   See docs/BOARDLY_DESIGN_SYSTEM.md for the full decision record.
   ========================================================================== */
:root{
  /* -- surfaces: the desk, layered -- */
  --paper:#EEF1F5;       /* the desk itself */
  --paper-2:#E4E9F2;      /* a paper pad set on the desk */
  --surface-1:#FFFFFF;    /* a sheet lifted off the pad (cards) */
  --surface-2:#F7F9FC;    /* a sheet under something (nested panels) */
  --surface-3:#FFFFFF;    /* the topmost sheet (modals, popovers) */
  --card:#FFFFFF;
  --grid-line:#D3DBE8;
  --line:#D7DEEA;
  --line-strong:#C1CBDE;

  /* -- ink: what's written on the desk -- */
  --ink:#12203A;
  --ink-soft:#546279;
  --ink-faint:#8A96AC;

  /* -- brand: the desk lamp glow (primary) + the ruled ruler (secondary) -- */
  --brand:#E8622C;
  --brand-dark:#C64F1F;
  --brand-soft:#FBE4D8;
  --secondary:#0F9A78;
  --secondary-dark:#0C7A5F;
  --secondary-soft:#DCF2EA;

  /* -- accents: colored pencils on the desk, used sparingly for tags/data -- */
  --violet:#6355C7;
  --pink:#DB4C8C;
  --amber:#C99118;

  /* -- semantic roles (never invent a new color for these; reuse) -- */
  --success:#0F9A78;
  --success-soft:#DCF2EA;
  --warning:#C99118;
  --warning-soft:#F7EACD;
  --critical:#D64545;
  --critical-soft:#FBE1E1;
  --info:#3E7BD6;
  --info-soft:#E1EBFB;

  /* legacy aliases kept so nothing already using --orange/--teal breaks */
  --orange:var(--brand);
  --orange-dark:var(--brand-dark);
  --teal:var(--secondary);

  /* -- elevation -- */
  --shadow-xs: 0 1px 2px rgba(18,32,58,.06);
  --shadow: 0 1px 2px rgba(18,32,58,.05), 0 2px 6px rgba(18,32,58,.04), 0 16px 40px -14px rgba(18,32,58,.16);
  --shadow-lg: 0 8px 16px -6px rgba(18,32,58,.14), 0 32px 70px -20px rgba(18,32,58,.34);
  --shadow-focus: 0 0 0 3px color-mix(in srgb, var(--brand) 28%, transparent);

  /* -- motion -- */
  --ease-premium: cubic-bezier(.16,1,.3,1);
  --ease-out: cubic-bezier(.22,1,.36,1);
  --dur-instant: 120ms;
  --dur-fast: 200ms;
  --dur-base: 350ms;
  --dur-slow: 550ms;

  /* -- type -- */
  --font-display:'Fraunces', Georgia, serif;
  --font-body:'Geist', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono:'IBM Plex Mono', ui-monospace, monospace;

  /* -- spacing scale (Boardly 2.0 Phase 1) -- an explicit 4px-based scale so
     new components stop hand-picking one-off rem values. Existing markup
     that already uses Tailwind's own spacing utilities (p-4, gap-3, etc.)
     is untouched and keeps working. This scale is for new CSS-level
     components (modals, sheets, tables) that need a named value, not a
     replacement for Tailwind's scale. -- */
  --space-1:4px;  --space-2:8px;  --space-3:12px; --space-4:16px;
  --space-5:20px; --space-6:24px; --space-8:32px; --space-10:40px;
  --space-12:48px; --space-16:64px; --space-20:80px;

  /* -- radius scale -- names the values already used ad hoc across the
     codebase (.5rem/8px on inputs, .75rem/12px on cards, 999px on pills)
     so new components reference the same handful of values on purpose. -- */
  --radius-sm:6px;  --radius-md:8px;  --radius-lg:12px;
  --radius-xl:16px; --radius-full:999px;

  /* -- IA-domain accents (Boardly 2.0) -- Section 38 of the Boardly 2.0
     brief asks for dedicated semantic tokens for the money/client/work
     domains introduced by the new navigation (Section 4), distinct from
     the general accent/success/warning/critical/info roles above. Mapped
     onto existing, already-dark-mode-paired accent colors rather than
     inventing new ones: Money reuses the secondary/teal (a color already
     read as "positive/ledger" in the existing UI), Clients reuses pink,
     Work reuses violet. New Money/Clients/Work-scoped UI should reach for
     these instead of --secondary/--pink/--violet directly, so the domain
     mapping stays swappable in one place later if needed. -- */
  --money:var(--secondary); --money-soft:var(--secondary-soft);
  --client:var(--pink);
  --work:var(--violet);
}

html.dark{
  --paper:#0D1220;
  --paper-2:#141B2C;
  --surface-1:#161D30;
  --surface-2:#111828;
  --surface-3:#1B2338;
  --card:#161D30;
  --grid-line:#1C2438;
  --line:#26304A;
  --line-strong:#33405E;

  --ink:#F1F4FA;
  --ink-soft:#9AA6BE;
  --ink-faint:#6C7898;

  --brand:#F3773D;
  --brand-dark:#FF8A52;
  --brand-soft:#3A2718;
  --secondary:#2BC79A;
  --secondary-dark:#4FE0B6;
  --secondary-soft:#0F2E27;

  --violet:#8B7CF0;
  --pink:#F06BA8;
  --amber:#E0AC3F;

  --success:#2BC79A;
  --success-soft:#0F2E27;
  --warning:#E0AC3F;
  --warning-soft:#332411;
  --critical:#EB6767;
  --critical-soft:#341717;
  --info:#6EA3EE;
  --info-soft:#152238;

  --shadow-xs: 0 1px 2px rgba(0,0,0,.4);
  --shadow: 0 1px 2px rgba(0,0,0,.35), 0 2px 8px rgba(0,0,0,.3), 0 18px 44px -14px rgba(0,0,0,.55);
  --shadow-lg: 0 10px 20px -6px rgba(0,0,0,.5), 0 36px 80px -20px rgba(0,0,0,.7);
  --shadow-focus: 0 0 0 3px color-mix(in srgb, var(--brand) 35%, transparent);
}

/* ---- TYPE SCALE ----
   Sizes are deliberate, not a generic 4/8/12/16/24 ramp. Display sizes lean
   on Fraunces' optical-size axis (opsz) so headlines get the "hand-inked"
   ink-trap detail while small sizes stay crisp. Use these classes instead
   of ad hoc text-[Npx] utilities wherever a new component is built. */
.text-display{ font-family:var(--font-display); font-weight:600; font-size:clamp(2.25rem,4vw + 1rem,3.5rem); line-height:1.05; letter-spacing:-.02em; font-variation-settings:'opsz' 144; }
.text-hero{ font-family:var(--font-display); font-weight:600; font-size:clamp(1.75rem,2.5vw + 1rem,2.5rem); line-height:1.1; letter-spacing:-.015em; font-variation-settings:'opsz' 90; }
.text-page-title{ font-family:var(--font-display); font-weight:600; font-size:1.5rem; line-height:1.2; letter-spacing:-.01em; font-variation-settings:'opsz' 40; }
.text-section-title{ font-family:var(--font-display); font-weight:600; font-size:1.125rem; line-height:1.3; letter-spacing:-.005em; font-variation-settings:'opsz' 28; }
.text-card-title{ font-family:var(--font-body); font-weight:600; font-size:.9375rem; line-height:1.35; }
.text-body{ font-family:var(--font-body); font-weight:400; font-size:.9375rem; line-height:1.55; }
.text-secondary{ font-family:var(--font-body); font-weight:400; font-size:.8125rem; line-height:1.5; color:var(--ink-soft); }
.text-meta{ font-family:var(--font-mono); font-weight:500; font-size:.6875rem; letter-spacing:.02em; color:var(--ink-soft); }
.text-label{ font-family:var(--font-body); font-weight:600; font-size:.75rem; letter-spacing:.03em; text-transform:uppercase; color:var(--ink-soft); }
.text-number{ font-family:var(--font-mono); font-weight:600; font-variant-numeric:tabular-nums; }

/* ==========================================================================
   COMPONENT LAYER - buttons, badges, focus
   Phase 3. Before this, every button was a hand-repeated string of Tailwind
   utilities (bg-orange text-white font-semibold px-5 py-2.5 rounded-full...)
   copy-pasted 7+ times across the file - the exact "assembled, not designed"
   problem the brief calls out. These classes are the single source of truth
   now; .btn-pop's existing tactile press animation (translateY + scale on
   hover/active) is preserved underneath every variant, not replaced.
   ========================================================================== */

.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:.5rem;
  font-family:var(--font-body); font-weight:600; font-size:.875rem;
  padding:.625rem 1.25rem; border-radius:999px; border:1px solid transparent;
  cursor:pointer; white-space:nowrap; line-height:1;
}
.btn:disabled, .btn[aria-disabled="true"]{ opacity:.5; cursor:not-allowed; transform:none !important; box-shadow:none !important; }
.btn:focus-visible{ outline:none; box-shadow:var(--shadow-focus); }
.btn-sm{ padding:.5rem 1rem; font-size:.8125rem; }
.btn-lg{ padding:.8125rem 1.75rem; font-size:.9375rem; }

.btn-primary{ background-image: linear-gradient(180deg, color-mix(in srgb, white 14%, var(--brand)), var(--brand) 60%); color:#fff; }
.btn-primary:hover:not(:disabled){ background-image: linear-gradient(180deg, color-mix(in srgb, white 18%, var(--brand)), var(--brand) 60%); }

.btn-secondary{ background-color:var(--surface-1); color:var(--ink); border-color:var(--line-strong); }
.btn-secondary:hover:not(:disabled){ border-color:var(--brand); color:var(--brand); }

.btn-ghost{ background-color:transparent; color:var(--ink-soft); border-color:var(--line); }
.btn-ghost:hover:not(:disabled){ color:var(--brand); border-color:var(--brand); }

.btn-destructive{ background-color:transparent; color:var(--critical); border-color:var(--critical-soft); }
.btn-destructive:hover:not(:disabled){ background-color:var(--critical-soft); }

.btn-icon{ padding:.5rem; border-radius:999px; width:2.25rem; height:2.25rem; background-color:transparent; color:var(--ink-soft); border-color:transparent; }
.btn-icon:hover:not(:disabled){ background-color:var(--surface-2); color:var(--ink); }

/* Loading state: add .is-loading + a nested .btn-spinner span. Text stays in
   the DOM (so width doesn't jump) but is hidden; spinner takes its place. */
.btn.is-loading{ color:transparent !important; pointer-events:none; }
.btn.is-loading .btn-spinner{ position:absolute; width:16px; height:16px; border-radius:50%; border:2px solid rgba(255,255,255,.4); border-top-color:#fff; animation:btn-spin .6s linear infinite; }
.btn-secondary.is-loading .btn-spinner, .btn-ghost.is-loading .btn-spinner{ border-color:var(--line-strong); border-top-color:var(--ink); }
@keyframes btn-spin{ to{ transform:rotate(360deg); } }

/* ---- badges / status pills ----
   Always one of these four semantic roles - never a one-off color. */
.badge{ display:inline-flex; align-items:center; gap:.375rem; font-family:var(--font-mono); font-weight:500; font-size:.6875rem; letter-spacing:.02em; padding:.3125rem .625rem; border-radius:999px; }
.badge-info{ background-color:var(--info-soft); color:var(--info); }
.badge-success{ background-color:var(--success-soft); color:var(--success); }
.badge-warning{ background-color:var(--warning-soft); color:var(--warning); }
.badge-critical{ background-color:var(--critical-soft); color:var(--critical); }

/* ---- shared focus ring for non-.btn interactive elements ---- */
a:focus-visible, [role="button"]:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible{
  outline:none; box-shadow:var(--shadow-focus); border-radius:6px;
}

/* ---- dropdown / context menus ----
   These already reused .ticket for the panel look, but had no motion at
   all - toggling the "hidden" class just snaps the menu on screen. Since
   display:none elements have animations reset, adding one keyframe here
   is enough: it replays automatically every time a menu goes from hidden
   to visible, with no JS changes needed anywhere the 7 menus in
   dashboard.html already toggle .hidden. */
.dropdown-menu{ animation: dropdown-in 160ms var(--ease-out); }
@keyframes dropdown-in{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:none; } }

/* ---- menu items ----
   Replaces "w-full text-left px-3.5 py-2 text-sm hover:bg-[var(--paper-2)]
   flex items-center gap-2", hand-repeated 12 times across the 7 dropdown
   menus (board switcher, export, more, templates, notes templates). */
.menu-item{ width:100%; text-align:left; padding:.5rem .875rem; font-size:.875rem; display:flex; align-items:center; gap:.5rem; border-radius:.375rem; }
.menu-item:hover{ background-color:var(--surface-2); }
.menu-item-accent{ color:var(--brand); }

/* ---- task card metadata chips ----
   Replaces "font-mono text-[10px] text-ink-soft flex items-center gap-1",
   which was hand-repeated 7 times inside taskCardHTML() in dashboard.js
   for due date / reminder / attachments / git / time-tracking / blocked
   indicators. Same fix as the button system, one component instead of a
   copy-pasted utility string. Color is still set per-instance via inline
   style where it's conditional (overdue = brand, tracking = brand, etc.)
   so no JS logic changes - this only removes the duplicated class list. */
.meta-chip{ font-family:var(--font-mono); font-size:10px; color:var(--ink-soft); display:inline-flex; align-items:center; gap:.25rem; }

/* ---- form inputs + labels ----
   The edit-task form had 18 labels and 18 near-identical input/select/
   textarea class strings (differing only in a stray mb-4 or px-3 vs
   px-3.5 that had drifted apart over time from copy-pasting). One input
   style now, spacing stays as separate utility classes (mb-4 etc.) next
   to it so layout is still adjustable per-field without touching the
   shared look. */
.input{
  width:100%; border:1px solid var(--line); border-radius:.5rem;
  padding:.625rem .875rem; font-size:.875rem; background-color:var(--card);
  font-family:var(--font-body); color:var(--ink);
}
.input:focus{ outline:none; border-color:var(--brand); box-shadow:0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent); }
.input-sm{ padding:.375rem .625rem; font-size:.75rem; }
.form-label{ display:block; font-family:var(--font-body); font-size:.75rem; color:var(--ink-soft); margin-bottom:.375rem; }

.btn-icon-sq{ height:2.25rem; width:2.25rem; flex-shrink:0; border-radius:.5rem; border:1px solid var(--line); display:flex; align-items:center; justify-content:center; color:var(--ink-soft); background-color:transparent; }
.btn-icon-sq:hover{ color:var(--brand); border-color:var(--brand); }

.icon-link{ color:var(--ink-soft); }
.icon-link:hover{ color:var(--brand); }

.btn-icon-xs{ height:1.5rem; width:1.5rem; border-radius:999px; display:flex; align-items:center; justify-content:center; color:var(--ink-soft); background-color:transparent; }
.btn-icon-xs:hover{ color:var(--brand); background-color:var(--surface-2); }

*{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }

/* ---- page-load entrance: every page's main content settles in instead
   of just snapping into place - small, but it's exactly the kind of
   detail that separates "a website" from "an app" ---- */
main{ animation: page-in .5s var(--ease-premium) backwards; }
@keyframes page-in{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:translateY(0); } }
@media (prefers-reduced-motion: reduce){ main{ animation:none; } }
html, body{ max-width:100%; overflow-x:clip; }

/* ---- native-feel polish for iOS ----
   Without these, tapping anything interactive on iOS Safari flashes a
   dull grey box over it (the default tap-highlight), long-pressing a
   card can pop up the "Copy/Look Up" text-selection callout instead of
   your drag/long-press handler, and any internally-scrolling panel
   (the edit modal, a popover) scrolls with visible per-frame stepping
   instead of the smooth deceleration native apps have. */
*{ -webkit-tap-highlight-color:transparent; }
button, a, .ticket, [role="button"], [data-id]{ -webkit-touch-callout:none; }
.tdp-popover, #edit-form, #cmdk-results, #ai-messages, main{ -webkit-overflow-scrolling:touch; }

/* Mobile browsers (iOS Safari, and several Android browsers too) zoom the
   whole page in when a focused form field's font-size is under 16px.
   Every input/select/textarea gets bumped to 16px here - unconditionally,
   not just under some breakpoint, since the same field can be focused in
   any orientation or window size and the zoom would still trigger.
   [contenteditable] covers the inline card-title editor - it's not a
   real <input>, but iOS zooms on it the exact same way. */
input, select, textarea, [contenteditable="true"]{ font-size:16px !important; }

/* iOS/WebKit sometimes still draws its own default focus ring on a text
   field even with outline:none set, unless -webkit-appearance is also
   explicitly cleared. Left off <select> deliberately - clearing it there
   removes the native dropdown arrow with nothing to replace it. */
input:not([type="checkbox"]):not([type="radio"]):not([type="color"]), textarea{ -webkit-appearance:none; appearance:none; box-shadow:none; }
input:focus, select:focus, textarea:focus, [contenteditable]:focus{ outline:none; box-shadow:none; }

/* Grid/flex items default to min-width:auto, which sizes them to fit their
   longest unbreakable "word" (an email, URL, or long code string). Without
   this reset, a single long word in a ticket title forces #board and its
   columns wider than the viewport, pushing content off-screen. Setting
   min-width:0 lets these shrink so the text can actually wrap instead. */
#board, #board > section, .kanban-col, .ticket{ min-width:0; }

/* Keep normal vertical scrolling in control on touch screens. Tickets move
   only from their deliberate grip handle; see initSortable(). */
#board, .kanban-col, .ticket[data-id]{ touch-action:pan-y; }

/* ---- kill the double-tap-to-zoom gesture on anything interactive -
   "manipulation" still allows normal pinch-zoom and scrolling, it only
   removes the ~300ms double-tap-zoom detection that was making fast
   double-taps on buttons/cards zoom the page instead of registering as
   two clicks. (.ticket[data-id] above already gets pan-y for dragging;
   manipulation here covers everything else clickable site-wide.) ---- */
button, a, input, select, textarea, label, .toolbar-btn, .btn-pop, [role="button"], [onclick], .icon-badge{
  touch-action: manipulation;
}
.drag-handle{ touch-action:none; cursor:grab; }
.drag-handle:active{ cursor:grabbing; }

:root{ color-scheme: light; }
html.dark{ color-scheme: dark; }

/* Without the color-scheme declarations above, a browser that sees the
   device's OS-level dark/light setting disagreeing with Boardly's own
   theme toggle will silently recolor native form-control internals
   (typed text, the caret, autofill) to match the OS instead of us - the
   classic "I can see the input box but not what I'm typing" bug. These
   force every field to use our own ink color regardless of what the
   browser guessed. */
input, select, textarea, [contenteditable="true"]{ color:var(--ink) !important; caret-color:var(--ink); }
input::placeholder, textarea::placeholder{ color:var(--ink-soft); opacity:1; }

body{
  background-color:var(--paper);
  color:var(--ink);
  font-family:var(--font-body);
  background-image:
    radial-gradient(60vw 40vh at 8% -8%, color-mix(in srgb, var(--brand) 12%, transparent), transparent 60%),
    radial-gradient(50vw 40vh at 96% 4%, color-mix(in srgb, var(--violet) 12%, transparent), transparent 60%),
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: auto, auto, 32px 32px, 32px 32px;
  background-position: 0 0, 0 0, -1px -1px, -1px -1px;
  transition: background-color .35s ease, color .35s ease;
}

.font-display{ font-family:var(--font-display); font-variation-settings:'opsz' 40; }
.font-mono{ font-family:var(--font-mono); }
.surface{ background-color:var(--paper-2); transition: background-color .35s ease; }

/* ---- color wash blobs (decorative, behind hero content) ---- */
.blob{
  position:absolute;
  border-radius:9999px;
  filter: blur(60px);
  opacity:.5;
  pointer-events:none;
  z-index:0;
  animation: blob-drift 18s ease-in-out infinite;
}
.blob:nth-of-type(2){ animation-duration:22s; animation-direction:reverse; }
.blob:nth-of-type(3){ animation-duration:26s; }
@keyframes blob-drift{ 0%,100%{ transform:translate(0, var(--py, 0px)) scale(1); } 50%{ transform:translate(24px, calc(-20px + var(--py, 0px))) scale(1.08); } }

/* ==========================================================================
   Cinematic layer - additive on purpose, nothing above this line changes.
   See GUIDE.md "The cinematic pass, explained slowly" for how each piece
   works. Ported from the same system built for Pulse, adapted to
   Boardly's warm-paper palette and kept clear of anything SortableJS
   touches (kanban tickets get the spotlight glow, since that's just a
   background glow with no transform, but not the pointer-tilt, since a
   3D rotation fighting a drag transform is exactly the kind of thing
   that looks buggy rather than delightful).
   ========================================================================== */

/* ---- 3D tilt: a container gets perspective, the child inside it rotates
   on pointermove via --rx/--ry that site.js writes. Applied to a few
   showcase elements (the hero ticket stack, the browser mockup), not
   every card on the page. ---- */
.tilt-wrap{ perspective:1200px; }
.tilt-el{
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateZ(0);
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
  transform-style: preserve-3d;
}
.tilt-wrap:hover .tilt-el{ transition: transform .08s linear; }

/* ---- hero scroll-scrub: the hero ticket stack leans back and shrinks
   slightly as you scroll past it. Lives on its own transform layer,
   separate from the pointer-tilt above, so the two never fight - no
   transition here on purpose, scroll-linked motion needs to track the
   scrollbar exactly. ---- */
.hero-scrub{
  transform: rotateX(var(--scroll-rx, 0deg)) scale(var(--scroll-scale, 1));
  transform-style: preserve-3d;
  will-change: transform;
}

/* ---- cursor spotlight: a soft glow that follows your pointer inside a
   card. Safe to put on .ticket-hover globally, including kanban tickets,
   since it's a background glow with no transform of its own - it won't
   fight SortableJS while you're dragging. ---- */
.ticket-hover{ isolation:isolate; }
.ticket-hover::before{
  content:""; position:absolute; inset:0; border-radius:inherit; z-index:0;
  background:radial-gradient(280px circle at var(--mx,50%) var(--my,50%), color-mix(in srgb, var(--orange) 10%, transparent), transparent 70%);
  opacity:0; transition:opacity .35s ease; pointer-events:none;
}
.ticket-hover:hover::before{ opacity:1; }
.ticket-hover > *{ position:relative; z-index:1; }

/* ---- magnetic buttons: pulls gently toward the cursor as it nears,
   springs back on leave (see initMagnetic in site.js) ---- */
.magnetic{
  transform: translate(var(--mx-btn, 0px), var(--my-btn, 0px));
  transition: transform .25s cubic-bezier(.2,.8,.2,1);
  will-change: transform;
}
.magnetic:active{ transition-duration: .08s; }

/* ---- smooth crossfade between page loads instead of a hard flash -
   native browser feature, one line, silently ignored by browsers that
   don't support it yet ---- */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root){ animation:none; mix-blend-mode:normal; }
::view-transition-old(root){ z-index:1; }
::view-transition-new(root){ z-index:2; }

/* ---- top-of-page loading bar during navigation ---- */
#nav-progress{
  position:fixed; top:0; left:0; height:3px; z-index:200;
  background:linear-gradient(90deg, var(--orange), var(--violet));
  width:0%; opacity:0;
  transition: width .3s ease, opacity .2s ease;
}
#nav-progress.nav-progress-active{ opacity:1; }

/* ---- back to top ---- */
#back-to-top{
  transition: opacity .25s ease, transform .25s ease, visibility .25s ease, bottom .25s var(--ease-premium);
}
#back-to-top[data-visible="false"]{ opacity:0; visibility:hidden; transform:translateY(8px); pointer-events:none; }
#back-to-top[data-visible="true"]{ opacity:1; visibility:visible; transform:translateY(0); }

/* ---- film grain: an almost-subliminal texture over the whole page ---- */
.grain-overlay{
  position:fixed; inset:0; z-index:999; pointer-events:none; opacity:.03; mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- text selection tint ---- */
::selection{ background:color-mix(in srgb, var(--orange) 30%, transparent); color:var(--ink); }

/* ---- staggered list entrance (command palette results, etc.) ---- */
.row-enter{ animation: row-in .35s cubic-bezier(.2,.8,.2,1) both; }
@keyframes row-in{ from{ opacity:0; transform:translateY(6px); } to{ opacity:1; transform:translateY(0); } }

/* ---- newly created ticket materializes in, instead of just appearing
   silently (see addTask in dashboard.js) ---- */
.ticket-pop-in{ animation: ticket-pop .4s cubic-bezier(.2,.9,.25,1.1) both; }
@keyframes ticket-pop{ from{ opacity:0; transform:scale(.85) translateY(-8px); } to{ opacity:1; transform:scale(1) translateY(0); } }

/* ---- confetti burst (see celebrate() in site.js) ---- */
.confetti-piece{
  position:fixed; z-index:300; pointer-events:none;
  width:7px; height:7px; border-radius:2px;
  transform:translate(-50%,-50%);
  animation: confetti-burst .7s cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes confetti-burst{
  0%{ transform:translate(-50%,-50%) rotate(0deg) scale(1); opacity:1; }
  100%{ transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy) + 40px)) rotate(var(--rot)) scale(.4); opacity:0; }
}

/* ---- dark mode's own "desk lamp" spotlight: a soft warm glow that
   follows your cursor across the whole page, not just inside cards -
   leaning into the theme this palette already describes itself as
   (see the comment at the top of this file) rather than adding an
   unrelated effect. Light mode doesn't get this; a lamp glow makes no
   visual sense against a bright paper background. ---- */
#desk-lamp{
  position:fixed; inset:0; z-index:1; pointer-events:none;
  opacity: var(--lamp-opacity, 0);
  background:radial-gradient(420px circle at var(--lamp-x,50%) var(--lamp-y,50%), color-mix(in srgb, var(--orange) 7%, transparent), transparent 70%);
}
html.dark{ --lamp-opacity: 1; }
@media (prefers-reduced-motion: reduce){
  #desk-lamp{ display:none; }
  .blob{ animation:none; }
  .tilt-el{ transform:none !important; }
  .magnetic{ transition:none; transform:none !important; }
  .row-enter{ animation:none !important; opacity:1 !important; transform:none !important; }
  .ticket-overdue, .kanban-col.drag-over{ animation:none !important; }
}

/* ---- card / ticket, the signature element (clean, no dashes) ---- */
.ticket{
  position:relative;
  background:var(--card);
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.5);
  transition: transform .35s var(--ease-premium), box-shadow .35s var(--ease-premium), background-color .35s ease, border-color .35s ease;
}
html.dark .ticket{ box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.04); }
.ticket-hover:hover{
  transform: translateY(-5px) scale(1.004);
  box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255,255,255,.5);
  border-color: color-mix(in srgb, var(--orange) 25%, var(--line));
}
html.dark .ticket-hover:hover{ box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255,255,255,.06); }

.rail-orange{ border-left:4px solid var(--orange); border-top-left-radius:4px; border-bottom-left-radius:4px; }
.rail-teal{ border-left:4px solid var(--teal); border-top-left-radius:4px; border-bottom-left-radius:4px; }
.rail-violet{ border-left:4px solid var(--violet); border-top-left-radius:4px; border-bottom-left-radius:4px; }
.rail-ink{ border-left:4px solid var(--ink-soft); border-top-left-radius:4px; border-bottom-left-radius:4px; }

/* ---- icon badges: small colorful circular icon chips, used anywhere an
   icon needs to feel like a real design element instead of a grey glyph
   (column headers, section headers, empty states). Each ships with its
   own soft tint background so the icon's color reads immediately. ---- */
.icon-badge{
  display:inline-flex; align-items:center; justify-content:center;
  width:22px; height:22px; border-radius:7px; font-size:11px;
  transition: transform .3s var(--ease-premium), box-shadow .3s var(--ease-premium);
}
.icon-badge-orange{ color:var(--orange); background:color-mix(in srgb, var(--orange) 14%, transparent); }
.icon-badge-teal{ color:var(--teal); background:color-mix(in srgb, var(--teal) 14%, transparent); }
.icon-badge-violet{ color:var(--violet); background:color-mix(in srgb, var(--violet) 14%, transparent); }
.icon-badge-pink{ color:var(--pink); background:color-mix(in srgb, var(--pink) 14%, transparent); }
section:hover > div > .icon-badge, .icon-badge:hover{ transform:scale(1.12) rotate(-4deg); }

/* ---- generic "alive icon" treatment: any icon marked .icon-live gets a
   playful little bounce/spin on hover instead of sitting static - used
   sparingly (buttons/badges), not blanket-applied to every fa- glyph on
   the page, which would just read as noisy rather than lively. ---- */
.icon-live{ display:inline-block; transition: transform .35s var(--ease-premium); }
.icon-live:hover{ transform: scale(1.2) rotate(8deg); }
button:hover > .icon-live, a:hover > .icon-live{ transform: scale(1.15) rotate(-6deg); }

/* ---- CINEMATIC ICONS, SITE-WIDE ----
   Rather than hand-tagging every icon in every file (hundreds of them,
   easy to miss half), every Font Awesome glyph living inside an
   interactive element gets the lively hover/press treatment
   automatically from here - this one block is what makes icons feel
   alive across the entire site at once. A handful of icons that are
   *about* motion/time/magic get a light idle animation too, since a
   still hourglass or still sparkle icon reads as a mistake; everything
   else stays still until touched, which is what makes the touched ones
   feel intentional rather than just noisy. */
button i[class*="fa-"], a i[class*="fa-"], .toolbar-btn i, .nav-link i, .icon-badge i{
  display:inline-block;
  transition: transform .4s var(--ease-premium), color .25s ease;
}
button:hover i[class*="fa-"], a:hover i[class*="fa-"]{ transform: scale(1.18) rotate(-8deg); }
button:active i[class*="fa-"], a:active i[class*="fa-"]{ transform: scale(.88) rotate(4deg); transition-duration:.12s; }

@keyframes icon-swing{ 0%,100%{ transform:rotate(0deg); } 20%{ transform:rotate(-14deg); } 40%{ transform:rotate(10deg); } 60%{ transform:rotate(-6deg); } 80%{ transform:rotate(3deg); } }
@keyframes icon-sparkle{ 0%,100%{ transform:scale(1) rotate(0deg); opacity:1; } 50%{ transform:scale(1.15) rotate(-10deg); opacity:.85; } }
@keyframes icon-tick{ 0%{ transform:rotate(0deg); } 50%{ transform:rotate(180deg); } 100%{ transform:rotate(360deg); } }
@keyframes icon-float{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-2px); } }

.fa-bell:not(.fa-slash), .fa-regular.fa-bell{ transform-origin:top center; }
button:hover .fa-bell:not(.fa-slash){ animation: icon-swing .6s var(--ease-premium); }
.fa-wand-magic-sparkles{ animation: icon-sparkle 3s ease-in-out infinite; }
.fa-hourglass-half{ animation: icon-tick 6s linear infinite; transform-origin:center; }
.fa-star, .fa-fire{ animation: icon-float 2.4s ease-in-out infinite; }
.fa-spinner{ animation: fa-spin 1s linear infinite; }

@media (prefers-reduced-motion: reduce){
  .fa-wand-magic-sparkles, .fa-hourglass-half, .fa-star, .fa-fire{ animation:none; }
  button:hover .fa-bell:not(.fa-slash){ animation:none; }
  button i[class*="fa-"], a i[class*="fa-"]{ transition:none; }
}


.stamp{
  font-family:var(--font-mono);
  letter-spacing:.05em;
  text-transform:uppercase;
  font-size:10.5px;
  font-weight:600;
  border:1.5px solid currentColor;
  border-radius:999px;
  padding:3px 10px;
  display:inline-block;
}

/* ---- kanban columns ---- */
.kanban-col{
  background: color-mix(in srgb, var(--card) 55%, transparent);
  border:1px solid var(--line);
  border-radius:12px;
  min-height:220px;
  transition: background-color .2s ease, border-color .2s ease;
}
.kanban-col.drag-over{
  background: color-mix(in srgb, var(--orange) 8%, transparent);
  border-color:var(--orange);
  animation: col-glow-pulse 1.3s ease-in-out infinite;
}
@keyframes col-glow-pulse{
  0%,100%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--orange) 22%, transparent); }
  50%{ box-shadow: 0 0 0 7px color-mix(in srgb, var(--orange) 0%, transparent); }
}

/* ---- Routines: a deliberately different visual register from a plain
   ticket - a repeating alarm/habit isn't a task with a deadline, so it
   shouldn't look like one. Soft radial glow behind a slow-pulsing clock
   ring, big monospace time readout (reusing the project's own data font
   rather than introducing a new one). Color varies by repeat pattern so
   a quick glance at the list tells daily/weekday/weekly apart. */
.routine-card{
  position:relative;
  border-radius:14px;
  padding:14px 16px;
  display:flex;
  align-items:center;
  gap:14px;
  overflow:hidden;
  border:1px solid var(--line);
  background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--routine-color) 10%, var(--card)) 0%, var(--card) 65%);
}
.routine-card[data-repeat="daily"]{ --routine-color: var(--teal); }
.routine-card[data-repeat="weekdays"]{ --routine-color: var(--orange); }
.routine-card[data-repeat="weekly"]{ --routine-color: var(--violet); }
.routine-ring{
  position:relative;
  width:44px; height:44px; shrink:0; flex-shrink:0;
  border-radius:999px;
  display:flex; align-items:center; justify-content:center;
  background: color-mix(in srgb, var(--routine-color) 16%, transparent);
  color: var(--routine-color);
  animation: routine-pulse 2.6s ease-in-out infinite;
}
@keyframes routine-pulse{
  0%,100%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--routine-color) 30%, transparent); }
  50%{ box-shadow: 0 0 0 8px color-mix(in srgb, var(--routine-color) 0%, transparent); }
}
.routine-time{
  font-family:"IBM Plex Mono", monospace;
  font-size:1.15rem;
  font-weight:600;
  letter-spacing:.01em;
}

/* ---- Resume Queue: same cinematic family as .routine-card, its own
   color so the two stay visually distinct at a glance - teal for
   "paused work waiting to resume" vs the driver/weekday/weekly colors
   Routines already uses. */
.resume-card{
  position:relative;
  border-radius:14px;
  padding:14px 16px;
  display:flex;
  align-items:center;
  gap:14px;
  overflow:hidden;
  border:1px solid var(--line);
  background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--teal) 10%, var(--card)) 0%, var(--card) 65%);
}
.resume-card.overdue{
  background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--critical) 12%, var(--card)) 0%, var(--card) 65%);
}
.resume-ring{
  position:relative;
  width:44px; height:44px; flex-shrink:0;
  border-radius:999px;
  display:flex; align-items:center; justify-content:center;
  background: color-mix(in srgb, var(--teal) 16%, transparent);
  color: var(--teal);
  --routine-color: var(--teal);
  animation: routine-pulse 2.6s ease-in-out infinite;
}
.resume-card.overdue .resume-ring{
  background: color-mix(in srgb, var(--critical) 16%, transparent);
  color: var(--critical);
  --routine-color: var(--critical);
}
.sortable-ghost{ opacity:0.35; }
.sortable-drag .ticket{ box-shadow: var(--shadow-lg); rotate:-1.5deg; }
/* The .ticket hover transition (meant for mouse-hover polish) also
   applies to SortableJS's dragged clone, which fights its own per-frame
   position updates - the drag visually lags a beat behind your finger/
   cursor instead of tracking it exactly. Killing the transition on the
   actively-dragged element is what makes the drag itself feel instant. */
.sortable-fallback, .sortable-drag, .sortable-drag *{ transition:none !important; }

/* ---- cursor trail (see initCursorTrail in site.js) ---- */
.cursor-trail-dot{
  position:fixed; top:0; left:0; z-index:150; pointer-events:none;
  width:7px; height:7px; border-radius:50%;
  background:var(--orange);
  will-change:transform, left, top;
}

/* ---- voice quick-add mic pulses red while actively listening ---- */
#voice-add-btn.voice-listening, #ai-voice-btn.voice-listening{ color:var(--orange); animation: mic-pulse 1s ease-in-out infinite; }
@keyframes mic-pulse{ 0%,100%{ opacity:1; transform:translateY(-50%) scale(1); } 50%{ opacity:.5; transform:translateY(-50%) scale(1.15); } }

/* ---- insights charts (see charts.js) ---- */
.bar-fill{ transform-origin: bottom; transform: scaleY(0); transition: transform .6s cubic-bezier(.2,.8,.2,1); }
.bars-ready .bar-fill{ transform: scaleY(1); }
.donut-seg{ animation: donut-draw .8s ease forwards; }
@keyframes donut-draw{ from{ opacity:0; } to{ opacity:1; } }
.heatmap-cell{ width:11px; height:11px; border-radius:3px; transition: background-color .4s ease; }

/* ---- kinetic text (see initKineticText in site.js) ---- */
.kinetic-word{
  display:inline-block; opacity:0; filter:blur(6px); transform:translateY(10px);
  animation: kinetic-in .6s cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes kinetic-in{ to{ opacity:1; filter:blur(0); transform:translateY(0); } }
@media (prefers-reduced-motion: reduce){
  .kinetic-word{ animation:none !important; opacity:1 !important; filter:none !important; transform:none !important; }
}

/* ---- board progress ring ---- */
.progress-ring-wrap{ position:relative; width:52px; height:52px; }
.progress-ring-wrap::before{
  content:""; position:absolute; inset:-6px; border-radius:50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--teal) 24%, transparent) 0%, transparent 70%);
  opacity:0; transition: opacity .5s var(--ease-premium);
}
.progress-ring-wrap:hover::before{ opacity:1; }
.progress-ring-label{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font-family:var(--font-mono); font-size:11px; font-weight:600; color:var(--ink);
}

/* ---- overdue tickets pulse gently - a real urgency signal, not just
   decoration; stops the moment the task is done (isOverdue() in
   dashboard.js already excludes done tasks) ---- */
.ticket-overdue{ animation: overdue-pulse 2.4s ease-in-out infinite; }
@keyframes overdue-pulse{
  0%,100%{ box-shadow: 0 0 0 0 color-mix(in srgb, var(--orange) 0%, transparent); }
  50%{ box-shadow: 0 0 0 4px color-mix(in srgb, var(--orange) 16%, transparent); }
}

/* ---- skeleton loading ---- */
.skeleton{
  background: linear-gradient(90deg, var(--paper-2) 25%, var(--line) 37%, var(--paper-2) 63%);
  background-size: 400% 100%;
  animation: skeleton-shine 1.4s ease infinite;
  border-radius:10px;
}
@keyframes skeleton-shine{ 0%{ background-position:100% 50%; } 100%{ background-position:0 50%; } }

/* ---- toast ---- */
.toast{ animation: toast-in .3s cubic-bezier(.2,.8,.2,1); }
@keyframes toast-in{ from{ transform:translateY(10px) scale(.98); opacity:0; } to{ transform:translateY(0) scale(1); opacity:1; } }

/* ---- modal shell (Boardly 2.0 Phase 1) ----
   Every existing modal on dashboard.html etc. is hand-built as
   `<div id="..." class="hidden fixed inset-0 z-50">` with its own inline
   backdrop + panel markup. That pattern isn't being ripped out here (24
   pages' worth of working modals is too much surface for one pass, per
   the brief's own Section 90 "do not build everything in one pass"). This
   is the named, reusable version of that same pattern for anything NEW:
   drop these two classes inside the existing `hidden fixed inset-0 z-50`
   wrapper and get consistent backdrop blur, panel elevation, and
   open/close motion for free, without hand-tuning it again per modal.
   Existing modals can adopt this incrementally later. */
.modal-backdrop{
  position:absolute; inset:0;
  background:rgba(10,16,30,.5); backdrop-filter:blur(3px);
  animation: modal-fade var(--dur-fast) var(--ease-out);
}
.modal-panel{
  position:relative; background:var(--card); border:1px solid var(--line);
  border-radius:var(--radius-xl); box-shadow:var(--shadow-lg);
  animation: modal-in var(--dur-base) var(--ease-premium);
}
@keyframes modal-fade{ from{ opacity:0; } to{ opacity:1; } }
@keyframes modal-in{ from{ opacity:0; transform:translateY(12px) scale(.98); } to{ opacity:1; transform:translateY(0) scale(1); } }

/* ---- bottom sheet (mobile) ----
   For actions that should feel native on a phone (Section 41, "use
   bottom sheets where appropriate") instead of a centered desktop modal.
   Pair with .modal-backdrop for the dimmed background. */
.sheet{
  position:fixed; left:0; right:0; bottom:0;
  background:var(--card); border-top:1px solid var(--line);
  border-radius:var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow:var(--shadow-lg);
  padding-bottom:max(var(--space-4), env(safe-area-inset-bottom));
  animation: sheet-in var(--dur-base) var(--ease-premium);
  max-height:85vh; overflow-y:auto;
}
.sheet-handle{
  width:36px; height:4px; border-radius:var(--radius-full);
  background:var(--line-strong); margin:var(--space-2) auto var(--space-3);
}
@keyframes sheet-in{ from{ transform:translateY(100%); } to{ transform:translateY(0); } }

/* ---- data table ----
   A named, consistent table pattern for the tabular data Money/Clients/
   Insights will need (invoices, transactions, client lists), dense,
   readable, tabular-numeral aligned, distinct from the card-based board
   UI everywhere else. */
.table-wrap{ overflow-x:auto; border:1px solid var(--line); border-radius:var(--radius-lg); }
.table{ width:100%; border-collapse:collapse; font-family:var(--font-body); font-size:.8125rem; }
.table thead th{
  text-align:left; font-family:var(--font-body); font-weight:600; font-size:.6875rem;
  letter-spacing:.03em; text-transform:uppercase; color:var(--ink-soft);
  background:var(--paper-2); padding:var(--space-3) var(--space-4);
  border-bottom:1px solid var(--line); white-space:nowrap;
}
.table tbody td{
  padding:var(--space-3) var(--space-4); border-bottom:1px solid var(--line);
  color:var(--ink); vertical-align:middle;
}
.table tbody tr:last-child td{ border-bottom:none; }
.table tbody tr:hover{ background:var(--paper-2); }
.table .table-num{ font-family:var(--font-body); font-variant-numeric:tabular-nums; text-align:right; }

/* ---- loading state ----
   Section 60: never a blank screen. Pairs with .skeleton above for
   content placeholders; this is for a full-section/page loading message
   (e.g. "Fetching your invoices..."), distinct from a skeleton grid. */
.loading-state{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--space-3); padding:var(--space-16) var(--space-4); text-align:center;
  color:var(--ink-soft);
}
.loading-state .spinner{
  width:22px; height:22px; border-radius:999px;
  border:2.5px solid var(--line-strong); border-top-color:var(--brand);
  animation: spin .7s linear infinite;
}
@keyframes spin{ to{ transform:rotate(360deg); } }

/* ---- error state ----
   Section 61: explain what happened, why, and what the user can do -
   never a bare "Something went wrong." Structure: icon slot, headline
   (what happened), detail (why / what we did instead), retry action. */
.error-state{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--space-2); padding:var(--space-12) var(--space-4); text-align:center;
}
.error-state > p:first-of-type{
  font-family:var(--font-display); font-weight:600; font-size:16px;
  letter-spacing:-.015em; font-variation-settings:'opsz' 28; color:var(--ink);
}
.error-state > p:last-of-type{ font-family:var(--font-body); font-size:12px; line-height:1.55; color:var(--ink-soft); max-width:32ch; }

/* ---- pill tabs -----------------------------------------------------------
   A simple tab switcher (Money's Invoices/Expenses/Ledger/Profitability,
   Clients' Active/Leads/All). Started as money.html's own local
   ".money-tab" styling, promoted here once a second page (clients.html)
   needed the exact same pattern, so both share one definition instead
   of two copies drifting apart. Pair with [data-tab-panel] below, or
   any element toggling a "data-active" attribute directly. --------- */
.pill-tab{ padding:.5rem 1rem; border-radius:var(--radius-lg); font-size:.8125rem; font-weight:600; color:var(--ink-soft); flex-shrink:0; white-space:nowrap; }
.pill-tab[data-active="true"]{ background:var(--paper-2); color:var(--ink); }
[data-tab-panel][data-active="false"]{ display:none; }

/* ---- command palette ---- */
.cmdk-backdrop{ backdrop-filter: blur(3px); background: rgba(10,16,30,.5); }
kbd{
  font-family:var(--font-mono);
  font-size:11px;
  background:var(--paper-2);
  border:1px solid var(--line);
  border-bottom-width:2px;
  border-radius:5px;
  padding:1px 5px;
  color:var(--ink-soft);
}

.task-title.done{ text-decoration: line-through; text-decoration-color: var(--teal); color: var(--ink-soft); }
/* Belt-and-suspenders for long unbroken strings (emails, URLs): break-word
   alone isn't always honored for intrinsic sizing across browsers, so force
   a break anywhere it's needed once the line runs out of room. */
.task-title{ overflow-wrap:anywhere; word-break:break-word; }

/* ---- scroll-reveal ---- */
[data-reveal]{
  opacity:0;
  transform: perspective(900px) translateY(20px) rotateX(8deg) scale(.98);
  transform-origin: center bottom;
  transition: opacity .7s cubic-bezier(.16,.8,.24,1), transform .7s cubic-bezier(.16,.8,.24,1);
  will-change: transform, opacity;
}
[data-reveal].is-visible{ opacity:1; transform:none; will-change:auto; }
[data-reveal-delay="1"]{ transition-delay:.08s; }
[data-reveal-delay="2"]{ transition-delay:.16s; }
[data-reveal-delay="3"]{ transition-delay:.24s; }
[data-reveal-delay="4"]{ transition-delay:.32s; }

/* ---- theme switch (the OFF/ON pill toggle) ---- */
.theme-switch{
  --w:52px; --h:28px;
  width:var(--w); height:var(--h);
  border-radius:999px;
  background: var(--ink);
  position:relative;
  cursor:pointer;
  border:1px solid var(--line);
  flex-shrink:0;
  transition: background-color .3s ease;
}
.theme-switch .knob{
  position:absolute;
  top:2px; left:2px;
  height:calc(var(--h) - 6px);
  width:calc(var(--h) - 6px);
  border-radius:999px;
  background:#fff;
  display:flex; align-items:center; justify-content:center;
  font-size:10px;
  color:#12203A;
  box-shadow:0 1px 3px rgba(0,0,0,.3);
  transition: transform .3s cubic-bezier(.5,-0.2,.3,1.4);
}
html.dark .theme-switch .knob{ transform: translateX(calc(var(--w) - var(--h))); }
.theme-switch .icon-moon{ display:none; }
html.dark .theme-switch .icon-sun{ display:none; }
html.dark .theme-switch .icon-moon{ display:inline; }

/* ---- nav underline hover ---- */
.nav-link{ position:relative; }
.nav-link::after{
  content:""; position:absolute; left:0; right:100%; bottom:-4px; height:2px;
  background:var(--orange); transition: right .25s ease;
}
.nav-link:hover::after{ right:0; }

/* ---- buttons ---- */
.btn-pop{ transition: transform .18s cubic-bezier(.3,1.5,.5,1), box-shadow .18s var(--ease-premium), background-color .2s ease; box-shadow: 0 1px 2px rgba(18,32,58,.08); }
.btn-pop:hover{ transform: translateY(-2px) scale(1.015); box-shadow: 0 10px 24px -8px rgba(18,32,58,.28); }
.btn-pop:active{ transform: translateY(0) scale(.97); box-shadow: 0 1px 2px rgba(18,32,58,.08); }
html.dark .btn-pop{ box-shadow: 0 1px 2px rgba(0,0,0,.3); }
html.dark .btn-pop:hover{ box-shadow: 0 10px 26px -8px rgba(0,0,0,.5); }

/* Primary orange CTAs (bg-orange) get a soft top-lit gloss so they read
   as the one glossy, "press me" surface on the page - everything else
   stays flat/matte, which is what makes this feel intentional. */
.btn-pop.bg-orange{ background-image: linear-gradient(180deg, color-mix(in srgb, white 14%, var(--orange)), var(--orange) 60%); }
.btn-pop.bg-orange:hover{ background-image: linear-gradient(180deg, color-mix(in srgb, white 18%, var(--orange)), var(--orange) 60%); }

.toolbar-btn:hover{ transform: translateY(-1px); box-shadow: 0 6px 16px -8px rgba(18,32,58,.22); }
html.dark .toolbar-btn:hover{ box-shadow: 0 6px 16px -8px rgba(0,0,0,.45); }
.toolbar-btn:active{ transform: translateY(0) scale(.97); }

/* ---- mobile menu ---- */
#mobile-menu{
  transition: opacity .3s var(--ease-premium), visibility .3s var(--ease-premium);
  z-index:70; /* above #bottom-tab-bar (60) - otherwise the tab bar covers
    the bottom of the open menu, including the Log out button */
}
#mobile-menu[data-open="false"]{ opacity:0; visibility:hidden; pointer-events:none; }
#mobile-menu[data-open="true"]{ opacity:1; visibility:visible; }

/* Every full-screen modal in dashboard.html uses Tailwind's z-50 in its
   HTML class, which sits BELOW #bottom-tab-bar's z-index:60. Any one of
   them, once it holds enough content to reach near the bottom of the
   screen (the edit ticket modal, once a task has comments, is a common
   case) gets its lower portion visually covered by the tab bar - the
   exact same problem #mobile-menu had above, just not yet fixed for
   every other modal. This one rule fixes all of them at once, so a
   future modal doesn't quietly repeat the same bug. */
#qr-handoff-modal, #cmdk, #shortcuts-modal, #prompt-modal, #confirm-modal,
#edit-modal, #ai-brief-modal, #import-modal, #templates-modal, #ai-panel, #share-settings-modal, #waiting-room-modal, #commitments-modal, #capture-modal, #decisions-modal, #morning-modal, #done-archive-modal, #people-modal, #control-tower-modal, #classroom-modal, #classroom-roster-modal, #classroom-rubric-modal, #dispatch-modal, #care-rounds-modal, #content-calendar-modal, #client-work-modal, #dev-board-modal, #routines-modal, #memory-vault-modal, #cc-preview-modal, #resume-queue-modal, #marketplace-modal, #post-preview-modal, #ai-plan-review-modal, #upgrade-prompt-modal, #proofing-modal, #custom-forms-modal, #custom-form-builder-modal, #form-submissions-modal, #proposals-modal, #proposal-builder-modal, #documents-modal, #document-editor-modal{
  z-index:70;
}

/* Same root cause as the mobile menu above: any other full-screen
   overlay panel that also has an interactive element pinned to its own
   bottom edge (the AI chat's input box, a modal's Save/Cancel row) was
   sitting underneath the bottom tab bar (z-index:60), covering it - see
   the "can't see AI typing or text box" report. */
#ai-panel, #cmdk, #edit-modal, #import-modal, #post-preview-modal, #qr-handoff-modal{ z-index:70; }

#mobile-panel{
  transition: transform .4s var(--ease-premium);
  box-shadow: var(--shadow-lg);
}
#mobile-menu[data-open="false"] #mobile-panel{ transform: translateX(100%); }
#mobile-menu[data-open="true"] #mobile-panel{ transform: translateX(0); }

/* Each row/link slides in with a short stagger, cinematic-menu style -
   only while the panel is actually open, so closing doesn't replay it. */
#mobile-menu[data-open="true"] #mobile-panel > *{ animation: mnav-in .45s var(--ease-premium) backwards; }
#mobile-menu[data-open="true"] #mobile-panel > *:nth-child(1){ animation-delay:.03s; }
#mobile-menu[data-open="true"] #mobile-panel > *:nth-child(2){ animation-delay:.07s; }
#mobile-menu[data-open="true"] #mobile-panel > *:nth-child(3){ animation-delay:.11s; }
#mobile-menu[data-open="true"] #mobile-panel > *:nth-child(4){ animation-delay:.15s; }
#mobile-menu[data-open="true"] #mobile-panel > *:nth-child(5){ animation-delay:.19s; }
@keyframes mnav-in{ from{ opacity:0; transform:translateX(18px); } to{ opacity:1; transform:translateX(0); } }

.mobile-nav-item{
  padding:12px 14px; border-radius:12px; font-size:14px; font-weight:500;
  color:var(--ink); background:transparent;
  transition: background-color .25s var(--ease-premium), transform .2s var(--ease-premium), color .2s ease;
}
.mobile-nav-item:hover, .mobile-nav-item:active{ background:var(--paper-2); transform:translateX(2px); }
.mobile-nav-item.nav-active{
  color:#fff !important; font-weight:600;
  background: linear-gradient(135deg, var(--orange), var(--pink));
  box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--orange) 60%, transparent);
}
.mobile-nav-item.nav-active i{ color:#fff !important; }

@media (prefers-reduced-motion: reduce){
  #mobile-menu[data-open="true"] #mobile-panel > *{ animation:none; }
}

.hamburger span{ transition: transform .3s ease, opacity .3s ease; transform-origin:center; }
.hamburger[data-open="true"] span:nth-child(1){ transform: translateY(6px) rotate(45deg); }
.hamburger[data-open="true"] span:nth-child(2){ opacity:0; }
.hamburger[data-open="true"] span:nth-child(3){ transform: translateY(-6px) rotate(-45deg); }

/* ---- browser chrome mockup for "screenshot" sections ---- */
.browser-chrome{
  background:var(--card);
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow: var(--shadow-lg);
  overflow:hidden;
}
.browser-chrome .bar{ background:var(--paper-2); border-bottom:1px solid var(--line); }
.browser-dot{ width:10px; height:10px; border-radius:999px; }

a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible{
  outline: 2px solid var(--orange);
  outline-offset: 2px;
}

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

::-webkit-scrollbar{ width:10px; height:10px; }
::-webkit-scrollbar-track{ background:transparent; }
::-webkit-scrollbar-thumb{ background:var(--line); border-radius:6px; }

@keyframes float{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-10px); } }
.float{ animation: float 5s ease-in-out infinite; }
.float-delay{ animation-delay: 1.4s; }

/* ---- compact density mode (toggled from the toolbar's "More" menu) ---- */
.density-compact .ticket{ padding:8px 10px !important; margin-bottom:6px !important; }
.density-compact .task-title{ font-size:12.5px; line-height:1.3; }
.density-compact .stamp{ font-size:9px; padding:1px 6px; }
.density-detailed .ticket{ padding:16px !important; }
.density-detailed .task-title{ font-size:15px; }
.swimlane + .swimlane{ margin-top:10px; padding-top:10px; border-top:1px dashed var(--line); }
.task-title[contenteditable="true"]{ outline:none; cursor:text; }
#timely-streak-badge{ font-weight:600; }
[data-add-date]{ transition:opacity .15s; }
.kanban-col.drag-over-day{ outline:2px dashed var(--orange); outline-offset:-2px; }

/* ---- zen / focus mode: shrink the board grid down to one column ---- */
#board.zen-active{ display:block; }
#board.zen-active section[data-col]{ display:none; }
#board.zen-active section[data-col].zen-visible{ display:block; max-width:520px; margin:0 auto; }

/* ---- accent color swatches (More menu) ---- */
.accent-swatch{ width:22px; height:22px; border-radius:999px; cursor:pointer; border:2px solid transparent; transition:transform .15s ease; }
.accent-swatch:hover{ transform:scale(1.12); }
.accent-swatch.active{ border-color:var(--ink); }

/* ---- active-page highlighting: whichever nav/mobile-menu/footer link
   matches the current page, everywhere it appears ---- */
/* ---- keyboard-nav focus ring on a card ---- */
.ticket-kbd-focus{ outline:2px solid var(--violet); outline-offset:1px; }

/* ---- presentation / TV mode: strip everything except the board itself ---- */
body.presentation-mode #site-nav,
body.presentation-mode #board-header-row,
body.presentation-mode #board-toolbar,
body.presentation-mode #quick-add-form,
body.presentation-mode #quick-add-hint,
body.presentation-mode #bulk-bar,
body.presentation-mode footer,
body.presentation-mode #back-to-top,
body.presentation-mode #cookie-banner{ display:none !important; }
body.presentation-mode main{ max-width:100% !important; padding:24px 32px !important; }
body.presentation-mode .ticket{ font-size:1.05em; }

/* ---- app lock wrong-passcode shake ---- */
@keyframes shake{
  10%,90%{ transform:translateX(-1px); }
  20%,80%{ transform:translateX(2px); }
  30%,50%,70%{ transform:translateX(-4px); }
  40%,60%{ transform:translateX(4px); }
}

/* ---- keyboard-aware toasts: lift clear of the iOS keyboard instead of
   rendering underneath it (see initKeyboardAwareViewport in site.js) ---- */
#toast-wrap{ bottom:calc(1.25rem + var(--kb-inset, 0px)) !important; }

/* ---- narrow tablets in portrait (iPad Mini/Air portrait, most Android
   tablets, big phones in split-view) ----
   Tailwind's own breakpoint flips #board from 1 column straight to 3 at
   768px. Between roughly 768-899px that gives three ~220px columns,
   which is workable but cramped for real ticket titles/badges - a
   single stacked column reads much better in that band. 900px+ (iPad
   landscape, laptops, desktops) goes back to the normal 3-column board. */
@media (min-width:768px) and (max-width:899px){
  #board{ grid-template-columns:1fr !important; }
}

/* ---- calendar view ---- */
#cal-grid .kanban-col{ min-height:80px; }
@media (max-width: 480px){
  #cal-grid{ gap:3px !important; }
  #cal-grid .kanban-col{ min-height:52px; padding:4px !important; }
  #cal-month-label{ width:auto !important; font-size:14px; }
  #calendar-view .edit-target{ font-size:9px !important; padding:1px 3px !important; }
}

/* ---- live collaborator cursors ---- */
#cursor-layer > div{ will-change: left, top; }

/* ---- level pill active/glow state (reuses .toolbar-btn's active look via a border) ---- */
#level-pill:hover{ border-color:var(--orange); color:var(--orange); }

/* ---- active-page highlighting: whichever nav/mobile-menu/footer link
   matches the current page, everywhere it appears ---- */
.nav-active{ color:var(--orange) !important; font-weight:600; }
#site-nav .nav-active{ position:relative; }
#site-nav .nav-active::after{ content:""; position:absolute; left:0; right:0; bottom:-19px; height:2px; background:var(--orange); border-radius:2px; }
#mobile-panel .nav-active{ border-color:var(--orange); }
footer .nav-active{ text-decoration:underline; text-underline-offset:3px; }

/* ---- footer: explicit background/color so it can never render as a
   blank white block regardless of inheritance or theme state ---- */
footer{ background-color:var(--paper); color:var(--ink-soft); }
footer a:hover{ color:var(--orange); }
footer{ border-top:1px solid var(--line); padding-bottom:max(2rem, env(safe-area-inset-bottom)); }
footer h2, footer h3{ color:var(--ink); letter-spacing:-.01em; }
footer a{ text-underline-offset:3px; }

/* The empty board is an invitation, not an error state. */
.empty-state > p:first-of-type{ font-family:var(--font-display); font-weight:600; font-size:16px; letter-spacing:-.015em; font-variation-settings:'opsz' 28; }
.empty-state > p:last-of-type{ font-family:var(--font-body); font-size:12px; line-height:1.55; }

/* ==========================================================================
   ULTRA RESPONSIVE PASS - small phone screens (< 480px)
   Tightens up the busiest pieces of the dashboard UI so nothing crowds,
   overflows sideways, or turns into an unreadable row of tiny icons.
   ========================================================================== */
@media (max-width: 480px){
  main{ padding-left:14px !important; padding-right:14px !important; }
  footer .grid{ text-align:center; gap:28px !important; }
  footer .grid > div{ display:flex; flex-direction:column; align-items:center; }
  footer ul{ text-align:center; }
  footer{ padding:34px 20px max(34px, env(safe-area-inset-bottom)) !important; }
  footer .grid{ grid-template-columns:1fr !important; }
  footer p, footer li, footer a{ font-size:14px; line-height:1.65; }

  /* toolbar row: smaller icon buttons, tighter gaps, wraps cleanly */
  #board-toolbar{ gap:6px; }
  #board-toolbar .board-search-wrap{ flex:0 0 100%; width:100%; }
  .toolbar-btn{ padding:7px 10px; font-size:12px; }
  #board-search{ min-width:0; }

  /* A compact, readable board beats accidental drag gestures on a phone. */
  .kanban-col{ min-height:150px; }
  .ticket[data-id]{ cursor:pointer !important; }
  .drag-handle{ min-width:36px; min-height:36px; }

  /* quick add: stack the input above the action buttons instead of
     squeezing three things into one row */
  #quick-add-form{ flex-wrap:wrap; }
  #quick-add-form > div.flex-1{ flex-basis:100%; }
  #quick-add-form button[type="submit"]{ flex:1; padding-top:12px; padding-bottom:12px; }

  /* board switcher / more / templates / export / invite dropdown menus:
     these are absolutely positioned relative to a small button near the
     screen edge, which on a narrow phone pushes them half off-screen.
     Pin them to the viewport instead so they always land centered and
     fully visible no matter which button opened them.
     z-index:70 matches #mobile-menu's fix above - without it, these
     sit at the default z-20 from their HTML class, which is BELOW
     #bottom-tab-bar's z-index:60, so the tab bar visually covers their
     bottom portion even though the panel itself can still scroll. */
  #board-switcher-menu, #more-menu, #templates-menu, #export-menu, #progress-popover, #invite-member-menu{
    position:fixed !important; top:auto !important;
    left:14px !important; right:14px !important; width:auto !important; max-width:none !important;
    bottom:14px !important;
    max-height:70vh; overflow-y:auto;
    z-index:70;
  }

  /* views / more-tools / work-type / notes-template menus, and the board's
     nested template submenu: same root cause as the six dropdowns just
     above (below #bottom-tab-bar's z-index:60 by default, at their
     HTML class's z-20/z-30/z-40), just missed when that list was written.
     These stay in-place rather than becoming a bottom sheet like the ones
     above, since board-template-menu is a submenu anchored to its parent
     menu and turning it into a fixed bottom sheet would visually detach
     it from that parent. */
  #board-template-menu, #more-tools-menu, #views-menu, #work-type-menu, #edit-notes-templates-menu{
    z-index:70;
  }

  /* edit modal + import modal: edge-to-edge with breathing room instead
     of a fixed max-width that can crowd a 320px-wide screen */
  #edit-form, #prompt-form, #confirm-modal > div:not(.cmdk-backdrop), #import-modal > div:not(.cmdk-backdrop), #templates-modal > div:not(.cmdk-backdrop),
  #levelup-popup > div, #board-cleared-banner > div{
    margin-left:10px !important; margin-right:10px !important; max-width:none !important;
  }

  /* AI panel: full width slide-over instead of a fixed sidebar width */
  #ai-panel > div{ max-width:100% !important; }

  /* onboarding tour card: never wider than the screen */
  #tour-card{ position:fixed !important; width:calc(100vw - 28px) !important; max-width:none; left:14px !important; right:14px !important; top:auto !important; bottom:max(14px, env(safe-area-inset-bottom)) !important; }
  #tour-card #tour-step-body{ font-size:14px; line-height:1.45; }

  /* board header row: board name + progress ring stay on one line but
     shrink gracefully instead of pushing the ring off-screen */
  #board-switcher-label{ font-size:19px; }
}

@media (max-width: 360px){
  .toolbar-btn span{ display:none; } /* icon-only on the smallest screens, even below the sm: breakpoint */
  .progress-ring-wrap{ display:none; } /* the % is still shown elsewhere (mini-donut); saves real width */
}

/* ---- toolbar buttons (search bar row: select, export, import, print, etc.) ---- */
.toolbar-btn{
  display:inline-flex; align-items:center; gap:6px;
  border:1px solid var(--line); background:var(--card); color:var(--ink-soft);
  border-radius:999px; padding:8px 14px; font-size:13px; font-weight:500;
  transition: border-color .2s ease, color .2s ease, background-color .2s ease, transform .2s var(--ease-premium), box-shadow .2s var(--ease-premium);
}
.toolbar-btn:hover{ border-color:var(--orange); color:var(--orange); }
.toolbar-btn.active{ border-color:var(--orange); color:var(--orange); background: color-mix(in srgb, var(--orange) 8%, var(--card)); }
.toolbar-btn-danger:hover{ border-color:var(--orange); color:var(--orange); }

/* ---- bulk-select checkbox on a ticket ---- */
.ticket-select-box{
  width:16px; height:16px; border-radius:5px; border:2px solid var(--ink-soft);
  display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2px;
  transition: border-color .15s ease, background-color .15s ease;
  cursor:pointer;
}
.ticket-select-box.checked{ border-color:var(--orange); background:var(--orange); }
.ticket.ticket-selected{ outline:2px solid var(--orange); outline-offset:1px; }

/* ---- print-friendly board view ----
   Everything not part of the actual board content is hidden; the three
   columns stack into simple printable lists instead of a drag-and-drop
   grid (dragging obviously means nothing on paper), and colors are
   flattened to plain borders so it doesn't waste ink. ---- */
@media print{
  #site-nav, #mobile-menu, #back-to-top, #nav-progress, .grain-overlay, #desk-lamp,
  #toast-wrap, #cmdk, #shortcuts-modal, #edit-modal, #import-modal, #ai-panel,
  #board-toolbar, #bulk-bar, #quick-add-form, #quick-add-hint, #offline-badge,
  #copy-summary-btn, .theme-switch, .delete-btn, .check-btn, .select-box,
  #open-cmdk-btn, .progress-ring-wrap, #all-caught-up, #more-menu, #tour-overlay,
  #exit-focus-bar, [data-clear-column], [data-zen]{ display:none !important; }

  body{ background:#fff !important; color:#000 !important; }
  main{ max-width:100% !important; padding:0 !important; }
  #board{ display:block !important; }
  #board section{ break-inside:avoid; margin-bottom:18px; }
  .kanban-col{ border:none !important; background:transparent !important; min-height:0 !important; }
  .ticket{ box-shadow:none !important; border:1px solid #999 !important; break-inside:avoid; }
  .ticket-hover:hover{ transform:none !important; }
}

/* ==========================================================================
   COLLABORATION (v17 add-on) - board member avatars + task comments
   Uses only existing tokens (--brand, --surface-2, --line, --ink*) so it
   inherits light/dark mode automatically, same as every other component.
   ========================================================================== */
.member-avatar{
  width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font:600 11px var(--font-mono);
  background:var(--brand-soft); color:var(--brand-dark);
  border:2px solid var(--surface-1);
}
.member-avatar.pending{ background:var(--surface-2); color:var(--ink-faint); border-style:dashed; }

.comment-row{ padding:6px 8px; border-radius:8px; }
.comment-row + .comment-row{ border-top:1px solid var(--line); }
.comment-body{ font-size:13px; color:var(--ink); word-break:break-word; }
.comment-body .mention{ color:var(--brand); font-weight:600; }
.comment-delete-btn{ color:var(--ink-faint); text-decoration:underline; margin-left:6px; cursor:pointer; background:none; border:none; padding:0; font:inherit; }
.comment-delete-btn:hover{ color:var(--critical); }

/* ==========================================================================
   TIMELINE / GANTT VIEW (add-on) - uses existing tokens, no new colors
   ========================================================================== */
#tl-titles{ background:var(--surface-1); }
.tl-title-header{ border-bottom:1px solid var(--line); }
.tl-title-row{
  display:flex; align-items:center; padding:0 10px;
  font-size:12px; color:var(--ink); border-bottom:1px solid var(--line);
}
.tl-header{ position:relative; border-bottom:1px solid var(--line); }
.tl-day-col{
  position:absolute; top:0; bottom:0; border-right:1px solid var(--line);
  display:flex; align-items:center; justify-content:center;
  font:600 10px var(--font-mono); color:var(--ink-faint);
}
.tl-day-col.today{ background:var(--brand-soft); color:var(--brand-dark); }
.tl-dep-svg{ position:absolute; top:0; left:0; pointer-events:none; }
.tl-dep-line{ fill:none; stroke:var(--ink-faint); stroke-width:1.5; opacity:.6; }
.tl-dep-arrow{ fill:var(--ink-faint); opacity:.6; }
.tl-bar{
  position:absolute; display:flex; align-items:center;
  background:var(--brand); color:#fff; border-radius:8px;
  font-size:12px; font-weight:600; cursor:grab; user-select:none;
  box-shadow:0 1px 2px rgba(0,0,0,.15);
  touch-action:none; /* so pointer drag works on mobile without scrolling the page */
}
.tl-bar:active{ cursor:grabbing; }
.tl-bar.done{ background:var(--surface-3); color:var(--ink-soft); }
.tl-bar.overdue{ background:var(--critical); }
.tl-zoom-btn.active{ background:var(--brand-soft); color:var(--brand-dark); }

/* Timeline header: on a narrow phone, the date-nav group and the
   zoom-button group can't both fit on one line - flex-wrap (set in
   dashboard.html) lets them drop to two lines instead of overflowing,
   this just centers each line so it looks intentional, not broken. */
@media (max-width:479px){
  #tl-header-controls{ justify-content:center; }
  #tl-header-controls > div{ justify-content:center; flex:1 1 100%; }
  #tl-range-label{ width:auto !important; }
}

/* Unit converter (tools.html): the value box + "from" unit + arrow +
   "to" unit used to be forced onto one line with flex-wrap missing, so
   on a narrow phone the "to" unit select had nowhere to go but off the
   right edge of the card. flex-wrap (added in tools.html) lets it drop
   the arrow + "to" select onto their own line instead; this just gives
   both selects a sane minimum width so they wrap instead of squeezing
   into an unreadable sliver first. */
#unit-converter-row #unit-from, #unit-converter-row #unit-to{ min-width:130px; }
@media (max-width:479px){
  #unit-converter-row #unit-from-value{ width:80px; }
}
