/* Timekeep — Material Design 3 (dark scheme).
 *
 * Tokens follow md.sys / md.comp naming from the M3 spec:
 * ref -> sys (these roles) -> comp (the classes below).
 * Seed: pumpkin (var(--primary) family). Dark scheme, tonal spot (no shadows).
 */

/* ---------------- sys tokens: color (dark) ---------------- */
:root {
  color-scheme: dark;
  /* #841: theme scheme — the single source of truth. Change the scheme here;
     every rule below references these variables, never a hardcoded hex. */
  --primary: #fb923c;
  --on-primary: #442100;
  --primary-container: #713d00;
  --on-primary-container: #ffdcc2;
  --md-sys-color-primary: var(--primary);
  --md-sys-color-on-primary: var(--on-primary);
  --md-sys-color-primary-container: var(--primary-container);
  --md-sys-color-on-primary-container: var(--on-primary-container);
  --md-sys-color-secondary-container: #454654;
  --md-sys-color-on-secondary-container: #e4e1f2;
  --md-sys-color-error: #ffb4ab;
  --md-sys-color-on-error: #690005;
  --md-sys-color-error-container: #93000a;
  --md-sys-color-on-error-container: #ffdad6;
  --md-sys-color-success-container: #1e4d2c;
  --md-sys-color-on-success-container: #b9efc5;
  --md-sys-color-surface: #131318;
  --md-sys-color-on-surface: #e5e1e9;
  --md-sys-color-on-surface-variant: #cac4cf;
  --md-sys-color-inverse-surface: #e6e1e9;
  --md-sys-color-inverse-on-surface: #322f35;
  --md-sys-color-surface-container-lowest: #0d0d12;
  --md-sys-color-surface-container-low: #1a1a20;
  --md-sys-color-surface-container: #1e1e24;
  --md-sys-color-surface-container-high: #28282f;
  --md-sys-color-surface-container-highest: #33333b;
  --md-sys-color-outline: #948f99;
  --md-sys-color-outline-variant: #48454e;

  /* shape */
  --md-sys-shape-corner-extra-small: 4px;
  --md-sys-shape-corner-small: 8px;
  --md-sys-shape-corner-medium: 12px;
  --md-sys-shape-corner-large: 16px;
  --md-sys-shape-corner-extra-large: 28px;
  --md-sys-shape-corner-full: 999px;

  /* #849: sticky day-header height — the lane's top cutter pins just under it */
  --cal-head-h: 64px;

  /* state-layer opacities */
  --md-state-hover: 0.08;
  --md-state-focus: 0.1;
  --md-state-pressed: 0.1;

  /* motion (M3 sys tokens) */
  --motion-easing-standard: cubic-bezier(0.2, 0, 0, 1);
  --motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
  --motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1);
  --motion-easing-emphasized: cubic-bezier(0.05, 0.7, 0.1, 1);
  --motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
  --motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
  --motion-duration-short1: 50ms;
  --motion-duration-short2: 100ms;
  --motion-duration-short3: 150ms;
  --motion-duration-short4: 200ms;
  --motion-duration-medium1: 250ms;
  --motion-duration-medium2: 300ms;
  --motion-duration-medium3: 350ms;
  --motion-duration-medium4: 400ms;
  --motion-duration-long1: 450ms;
  --motion-duration-long2: 500ms;
}

/* ---------------- sys tokens: layout (M3 window size classes) ----------------
 * Breakpoints (dp map 1:1 to px on web): compact <600 · medium 600–839 ·
 * expanded 840–1199 · large 1200–1599 · extra-large 1600+. Body margins: 16px
 * on compact, 24px on medium+. Inter-pane spacer: 24px everywhere. Fixed
 * supporting-pane width: 360px at expanded, 412px at large/XL. CSS variables
 * cannot drive media queries, so the breakpoints appear as literals in the
 * queries below; these tokens are the single source of truth for the values
 * components consume. (M3 layout foundations briefing:
 * research_notes/m3-layout-foundations-20260918-2027/report.md) */
:root {
  --layout-body-margin: 16px;
  --layout-pane-gap: 24px;
  --layout-supporting-pane: 360px;
  /* #354: primary navigation dimensions. #397: the collapsed rail is 72px —
     a deliberate deviation from the M3 80dp default (same standing as the
     56px app bar, #389); the 56px active pill still has 8px clearance and
     the one-word labels still fit. The 80px bottom bar height is the M3
     default. The /specs measurement values weren't extractable in the
     research pass, so these are judgment calls (noted on #354). */
  --md-nav-rail-width: 72px;
  --md-nav-bar-height: 80px;
}
@media (min-width: 600px) {
  :root { --layout-body-margin: 24px; }
}
@media (min-width: 1200px) {
  :root { --layout-supporting-pane: 412px; }
}

/* Staggered content entrance (#48): page regions fade + rise in sequence
   on page load. Attributes sit on shells HTMX never replaces, so the
   entrance plays once per page load and never replays on swaps. */
[data-enter] {
  animation: content-enter var(--motion-duration-medium3) var(--motion-easing-emphasized-decelerate) backwards;
}
[data-enter="2"] { animation-delay: 60ms; }
[data-enter="3"] { animation-delay: 120ms; }
[data-enter="4"] { animation-delay: 180ms; }
@keyframes content-enter { from { opacity: 0; transform: translateY(16px); } }

/* ---------------- global scrollbars (M3 tonal, #191) ---------------- */
/* Thin rounded tonal thumbs, transparent track — every scroll region
   (calendar grid, lists, dialogs, page) inherits this. Pumpkin stays
   reserved for CTAs, so thumbs use surface tokens only. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--md-sys-color-surface-container-highest) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background-color: var(--md-sys-color-surface-container-highest);
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: var(--md-sys-shape-corner-full);
}
::-webkit-scrollbar-thumb:hover {
  background-color: var(--md-sys-color-outline-variant);
  border-width: 2px;
}
::-webkit-scrollbar-thumb:active {
  background-color: var(--md-sys-color-outline);
  border-width: 2px;
}

/* ---------------- sys tokens: type ----------------
 * Type sizes are rem so text follows the user's root font size (accessibility).
 * Layout chrome stays px on purpose — it maps 1:1 to M3 dp and browser zoom
 * covers enlargement. */
*, *::before, *::after { box-sizing: border-box; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
  margin: 0;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
.md-headline-small { font-size: 1.5rem; line-height: 32px; font-weight: 400; letter-spacing: 0; }
.md-title-large { font-size: 1.375rem; line-height: 28px; font-weight: 400; }
.md-title-medium { font-size: 1rem; line-height: 24px; font-weight: 500; letter-spacing: 0.15px; }
.md-title-small { font-size: 0.875rem; line-height: 20px; font-weight: 500; letter-spacing: 0.1px; }
.md-body-medium { font-size: 0.875rem; line-height: 20px; letter-spacing: 0.25px; }
.md-label-large { font-size: 0.875rem; line-height: 20px; font-weight: 500; letter-spacing: 0.1px; }
.md-label-medium { font-size: 0.75rem; line-height: 16px; font-weight: 500; letter-spacing: 0.5px; }
/* #871: calendar.ts's agenda time range referenced .md-label-small but no rule
   existed — it fell back to inherited body size next to the defined
   .md-label-medium duration. M3 Label Small: 11sp/16sp, 500, 0.5 tracking. */
.md-label-small { font-size: 0.6875rem; line-height: 16px; font-weight: 500; letter-spacing: 0.5px; }

.muted { color: var(--md-sys-color-on-surface-variant); }
.tiny { font-size: 0.75rem; }
.error { color: var(--md-sys-color-error); }
/* the UA [hidden] rule loses to component display styles; enforce it */
[hidden] { display: none !important; }

/* visible focus everywhere (M3: focus +10% state layer + ring) */
:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* ---------------- top app bar (slim, 56px) ----------------
 * #389: deliberately slimmer than the M3 small app bar spec (64dp) — the
 * chromeless bar read too tall. Logo is 34px, actions are 48px icon
 * buttons, so 56px keeps comfortable clearance and full touch targets. */
.md-top-app-bar {
  position: sticky;
  /* #387: sticks 12px off the viewport top so the chrome floats with a
     page-background gutter while scrolling. */
  top: 12px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 56px;
  /* #361: leading padding tracks the M3 body margin (16px compact, 24px
     medium+) so the brand mark aligns with the content ruler. #362: no hard
     border under the app bar — M3 separates it tonally. */
  padding: 0 16px 0 var(--layout-body-margin);
  /* #377: tonal default (surface-container); the app override below (#387)
     makes it transparent so it inherits the page bg. */
  background: var(--md-sys-color-surface-container);
}
/* ---------------- M3 tonal separation: nav chrome (#377, #385, #387) ----------------
 * #387: the page background is surface-container and the app bar + rail sit
 * chromeless on it; only the content column is the rounded surface sheet.
 * In-content supporting/detail panes stay flat surface. */
.md-top-app-bar-title {
  font-size: 1.375rem;
  line-height: 28px;
  color: var(--md-sys-color-on-surface);
  white-space: nowrap;
}
.md-top-app-bar-spacer { flex: 1; }

/* ---------------- trial-expiry nudge banner (#923) ----------------
 * Full-width tonal strip between the app bar and the page chrome: one
 * line of copy, a pumpkin Subscribe CTA, and a dismiss button. Dismissal
 * is client-side (localStorage) — no route, no DB. Wraps on narrow
 * screens; never scrolls horizontally. */
.trial-banner {
  display: flex;
  align-items: center;
  gap: 8px 16px;
  flex-wrap: wrap;
  padding: 10px 16px 10px var(--layout-body-margin);
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
}
.trial-banner-text { margin: 0; flex: 1 1 240px; }
.trial-banner-cta { margin: 0; display: flex; }

/* ---------------- slim top app bar: brand mark + account actions only ----------------
 * #354: primary navigation moved out of the app bar into the bottom bar / rail
 * below, per M3. The app bar holds the title and essential actions only. */
.md-top-app-bar-title { display: inline-flex; align-items: center; gap: 10px; }
.md-top-app-bar-title svg { width: 34px; height: 34px; color: var(--md-sys-color-primary); }

/* Body margins follow the M3 window size classes: 16px on compact (<600px),
   24px on medium and wider (via --layout-body-margin). */
/* #966: content spans the full width of the main region — no max-width caps. */
main { padding: 24px var(--layout-body-margin) 96px; }
main.login { max-width: 420px; margin: 0 auto; margin-top: 8vh; }
/* #361: no horizontal inset — headings align with the M3 body-margin ruler. */
h2.md-headline-small { margin: 24px 0 12px; }

/* ---------------- skip link: keyboard users jump past nav chrome (#511) ----------------
 * Visually hidden until keyboard focus; styled as an M3 tonal pill so the one
 * visible element matches the house vocabulary. main:focus has no outline:
 * the skip action already announces the new location. */
.skip-link {
  position: fixed;
  left: 16px;
  top: -72px;
  z-index: 200;
  padding: 10px 20px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  transition: top var(--motion-duration-short4) var(--motion-easing-standard);
}
.skip-link:focus-visible { top: 12px; }
main:focus { outline: none; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

/* ---------------- cards (filled: surface-container-high, 12dp) ---------------- */
.md-card {
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-medium);
  padding: 20px;
  margin-bottom: 16px;
}
.md-card.tracking {
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}
.md-card.tracking .muted { color: var(--md-sys-color-on-primary-container); opacity: 0.75; }

/* ---------------- buttons (4 types, pill, 40dp, 48dp target) ---------------- */
.md-filled-button, .md-tonal-button, .md-text-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 40px;
  min-width: 48px;
  padding: 0 24px;
  border-radius: var(--md-sys-shape-corner-full);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.1px;
  cursor: pointer;
  border: none;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  white-space: nowrap;
}
/* hover state layer 8% */
.md-filled-button:hover::after, .md-tonal-button:hover::after,
.md-text-button:hover::after,
.md-icon-button:hover::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: var(--md-state-hover);
  pointer-events: none;
}
.md-filled-button:active::after, .md-tonal-button:active::after,
.md-text-button:active::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: var(--md-state-pressed);
  pointer-events: none;
}

.md-filled-button { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.md-tonal-button { background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
.md-text-button { background: transparent; color: var(--md-sys-color-primary); padding: 0 16px; }
.md-text-button.danger { color: var(--md-sys-color-error); }

/* ---------------- icons (inline SVG, M3 outlined 24px) ---------------- */
.md-filled-button svg, .md-tonal-button svg, .md-text-button svg {
  width: 18px; height: 18px; flex: none; }

/* ---------------- update-check progress state (#406) ---------------- */
/* While the check is in flight htmx adds .htmx-request to the button: the
   squiggly logo replaces the refresh icon in the same 18px box, so the
   button never jumps, and the clock hands spin. hx-disabled-elt disables
   the button for the duration of the request. */
#check-updates-btn .btn-spinner { display: none; width: 18px; height: 18px; flex: none; }
#check-updates-btn.htmx-request .btn-spinner { display: inline-flex; }
#check-updates-btn.htmx-request > svg { display: none; }
.tk-logo-spinning .tk-logo-hands {
  transform-box: view-box;
  transform-origin: 33px 22px;
  animation: tk-logo-spin 1.1s linear infinite;
}
@keyframes tk-logo-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .tk-logo-spinning .tk-logo-hands { animation: none; }
}
/* disabled buttons (used by hx-disabled-elt on #check-updates-btn) */
.md-filled-button:disabled, .md-tonal-button:disabled,
.md-text-button:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---------------- FAB (extended: 56dp high, pill, icon + label) ---------------- */
/* ---------------- icon buttons (48dp target, 24px glyph) ---------------- */
.md-icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: var(--md-sys-shape-corner-full);
  border: none;
  background: transparent;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  flex: none;
}
.md-icon-button svg { width: 24px; height: 24px; }
.md-icon-button:hover { color: var(--md-sys-color-on-surface); }
.md-icon-button.danger:hover { color: var(--md-sys-color-error); }
/* #432: M3 plain tooltip for icon buttons (the actions that lost their text
   labels). ::before so it doesn't clobber the ::after hover state layer;
   overflow:visible so the tooltip escapes the button's clip. */
.md-icon-button[data-tip] { overflow: visible; }
/* #556: hidden tooltips collapse to zero size. The pseudo-element renders
   even while invisible (opacity: 0), and its content-sized box (up to 200px
   centered on a 36px button) widened the scrollable overflow area of
   overflow-y:auto panes — overflow-x computes to auto there, so a phantom
   horizontal scrollbar appeared. max-width: 0 + overflow: hidden keeps the
   hidden box inside its anchor; the shown states restore the real width.
   Visible rendering and the fade are unchanged. */
.md-icon-button[data-tip]::before {
  content: attr(data-tip);
  position: absolute; left: 50%; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  transform: translateX(-50%) translateY(-2px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small); /* M3 plain tooltip shape */
  padding: 4px 8px; /* M3 plain tooltip padding */
  white-space: nowrap; max-width: 0; overflow: hidden;
  font-size: 0.75rem; line-height: 16px; font-weight: 400;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
}
.md-icon-button[data-tip]:hover::before,
.md-icon-button[data-tip]:focus-visible::before,
.md-icon-button[data-tip].tip-show::before {
  opacity: 1; transform: translateX(-50%) translateY(0);
  max-width: 200px; overflow: visible;
}
/* #434: tooltips on right-edge buttons anchor to the button's right edge
   instead of centering, so they extend leftward and can't run off the
   viewport (the detail action buttons sit at the header's right edge). */
.md-icon-button[data-tip].tip-right::before { left: auto; right: 0; transform: translateY(-2px); }
.md-icon-button[data-tip].tip-right:hover::before,
.md-icon-button[data-tip].tip-right:focus-visible::before,
.md-icon-button[data-tip].tip-right.tip-show::before { transform: translateY(0); }
/* #532: M3 plain tooltip for filled buttons — the timer Start/Stop buttons
   advertise the t shortcut. Mirrors the #432 icon-button tooltip: ::before
   so it doesn't clobber the ::after hover state layer; overflow:visible so
   the tooltip escapes the button's clip. */
.md-filled-button[data-tip] { overflow: visible; }
.md-filled-button[data-tip]::before {
  content: attr(data-tip);
  position: absolute; left: 50%; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  transform: translateX(-50%) translateY(-2px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small); /* M3 plain tooltip shape */
  padding: 4px 8px; /* M3 plain tooltip padding */
  white-space: nowrap; max-width: 0; overflow: hidden; /* #556: collapse while hidden */
  font-size: 0.75rem; line-height: 16px; font-weight: 400;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
}
.md-filled-button[data-tip]:hover::before,
.md-filled-button[data-tip]:focus-visible::before,
.md-filled-button[data-tip].tip-show::before {
  opacity: 1; transform: translateX(-50%) translateY(0);
  max-width: 200px; overflow: visible; /* #556 */
}

/* ---------------- text fields (borderless tonal, 56dp) ----------------
 * No indicator lines: separation comes from the tonal container + whitespace.
 * Number inputs are typing-first: the native spinner arrows ignore the dark
 * scheme and clutter compact fields, so they're hidden (same rationale as the
 * custom select chevron). */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}
.md-text-field { position: relative; display: block; }
.md-text-field input, .md-text-field select {
  width: 100%;
  height: 56px;
  padding: 22px 16px 8px;
  background: var(--md-sys-color-surface-container-highest);
  border: none;
  border-radius: var(--md-sys-shape-corner-medium);
  color: var(--md-sys-color-on-surface);
  font-size: 1rem;
  letter-spacing: 0.5px;
  outline: none;
}
.md-text-field select {
  padding: 26px 44px 8px 16px;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cac4cf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  background-size: 20px 20px;
}
.md-text-field label {
  position: absolute;
  left: 16px;
  top: 18px;
  font-size: 1rem;
  letter-spacing: 0.5px;
  color: var(--md-sys-color-on-surface-variant);
  pointer-events: none;
  transition: top var(--motion-duration-short3) var(--motion-easing-standard),
    font-size var(--motion-duration-short3) var(--motion-easing-standard),
    color var(--motion-duration-short3) var(--motion-easing-standard);
}
.md-text-field input:focus + label,
.md-text-field input:not(:placeholder-shown) + label,
/* #337: a select always shows a value, so its label always floats —
   selects never match :placeholder-shown, and the manual .filled
   workaround shouldn't be required on every select wrapper. */
.md-text-field:has(select) label,
.md-text-field.filled label {
  top: 8px;
  font-size: 0.75rem;
  letter-spacing: 0.4px;
}
.md-text-field input:focus, .md-text-field select:focus {
  background-color: var(--md-sys-color-surface-container-high);
}
/* keyboard focus ring (the base `outline: none` would otherwise kill the global one).
   #372: drawn as box-shadow, not outline — outline + border-radius +
   outline-offset mis-rasterizes at high device pixel ratios (the field's own
   background paints over the ring's bottom corners on phones). box-shadow
   follows the corner radius exactly at any DPR; the 2px surface ring keeps
   the same 2px gap + 2px ring the outline had. */
.md-text-field input:focus-visible, .md-text-field select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--md-sys-color-surface), 0 0 0 4px var(--md-sys-color-primary);
}
.md-text-field input:focus + label { color: var(--md-sys-color-primary); }
.md-text-field input[type="color"] { padding: 8px 16px; height: 56px; }
/* #627: textarea variant (client address) — same tonal treatment, auto height. */
.md-text-field textarea {
  width: 100%;
  min-height: 76px;
  padding: 22px 16px 8px;
  background: var(--md-sys-color-surface-container-highest);
  border: none;
  border-radius: var(--md-sys-shape-corner-medium);
  color: var(--md-sys-color-on-surface);
  font-size: 1rem;
  letter-spacing: 0.5px;
  outline: none;
  resize: vertical;
  font-family: inherit;
}
.md-text-field textarea:focus + label,
.md-text-field textarea:not(:placeholder-shown) + label {
  top: 8px;
  font-size: 0.75rem;
  letter-spacing: 0.4px;
}
.md-text-field textarea:focus { background-color: var(--md-sys-color-surface-container-high); }
.md-text-field textarea:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--md-sys-color-surface), 0 0 0 4px var(--md-sys-color-primary);
}
.md-text-field textarea:focus + label { color: var(--md-sys-color-primary); }

/* prefix/suffix affixes (M3 text-field anatomy: prefix/suffix in on-surface-variant) */
.md-text-field .affix {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 1rem;
  letter-spacing: 0.5px;
  pointer-events: none;
  z-index: 1;
}
.md-text-field .affix.prefix { left: 16px; }
.md-text-field .affix.suffix { right: 16px; }
.md-text-field.has-prefix input { padding-left: 32px; }
.md-text-field.has-suffix input { padding-right: 46px; }
.md-text-field.has-prefix label { left: 32px; }
/* compact (40dp) field for dense rows */
.md-text-field.compact input { height: 40px; padding-top: 8px; padding-bottom: 8px; font-size: 0.875rem; }
.md-text-field.compact .affix { font-size: 0.875rem; }
/* compact field WITH a floating label (nested add-project rows): compact was
   built for label-less affix fields, so labeled ones need their own label
   geometry plus room up top for the floated state. */
.md-text-field.compact:has(label) input { padding-top: 18px; padding-bottom: 4px; }
.md-text-field.compact:has(label) label { top: 12px; font-size: 0.875rem; }
.md-text-field.compact:has(label) input:focus + label,
.md-text-field.compact:has(label) input:not(:placeholder-shown) + label,
/* #627: the client-address textarea floats its label too. */
.md-text-field.compact:has(label) textarea:focus + label,
.md-text-field.compact:has(label) textarea:not(:placeholder-shown) + label,
.md-text-field.compact:has(label):has(select) label { top: 5px; font-size: 0.6875rem; }
.md-text-field.compact:has(label) textarea { padding-top: 18px; padding-bottom: 4px; min-height: 64px; }

/* color control: tonal container with palette icon, label, and swatch */
.md-color-control {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  height: 56px;
  padding: 8px 12px 8px 16px;
  background: var(--md-sys-color-surface-container-highest);
  border: none;
  border-radius: var(--md-sys-shape-corner-medium);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.1px;
  cursor: pointer;
  flex: none;
}
.md-color-control svg { width: 20px; height: 20px; }
.md-color-control input[type="color"] {
  width: 36px;
  height: 36px;
  border: none;
  padding: 0;
  background: none;
  cursor: pointer;
}
.md-color-control input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.md-color-control input[type="color"]::-webkit-color-swatch { border: none; border-radius: 50%; }
.md-color-control input[type="color"]::-moz-color-swatch { border: none; border-radius: 50%; }

/* ---------------- lists (expressive: rounded, inset dividers) ---------------- */
.md-list {
  list-style: none;
  margin: 0 0 16px;
  padding: 8px 0;
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-large);
}
.md-list-item {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 72px;
  padding: 8px 8px 8px 16px;
}
/* #362: no hard dividers between list rows — separation by whitespace and
   tonal cards, matching the Manage segmented list since #271. */
.md-list-item-content { flex: 1; min-width: 0; }

/* Entry row motion (#46): enter with fade+rise and per-row stagger
   (animation-delay rendered inline by entryRow); exit collapses during
   the delete swap:220ms so siblings glide instead of jumping.
   #901: the row is overflow:visible so icon-button tooltips (::before,
   below the button) can overlay the next row; overflow:hidden lives only
   on .htmx-swapping, where the collapse needs the clip. */
#today-list .md-list-item {
  animation: row-enter var(--motion-duration-short4) var(--motion-easing-emphasized-decelerate) backwards;
  transition: opacity var(--motion-duration-short4) var(--motion-easing-standard),
    transform var(--motion-duration-short4) var(--motion-easing-standard),
    max-height var(--motion-duration-short4) var(--motion-easing-standard),
    padding-top var(--motion-duration-short4) var(--motion-easing-standard),
    padding-bottom var(--motion-duration-short4) var(--motion-easing-standard);
  max-height: 120px;
  overflow: visible;
}
@keyframes row-enter { from { opacity: 0; transform: translateY(12px); } }
#today-list .md-list-item.htmx-swapping {
  opacity: 0;
  transform: translateX(24px);
  max-height: 0;
  min-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  overflow: hidden;
}
.md-list-item-headline {
  font-size: 1rem;
  line-height: 24px;
  letter-spacing: 0.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.md-list-item-supporting {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.875rem;
  line-height: 20px;
  letter-spacing: 0.25px;
  color: var(--md-sys-color-on-surface-variant);
  margin-top: 2px;
}
.md-list-item-trailing {
  display: flex;
  align-items: center;
  gap: 0;
  flex: none;
}
.md-list-item-trailing .dur {
  font-size: 0.875rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  margin-right: 4px;
  white-space: nowrap;
}
/* #473: repeat is a mini-form in the row's trailing area — keep it inline
   with the icon buttons and strip the UA form margin. */
.repeat-form { display: inline-flex; margin: 0; }

/* ---------------- chips (32dp, 8dp radius, outline-variant) ---------------- */
.md-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-small);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.5px;
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}
.md-chip .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
/* chip icons (e.g. the paid-date chip) render at M3's 18px chip icon size. */
.md-chip svg { width: 18px; height: 18px; flex: none; }

/* ---------------- timer ---------------- */
/* HTMX swap choreography (#45): outgoing card sinks/fades during the
   swap:200ms delay; incoming card rises/fades in from .htmx-settling. */
#timer-card {
  transition: opacity var(--motion-duration-short4) var(--motion-easing-standard),
    transform var(--motion-duration-short4) var(--motion-easing-emphasized-decelerate);
}
#timer-card.htmx-swapping { opacity: 0; transform: translateY(6px) scale(0.99); }
#timer-card.htmx-settling { opacity: 0; transform: translateY(-8px); }
.timer-state-label {
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.5px;
  color: var(--md-sys-color-on-surface-variant);
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Live tracking indicator (#51): pumpkin dot pulsing while a timer runs.
   The #41 reduced-motion guard collapses this to a static dot. */
.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--md-sys-color-primary);
  animation: live-pulse 1.6s ease-in-out infinite;
}
@keyframes live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.85); }
}
.md-card.tracking .timer-state-label { color: var(--md-sys-color-on-primary-container); }
/* #259: Stop sits top-right on the TRACKING row; elapsed time drops to the
   bottom-left where Stop used to be. The 56px FAB is taken out of flow so it
   doesn't stretch the label row and push the description down (#261). */
.md-card.tracking { position: relative; }
.timer-top-row .stop { position: absolute; top: 20px; right: 20px; }
.timer-top-row .timer-state-label { padding-right: 140px; }
/* #718: the project name never breaks mid-phrase on narrow screens — the
   label row is padded for the absolute Stop button, so a long name wraps
   awkwardly there. One line with an ellipsis instead. */
.timer-state-proj { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* #761: the running card's description sat in normal flow while the Stop
   button is absolutely positioned — a long description slid under the button
   and clipped mid-word on narrow screens. Same one-line ellipsis treatment
   as the label row, with matching right padding to clear the button. */
.timer-desc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 140px; }
.timer-elapsed {
  font-size: 2.8125rem;
  line-height: 52px;
  font-variant-numeric: tabular-nums;
  margin: 8px 0 16px;
}
.start-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-top: 12px; }
.start-form .grow { flex: 2; min-width: 220px; }
.start-form .shrink { flex: 1; min-width: 160px; }

/* ---------------- calendar (time grid) ---------------- */
.week-nav { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 4px; }
.week-nav h2 { margin: 0; }
/* M3 date-picker header pattern: headline leading, nav icon buttons grouped trailing. */
.week-nav-actions { display: flex; align-items: center; gap: 4px; }
.cal-hint { margin: 0 0 12px; }
/* ---------------- fixed viewport: regions own the scroll (Google app pattern) ----
 * Pages that opt in via layout(..., fixed=true) never scroll the page itself:
 * body is a 100dvh flex column and exactly one region owns the vertical scroll.
 * Document-like pages (invoice detail/new/edit entry) keep page scroll — the
 * invoice detail must print. */
body.app-fixed { height: 100vh; height: 100dvh; overflow: hidden; display: flex; flex-direction: column; }
body.app-fixed main {
  flex: 1; min-height: 0; width: 100%;
  display: flex; flex-direction: column;
  padding-bottom: 16px;
}
/* the region that owns the scroll on fixed-viewport pages.
 * 4px breathing room: the :focus-visible ring extends 2px + 2px offset beyond a
 * field, and fields sit flush against this region's clip edge (overflow-y: auto
 * clips the ring otherwise). */
.scroll-region { flex: 1; min-height: 0; overflow-y: auto; padding: 4px; }
/* ---------------- rounded app sheet (#385, refined #387) ----------------
 * M3 doesn't do sharp corners. The page background is surface-container; the
 * top app bar and nav rail sit chromeless on it (no background of their own),
 * and only the content column is a surface sheet with extra-large corners
 * floating on the page. Scoped to layout() via body.app — public pages
 * (login, invoice links, error pages) keep the flat surface background. */
body.app { background: var(--md-sys-color-surface-container); }
/* #387: the sheet is the content column of .app-chrome (rail beside it).
 * Gutters live on .app-chrome's padding; overflow: clip rounds the sheet's
 * inner content corners without becoming a scroll container. */
.app-sheet {
  background: var(--md-sys-color-surface);
  border-radius: var(--md-sys-shape-corner-extra-large);
  overflow: clip;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}
/* #387: the app bar floats chromeless on the page background — no tonal band
 * of its own, so it inherits the page bg exactly. */
body.app .md-top-app-bar {
  background: transparent;
  margin: 12px 12px 0;
}
/* fixed-viewport pages: the chrome row fills the 100dvh body and the sheet
   stretches with it. The rail stops being sticky here — its 100dvh-based
   height would poke past the chrome — and just stretches with the row. */
body.app-fixed .app-sheet { min-height: 0; }
body.app-fixed .md-nav-rail { position: static; height: auto; }
/* ---------------- primary navigation: bottom bar (compact) / rail (medium+) ----
 * #354: M3 per window size class. Compact (<600px): bottom navigation bar,
 * fixed to the bottom edge. Medium and wider (600px+): collapsed leading
 * navigation rail, outside all panes on the leading edge, below the app bar.
 * The switch is the 600px breakpoint — rail and bar never show together.
 * Rail width (72px, #397) is a deliberate deviation from the M3 80dp
 * default; bar height (80px) is the M3 default. */
/* app chrome: rail beside the content column (banner + main). On
   fixed-viewport pages the row fills the 100dvh body; on document pages the
   rail sticks below the app bar while the page scrolls. #387: the 12px
   gutters around the floating sheet live on this row's padding/gap. */
.app-chrome { display: flex; flex: 1; min-height: 0; gap: 12px; padding: 12px; }
.md-nav-rail {
  flex: none; width: var(--md-nav-rail-width);
  /* #387: sticks below the floating app bar (12px gutter + 56px bar + 12px
     chrome gap) so the rail keeps the same offset at rest and while stuck.
     Chromeless — inherits the page background. #389: 88px → 80px follows
     the slimmer app bar. */
  position: sticky; top: 80px; height: calc(100dvh - 80px);
  display: flex; flex-direction: column; align-items: stretch;
  padding: 12px 0; gap: 4px;
  background: transparent;
}
/* destinations: icon in an active-indicator pill + one-word label below.
   Shared by rail and bottom bar; labels are never truncated. */
.md-rail-item, .md-bar-item {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; text-decoration: none; color: var(--md-sys-color-on-surface-variant);
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.5px; line-height: 16px;
  position: relative;
}
.md-rail-item { padding: 12px 0 10px; }
.nav-pill {
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 32px; border-radius: var(--md-sys-shape-corner-full);
}
.nav-pill svg { width: 24px; height: 24px; }
.md-rail-item:hover .nav-pill, .md-bar-item:hover .nav-pill {
  background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
  color: var(--md-sys-color-on-surface);
}
.md-rail-item.active, .md-bar-item.active { color: var(--md-sys-color-on-surface); }
.md-rail-item.active .nav-pill, .md-bar-item.active .nav-pill {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
/* bottom navigation bar: compact only, fixed to the bottom edge */
.md-bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  height: calc(var(--md-nav-bar-height) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  display: flex; align-items: stretch;
  background: var(--md-sys-color-surface-container);
}
.md-bar-item { flex: 1; min-width: 0; }
.md-bar-item .nav-label {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* the 600px switch: never rail + bar together */
@media (max-width: 599px) {
  .md-nav-rail { display: none; }
  /* compact clearance: the fixed bar overlays the bottom 80px */
  body main { padding-bottom: calc(96px + var(--md-nav-bar-height)); }
  body.app-fixed main { padding-bottom: calc(16px + var(--md-nav-bar-height)); }
  .tk-snackbar { bottom: calc(24px + var(--md-nav-bar-height)); }
}
@media (min-width: 600px) {
  .md-bottom-nav { display: none; }
}
/* ---------------- invoices list-detail (M3 canonical, #364) ----------------
 * The invoice list → invoice document is a parent-child pairing, so M3
 * expects the list-detail layout: two panes at expanded+, a single pane
 * below. The detail pane reuses invoiceDetailBody; its back link shows only
 * in single-pane mode and the selected-row state only in two-pane mode
 * (both per M3). Panes scroll independently on the fixed-viewport page. */
.invoice-list-detail { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.invoice-list-pane, .invoice-detail-pane { min-width: 0; }
@media (min-width: 840px) {
  .invoice-list-detail { flex-direction: row; gap: var(--layout-pane-gap); align-items: stretch; }
  .invoice-list-pane { flex: 0 0 400px; overflow-y: auto; }
  .invoice-detail-pane { flex: 1; min-width: 0; overflow-y: auto; }
  .invoice-detail-pane .detail-back { display: none; }
  .inv-row.selected { background: var(--md-sys-color-secondary-container); border-radius: 12px; }
}
@media (max-width: 839px) {
  .invoice-list-pane, .invoice-detail-pane { flex: 1; min-height: 0; overflow-y: auto; }
  .invoice-list-detail.has-selection .invoice-list-pane { display: none; }
  .invoice-list-detail:not(.has-selection) .invoice-detail-pane { display: none; }
}
/* whole-row tap target for invoice rows */
.inv-row-hit { display: flex; align-items: center; gap: 8px; width: 100%;
  color: inherit; text-decoration: none; }
/* #442: date phrases in list rows never split mid-date; the supporting line
   wraps at the middot separators instead. */
.inv-date { white-space: nowrap; }
/* #588: the list-row date phrases share one nowrap group, so a narrow pane
   wraps between the client name and the dates — never between segments. */
.inv-row .inv-dates { white-space: nowrap; }
/* empty detail pane */
.detail-placeholder { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 12px; padding: 64px 24px; text-align: center; min-height: 320px; }
.detail-placeholder > svg { width: 48px; height: 48px; color: var(--md-sys-color-on-surface-variant); }
/* ---------------- supporting-pane layout (M3 canonical, timer page) ----------------
 * <840px: single stacked column — timer card stays pinned above, entries + revenue
 * share one scroll region. ≥840px (M3 expanded): focus pane (entries) + trailing
 * fixed supporting pane (revenue) with a 24px gap — 360px at expanded, 412px at
 * large/XL (via --layout-supporting-pane) — each scrolling independently.
 * Spec: supporting-pane example. */
.pane-layout { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.pane-focus, .pane-supporting { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.pane-supporting { container-type: inline-size; }
/* #326: the global h2.md-headline-small 24px top margin is for page-level
   section headings — inside the pane it just shoves "Revenue" down. */
.pane-supporting h2.md-headline-small { margin: 0 0 12px; }
@media (max-width: 839px) {
  .pane-layout { overflow-y: auto; }
  .pane-focus, .pane-supporting { flex: none; }
  .pane-layout .scroll-region { flex: none; overflow: visible; }
  .pane-supporting { margin-top: var(--layout-pane-gap); } /* match the desktop pane gap */
}
@media (min-width: 840px) {
  .pane-layout { flex-direction: row; gap: var(--layout-pane-gap); }
  .pane-focus { flex: 1; }
  .pane-supporting { flex: 0 0 var(--layout-supporting-pane); }
}
/* ---------------- calendar supporting pane (M3 canonical, #67) ----------------
 * The week grid is the primary content, the week summary is supplementary —
 * the same canonical layout as the timer page (840px breakpoint; supporting
 * pane 360px at expanded, 412px at large/XL via --layout-supporting-pane).
 * <840px: single column, the summary keeps its position above the grid (its
 * long-standing spot; the grid is an internally-scrolling 24h surface, so the
 * spec's below-the-fold placement would bury the summary and its invoice CTA).
 * ≥840px: the summary trails beside the grid, which takes the freed vertical
 * space. #347: the week-nav lives at the top of the summary aside, so the
 * grid is just summary + grid areas; DOM order aside → grid stacks the
 * summary above the grid on narrow screens without a wrapper. */
.cal-pane-layout { display: grid; flex: 1; min-height: 0; min-width: 0;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "summary" "grid";
  grid-template-rows: auto minmax(0, 1fr); }
.cal-supporting { grid-area: summary; display: flex; flex-direction: column; gap: 14px; margin: 10px 0 12px; min-width: 0; }
.cal-supporting .week-nav { margin: 0; }
.cal-supporting .m3-progress, .cal-supporting .week-clients, .cal-supporting .cal-hint { margin: 0; }
/* #542: the week-over-week delta's trend icon rides the muted label baseline
   at label size — the same 14px treatment the Stripe paid chip's icon gets. */
.cal-summary .cal-delta svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: 2px; }
.cal-pane-layout .cal-carousel { grid-area: grid; }
@media (min-width: 840px) {
  .cal-pane-layout { grid-template-columns: minmax(0, 1fr) var(--layout-supporting-pane);
    grid-template-areas: "grid summary"; grid-template-rows: minmax(0, 1fr); column-gap: var(--layout-pane-gap); }
  .cal-supporting { margin: 8px 0 0; min-height: 0; overflow-y: auto; }
}
/* calendar specifics: the week grid is the scroll region, headers stick */
body.app-fixed #week { display: flex; flex-direction: column; flex: 1; min-height: 0; }
body.app-fixed .cal-scroll { flex: 1; min-height: 0; overflow-y: auto; }
body.app-fixed .cal-day-head,
body.app-fixed .cal-gutter-spacer {
  position: sticky; top: 0; z-index: 5;
  background: var(--md-sys-color-surface);
}
/* #881: the sticky day headers are per-lane, so the 8px inter-lane gutters
   broke the header band — the now-line dot (which correctly straddles the
   lane edge at left:-5px) stayed visible through the gap after scrolling
   past the now-line. Each head overlaps 4px into the gutters on both sides
   (4+4 covers the 8px gap, so the header band is continuous and the dot's
   gutter half hides under it with the line). Symmetric, so centered day
   names/numbers don't shift. Sunday's right margin is zeroed: nothing may
   overhang the grid's right edge. Lane gaps BELOW the header band stay 8px
   page-bg — only the header band closes. (#880 moved the dot inside the
   lane instead; reverted — the straddle is the correct design.) */
.cal-grid > .cal-day .cal-day-head { margin-left: -4px; margin-right: -4px; }
.cal-grid > .cal-day:nth-child(8) .cal-day-head { margin-right: 0; }
/* #847: the sticky header is opaque page-bg and square. #845 gave it the lane
   tone + rounded top, but the lane's purple night band slides underneath while
   scrolling and peeked through the header's rounded corner cutouts. Square and
   opaque, nothing can ever peek through it. The lane's visible top edge is
   re-rounded below it by the lane's ::before cutter, so columns still read rounded
   on top at every scroll position. (::after is the day-stats tooltip.) */
/* #849: the top cutter lives on the lane itself (not the header) so it paints
   BELOW entries. First flex child, sticky just under the sticky header; the
   negative margin collapses its layout footprint. Entries (z-2) cover it, so
   it can never bite an entry's top corners the way the old header-anchored
   cutter did. Same crescent mask as the ::after bottom cutter, mirrored.
   #877: z-1, above the translucent night bands (z-auto) — at scroll-top the
   band's opaque top-12px backing covered the cutter, but scrolled past 12px
   the band's translucent region slid over the cutter's dark crescents and
   they read through as notches. Above the band, the crescents are clean
   dark cuts at every scroll offset; opaque entries still paint over it. */
.cal-day-col::before {
  content: ""; display: block; flex: none; width: 100%;
  height: var(--md-sys-shape-corner-medium);
  margin-bottom: calc(-1 * var(--md-sys-shape-corner-medium));
  position: sticky; top: var(--cal-head-h); z-index: 3;
  background: var(--md-sys-color-surface);
  -webkit-mask:
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 100% 100%, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) left top / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat,
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 0 100%, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) right top / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat;
  mask:
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 100% 100%, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) left top / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat,
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 0 100%, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) right top / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat;
  pointer-events: none;
}
/* ---------------- calendar: tonal day lanes, no hard dividers ----------------
 * Separation comes from spacing (8px gutters) + tone (lane surfaces). */
.cal-scroll { overflow-x: clip; border-radius: var(--md-sys-shape-corner-large); }
.cal-grid {
  display: grid;
  grid-template-columns: 44px repeat(7, minmax(0, 1fr));
  gap: 8px;
}
.cal-gutter-spacer { height: var(--cal-head-h); } /* matches the two-line day header */
.cal-gutter { position: relative; }
.cal-hours { height: calc(var(--cal-hour) * 24); }
.cal-hour-label {
  position: absolute; right: 8px; transform: translateY(-50%);
  font-size: 0.6875rem; color: var(--md-sys-color-on-surface-variant); white-space: nowrap;
}
/* Night hours (7 PM – 6 AM) read lavender in the gutter. */
.cal-hour-label.night { color: #b9a8e8; }
.cal-day { display: flex; flex-direction: column; min-width: 0; }
/* Grid entrance (#49): columns stagger in left→right inside the #48 region
   entrance; per-column delay rendered inline by the server. */
.cal-grid .cal-day {
  animation: cal-day-enter var(--motion-duration-medium2) var(--motion-easing-emphasized-decelerate) backwards;
}
@keyframes cal-day-enter { from { opacity: 0; transform: translateY(12px); } }
/* #69: header is two lines (weekday + date) — per-day stats moved into an M3 plain tooltip */
.cal-day-head { position: relative; text-align: center; padding: 10px 4px 8px; min-height: var(--cal-head-h); }
.cal-day-head[data-tip] { cursor: default; }
.cal-day-head[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: 50%; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  transform: translateX(-50%) translateY(-2px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small); /* M3 plain tooltip shape */
  padding: 4px 8px; /* M3 plain tooltip padding */
  white-space: nowrap; max-width: 0; overflow: hidden; /* #556: collapse while hidden */
  font-size: 0.75rem; line-height: 16px;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
}
.cal-day-head[data-tip]:hover::after,
.cal-day-head[data-tip]:focus-visible::after,
.cal-day-head[data-tip].tip-show::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
  max-width: 200px; overflow: visible; /* #556 */
}
/* keep edge-column tooltips inside the scroll region instead of clipping */
.cal-day:first-child .cal-day-head[data-tip]::after { left: 0; transform: translateY(-2px); }
.cal-day:last-child .cal-day-head[data-tip]::after { left: auto; right: 0; transform: translateX(0) translateY(-2px); }
.cal-day:first-child .cal-day-head[data-tip]:hover::after,
.cal-day:first-child .cal-day-head[data-tip]:focus-visible::after,
.cal-day:first-child .cal-day-head[data-tip].tip-show::after,
.cal-day:last-child .cal-day-head[data-tip]:hover::after,
.cal-day:last-child .cal-day-head[data-tip]:focus-visible::after,
.cal-day:last-child .cal-day-head[data-tip].tip-show::after { transform: translateY(0); }
.cal-day-num { font-size: 1.375rem; line-height: 28px; }
.cal-day-num.today {
  /* #69: today is marked with the primary color, not a filled badge */
  color: var(--md-sys-color-primary); font-weight: 700;
}
/* #845: per-day lane tone. #847: the sticky header no longer shares it — it is
   opaque page-bg so the scrolling purple night band can never peek through
   rounded corner cutouts; the ::before top cutter re-rounds the lane's
   visible top edge instead. */
.cal-day { --lane-bg: var(--md-sys-color-surface-container-low); }
.cal-day.today { --lane-bg: color-mix(in srgb, var(--md-sys-color-primary) 10%, var(--md-sys-color-surface-container-low)); }
/* Past days under 8h get a subtle one-step tonal lift; today and future
   days are never tinted. Background only, so no layout shift. */
.cal-day.paced-short { --lane-bg: var(--md-sys-color-surface-container); }
.cal-day-col {
  position: relative; height: calc(var(--cal-hour) * 24); flex: none;
  display: flex; flex-direction: column;
  background-color: var(--lane-bg);
  border-radius: var(--md-sys-shape-corner-medium);
  background-image: repeating-linear-gradient(to bottom,
    transparent 0, transparent calc(var(--cal-hour) - 1px),
    color-mix(in srgb, var(--md-sys-color-outline-variant) 30%, transparent) calc(var(--cal-hour) - 1px),
    color-mix(in srgb, var(--md-sys-color-outline-variant) 30%, transparent) var(--cal-hour));
  cursor: crosshair; touch-action: none;
}
/* #845: the lane's own rounded bottom scrolls out of view — a sticky cutter
   re-rounds the visible bottom edge at every scroll position. Only the two
   bottom-corner crescents paint (page bg). #849: the cutter sits at z-0,
   below entries (z-1) so it can never bite them; night bands sliding
   underneath still get clean rounded cutouts instead of square cuts. */
.cal-day-col::after {
  content: ""; display: block; flex: none;
  /* margin-top: auto parks the cutter at the lane's end in normal flow, so
     sticky bottom:0 pins it to the viewport's bottom edge whenever the lane's
     end is scrolled below it. At max scroll it sits exactly over the lane's
     own rounded corners and its crescents blend invisibly into them. */
  margin-top: auto;
  position: sticky; bottom: 0; z-index: 0;
  height: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface);
  -webkit-mask:
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 100% 0, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) left bottom / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat,
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 0 0, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) right bottom / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat;
  mask:
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 100% 0, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) left bottom / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat,
    radial-gradient(circle var(--md-sys-shape-corner-medium) at 0 0, transparent calc(var(--md-sys-shape-corner-medium) - 1px), #000 var(--md-sys-shape-corner-medium)) right bottom / var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) no-repeat;
  pointer-events: none;
}
/* Night bands (7 PM – 6 AM): deep purple with stars. Translucent so the hour
   lines show through; behind entries; pointer-events none so drag-create
   still works over them. */
.cal-night {
  position: absolute; left: 0; right: 0; pointer-events: none;
  background: color-mix(in srgb, #4a2b8f 48%, transparent);
}
/* Wavy horizon on the day-facing edge of each night band. The wave fill is
   the same translucent purple, so it composites seamlessly with the band. */
.cal-wave { position: absolute; left: 0; width: 100%; height: 12px; display: block; }
.cal-wave path { fill: color-mix(in srgb, #4a2b8f 48%, transparent); }
.cal-wave-am { bottom: -10px; }
.cal-wave-pm { top: -10px; }
.cal-night-am { top: 0; height: calc(var(--cal-hour) * 6);
  border-radius: var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium) 0 0;
  /* #853: the ::before top cutter's page-bg crescents sit directly under this
     translucent band and read as dark notches through it at the rounded top
     corners (the band already carries the lane's medium radius, matching the
     cutter's crescent exactly). The top 12px gets an opaque lane-bg backing
     (purple composited over it) so the corners render cleanly; below that the
     band stays translucent so the hour lines show through. All layers are
     clipped to the band's own border-radius. */
  background:
    linear-gradient(color-mix(in srgb, #4a2b8f 48%, transparent), color-mix(in srgb, #4a2b8f 48%, transparent)) left top / 100% var(--md-sys-shape-corner-medium) no-repeat,
    linear-gradient(var(--lane-bg), var(--lane-bg)) left top / 100% var(--md-sys-shape-corner-medium) no-repeat,
    color-mix(in srgb, #4a2b8f 48%, transparent); }
.cal-night-pm { top: calc(var(--cal-hour) * 19); height: calc(var(--cal-hour) * 5);
  border-radius: 0 0 var(--md-sys-shape-corner-medium) var(--md-sys-shape-corner-medium); }
.cal-star {
  position: absolute; color: #d9cdf7; font-size: 0.625rem; line-height: 1;
  opacity: .55; animation: cal-twinkle 3.2s ease-in-out infinite;
}
.cal-star:nth-child(3n) { font-size: 0.5rem; opacity: .4; }
.cal-star:nth-child(4n) { font-size: 0.75rem; opacity: .65; }
@keyframes cal-twinkle { 50% { opacity: .22; } }
@media (prefers-reduced-motion: reduce) {
  .cal-star { animation: none; }
}
.cal-entry {
  position: absolute; left: 4px; right: 4px; border-radius: var(--md-sys-shape-corner-small);
  background: var(--c, var(--md-sys-color-primary)); color: #16161c;
  padding: 4px 8px; overflow: hidden; cursor: pointer;
  font-size: 0.75rem; line-height: 1.35;
  min-width: 0;
  /* #849: above the lane's ::before/::after corner cutters (z-1) so they can
     never bite an entry's corners; still below the sticky header (z-5).
     #877: the top cutter moved to z-1 (above the translucent night bands),
     so entries move to z-2 to stay above the cutters. */
  z-index: 2;
}
/* #275: sub-30-minute entries render at the 22px floor — tighter padding,
   smaller single-line text, and no duration line (no room for two lines).
   #281: refine into a true single-line pill — the line is vertically
   centered (flex), full chip rounding like an M3 chip, desc takes the free
   space with ellipsis. No client dot: the card is already client-colored,
   which is what the reference app's avatar is for on its monochrome cards.
   #287: 10px text @ 1.2 (12px line) fits the 16px-painted / 12px-content
   box of a 15-min slot at 80px/hr exactly. */
.cal-entry.cal-compact {
  display: flex; align-items: center;
  padding: 2px 10px; font-size: 0.625rem; line-height: 1.2;
  border-radius: 999px;
}
.cal-entry.cal-compact .cal-entry-desc { flex: 1; min-width: 0; }
.cal-entry.cal-compact .cal-entry-dur { display: none; }
.cal-entry:hover { filter: brightness(1.08); }
/* #851: an entry touching the lane's bottom edge (server-flagged
   cal-edge-bottom) gets the lane's medium corner radius on its bottom corners.
   Pre-#849 the z-6 bottom cutter stamped that rounding over entries; post-#849
   the cutter sits below entries so it can't fringe them, but edge entries fell
   back to small corners and read as sharp. Compact pills keep their 999px. */
.cal-entry.cal-edge-bottom:not(.cal-compact) {
  border-bottom-left-radius: var(--md-sys-shape-corner-medium);
  border-bottom-right-radius: var(--md-sys-shape-corner-medium);
}
/* #853: an entry starting at midnight touches the lane's top edge
   (server-flagged cal-edge-top) gets the lane's medium corner radius on its
   top corners. Mirrors cal-edge-bottom (#851): entries paint above the z-0
   top cutter, so without this their small corners would cover the cutter's
   rounding and read as sharp. Compact pills keep their 999px. */
.cal-entry.cal-edge-top:not(.cal-compact) {
  border-top-left-radius: var(--md-sys-shape-corner-medium);
  border-top-right-radius: var(--md-sys-shape-corner-medium);
}
/* In-place week refresh (#65): load-entrance animations are suppressed while
   the #week section is swapped, so saving an entry never replays the page. */
#week.no-enter [data-enter],
#week.no-enter .nudge-chip,
#week.no-enter .cal-day,
#week.no-enter .m3-progress-fill,
#week.no-enter .m3-progress-fill-wrap { animation: none !important; }
/* #263: .m3-progress-fill-wave is deliberately NOT in the list above — the
   ripple is a perpetual 1.6s loop, not a load-entrance animation, and the
   no-enter class is never removed after a swap. Suppressing it here froze the
   wave permanently after the first week-nav swap or entry save. */
/* One-time highlight on the entry that was just created/updated. */
.cal-entry.cal-flash { animation: cal-flash 1.2s var(--motion-easing-standard); }
@keyframes cal-flash {
  0% { filter: brightness(1.7); }
  100% { filter: brightness(1); }
}
/* overlap: soft layered top shadow (tailwind-style), reads as elevation.
   #283: tightened — same color, less spread. */
.cal-entry.overlap {
  box-shadow: 0 -4px 8px -2px rgb(0 0 0 / 0.3), 0 -2px 4px -2px rgb(0 0 0 / 0.25);
}
.cal-entry:focus-visible { outline: 2px solid var(--md-sys-color-on-surface); outline-offset: 1px; }
.cal-entry.running { border: 2px dashed #16161c; }
.cal-entry-desc { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-entry-desc strong { font-weight: 700; }
.cal-entry-dur { opacity: 0.75; white-space: nowrap; }
.cal-now { position: absolute; left: 0; right: 0; height: 2px; background: var(--md-sys-color-error); z-index: 4; pointer-events: none; }
.cal-now::before {
  content: ""; position: absolute; left: -5px; top: -4px;
  width: 10px; height: 10px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-error);
}
.cal-drag-preview {
  position: absolute; left: 2px; right: 2px; border-radius: var(--md-sys-shape-corner-small);
  background: color-mix(in srgb, var(--md-sys-color-primary) 35%, transparent);
  border: 1px solid var(--md-sys-color-primary); pointer-events: none; z-index: 3;
}
/* M3 dialog */
.cal-scrim {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, 0.5);
  display: flex; align-items: center; justify-content: center; padding: 16px;
  opacity: 0;
  transition: opacity var(--motion-duration-short4) var(--motion-easing-standard);
}
.cal-scrim[hidden] { display: none; }
.cal-scrim.open { opacity: 1; }
.cal-dialog {
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-extra-large);
  padding: 24px; width: min(480px, 100%);
  display: grid; gap: 16px;
  transform: scale(0.9);
  opacity: 0;
  transition: transform var(--motion-duration-medium2) var(--motion-easing-emphasized),
    opacity var(--motion-duration-short4) var(--motion-easing-standard);
}
.cal-scrim.open .cal-dialog { transform: scale(1); opacity: 1; }
.cal-dialog h2 { margin: 0; }
.cal-dialog p { margin: 0; }
/* #464: shared confirm dialog — same scrim/motion as the app dialogs,
   narrower than the form dialogs. */
.tk-confirm { width: min(400px, 100%); }
.cal-time-row { display: flex; gap: 12px; align-items: flex-end; }
.cal-time-row .md-text-field { flex: 1; min-width: 0; }
#cal-dur { white-space: nowrap; flex-shrink: 0; align-self: center; }
.cal-dialog-actions { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.cal-spacer { flex: 1; }
.md-text-button.danger { color: var(--md-sys-color-error); }

/* Issue #245: settings modal — same scrim/dialog motion pattern as the
   calendar dialog, with a header row (title + close) and a scrollable body. */
.settings-dialog {
  background: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-extra-large);
  padding: 24px; width: min(480px, 100%);
  max-height: calc(100dvh - 64px); overflow-y: auto;
  display: grid; gap: 16px; align-content: start;
  transform: scale(0.9);
  opacity: 0;
  transition: transform var(--motion-duration-medium2) var(--motion-easing-emphasized),
    opacity var(--motion-duration-short4) var(--motion-easing-standard);
}
.cal-scrim.open .settings-dialog { transform: scale(1); opacity: 1; }
.settings-dialog-head { display: flex; align-items: center; justify-content: space-between; }
.settings-dialog-head h2 { margin: 0; }
#settings-body p { margin: 0; }
#biz-profile h2 { margin: 0 0 4px; }
/* #884: theme dots — accent swatches in the settings modal. #895: the active
   theme gets a white border/ring so the selection is obvious; each dot is a
   true circle in its own theme color. The check is dark-on-light for every
   palette. Focus uses the global :focus-visible ring. */
#theme-section h2 { margin: 0 0 4px; }
.theme-dots { display: flex; flex-wrap: wrap; gap: 12px; margin: 4px 0 8px; }
.theme-dot {
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--dot);
  border: none; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform 120ms ease;
}
.theme-dot:hover { transform: scale(1.08); }
.theme-dot.selected { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--dot); }
.theme-dot svg { width: 20px; height: 20px; color: rgba(0, 0, 0, 0.75); }
/* #355: the calendar dialog gets the same title/close header row as the
   settings dialog; the close button is compact-only (600px+ keeps the
   centered basic-dialog treatment with its Cancel/Save actions). */
.cal-dialog-head { display: flex; align-items: center; justify-content: space-between; }
.cal-dialog-head h2 { margin: 0; }
.cal-dialog-head .md-icon-button { display: none; }
/* #355: full-screen dialogs on compact. Below 600px the calendar entry and
   settings dialogs go full bleed: no scrim padding, no corner radius, full
   viewport height, with a title/close app-bar row pinned at the top. Escape,
   scrim dismissal, focus behavior, close/save/delete behavior, and motion
   are unchanged — only the presentation switches. */
@media (max-width: 599px) {
  .cal-scrim { padding: 0; }
  .cal-dialog, .settings-dialog {
    width: 100%; height: 100dvh; max-height: 100dvh;
    border-radius: 0;
    /* the dialogs are grids: keep rows content-sized when stretched full-height */
    align-content: start;
  }
  .cal-dialog-head, .settings-dialog-head {
    margin: -24px -24px 0; padding: 12px 8px 12px 24px;
    position: sticky; top: -24px; z-index: 1;
    background: var(--md-sys-color-surface-container-high);
  }
  .cal-dialog-head .md-icon-button { display: inline-flex; }
}

/* #654: the account menu (#151/#155/#253) was replaced by app-bar action icons —
   Settings, Export entries CSV, Download backup, Log out as .md-icon-buttons
   on the right side of the top app bar. The .m3-menu CSS went with it. */
.app-bar-actions { display: inline-flex; align-items: center; gap: 4px; flex: none; }
/* #912: logout is a POST form in the app bar — the form must not disturb the
   icon row's layout. */
.app-bar-logout { display: inline-flex; margin: 0; }
/* #949: the hamburger replaces the icon row on compact screens only — the
   labeled navigation drawer below carries the four actions there. */
.app-bar-menu-btn { display: none; }
@media (max-width: 599px) {
  .app-bar-actions { display: none; }
  .app-bar-menu-btn { display: inline-flex; }
}
/* #949: M3 modal navigation drawer, per the official spec measurements:
   360dp container on surface-container-low, 0/16/16/0 corners, 32% scrim,
   56dp full-round rows (label-large, on-surface-variant; the indicator sits
   12dp from the container edge and content 28dp). Dismissal is scrim tap or
   Escape — the spec has no close button. */
.nav-drawer-scrim {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, 0.32);
  opacity: 0;
  transition: opacity var(--motion-duration-short4) var(--motion-easing-standard);
}
.nav-drawer-scrim[hidden] { display: none; }
.nav-drawer-scrim.open { opacity: 1; }
.nav-drawer {
  position: absolute; top: 0; left: 0; bottom: 0;
  touch-action: pan-y; /* horizontal drags reach the swipe-to-close handler */
  width: min(360px, 100vw);
  background: var(--md-sys-color-surface-container-low);
  border-radius: 0 var(--md-sys-shape-corner-large) var(--md-sys-shape-corner-large) 0;
  padding: 12px;
  display: flex; flex-direction: column;
  transform: translateX(-100%);
  transition: transform var(--motion-duration-short4) var(--motion-easing-standard);
}
.nav-drawer-scrim.open .nav-drawer { transform: translateX(0); }
.nav-drawer-head {
  padding: 12px 16px 16px;
  font-size: 0.875rem; font-weight: 500;
  color: var(--md-sys-color-on-surface-variant);
}
.nav-drawer-row {
  display: flex; align-items: center; gap: 12px;
  min-height: 56px; padding: 0 16px;
  border: 0; border-radius: var(--md-sys-shape-corner-full);
  background: transparent; color: var(--md-sys-color-on-surface-variant);
  font: inherit; font-size: 0.875rem; font-weight: 500; text-align: left;
  text-decoration: none; cursor: pointer; width: 100%;
}
.nav-drawer-row:hover, .nav-drawer-row:focus-visible {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
.nav-drawer-row svg { flex: none; }
.nav-drawer-logout { margin: 0; }
.nav-drawer-form { margin: 0; }
/* #949: the demo row is a separate group — separated by space, not a divider. */
.nav-drawer-demo { margin-top: 12px; }

/* ---------------- forms / manage ---------------- */
.form { display: grid; gap: 16px; max-width: 480px; }
.form .row { display: flex; gap: 8px; align-items: center; }
.form .row.end { justify-content: flex-end; }
/* stacked add-forms: name row, then a control row with the primary action end-aligned */
.manage-form { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.manage-form-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.manage-form-row .grow { flex: 1; min-width: 140px; }
.manage-form-spacer { flex: 1; }
/* ---------------- clients list-detail (M3 canonical, #367) ----------------
 * Client → projects/rate/terms is a parent-child pairing, so M3 expects the
 * list-detail layout: two panes at expanded+, a single pane below. Mirrors
 * the invoices list-detail (#364). The #271 folder-like expand/collapse is
 * superseded by selection and is gone. */
.client-list-detail { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.client-list-pane, .client-detail-pane { min-width: 0; }
@media (min-width: 840px) {
  .client-list-detail { flex-direction: row; gap: var(--layout-pane-gap); align-items: stretch; }
  .client-list-pane { flex: 0 0 400px; overflow-y: auto; }
  .client-detail-pane { flex: 1; min-width: 0; overflow-y: auto; }
  .client-detail-pane .detail-back { display: none; }
  .client-row.selected { background: var(--md-sys-color-secondary-container); border-radius: 12px; }
}
@media (max-width: 839px) {
  .client-list-pane, .client-detail-pane { flex: 1; min-height: 0; overflow-y: auto; }
  .client-list-detail.has-selection .client-list-pane { display: none; }
  .client-list-detail:not(.has-selection) .client-detail-pane { display: none; }
}
/* whole-row tap target for client rows */
.client-row-hit { display: flex; align-items: center; gap: 12px; width: 100%;
  color: inherit; text-decoration: none; }
.client-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.client-dot.large { width: 16px; height: 16px; }
.client-dot.none { width: 24px; height: 24px; background: transparent;
  color: var(--md-sys-color-on-surface-variant); }
.client-dot.none svg { width: 24px; height: 24px; }
.client-detail-head { display: flex; align-items: center; gap: 12px; margin: 4px 0 16px; }
.client-detail-head .md-headline-small { margin: 0; }
/* #633: name left, cog pushed to the right edge via space-between. */
.client-detail-head.has-settings { justify-content: space-between; }
.client-detail-head .client-settings-open { flex: none; }
.client-add { margin-bottom: 8px; }
/* #383: the add-client field sat tight against the column's left edge — give
   it left breathing room (aligns the field with the toggle text above it). */
#client-add-form { padding-left: 16px; }
#client-add-form[hidden] { display: none; }
.detail-form { margin-bottom: 12px; }
.manage-subhead { margin: 20px 4px 8px; color: var(--md-sys-color-on-surface-variant); }
.add-project-form { display: flex; align-items: center; gap: 8px; flex: 1 1 100%; }
.add-project-form .md-text-field { flex: 1; min-width: 120px; }
/* #293: canned payment-terms description under the dropdown (M3 supporting text). */
.terms-desc { flex: 1 1 100%; margin: 0; font-size: 0.75rem; line-height: 1.4; }
#client-detail .terms-desc { padding: 0 8px 4px 0; }
#biz-profile .terms-desc { margin-top: 8px; }

.page-head-actions { display: flex; gap: 8px; align-items: center; }
/* Milestone 17: bottom-edge resize handle for calendar entries. */
.cal-resize {
  position: absolute; left: 0; right: 0; bottom: 0; height: 12px;
  cursor: ns-resize; border-radius: 0 0 8px 8px; touch-action: none;
}
.cal-resize.top { top: 0; bottom: auto; border-radius: 8px 8px 0 0; }
.cal-resize.top::after { top: 4px; bottom: auto; }
.cal-resize::after {
  content: ""; position: absolute; left: 50%; bottom: 4px; transform: translateX(-50%);
  width: 32px; height: 3px; border-radius: 2px;
  background: currentColor; opacity: 0; transition: opacity 150ms cubic-bezier(0.05, 0.7, 0.1, 1);
}
.cal-entry:hover .cal-resize::after, .cal-entry:focus-visible .cal-resize::after { opacity: 0.55; }
.cal-entry.resizing { z-index: 5; transition: none; }
.cal-entry.resizing .cal-resize::after { opacity: 0.9; }
@media (prefers-reduced-motion: reduce) {
  .cal-resize::after { transition: none; }
}
/* Milestone 16.2: M3 snackbar for the global timer shortcut. */
.tk-snackbar {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(16px);
  background: var(--md-sys-color-inverse-surface); color: var(--md-sys-color-inverse-on-surface);
  padding: 14px 20px; border-radius: 8px; font-size: 0.875rem;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45);
  opacity: 0; pointer-events: none; z-index: 200;
  transition: opacity 200ms cubic-bezier(0.05, 0.7, 0.1, 1), transform 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
}
.tk-snackbar.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .tk-snackbar { transition: none; }
}
/* Milestone 11: demo onboarding banner. */
.demo-banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; /* #300: two CTAs now */
  margin: 12px 0 16px; padding: 12px 16px; border-radius: 16px;
  background: var(--md-tertiary-container, #3a2f10); color: var(--md-on-tertiary-container, #f2e3b3);
}
.demo-banner svg { flex: none; width: 24px; height: 24px; }
.demo-banner > span { flex: 1 1 16rem; } /* #562: basis floor so CTAs wrap below instead of squeezing the text */
.demo-banner .md-text-button { color: inherit; }
@media (prefers-reduced-motion: no-preference) {
  .demo-banner { animation: banner-in .45s cubic-bezier(0.05, 0.7, 0.1, 1); }
}
@keyframes banner-in { from { opacity: 0; transform: translateY(-8px); } }
/* #402: status rows (settings Updates section) — proper M3
   two-line list-item anatomy: leading icon + headline + supporting text.
   The icon top-aligns with the first line, and wrapped supporting text keeps
   its hanging indent because the text lives in its own flex child. */
.status-row { display: flex; align-items: flex-start; gap: 16px; margin: 0; }
.status-row > svg { flex: none; width: 24px; height: 24px; margin-top: 2px;
  color: var(--md-sys-color-on-surface-variant); }
/* #404: per-status update icons — an available update prompts a primary
   action, so it takes primary; unreachable is an error state. */
.status-row-primary > svg { color: var(--md-sys-color-primary); }
.status-row-error > svg { color: var(--md-sys-color-error, #ffb4ab); }
.status-row-error .status-row-headline { color: var(--md-sys-color-error, #ffb4ab); }
.status-row-text { flex: 1; min-width: 0; }
.status-row-headline { margin: 0; font-size: 1rem; line-height: 24px;
  letter-spacing: 0.5px; color: var(--md-sys-color-on-surface); }
.status-row-supporting { margin: 2px 0 0; font-size: 0.875rem; line-height: 20px;
  letter-spacing: 0.25px; color: var(--md-sys-color-on-surface-variant); }
.status-row-supporting code { font-size: 0.8125rem; }
/* #340: update command shown by the update check — tonal, monospace, wraps. */
.update-cmd { display: block; font-family: ui-monospace, monospace; font-size: 0.8125rem;
  background: var(--md-sys-color-surface-container-highest, #2b2b2b);
  color: var(--md-sys-color-on-surface, #e3e3e3);
  border-radius: 8px; padding: 10px 12px; overflow-wrap: anywhere; }
/* #300: first-run empty states — centered icon, headline, copy, actions. */
.empty-state { text-align: center; padding: 56px 24px; max-width: 440px; margin: 0 auto; }
.empty-state > svg { width: 48px; height: 48px; color: var(--md-sys-color-on-surface-variant); }
.empty-state h3 { margin: 16px 0 8px; }
.empty-state p { margin: 0 0 20px; }
.empty-state .row { display: flex; gap: 8px; align-items: center; justify-content: center; flex-wrap: wrap; }
/* #300: first-run hint inside the calendar supporting pane. */
.cal-first-run { margin-top: 12px; padding: 12px 14px; border-radius: 12px;
  background: var(--md-sys-color-surface-container); }
.cal-first-run p { margin: 8px 0; }
/* #949: below 600px the in-content demo cards hide — the load/remove action
   lives in the drawer instead (demoDrawerRow). Placed after the base rules
   above so the display:none wins at equal specificity. */
@media (max-width: 599px) {
  .demo-banner, .cal-first-run { display: none; }
  .empty-state form[action="/demo/load"] { display: none; }
}

/* #353: content-driven thresholds, not M3 window size classes. Main's extra
   bottom padding keeps the fixed snackbar from covering trailing content on
   narrower viewports. */
@media (max-width: 900px) {
  main { padding-bottom: 120px; }
}

/* ---------------- invoices ---------------- */
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; flex-wrap: wrap; }
.page-head h2 { margin: 0; }
.page-head .row { display: flex; gap: 8px; align-items: center; }
.invoice-detail-head { display: flex; align-items: center; gap: 12px; }
/* #328: "Unbilled time" section head on the invoices page. */
.unbilled-head { margin: 16px 4px 4px; }

.pill { display: inline-block; padding: 2px 12px; border-radius: var(--md-sys-shape-corner-full);
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.4px; vertical-align: middle; }
.pill-draft { background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
.pill-sent { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
.pill-paid { background: var(--md-sys-color-success-container); color: var(--md-sys-color-on-success-container); }

/* #330: "Invoiced"/"Draft" mark on timer entry rows — tonal pill, no border. */
.inv-mark { display: inline-flex; align-items: center; height: 18px; padding: 0 8px;
  border-radius: var(--md-sys-shape-corner-full); font-size: 0.625rem; font-weight: 600;
  letter-spacing: 0.4px; white-space: nowrap; flex: none; }
/* #332: calendar slots carry no chip — the grid is too dense for a pill on
   every card. Invoiced cards dim instead (bright = still needs billing); the
   invoice number is in the hover tooltip. */
.cal-entry.invoiced { opacity: 0.72; }
/* Timer entry rows: tonal on the dark surface. */
.md-list-item-supporting .inv-mark { background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container); margin-right: 6px; }
.md-list-item-supporting .inv-mark.draft { opacity: 0.7; }
/* #500: house M3 plain tooltip for the calendar entry card's invoiced mark
   (hover + :focus-visible + 450ms touch long-press via the site-wide
   [data-tip] handler). Mirrors the inv-mark tooltip (#498); the card uses no
   other pseudo-element. overflow:visible scoped to [data-tip] so the tooltip
   paints outside the card — the desc keeps its own overflow:hidden ellipsis. */
.cal-entry[data-tip] { overflow: visible; }
.cal-entry[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: 50%; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  transform: translateX(-50%) translateY(-2px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small); /* M3 plain tooltip shape */
  padding: 4px 8px; /* M3 plain tooltip padding */
  white-space: nowrap; max-width: 0; overflow: hidden; /* #556: collapse while hidden */
  font-size: 0.75rem; line-height: 16px; font-weight: 400;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
}
.cal-entry[data-tip]:hover::after,
.cal-entry[data-tip]:focus-visible::after,
.cal-entry[data-tip].tip-show::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
  max-width: 200px; overflow: visible; /* #556 */
}
/* #659: the entry-card tooltip centers on the card, so in the first/last day
   columns it runs past the scroll container edge and clips under
   overflow-x: clip. Anchor it to the column edge instead — mirrors the
   day-header tooltip rules above. */
.cal-day:first-child .cal-entry[data-tip]::after { left: 0; transform: translateY(-2px); }
.cal-day:last-child .cal-entry[data-tip]::after { left: auto; right: 0; transform: translateY(-2px); }
.cal-day:first-child .cal-entry[data-tip]:hover::after,
.cal-day:first-child .cal-entry[data-tip]:focus-visible::after,
.cal-day:first-child .cal-entry[data-tip].tip-show::after,
.cal-day:last-child .cal-entry[data-tip]:hover::after,
.cal-day:last-child .cal-entry[data-tip]:focus-visible::after,
.cal-day:last-child .cal-entry[data-tip].tip-show::after { transform: translateY(0); }
/* #498: M3 plain tooltip for the inv-mark chip (hover + 450ms touch long-press
   via the site-wide [data-tip] handler). ::after; the chip uses no other
   pseudo-element, and the supporting line clips nothing. */
.inv-mark[data-tip] { position: relative; overflow: visible; }
.inv-mark[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: 50%; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  transform: translateX(-50%) translateY(-2px);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small);
  padding: 4px 8px; /* M3 plain tooltip padding */
  white-space: nowrap; max-width: 0; overflow: hidden; /* #556: collapse while hidden */
  font-size: 0.75rem; line-height: 16px; font-weight: 400;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
}
.inv-mark[data-tip]:hover::after,
.inv-mark[data-tip].tip-show::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
  max-width: 200px; overflow: visible; /* #556 */
}

.two-field { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
/* #353: content-driven — form field stacking, not an M3 window size class. */
@media (max-width: 560px) { .two-field { grid-template-columns: 1fr; } }

.table-wrap { overflow-x: auto; margin: 0 -4px; padding: 0 4px; }
.md-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.md-table th, .md-table td { text-align: left; padding: 10px 8px; }
.md-table th.num, .md-table td.num { text-align: right; white-space: nowrap; }
.md-table thead th { color: var(--md-sys-color-on-surface-variant); font-weight: 500; font-size: 0.75rem;
  letter-spacing: 0.4px; text-transform: uppercase; }
.md-table tbody tr { background: var(--md-sys-color-surface-container-low); }
.md-table tbody tr + tr { border-top: 8px solid transparent; background-clip: padding-box; }
.md-table tbody td:first-child { border-radius: 12px 0 0 12px; }
.md-table tbody td:last-child { border-radius: 0 12px 12px 0; }
.md-table tfoot td { padding: 6px 8px; color: var(--md-sys-color-on-surface-variant); }
.md-table tfoot tr.grand td { color: var(--md-sys-color-on-surface); font-weight: 700; font-size: 1rem;
  padding-top: 10px; }
/* #366: the 5-column invoice table must fit a 390px phone with the Amount
   column visible — no hiding it behind a scroll. Compact the table chrome
   below 480px; the .table-wrap inner scroll stays as the fallback for
   unusually wide data. Print is untouched. */
@media (max-width: 480px) {
  .md-table { font-size: 0.8125rem; }
  .md-table th, .md-table td { padding: 8px 6px; }
  .md-table thead th { font-size: 0.6875rem; letter-spacing: 0; }
  .md-table tfoot td { padding: 6px; }
  .md-table td.desc { overflow-wrap: anywhere; }
  /* the DESCRIPTION header is one long word — let it break too, otherwise
     its min-content alone keeps the column (and the table) too wide. */
  .md-table th:nth-child(2) { overflow-wrap: anywhere; }
}
/* #366, continued: on phones the invoice document's line items stack instead
   of squeezing into five columns — description on its own line, then a muted
   meta line (date · hours × rate) with the amount right-aligned. The thead is
   visually hidden (kept for assistive tech); tfoot becomes label/amount rows.
   Print and desktop keep the full table. */
@media (max-width: 480px) {
  .invoice-sheet .md-table thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  .invoice-sheet .md-table,
  .invoice-sheet .md-table tbody,
  .invoice-sheet .md-table tfoot { display: block; }
  /* #656: the Issued/Due/Billing-period line clips off the right edge on
     phones — stack it instead. Separators are hidden in the stack; frozen
     pre-#656 snapshots (literal middots, no sep spans) degrade to the same
     stack with the middots reading as list markers. */
  .invoice-sheet .inv-dates-line {
    display: flex; flex-direction: column; gap: 3px; margin-bottom: 20px;
  }
  .invoice-sheet .inv-dates-line .inv-date { white-space: normal; }
  .invoice-sheet .inv-dates-line .inv-date-sep { display: none; }
  .invoice-sheet .md-table tbody tr,
  .invoice-sheet.template-modern .md-table tbody tr {
    display: flex; flex-wrap: wrap; align-items: baseline;
    background: var(--md-sys-color-surface-container-low);
    border-radius: 12px; padding: 16px; margin: 0 0 12px;
  }
  .invoice-sheet .md-table tbody tr + tr { border-top: 0; }
  .invoice-sheet .md-table tbody td { padding: 0; }
  .invoice-sheet .md-table tbody td:first-child,
  .invoice-sheet .md-table tbody td:last-child { border-radius: 0; }
  .invoice-sheet .md-table td.desc { flex: 1 1 100%; order: -1; font-size: 0.9375rem; font-weight: 600; margin-bottom: 8px; }
  .invoice-sheet .md-table td.inv-date:empty { display: none; }
  /* #656: the date range gets its own row instead of crowding into the
     hours × rate meta line. */
  .invoice-sheet .md-table td.inv-date {
    flex: 1 1 100%; display: block; font-size: 0.8125rem;
    color: var(--md-sys-color-on-surface-variant); margin-bottom: 6px;
  }
  .invoice-sheet .md-table td.inv-date::after { content: none; }
  .invoice-sheet .md-table td.inv-hours,
  .invoice-sheet .md-table td.inv-rate {
    display: inline; font-size: 0.8125rem; color: var(--md-sys-color-on-surface-variant);
  }
  .invoice-sheet .md-table td.inv-hours::after { content: " hrs\00a0×\00a0"; }
  /* #679: the singular hour reads "1 hr × …", not "1 hrs × …". */
  .invoice-sheet .md-table td.inv-hours.inv-hours-1::after { content: " hr\00a0×\00a0"; }
  .invoice-sheet .md-table td.inv-amount {
    margin-left: auto; font-size: 0.9375rem; font-weight: 700; white-space: nowrap;
  }
  .invoice-sheet .md-table tfoot tr { display: flex; justify-content: space-between; }
  .invoice-sheet .md-table tfoot td { padding: 4px 0; }
  .invoice-sheet .md-table tfoot tr.grand td { padding-top: 8px; }
  /* the cream total bar: paint it on the row, not the cells, so the flex
     space-between doesn't split it into two chips. */
  .invoice-sheet.template-modern .md-table tfoot tr.grand {
    background: #fdf3dd; border-radius: 8px; padding: 6px 12px; margin-top: 4px;
  }
  .invoice-sheet.template-modern .md-table tfoot tr.grand td { background: none; border-radius: 0; }
}
.cell-input { width: 100%; background: var(--md-sys-color-surface-container-highest); color: inherit;
  border: none; border-radius: 8px; padding: 8px 10px; font: inherit; }
.cell-input.num { text-align: right; max-width: 110px; }
.cell-input:focus { outline: 2px solid var(--md-sys-color-primary); }

/* ---------------- form cards: capped like the invoice sheet ---------------- */
.md-card.form-card { max-width: 720px; }

/* ---------------- focused task pages: centered narrow body column ---------------- */
.page-narrow { max-width: 720px; margin-inline: auto; }
.page-narrow .md-card.form-card { max-width: none; }

/* ---------------- form info callout (tonal container + leading icon) ---------------- */
.form-info { display: flex; gap: 12px; align-items: flex-start;
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface-variant);
  border-radius: 12px; padding: 12px 16px; margin: 0 0 16px;
  font-size: 0.8125rem; line-height: 1.5; }
.form-info svg { flex: none; width: 20px; height: 20px; margin-top: 1px; color: var(--md-sys-color-primary); }
.form-info p { margin: 0; }
.form-info a { color: var(--md-sys-color-primary); }

/* #969: the preview sheet centers in its pane. */
.invoice-sheet { max-width: 720px; margin: 0 auto; }

/* Public client invoice page: centered sheet, no app chrome. */
.page-public-invoice { max-width: 800px; margin: 0 auto; padding: 32px 24px 48px; }
.page-public-invoice .invoice-sheet { max-width: none; }
/* #303: public changelog shares the minimal public chrome. */
.page-changelog { max-width: 720px; margin: 0 auto; padding: 32px 24px 48px; }
.changelog-title { display: flex; align-items: center; gap: 12px; margin-bottom: 8px;
  color: var(--md-sys-color-primary); } /* branded pumpkin */
.changelog-release { margin-top: 24px; }
.changelog-release h2 { margin: 0 0 8px; color: var(--md-sys-color-primary); }
.changelog-release ul { margin: 0; padding-left: 20px; display: grid; gap: 8px; }
/* #867: inline code spans in release notes — tonal, monospace, M3 4px radius. */
/* #873: nowrap — a hyphen-minus is a legal break point, so a --var name
   could wrap mid-identifier (e.g. "--" then "primary,"). */
.page-changelog code { font-family: ui-monospace, monospace; font-size: 0.875em;
  background: var(--md-sys-color-surface-container-highest, #2b2b2b);
  color: var(--md-sys-color-on-surface, #e3e3e3);
  border-radius: 4px; padding: 1px 6px; white-space: nowrap; }
.public-foot { text-align: center; margin-top: 28px; }
.public-pay { text-align: center; margin: 28px 0 8px; }
.public-pay-btn { font-size: 1.0625rem; padding: 14px 32px; border-radius: 999px; }
/* #731: hosted landing page — hero, feature grid, pricing. Minimal public
   chrome (no app nav), M3 dark, pumpkin reserved for the primary CTA. */
.page-landing { max-width: 1080px; margin: 0 auto;
  padding: clamp(24px, 7vw, 40px) var(--layout-body-margin) clamp(40px, 9vw, 56px); }

.landing-hero { display: grid; gap: 28px; align-items: center; }
@media (min-width: 840px) {
  .landing-hero { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: 56px; }
}
.landing-hero-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }
.landing-brand { display: inline-flex; align-items: center; gap: 10px; margin: 0;
  color: var(--md-sys-color-primary); font-size: 1.125rem; font-weight: 600; letter-spacing: 0.2px; }
.landing-eyebrow { display: inline-flex; align-items: center; gap: 8px; margin: 0;
  padding: 6px 14px 6px 10px; border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 0.8125rem; font-weight: 500; letter-spacing: 0.3px; }
.landing-eyebrow-dot { width: 8px; height: 8px; border-radius: 50%;
  background: var(--md-sys-color-primary); flex: none; }
.landing-headline { margin: 0; font-size: clamp(2.25rem, 6vw, 3.5rem); line-height: 1.06;
  font-weight: 600; letter-spacing: -0.02em; color: var(--md-sys-color-on-surface); }
.landing-sub { margin: 0; max-width: 42ch; font-size: 1.0625rem; line-height: 1.6;
  color: var(--md-sys-color-on-surface-variant); }
.landing-cta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.landing-cta-btn { font-size: 1rem; min-height: 48px; padding: 0 32px; }
.landing-price { margin: 0; font-size: 0.9375rem; }
.landing-price strong { color: var(--md-sys-color-on-surface); font-weight: 600; }
.landing-selfhost { margin: 0; font-size: 0.8125rem; }
.landing-selfhost a { color: var(--md-sys-color-primary); }

.landing-hero-visual { position: relative; display: flex; justify-content: center; }
.landing-hero-visual::before { content: ""; position: absolute; inset: 8% 12%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(242, 193, 78, 0.22), rgba(242, 193, 78, 0) 72%);
  pointer-events: none; }
.landing-hero-visual .landing-mascot { position: relative; width: min(300px, 72vw); height: auto; margin: 0; }
@media (min-width: 840px) {
  .landing-hero-visual .landing-mascot { width: min(400px, 80vw); }
}

.landing-section { margin-top: clamp(40px, 8vw, 80px); scroll-margin-top: 16px; }
.landing-section-title { margin: 0; font-size: clamp(1.5rem, 3.4vw, 2rem); line-height: 1.2;
  font-weight: 600; letter-spacing: -0.01em; }
.landing-section-sub { margin: 8px 0 0; max-width: 56ch; }

.landing-feature-grid { display: grid; gap: 12px; margin-top: 24px; }
@media (min-width: 600px) {
  .landing-feature-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 28px; }
}
@media (min-width: 1200px) { .landing-feature-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.landing-feature { display: flex; flex-direction: column; gap: 8px; padding: 18px;
  border-radius: var(--md-sys-shape-corner-large);
  background: var(--md-sys-color-surface-container-low); }
@media (min-width: 600px) { .landing-feature { padding: 20px; } }
.landing-feature-icon { display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface-variant); }
.landing-feature-icon svg { width: 24px; height: 24px; }
.landing-feature-title { margin: 4px 0 0; font-size: 1rem; line-height: 24px; font-weight: 500;
  letter-spacing: 0.15px; color: var(--md-sys-color-on-surface); }
.landing-feature p { margin: 0; }

.landing-plan-grid { display: grid; gap: 12px; margin-top: 24px; }
@media (min-width: 840px) {
  .landing-plan-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 28px; }
}
.landing-plan { display: flex; flex-direction: column; gap: 12px; padding: 24px 20px;
  border-radius: var(--md-sys-shape-corner-extra-large);
  background: var(--md-sys-color-surface-container-low); }
@media (min-width: 840px) { .landing-plan { padding: 28px; } }
.landing-plan-featured { background:
  linear-gradient(180deg, rgba(242, 193, 78, 0.12), rgba(242, 193, 78, 0.02)),
  var(--md-sys-color-surface-container-high); }
.landing-plan-label { margin: 0; font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.4px;
  text-transform: uppercase; color: var(--md-sys-color-on-surface-variant); }
.landing-plan-featured .landing-plan-label { color: var(--md-sys-color-primary); }
.landing-plan-price { display: flex; align-items: baseline; gap: 2px; margin: 0; }
.landing-plan-amount { font-size: 2.5rem; font-weight: 700; letter-spacing: -0.02em; }
.landing-plan-featured .landing-plan-amount { color: var(--md-sys-color-primary); }
.landing-plan-copy { margin: 0; color: var(--md-sys-color-on-surface-variant); }
.landing-plan-list { list-style: none; margin: 4px 0 0; padding: 0; display: grid; gap: 10px; }
.landing-plan-list li { display: flex; align-items: flex-start; gap: 10px;
  font-size: 0.875rem; line-height: 20px; }
.landing-plan-list svg { width: 18px; height: 18px; flex: none; margin-top: 1px;
  color: var(--md-sys-color-on-surface-variant); }
.landing-plan .md-filled-button, .landing-plan .md-text-button { align-self: stretch; margin-top: 4px; }
@media (min-width: 840px) {
  .landing-plan .md-filled-button, .landing-plan .md-text-button { align-self: flex-start; }
}

.landing-foot { margin-top: clamp(40px, 8vw, 80px); text-align: center; }
.landing-foot-links { margin: 0; line-height: 1.9; }
.landing-support { margin: 8px 0 0; }
.landing-foot a { color: var(--md-sys-color-on-surface-variant); }
.landing-foot a:hover { color: var(--md-sys-color-on-surface); }

/* Reusable M3 Expressive determinate progress bar, ported from
   homeschool-planner's dashboard (#85). The pumpkin fill is wave-masked and
   ripples while under the target; once the target is met it settles to a flat
   fill with a one-pass sheen. The grow-in sweep (#44) is kept, `no-enter`
   still suppresses it, and the ripple/sheen only run with motion allowed.
   Instances: the calendar's 40h week bar and the timer page's 8h today bar. */
.m3-progress { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* Instance spacing: the calendar's 40h week bar and the timer page's 8h today bar. */
.week-target { margin: 2px 0 14px; }
.today-target { margin: 0 0 12px; }
.m3-progress-track { flex: 1; min-width: 120px; height: 14px; display: flex; align-items: center; gap: 4px; }
.m3-progress-fill-wrap { height: 100%; overflow: hidden; border-radius: 999px; transition: width 300ms;
  animation: m3-progress-fill var(--motion-duration-medium4) var(--motion-easing-emphasized-decelerate) var(--motion-duration-short2) backwards; }
.m3-progress-fill-wave { height: 100%; width: 100%; background: var(--primary);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 14'%3E%3Cpath d='M0 7 Q10 2 20 7 T40 7' fill='none' stroke='white' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E") 0 50% / 40px 100% repeat-x;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 14'%3E%3Cpath d='M0 7 Q10 2 20 7 T40 7' fill='none' stroke='white' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E") 0 50% / 40px 100% repeat-x; }
.m3-progress-remainder { height: 4px; flex: 1; min-width: 0; border-radius: 999px;
  background: var(--md-sys-color-surface-container-highest); }
.m3-progress-dot { height: 4px; width: 4px; flex: none; border-radius: 999px; background: var(--primary); }
@media (prefers-reduced-motion: no-preference) {
  .m3-progress-fill-wave { animation: m3-progress-wave-slide 1.6s linear infinite; }
}
@keyframes m3-progress-wave-slide {
  from { -webkit-mask-position: 0 50%; mask-position: 0 50%; }
  to { -webkit-mask-position: 40px 50%; mask-position: 40px 50%; }
}
.m3-progress-track.complete { height: 4px; display: block; overflow: hidden; border-radius: 999px;
  background: var(--md-sys-color-surface-container-highest); }
.m3-progress-track.complete .m3-progress-fill { height: 100%; border-radius: inherit; background: var(--primary);
  position: relative; overflow: hidden;
  animation: m3-progress-fill var(--motion-duration-medium4) var(--motion-easing-emphasized-decelerate) var(--motion-duration-short2) backwards; }
.m3-progress-track.complete .m3-progress-fill::after { content: ""; position: absolute; inset: 0; width: 28%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent); opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .m3-progress-track.complete .m3-progress-fill::after { animation: m3-progress-sheen 900ms ease-out 120ms 1; }
}
@keyframes m3-progress-sheen {
  0% { opacity: 0; transform: translateX(-140%); }
  25% { opacity: 1; }
  100% { opacity: 0; transform: translateX(380%); }
}
@keyframes m3-progress-fill { from { width: 0; } }
.m3-progress-label { white-space: nowrap; }
/* Collection progress bar under each revenue "By client" row (#89). */
.rev-collection { margin-top: 8px; }
/* The today bar lives inside #today-list, which HTMX re-renders on every
   timer start/stop: suppress the grow-in sweep there so the bar doesn't
   re-sweep on each swap. The wave ripple and completion sheen still run. */
#today-list .m3-progress-fill-wrap, #today-list .m3-progress-fill { animation: none; }

/* Per-client week breakdown strip on the calendar. */
.week-clients { display: flex; flex-wrap: wrap; gap: 8px 20px; margin: 0 0 14px; }
.week-client { display: inline-flex; align-items: center; gap: 8px; }
.week-client-dot { width: 10px; height: 10px; border-radius: 999px; flex: none;
  background: var(--md-sys-color-surface-container-highest); }

/* Uninvoiced hours nudge chip on the calendar. */
.nudge-chip { display: inline-flex; align-items: center; gap: 8px;
  align-self: flex-start; /* #week is a flex column; don't stretch full width */
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  border-radius: 999px; padding: 8px 16px; margin: 0 0 14px;
  text-decoration: none;
  /* #715: on narrow screens the chip may wrap — let the wrap land between the
     two segments, never inside "Invoice this week". */
  flex-wrap: wrap; }
.nudge-chip:hover { background: var(--md-sys-color-surface-container-highest); }
/* #715: keep the receipt icon glued to its label — the icon and "Invoice this
   week" never split across lines. */
.nudge-chip .nudge-go { display: inline-flex; align-items: center; gap: 4px;
  white-space: nowrap; color: var(--md-sys-color-primary); }
/* Nudge-chip entrance (#52): scale/fade from the chip's top-left anchor
   corner, landing just after the week-nav entrance above it. */
.nudge-chip {
  transform-origin: 0 0;
  animation: nudge-in var(--motion-duration-medium2) var(--motion-easing-emphasized-decelerate) var(--motion-duration-short2) backwards;
}
@keyframes nudge-in { from { opacity: 0; transform: scale(0.8); } }
.inv-notes { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); white-space: pre-wrap; }
/* #243: plain-language payment terms, part of the invoice document. */
.inv-terms { margin-top: 16px; }
/* #302: internal-only notes — app surface only, never printed or shared. */
.internal-notes { margin: 20px 0 0; padding: 14px 16px; border-radius: 12px;
  background: var(--md-sys-color-surface-container); }
.internal-notes-label { font-size: 0.75rem; letter-spacing: .4px; text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant); margin-bottom: 6px; }
.internal-notes p { margin: 0; white-space: pre-wrap; }
/* #956: proof-of-payment section — app surface only, tonal like internal notes. */
.payment-proof { margin: 20px 0 0; padding: 14px 16px; border-radius: 12px;
  background: var(--md-sys-color-surface-container); }
.payment-proof .internal-notes-label svg { vertical-align: -4px; margin-right: 4px; }
.proof-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.proof-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proof-meta { font-size: 0.85rem; white-space: nowrap; }
.proof-icons { display: flex; align-items: center; gap: 4px; margin-left: auto; }
.proof-icons .inline { display: inline; }
.proof-nudge { margin: 4px 0 10px; }
.proof-upload { display: flex; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.proof-file-label { font-size: 0.9rem; color: var(--md-sys-color-on-surface-variant);
  padding: 8px 16px; border-radius: 20px; background: var(--md-sys-color-surface-container-high);
  cursor: pointer; white-space: nowrap; }
.proof-upload input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; }
.proof-hint { margin: 8px 0 0; font-size: 0.8rem; }

/* ---------------- Modern invoice template (#233) ---------------- */
.template-modern { overflow: hidden; }
.inv-modern-band { background: #1c1c1f; color: #fff; margin: -20px -20px 20px; padding: 28px 20px 22px;
  display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
/* #612: 20px side padding matches the sheet padding, so the title left-aligns
   with the body content below. */
.inv-modern-title { font-size: 1.875rem; line-height: 34px; font-weight: 700; letter-spacing: -0.5px; }
.inv-modern-title::after { content: ""; display: block; width: 56px; height: 4px; border-radius: 2px;
  background: var(--primary); margin-top: 10px; }
.inv-modern-right { text-align: right; }
.inv-modern-number { font-size: 1.0625rem; font-weight: 700; }
.inv-modern-right .inv-modern-badge { margin-top: 12px; }
/* #612: issued/billing dates sit below the band, above the table. */
.inv-dates-line { font-size: 0.8125rem; margin: 0 0 16px; }
/* #656: the middot between date phrases is its own span — the margin
   replaces the surrounding space it used to have, so desktop rendering
   is unchanged. */
.inv-dates-line .inv-date-sep { margin: 0 0.25em; }
/* #612: Bill to left, From right. */
.inv-from { margin-left: auto; text-align: right; }
.inv-modern-badge { display: inline-block; background: var(--primary); color: var(--on-primary); font-size: 0.6875rem; font-weight: 700;
  letter-spacing: 1px; text-transform: uppercase; padding: 6px 14px; border-radius: 4px; }
.inv-modern-badge.paid { background: #2e9e4f; color: #fff; }
/* #831: draft badge mirrors .pill-draft — a draft was never sent, so it gets
   the neutral secondary-container treatment, not the amber unpaid attention color. */
.inv-modern-badge.draft { background: var(--md-sys-color-secondary-container); color: var(--md-sys-color-on-secondary-container); }
.inv-modern-parties { display: flex; gap: 40px; margin: 0 0 20px; }
.inv-modern-label { font-size: 0.6875rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant); margin-bottom: 6px; }
.template-modern .md-table thead th { background: #1c1c1f; color: #fff; font-weight: 700; font-size: 0.6875rem;
  letter-spacing: 1.5px; text-transform: uppercase; }
.template-modern .md-table thead th:first-child { border-radius: 8px 0 0 8px; }
.template-modern .md-table thead th:last-child { border-radius: 0 8px 8px 0; }
.template-modern .md-table tbody tr { background: transparent; }
.template-modern .md-table tbody tr:nth-child(even) { background: rgba(127,127,127,.12); }
.template-modern .md-table tbody tr + tr { border-top: none; }
.template-modern .md-table tbody td:first-child { border-radius: 0; }
.template-modern .md-table tbody td:last-child { border-radius: 0; }
.template-modern .md-table tfoot tr.grand td { background: #fdf3dd; color: #1c1c1f; font-size: 1.125rem; }
.template-modern .md-table tfoot tr.grand td:first-child { border-radius: 8px 0 0 8px; }
.template-modern .md-table tfoot tr.grand td:last-child { border-radius: 0 8px 8px 0; }

/* ---------------- Share & collect (invoice detail, #229) ---------------- */
.share-lines { margin: 28px 0 8px; }
.share-line { display: flex; align-items: center; gap: 12px; padding: 7px 0; min-width: 0; }
.share-label { font-weight: 500; white-space: nowrap; }
.share-label.warn { color: var(--md-sys-color-error); }
.share-url { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--md-sys-color-on-surface-variant); font-size: 0.8125rem; }
/* #502: the client link truncates with ellipsis — its full URL lives in the
   house M3 plain tooltip (hover / focus / 450ms touch long-press via the
   site-wide [data-tip] handler) instead of a native title. The wrapper anchors
   the tooltip because the ellipsis span's overflow:hidden would clip it; the
   tooltip wraps the long URL across lines instead of clipping it. */
.share-url-wrap { flex: 1; min-width: 0; position: relative; }
.share-url-wrap > .share-url { display: block; flex: none; }
.share-url-wrap[data-tip]::after {
  content: attr(data-tip);
  position: absolute; left: 0; top: calc(100% + 4px); /* M3: 4dp anchor gap */
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  border-radius: var(--md-sys-shape-corner-extra-small); /* M3 plain tooltip shape */
  padding: 4px 8px; /* M3 plain tooltip padding */
  font-size: 0.75rem; line-height: 16px;
  max-width: 0; overflow: hidden; /* #556: collapse while hidden */
  max-height: 0; /* #650: the URL wraps at zero width into dozens of lines —
     without this the hidden box is ~960px tall and stretches the detail
     pane's scrollable overflow far past the content */
  white-space: normal; overflow-wrap: anywhere;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity var(--motion-duration-short2) var(--motion-easing-standard),
    transform var(--motion-duration-short2) var(--motion-easing-standard);
  transform: translateY(-2px);
}
.share-url-wrap[data-tip]:hover::after,
.share-url-wrap[data-tip]:focus-visible::after,
.share-url-wrap[data-tip].tip-show::after {
  opacity: 1; transform: translateY(0);
  max-width: 320px; max-height: none; overflow: visible; /* #556, #650 */
}
.share-icons { display: flex; align-items: center; gap: 2px; margin-left: auto; flex-shrink: 0; }
.share-icons form { display: inline; margin: 0; }
.share-icons .md-icon-button { width: 36px; height: 36px; }
.share-icons .md-icon-button.copied { color: var(--md-sys-color-primary); }
.share-icons .md-icon-button.danger { color: var(--md-sys-color-error); }
.stripe-paid-chip { display: inline-block; margin-left: 8px; background: rgba(99,91,255,.18); color: #b3aeff;
  padding: 4px 12px; border-radius: 999px; font-size: 0.75rem; font-weight: 500; vertical-align: middle; }
/* #448: the bolt is an inline SVG from the icon system — size it to the chip's text. */
.stripe-paid-chip svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: 4px; }


/* ---------------- print ---------------- */
@media print {
  .md-top-app-bar, .md-nav-rail, .md-bottom-nav, .nav-drawer-scrim, .no-print, .page-head { display: none !important; }
  .invoice-list-pane { display: none !important; } /* #364: print only the invoice document */
  .invoice-list-detail { display: block; }
  .invoice-detail-pane { overflow: visible; }
  body { background: #fff; color: #111; }
  body.app { background: #fff; }
  /* #387: flatten the app sheet for print — no radius, no gutters. */
  body.app .app-chrome { display: block; padding: 0; gap: 0; }
  .app-sheet { background: #fff; border-radius: 0; overflow: visible; }
  main { max-width: none; padding: 0; }
  .md-card.invoice-sheet { box-shadow: none; background: #fff; color: #111; max-width: none; }
  .invoice-sheet .muted { color: #555; }
  .md-table tbody tr { background: #f5f5f5; }
  .md-table tbody tr + tr { border-top-color: #fff; }
  .md-table th { color: #555; }
  .md-table tfoot td { color: #333; }
  .md-table tfoot tr.grand td { color: #000; }
  .pill { border: 1px solid #999; background: #fff !important; color: #111 !important; }
  .inv-notes { color: #333; }
}

/* ---------------- revenue (milestone 1) ---------------- */
.rev-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
/* feed-style adaptive grid inside the supporting pane: stack when the pane is narrow */
@container (max-width: 480px) { .rev-cards { grid-template-columns: 1fr; } }
.rev-card { padding: 16px 20px; }
.rev-card .md-title-large { margin-top: 4px; }
.rev-paid { color: var(--md-sys-color-primary); }
.rev-chart-card { padding: 16px 20px 12px; margin-top: 16px; }
.rev-chart-title { margin-bottom: 8px; }
.rev-chart { width: 100%; height: auto; display: block; }
.rev-chart .bar-out { fill: var(--md-sys-color-surface-container-highest); }
.rev-chart .bar-paid { fill: var(--md-sys-color-primary); }
/* Bar grow-in (#47): each segment scales from its own base so the combined
   bar grows uniformly from the baseline; stagger via inline animation-delay. */
.rev-chart .bar-out, .rev-chart .bar-paid {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: bar-grow var(--motion-duration-medium2) var(--motion-easing-emphasized-decelerate) backwards;
}
@keyframes bar-grow { from { transform: scaleY(0); } }
.rev-chart text { fill: var(--md-sys-color-on-surface-variant); font-size: 0.625rem; }
/* #899: time-breakdown chart on the zinvoices sheet — same bar treatment
   as the revenue chart, single-series (no animation: it prints). */
.inv-breakdown { margin-top: 20px; }
.inv-breakdown-sub { margin: 2px 0 8px; }
.tb-chart { width: 100%; height: auto; display: block; }
.tb-chart .tb-bar { fill: var(--md-sys-color-primary); }
.tb-chart text { fill: var(--md-sys-color-on-surface-variant); font-size: 0.625rem; }
.rev-legend { display: flex; align-items: center; gap: 8px; margin-top: 8px;
  color: var(--md-sys-color-on-surface-variant); font-size: 0.75rem; }
.rev-swatch { width: 12px; height: 12px; border-radius: 4px; display: inline-block; }
.rev-swatch.paid { background: var(--md-sys-color-primary); }
.rev-swatch.out { background: var(--md-sys-color-surface-container-highest); margin-left: 12px; }
.rev-clients-card { padding: 16px 20px 8px; margin-top: 16px; }
.rev-clients-card .md-list { margin-bottom: 8px; }
.rev-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }

/* Error pages (404/500): centered, tonal, no hard borders. */
.error-page { max-width: 560px; margin: 0 auto; padding: 96px 24px; text-align: center; }
.error-page h1 { margin: 0 0 12px; }
.error-page p { margin: 0 0 28px; }
.error-mark { margin-bottom: 20px; }
.error-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }

/* Reduced motion: collapse all motion to instant state changes.
   Duration tokens are zeroed so token-based transitions inherit this;
   the universal rule catches anything else. 0.01ms (not 0) keeps
   transitionend events firing for JS-driven sequences. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-duration-short1: 0ms;
    --motion-duration-short2: 0ms;
    --motion-duration-short3: 0ms;
    --motion-duration-short4: 0ms;
    --motion-duration-medium1: 0ms;
    --motion-duration-medium2: 0ms;
    --motion-duration-medium3: 0ms;
    --motion-duration-medium4: 0ms;
    --motion-duration-long1: 0ms;
    --motion-duration-long2: 0ms;
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------- skeleton loaders ----------------
 * Shimmer placeholders shown while HTMX partials load. Tonal shapes, no
 * hard borders; the shimmer sweep is killed by the global
 * prefers-reduced-motion guard (animation-duration override). */
.skel { position: relative; overflow: hidden; background: var(--md-sys-color-surface-container-highest); }
.skel::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 25%, rgba(255, 255, 255, 0.07) 50%, transparent 75%);
  animation: skel-shimmer 1600ms linear infinite;
}
@keyframes skel-shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.skel-stack { display: flex; flex-direction: column; gap: 12px; }
.skel-row { height: 64px; border-radius: var(--md-sys-shape-corner-medium); }

/* -------- calendar skeleton (#322): mirrors .cal-pane-layout so the htmx
   week swap has no layout flash. Same grid areas and 840px breakpoint as the
   real grid; lanes echo the 80px hour rhythm of .cal-day-col. The aside width
   tracks the real pane through --layout-supporting-pane. */
/* (dead pre-carousel skeleton rules removed, #576; bare .cal-skel grid removed, #685) */
.cal-skel-grid { grid-area: grid; display: grid; min-width: 0; min-height: 0;
  grid-template-columns: 44px repeat(7, minmax(0, 1fr)); gap: 8px; }
.cal-skel-day { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; }
.cal-skel-head { height: 64px; flex: none; border-radius: var(--md-sys-shape-corner-medium); }
.cal-skel-lane { flex: 1; min-height: 120px; border-radius: var(--md-sys-shape-corner-medium);
  background-color: var(--md-sys-color-surface-container-low);
  background-image: repeating-linear-gradient(to bottom,
    transparent 0, transparent 79px,
    color-mix(in srgb, var(--md-sys-color-outline-variant) 30%, transparent) 79px,
    color-mix(in srgb, var(--md-sys-color-outline-variant) 30%, transparent) 80px); }
/* #572: week carousel — three pre-rendered week strips in an overflow-hidden
   viewport; chevron pans translate the track one viewport (±100%) on a pure
   transform. Each strip is a container: wide strips show the 24h grid, narrow
   ones the week agenda. */
.cal-carousel { grid-area: grid; overflow: hidden; min-width: 0; min-height: 0;
  border-radius: var(--md-sys-shape-corner-large); }
.cal-track { display: flex; height: 100%; width: 100%; transform: translateX(-100%);
  transition: transform 260ms cubic-bezier(.2,0,0,1); }
.cal-track.no-anim { transition: none; }
.cal-strip { flex: 0 0 100%; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; container-type: inline-size; }
.cal-strip > .cal-scroll { flex: 1; min-height: 0; min-width: 0; }
/* Recycled strips repopulate in the background — no entrance replay. */
.cal-strip.no-enter [data-enter], .cal-strip.no-enter .cal-grid .cal-day { animation: none !important; }
@media (prefers-reduced-motion: reduce) {
  .cal-track { transition: none; }
}
/* Narrow strips: the 24h grid gives way to the week agenda. #580: the
   breakpoint is on the strip, which the rail + margins + summary pane
   narrow well below the viewport — 799px of container was ~1300px of
   viewport, so ordinary laptops got the fallback. 639px keeps the grid
   (lanes >= ~85px) on laptop widths; phones still get the agenda. */
.cal-agenda { display: none; }
@container (max-width: 639px) {
  .cal-strip > .cal-scroll { display: none; }
  .cal-agenda { display: flex; flex-direction: column; gap: 8px; flex: 1; min-height: 0;
    overflow-y: auto; padding: 2px 2px 16px; }
}
/* -------- week agenda (narrow calendar strips, #572) -------- */
.cal-ag-day { background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-medium); padding: 10px 12px; }
.cal-ag-day.today { background: color-mix(in srgb, var(--md-sys-color-primary) 10%, var(--md-sys-color-surface-container-low)); }
.cal-ag-head { display: flex; align-items: center; gap: 10px; }
.cal-ag-num { font-size: 1.375rem; line-height: 28px; }
.cal-ag-num.today { color: var(--md-sys-color-primary); font-weight: 700; }
.cal-ag-row { display: flex; align-items: baseline; gap: 10px; padding: 8px 10px;
  margin-top: 6px; border-radius: var(--md-sys-shape-corner-small);
  background: color-mix(in srgb, var(--c, var(--md-sys-color-primary)) 24%, transparent);
  cursor: pointer; }
.cal-ag-row.invoiced { opacity: .55; } /* match the grid's frozen-record dim */
.cal-ag-time { white-space: nowrap; flex: none; }
.cal-ag-desc { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-ag-dur { flex: none; }
.cal-ag-empty { margin: 6px 0 2px; }
/* #781: the week-summary hint teaches the grid's drag-to-create gesture, but
   on the narrow agenda the gesture is the per-day + button. Both variants
   render; the client toggles .cal-agenda-hint on <body> by strip width. A
   container query can't do this — the aside isn't a strip descendant — so the
   JS mirrors the 639px breakpoint above exactly. */
.cal-hint-agenda { display: none; }
.cal-agenda-hint .cal-hint-grid { display: none; }
.cal-agenda-hint .cal-hint-agenda { display: inline; }

/* M3 press ripple (JS-driven, see motion.js). Buttons already clip with
   overflow: hidden; the span paints above the ::after state layer. */
.md-ripple {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
}
.md-ripple.go {
  transition: transform var(--motion-duration-medium2) var(--motion-easing-emphasized),
    opacity var(--motion-duration-short4) var(--motion-easing-standard);
  transform: translate(-50%, -50%) scale(1);
  opacity: 0.14;
}
.md-ripple.go.fade {
  transition: opacity var(--motion-duration-short3) var(--motion-easing-standard);
  opacity: 0;
}

/* ---------------- needs-attention list (milestone 7) ---------------- */
.rev-attention { margin-bottom: 16px; }
.attn-ok { margin: 4px 0 0; }
.attn-link {
  display: flex; align-items: center; gap: 8px;
  flex: 1; min-width: 0; color: inherit; text-decoration: none;
}
.attn-chip {
  display: inline-block; flex: none; white-space: nowrap;
  background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
  padding: 4px 12px; border-radius: 999px;
  font-size: 0.75rem; font-weight: 500;
}

/* #753: Zimer mascot placements — subtle, never competing with CTAs. */
.landing-mascot { display: block; width: min(280px, 64vw); height: auto; margin: 0 auto 4px; }
.empty-mascot { width: 132px; height: auto; }
.demo-banner .demo-mascot { flex: none; width: 44px; height: 44px; }
