/* ---------- Design tokens ---------- */
/* Light palette lives on bare :root (the default / fallback, and what
   [data-theme="light"] resolves to). [data-theme="dark"] overrides the
   same token names. Theme is always explicit (see base.html's inline
   script + theme.js) — there's no ongoing "follow the OS" mode, so
   these are the only two blocks that need to exist. */
:root {
  --color-bg: #f7f7f5;
  --color-surface: #ffffff;
  --color-border: #e5e3df;
  --color-text: #1f1f1d;
  --color-text-muted: #6b6b66;
  --color-primary: #2f6f4f;
  --color-primary-hover: #255a40;
  --color-danger: #b3453c;
  --color-overdue-bg: #fbe9e7;
  --color-overdue-text: #b3453c;
  --color-badge-bg: #eef0ec;

  /* Semantic scale used for duration, energy, and "due today" badges:
     short/easy/soon = calm green, medium/normal = lavender,
     long/demanding = soft yellow. Deliberately no red in this scale —
     a long or demanding task isn't a bad thing, just a bigger one. */
  --color-scale-1-bg: #e3f5e9;
  --color-scale-1-text: #2f6f4f;
  --color-scale-2-bg: #eee7fa;
  --color-scale-2-text: #6b4fa0;
  --color-scale-3-bg: #fdf3d8;
  --color-scale-3-text: #8a6d1f;

  /* XP level-emblem gold face only — deliberately its own tokens, not
     --color-scale-3-text (that one's tuned for readable badge text on
     a pale background, which reads muddy/bronze at emblem size and
     saturation). Light theme gets a genuinely brighter, cleaner gold;
     dark theme's block below just re-derives the same values this
     emblem always used, so its appearance is unchanged. */
  --color-emblem-gold-highlight: #fff6df;
  --color-emblem-gold: #e8b93c;
  --color-emblem-gold-shadow: #b8975a;

  --color-success-bg: #e3f5e9;
  --color-shared-bg: #e6eef5;
  --color-shared-text: #2c5a82;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;

  --font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --touch-target: 44px;
  --transition-fast: 140ms ease;
  color-scheme: light;

  /* Elevation/depth — restrained "raised surface" shadows rather than
     relying on the border alone to separate a card from the page
     background (light-mode --color-bg/--color-surface are nearly
     identical in luminance, so the border was doing all the work).
     --focus-ring is derived from --color-primary via color-mix rather
     than a new hex value, kept for form-field focus specifically
     (buttons/links still use the global :focus-visible outline). */
  --shadow-card: 0 1px 2px rgba(20, 20, 18, 0.04), 0 2px 8px rgba(20, 20, 18, 0.05);
  --shadow-card-hover: 0 2px 4px rgba(20, 20, 18, 0.06), 0 8px 20px rgba(20, 20, 18, 0.08);
  --shadow-button: 0 1px 2px rgba(20, 20, 18, 0.08);
  --shadow-track-inset: inset 0 1px 2px rgba(20, 20, 18, 0.08);
  --focus-ring: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent);
}

:root[data-theme="dark"] {
  --color-bg: #17181a;
  --color-surface: #222325;
  --color-border: #35373a;
  --color-text: #eceae6;
  --color-text-muted: #9a9a96;
  --color-primary: #4f9c74;
  --color-primary-hover: #63b487;
  --color-danger: #e08078;
  --color-overdue-bg: #402522;
  --color-overdue-text: #e08078;
  --color-badge-bg: #2c2e31;

  --color-scale-1-bg: #1e3327;
  --color-scale-1-text: #7fcba0;
  --color-scale-2-bg: #2e2740;
  --color-scale-2-text: #bfa4ea;
  --color-scale-3-bg: #3a3320;
  --color-scale-3-text: #e0c374;

  /* Re-derives exactly the same gold this emblem used before these
     tokens existed (the radial-gradient stops that used to reference
     --color-scale-3-text directly) — dark theme's emblem is
     unchanged by this pass. */
  --color-emblem-gold-highlight: color-mix(in srgb, var(--color-scale-3-text) 55%, white 25%);
  --color-emblem-gold: var(--color-scale-3-text);
  --color-emblem-gold-shadow: color-mix(in srgb, var(--color-scale-3-text) 80%, black 20%);

  --color-success-bg: #1e3327;
  --color-shared-bg: #223140;
  --color-shared-text: #8fb8dc;

  /* Dark surfaces already separate reasonably from the page background
     by luminance alone, so shadows lean lighter/tighter than light
     mode's — a black shadow barely reads here. A faint inset top
     highlight does more of the "this is raised" work instead. */
  --shadow-card: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 2px 8px rgba(0, 0, 0, 0.35);
  --shadow-card-hover: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 8px 20px rgba(0, 0, 0, 0.45);
  --shadow-button: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-track-inset: inset 0 1px 2px rgba(0, 0, 0, 0.4);
  --focus-ring: 0 0 0 3px color-mix(in srgb, var(--color-primary) 32%, transparent);

  color-scheme: dark;
}

* { box-sizing: border-box; }

html {
  /* Always-present scrollbar rather than scrollbar-gutter: stable —
     the gutter approach still left a visible seam wherever a full-width
     element (the sticky header) couldn't paint into the reserved strip
     itself. A permanent real scrollbar removes the "unpainted reserved
     region" concept entirely: the content box is always the same
     narrower width, consistently, everywhere. */
  overflow-y: scroll;
  background: var(--color-bg);
}

body {
  margin: 0;
  font-family: var(--font-base);
  background: var(--color-bg);
  color: var(--color-text);
  -webkit-font-smoothing: antialiased;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}

a { color: var(--color-primary); text-decoration: none; }

/* Native form controls don't inherit page colors the way regular text
   does, so without this they stay browser-default white/black
   regardless of theme — the dark-mode "too bright" form fields and the
   black-on-charcoal login names both come from this same gap. */
input, select, textarea, button {
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
}

/* Soft focus glow instead of the generic outline for actual form
   fields specifically — buttons/links keep the plain outline below.
   border-color shift + a low-opacity ring derived from --color-primary
   (color-mix, not a new hex) reads as "this field is active" without
   a hard rectangle. */
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------- Header / nav ---------- */
.app-header {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  position: sticky;
  top: 0;
  z-index: 10;
}
.app-header-inner {
  max-width: 640px;
  margin: 0 auto;
  /* More air above the brand, less below the nav row — the brand-to-nav
     gap (the flex `gap` below) is trimmed to match, so the extra room
     reads as "breathing space at the top of the page," not evenly
     redistributed padding. Mobile's own padding override (420px
     breakpoint, below) is untouched. */
  padding: var(--space-3) var(--space-4) var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
/* Brand row: a separate, centered, minimal identity row above nav —
   deliberately understated (uppercase + tracking + lighter weight, not
   a bold logo-style title) so it reads as calm branding, not another
   nav-weight element competing with Today/All Tasks. */
.app-brand-row {
  display: flex;
  justify-content: center;
}
.app-brand {
  font-size: clamp(0.95rem, 4vw, 1.2rem);
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.app-nav-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-4);
}
.app-nav {
  display: flex;
  gap: var(--space-4);
  flex: 1 1 auto;
  min-width: 130px;
}
.app-nav a {
  color: var(--color-text-muted);
  font-size: 0.95rem;
  padding: var(--space-1) 0;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.app-nav a.active {
  color: var(--color-text);
  border-bottom-color: var(--color-primary);
}
/* Groups theme/user/settings so they wrap together as one unit on
   narrow screens (below the nav) instead of each control wrapping
   independently mid-row — the thing that broke with longer names. */
.app-header-controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.theme-toggle {
  /* A plain <button>'s own text isn't reliably centred within its
     padding box by browser default rendering the way a flex-centred
     control is (compare .task-icon-button, which is display:flex for
     exactly this reason) — that mismatch, not the row's own
     align-items, is what made this look shifted next to the other
     (already self-centred) header controls. Self-centring here fixes
     it regardless of font metrics, rather than nudging it with a
     one-off margin. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  font-size: 0.8rem;
  padding: var(--space-1) var(--space-2);
  min-height: 30px;
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
.theme-toggle:hover { border-color: var(--color-primary); color: var(--color-text); }
.theme-toggle:active { transform: scale(0.96); }
.logout-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}
.current-user {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  max-width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.app-header-controls .task-icon-button {
  width: 36px;
  height: 36px;
}
/* .btn-quiet is the canonical name for this role (PRODUCT_DESIGN.md
   Section 3). Phase 3B migrated every remaining .link-button template
   consumer app-wide onto this name, so the alias is retired. */
.btn-quiet {
  background: none;
  border: none;
  color: var(--color-primary);
  font-size: 0.85rem;
  cursor: pointer;
  padding: var(--space-1);
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}
.btn-quiet:hover { opacity: 0.75; }
.btn-quiet:active { transform: scale(0.96); }
.btn-quiet:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }

/* Below 420px, Today/Tasks/Habits/Finances plus the theme/user/Switch/
   Settings cluster no longer fit their desktop-sized gaps and controls
   on one line, and .app-nav-row's flex-wrap dropped the whole controls
   cluster to its own second row. Nav items stay the priority (a small
   font/gap trim, not a redesign); the secondary controls are trimmed
   more, but not shrunk into fiddly touch targets — the Settings button
   only drops from 36px to 32px, still above typical minimum touch-
   target guidance, and the theme toggle keeps real padding. No
   horizontal scrolling; this narrowing is what avoids needing it. */
@media (max-width: 420px) {
  .app-header-inner { padding-left: var(--space-2); padding-right: var(--space-2); }
  .app-nav-row { flex-wrap: nowrap; gap: var(--space-1) var(--space-2); }
  .app-nav { gap: var(--space-2); min-width: 0; }
  .app-nav a { font-size: 0.85rem; }
  .app-header-controls { flex-wrap: nowrap; gap: var(--space-1); }
  .logout-form { gap: var(--space-1); }
  .theme-toggle { font-size: 0.72rem; padding: 2px var(--space-1); min-height: 28px; }
  .current-user { max-width: 52px; font-size: 0.78rem; }
  .app-header-controls .btn-quiet { font-size: 0.78rem; padding: var(--space-1) 2px; }
  .app-header-controls .task-icon-button { width: 32px; height: 32px; }
  .app-header-controls .task-icon-button svg { width: 14px; height: 14px; }
}

/* ---------- Layout ---------- */
.app-main {
  max-width: 640px;
  margin: 0 auto;
  padding: var(--space-4);
}
.page-heading {
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: var(--space-2) 0 var(--space-4);
}

/* ---------- Login screen ---------- */
.login-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 70vh;
  text-align: center;
}
.login-heading {
  font-size: 1.6rem;
  margin-bottom: var(--space-5);
}
.user-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  max-width: 320px;
}
.user-picker button {
  width: 100%;
}
.user-picker-button {
  min-height: var(--touch-target);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  font-size: 1.05rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
}
.user-picker-button:hover { border-color: var(--color-primary); box-shadow: var(--shadow-card-hover); }
.user-picker-button:active { transform: scale(0.98); box-shadow: var(--shadow-card); }
.login-form {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 320px;
  text-align: left;
}
.login-field-label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-text-muted);
  margin-bottom: var(--space-3);
}
.login-error {
  color: var(--color-danger);
  background: var(--color-overdue-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  margin-bottom: var(--space-3);
}
.login-hint { color: var(--color-text-muted); max-width: 320px; }

/* ---------- Quick add form ---------- */
.quick-add-form {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  margin: var(--space-5) 0;
  box-shadow: var(--shadow-card);
}
/* .quick-add-input was a byte-for-byte duplicate of .field-control
   (see Shared field primitives below) aside from this one spacing
   rule -- box styling now lives in .field-control alone; this survives
   only as the layout-specific modifier still needed here. */
.quick-add-input { margin-bottom: var(--space-2); }
/* .btn-primary is the canonical name for this role (PRODUCT_DESIGN.md
   Section 3). Phase 3B migrated every remaining .quick-add-button
   template consumer app-wide onto this name, so the alias is retired. */
.btn-primary {
  min-height: var(--touch-target);
  min-width: var(--touch-target);
  padding: 0 var(--space-4);
  background: linear-gradient(180deg, var(--color-primary), var(--color-primary-hover));
  color: white;
  border: none;
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  width: 100%;
  box-shadow: var(--shadow-button);
  transition: box-shadow var(--transition-fast), transform var(--transition-fast), filter var(--transition-fast);
}
.btn-primary:hover { filter: brightness(1.05); box-shadow: var(--shadow-card-hover); }
.btn-primary:active { transform: scale(0.98); box-shadow: none; filter: none; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; filter: none; box-shadow: none; }

/* Shared disclosure-indicator treatment for any custom-triangle
   <summary> in the app (Tag-group headings, "More options" here) —
   one definition of the triangle's size/spacing/optical alignment/
   glyphs, so every disclosure that opts in matches exactly. Each
   summary keeps its own separate font/color/margin rules; only the
   triangle mechanics live here. `details[open] >` is generic (not
   scoped to a specific parent class), so it applies to any details/
   summary pair using this class, not just one particular feature.
   flex + align-items centres the glyph against the text as real boxes
   rather than via inline vertical-align, which doesn't land
   pixel-perfect across browsers for a glyph whose own ink sits
   differently in its em-box than text does; the small translateY is
   the last bit of optical correction for that ink position. Also
   quietly drops the native <summary> disclosure triangle on its own
   (no list-item box once display is flex) — the marker-suppression
   rule is kept anyway as a harmless cross-browser belt-and-suspenders. */
.disclosure-caret {
  display: flex;
  align-items: center;
  cursor: pointer;
  list-style: none;
}
.disclosure-caret::-webkit-details-marker { display: none; }
.disclosure-caret::before {
  content: "\25B6";
  display: inline-block;
  margin-right: var(--space-1);
  font-size: 0.78em;
  transform: translateY(-1px);
  color: inherit;
}
details[open] > .disclosure-caret::before { content: "\25BC"; }

.quick-add-more summary {
  margin-top: var(--space-3);
  color: var(--color-text-muted);
  font-size: 0.9rem;
}
/* Small fade-in for progressive-disclosure content (More options, the
   task edit panel) so it doesn't just snap into view. */
@keyframes reveal-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
.quick-add-more[open] > .quick-add-grid {
  animation: reveal-fade-in 180ms ease;
}
.quick-add-grid {
  display: grid;
  /* minmax(0, 1fr) — plain 1fr tracks won't shrink below the intrinsic
     content size of what's inside them (e.g. a <select> with a long
     option, or a date input), which overflows the container in any
     narrow context. minmax(0, 1fr) removes that floor. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-3);
  width: 100%;
  min-width: 0;
}
.quick-add-grid > label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.quick-add-notes { grid-column: 1 / -1; }
.field-label-icon { display: inline-flex; align-items: center; gap: var(--space-1); }
.field-label-icon svg { flex-shrink: 0; color: var(--color-text-muted); }
/* Box styling for the fields that have adopted .field-control (Energy,
   Duration, Due date, Assign to, Notes) now comes from that shared
   class instead -- :not(.field-control) keeps this rule serving only
   the recurrence sub-fields (Every / day(s)-week(s)-month(s)), which
   keep their existing look/layout unchanged in this pass (see
   .quick-add-repeat-fields below, which already carries their own
   near-identical copy of this same box styling). */
.quick-add-grid select:not(.field-control),
.quick-add-grid input:not([type="checkbox"]):not(.field-control),
.quick-add-grid textarea:not(.field-control) {
  width: 100%;
  min-width: 0;
  max-width: 100%;
  min-height: var(--touch-target);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font-size: 0.95rem;
  font-family: inherit;
}

/* Single column once a track would drop much below ~140px — the point
   a <select> or date input stops being comfortably usable at two per row. */
@media (max-width: 420px) {
  .quick-add-grid { grid-template-columns: 1fr; }
}

/* ---------- Task list ---------- */
.task-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.task-item {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-3);
  box-shadow: var(--shadow-card);
  transition: box-shadow var(--transition-fast), border-color var(--transition-fast),
    background-color var(--transition-fast);
}
.task-item:hover { box-shadow: var(--shadow-card-hover); }
.task-item-overdue { border-color: var(--color-overdue-text); }

.task-complete-form, .task-delete-form { margin: 0; }
.task-checkbox {
  width: 28px;
  height: 28px;
  min-width: 28px;
  border-radius: 50%;
  border: 2px solid var(--color-border);
  background: var(--color-surface);
  cursor: pointer;
  /* Centres the 28px circle against .task-title-row's ~32px compact
     line height (see .task-icon-button) -- .task-item aligns this
     column and .task-body to a shared flex-start top, so this offset
     is what actually lands the checkbox's centre on the title's line. */
  margin-top: 2px;
  position: relative;
  transition: border-color var(--transition-fast), background-color var(--transition-fast),
    transform var(--transition-fast);
}
.task-checkbox:hover { border-color: var(--color-primary); }
.task-checkbox:active { transform: scale(0.9); }

/* Pinned strip: a restrained shortcut/reference to one canonical Task
   that also keeps rendering normally in its own Tag/group below -- not
   a second card. Each row gets its own small card (reusing .task-item's
   own surface/border language at a smaller radius, no shadow) so the
   pinned area reads as clearly contained rather than a loose row of
   text -- but every dimension here (radius, padding, font-size) is
   deliberately smaller than .task-item's own, and there's still no
   title-row/badges/subtasks, so it stays substantially lighter/shorter
   than a real task card. Personal per viewer: what one household
   member pins here never affects another's own view of the same
   (possibly shared) Task -- see TaskPin. The list's own bottom margin
   is deliberately larger than the gap between pinned rows -- Expand
   all/Collapse all (when present) belongs to the grouped list below,
   not to this shortcut, and needs to read as a separate zone. */
.task-pinned-list {
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.task-pinned-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
}
/* Reuses .task-checkbox's own circle/border/hover/active styling —
   these two rules only override size, since .task-pinned-checkbox is
   declared after .task-checkbox with equal specificity. Deliberately
   skips the .task-item.htmx-swapping-driven fill/checkmark/particle
   sequence (that's scoped to full .task-item cards specifically) --
   completing from here still works correctly, just without that
   flourish, matching this row's overall restraint. */
.task-pinned-complete-form { margin: 0; position: relative; flex: 0 0 auto; }
.task-pinned-checkbox { width: 22px; height: 22px; min-width: 22px; margin-top: 0; }
.task-pinned-icon {
  flex: 0 0 auto;
  display: flex;
  width: 14px;
  height: 14px;
  color: var(--color-text-muted);
  opacity: 0.75;
}
.task-pinned-icon svg { width: 14px; height: 14px; }
.task-pinned-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.92rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Unpin stays a plain text-link affordance (.btn-quiet, unchanged)
   rather than a second button style -- available without visually
   competing with the checkbox/title for attention. */
.task-pinned-unpin { flex: 0 0 auto; margin: 0; }

/* Completion sequence, step 1 (check/fill): htmx adds .htmx-request to the
   form the instant the click fires, well before the server responds, so
   this starts immediately. It stays true (via the .htmx-swapping rule
   below) through the rest of the sequence so the fill never reverts. */
.task-complete-form.htmx-request .task-checkbox,
.task-item.htmx-swapping .task-checkbox {
  background: var(--color-primary);
  border-color: var(--color-primary);
}
.task-complete-form.htmx-request .task-checkbox::after,
.task-item.htmx-swapping .task-checkbox::after {
  /* Root-cause fix, not another nudge: the old approach drew an
     unrotated 6x11 box with only its right+bottom borders visible,
     then rotated it 45deg. That box's own visible ink (two thin lines
     meeting at its bottom-right corner) was never centred within its
     own bounding box to begin with -- rotating an off-centre shape
     around its box's centre relocates the ink to a *new* offset whose
     direction itself rotates, so no left/top value converges on true
     centre. That's why the prior "nudge" documented in this file's own
     history still wasn't right.
     This inlines Feather's "check" glyph (viewBox 0 0 24 24, polyline
     20,6 -> 9,17 -> 4,12) as a background-image instead: its raw path
     bounding box is x:4-20 / y:6-17, centred at (12, 11.5) against a
     24x24 canvas centred at (12,12) -- centred to within 0.5 unit of
     24 (~2%), imperceptible at rendered size. background-position:
     center places that whole (already-balanced) icon box in the
     middle of `inset:0` (the full circle, whatever its current
     diameter), and background-size is a percentage -- so this needs
     no left/top math and no per-size override: the same rule already
     covers both the 28px full-size and 18px subtask checkbox without
     .task-item-subtask needing its own copy. */
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 60%;
}

.task-body { flex: 1; min-width: 0; }
/* Title and Edit/Delete share this one short row — a real flex
   relationship (centred against each other), not simulated with
   absolute positioning. Everything else in .task-body (.task-meta,
   notes, subtasks) sits below as plain siblings of this row, so they
   get the full card width automatically; .task-quick-actions no
   longer competes for width in the outer .task-item row at all. A
   long title just wraps within its own flexed space and the row
   grows to match — no reserved padding on every line. */
.task-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.task-title-row .task-title { flex: 1; min-width: 0; }
.task-title {
  display: block;
  font-size: 1rem;
  font-weight: 600;
  word-break: break-word;
}
/* Inline title editing: the displayed title and its edit <input> are
   both always present (not conditionally rendered), swapped purely by
   the same .task-edit[open] :has() state that already reveals
   .task-edit-panel below -- one state, two consequences, no new JS
   needed for the swap itself (only for autofocus, handled separately
   in task-edit-controls.js). The input keeps .task-title's own
   font-size/weight so the row doesn't visibly grow when it appears --
   deliberately not reusing .quick-add-input, which is sized for the
   full-width capture field, not a compact inline row. */
.task-title-input {
  display: none;
  width: 100%;
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 1px var(--space-2);
}
.task-title-input:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
}
.task-item:has(.task-edit[open]) .task-title-display { display: none; }
.task-item:has(.task-edit[open]) .task-title-input { display: block; }
/* The title-only quick-edit affordance (see task-edit-controls.js'
   wireTitleDisplay) -- a real click/keyboard target, so it needs its
   own cursor and focus-visible ring like any other interactive
   control, even though it's a <span role="button"> rather than a
   native button. */
.task-title-display { cursor: pointer; }
.task-title-display:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}
.task-notes-preview {
  margin: var(--space-1) 0 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.task-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
/* inline-flex + align-items is the actual fix for pill text/icons
   sitting off-centre — a plain inline <span> only had padding to work
   with, and default line-height boxes don't distribute space
   symmetrically around a glyph. One declaration here, inherited by
   every variant below (XP, tags, duration, energy, due, overdue,
   shared) — no per-badge centring hacks.
   An explicit height (rather than letting it emerge from content) is
   what actually guarantees every badge is identical — before this,
   an icon-bearing badge (person 16px, calendar 17px) rendered taller
   than a text-only one (~18px), since align-items centres an
   oversized child rather than shrinking it. Capping .task-badge svg
   to a size that comfortably fits inside this height fixes that
   everywhere at once, including shrinking the calendar icon, without
   any per-badge-type rule. */
.task-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 20px;
  line-height: 1;
  font-size: 0.75rem;
  background: var(--color-badge-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  padding: 0 var(--space-2);
  border-radius: 999px;
}
.task-badge svg { flex-shrink: 0; width: 14px; height: 14px; }
.task-badge-shared { background: var(--color-shared-bg); color: var(--color-shared-text); }
.task-badge-overdue { background: var(--color-overdue-bg); color: var(--color-overdue-text); }
/* "Due today" isn't overdue and isn't a plain neutral badge either —
   same soft-yellow family as long/demanding, distinguished by its own
   text rather than a fourth colour. */
.task-badge-due-today { background: var(--color-scale-3-bg); color: var(--color-scale-3-text); }
/* The calendar icon stays purple regardless of the badge's own state
   colour (neutral/gold/red) — a consistent "this is date information"
   accent, same idea as the Due Soon preset icon. Colour set inline in
   the template on this wrapper specifically, not the whole badge, so
   the due-date TEXT keeps whatever colour its state already implies. */
.task-badge-due-icon { display: inline-flex; align-items: center; flex-shrink: 0; }

/* Semantic scale (short/easy -> medium/normal -> long/demanding): calm
   green -> lavender -> soft yellow. Deliberately not red — bigger or
   more demanding isn't "bad." Bucketing lives in Task.duration_bucket
   (Python), not here; energy's three values map onto the same scale. */
.task-badge-duration-short, .task-badge-energy-easy { background: var(--color-scale-1-bg); color: var(--color-scale-1-text); }
.task-badge-duration-medium, .task-badge-energy-normal { background: var(--color-scale-2-bg); color: var(--color-scale-2-text); }
.task-badge-duration-long, .task-badge-energy-demanding { background: var(--color-scale-3-bg); color: var(--color-scale-3-text); }

/* ---------- Completion sequence (step 2 onward) ----------
   Step 1 (check/fill) is the .htmx-request rule above, firing on click.
   Once the server confirms, htmx adds .htmx-swapping to the <li> and
   holds it there for the hx-swap "swap:900ms" delay before removing the
   element — that delay window is what these animations fill: strike-
   through -> a brief positive tint/scale -> spark burst + "+N XP"
   fly-up (both below) -> fade + height collapse. 900ms (up from the
   original 500ms) is what actually gives the extra reward beats room
   to read, rather than being crammed into the original timing; the
   click itself still reacts instantly (the checkbox fill above), so
   this doesn't cost perceived responsiveness. htmx owns all the
   timing/DOM work; this is just CSS reacting to the class it adds, per
   the "use htmx's lifecycle, not custom JS" brief — the one exception
   is the completion sound, which can't be done in CSS at all (see
   sound-effects.js). */
.task-item.htmx-swapping {
  overflow: hidden;
  pointer-events: none;
  animation: task-card-feedback 900ms ease-out forwards;
}
@keyframes task-card-feedback {
  /* 800px comfortably covers a card with notes, several badges, and a
     handful of nested subtasks — taller than any realistic card, so the
     collapse always animates smoothly rather than jumping. */
  0% { max-height: 800px; background-color: var(--color-surface); transform: scale(1); }
  18% { background-color: var(--color-success-bg); transform: scale(1.015); }
  30% { background-color: var(--color-success-bg); transform: scale(1); }
  80% { background-color: var(--color-success-bg); transform: scale(1); }
  100% {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    opacity: 0;
    background-color: var(--color-success-bg);
    transform: scale(1);
  }
}
.task-item.htmx-swapping:not(.task-item-deleting) .task-title {
  animation: task-title-strike 900ms ease-out forwards;
}
@keyframes task-title-strike {
  0%, 8% { text-decoration-line: none; }
  10%, 100% { text-decoration-line: line-through; color: var(--color-text-muted); }
}

/* Small completion "reward" flourishes — a few sparks and a "+N XP"
   fly-up, anchored to the (already position-relative-sized) complete
   form so they sit right at the completion control. Both are driven
   by the exact same .htmx-swapping class the card/title animations
   use, which htmx only adds after a genuinely successful response —
   so these can never fire from a failed request, and never for
   Undo/Delete/Edit (which don't add this class via this form, or use
   their own separate .task-item-deleting animation instead). Travel
   distances are kept small deliberately: .task-item has
   overflow:hidden during this whole sequence (needed for the height
   collapse), so anything that moves too far gets visibly clipped —
   small movement here isn't just restraint, it's what stays inside
   the card's own bounds. Explicitly gated behind prefers-reduced-
   motion (stricter than the blanket duration-crush rule above) so
   reduced-motion users get no particle motion or fly-up at all, not
   just a faster version of it. */
.task-complete-form { position: relative; }
.task-complete-burst {
  position: absolute;
  top: 50%;
  left: 50%;
  pointer-events: none;
}
.task-complete-burst span {
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: var(--color-scale-3-text);
  opacity: 0;
}
.task-complete-burst span:nth-child(1) { --spark-x: -10px; --spark-y: -8px; }
.task-complete-burst span:nth-child(2) { --spark-x: 10px; --spark-y: -9px; }
.task-complete-burst span:nth-child(3) { --spark-x: -12px; --spark-y: 4px; }
.task-complete-burst span:nth-child(4) { --spark-x: 12px; --spark-y: 5px; }
.task-complete-burst span:nth-child(5) { --spark-x: 0; --spark-y: -12px; }
@media (prefers-reduced-motion: no-preference) {
  .task-item.htmx-swapping:not(.task-item-deleting) .task-complete-burst span {
    animation: task-spark 550ms ease-out forwards;
  }
}
@keyframes task-spark {
  0% { opacity: 0; transform: translate(0, 0) scale(0.4); }
  25% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--spark-x), var(--spark-y)) scale(1); }
}

/* Text content is filled in server-side via an OOB swap using the
   real, server-confirmed xp_awarded (see complete_task/adjust_habit
   and the shared partials/_xp_fly_oob.html) — never a client-side
   guess or the pre-completion preview value. data-xp-awarded="0" by
   default (no OOB update = no positive award = never animates, since
   a 0-XP action never gets that OOB fragment at all). .task-xp-fly and
   .habit-xp-fly share this same look; only their trigger condition
   below differs, since Task rows get removed via a delayed swap
   (see .htmx-swapping below) while Habit rows never do. */
.task-xp-fly,
.habit-xp-fly {
  position: absolute;
  top: 50%;
  left: 50%;
  white-space: nowrap;
  pointer-events: none;
  font-size: 0.95rem;
  font-weight: 700;
  padding: 2px var(--space-2);
  border-radius: 999px;
  color: var(--color-primary);
  background: color-mix(in srgb, var(--color-scale-3-bg) 75%, transparent);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .task-item.htmx-swapping:not(.task-item-deleting) .task-xp-fly {
    animation: xp-fly-up 900ms ease-out forwards;
  }
  /* Habit rows are never removed/swapped out on success (they just
     stay put or move section), so there's no delayed-swap transition
     to gate on — each OOB replace already inserts a genuinely new
     element, which is enough to (re)trigger the animation on its own;
     data-xp-awarded="0" on the permanent placeholder never matches. */
  .habit-item .habit-xp-fly:not([data-xp-awarded="0"]) {
    animation: xp-fly-up 900ms ease-out forwards;
  }
}
@keyframes xp-fly-up {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.85); }
  15% { opacity: 1; transform: translate(-50%, calc(-50% - 3px)) scale(1); }
  70% { opacity: 1; transform: translate(-50%, calc(-50% - 12px)) scale(1); }
  100% { opacity: 0; transform: translate(-50%, calc(-50% - 16px)) scale(1); }
}

/* Delete gets its own plain fade/collapse — no success-tint or
   strikethrough, since those specifically signal "this task is done,"
   which is the wrong message for a deletion. Same specificity as
   .task-item.htmx-swapping (two classes each) and declared after it,
   so this animation value wins outright on a deleting card without
   needing to fight that shared rule; overflow/pointer-events still
   come from .htmx-swapping itself, unchanged. Added via hx-on before
   the request fires (task_item.html), so it's already present
   alongside .htmx-swapping once htmx starts the swap-delay window. */
.task-item.task-item-deleting {
  animation: task-card-remove 400ms ease-out forwards;
}
@keyframes task-card-remove {
  0% { max-height: 800px; opacity: 1; }
  100% {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    opacity: 0;
  }
}

/* ---------- Task quick actions (Edit / Delete icons) ---------- */
/* Two direct, always-visible icon buttons instead of a "..." dropdown —
   fewer clicks, and at this size (two 44px touch targets) it's no more
   visually heavy than the single toggle it replaces. Pushed to the
   far end of .task-title-row via that row's own justify-content —
   margin-left: auto isn't needed now that this lives inside the row
   instead of being a flex sibling of .task-body. */
.task-quick-actions {
  display: flex;
  gap: var(--space-1);
  flex-shrink: 0;
}
.task-icon-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-target);
  height: var(--touch-target);
  /* The 44px tap target itself is untouched -- this only shrinks what
     the button contributes to .task-title-row's height calculation.
     Flexbox sizes an auto-height row to its tallest item's margin box,
     so without this, the 44px button (nested two levels inside the
     row via .task-quick-actions) forced the whole row to 44px even
     though the title text next to it is ~20px tall, reading as a big
     dead gap above/below the title before .task-meta even starts.
     Equal top/bottom margins keep the button's own vertical centre
     (where its icon is centred via align-items above) exactly on the
     row's centreline, same as the title -- an asymmetric split would
     save more space but visibly shift the icon off the title's line.
     -6px/-6px nets the row down to 32px (44 - 12), which is about the
     tightest this can go without .task-meta's own top margin visually
     touching the button below it (that margin is deliberately left
     alone -- see .task-meta). 32px also happens to match what
     .task-checkbox's existing 2px margin-top already centres against,
     so the completion circle needed no separate nudge. */
  margin-top: -6px;
  margin-bottom: -6px;
  border: none;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  list-style: none;
  transition: background-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}
.task-icon-button::-webkit-details-marker { display: none; }
.task-icon-button svg { width: 16px; height: 16px; }
.task-icon-button:active { transform: scale(0.9); }
.task-icon-button:hover { background: var(--color-badge-bg); color: var(--color-text); }
.task-edit[open] > .task-icon-button { background: var(--color-badge-bg); color: var(--color-primary); }
.task-delete-icon:hover { background: var(--color-overdue-bg); color: var(--color-danger); }

/* Edit is a real <details>/<summary> (not JS) so it keeps native
   keyboard operability; it just opens the edit panel below the whole
   card rather than revealing its own content — :has() relocates where
   the result shows. */
.task-edit-panel {
  display: none;
  flex: 1 1 100%;
  min-width: 0;
  width: 100%;
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.task-item:has(.task-edit[open]) .task-edit-panel {
  display: block;
  animation: reveal-fade-in 180ms ease;
}

.task-edit-form {
  display: flex;
  flex-direction: column;
  /* space-3 (not the tighter space-2 used between two fields *within*
     the same field group) -- this is the boundary between "editing
     fields" and "committing the edit", one step up in the same rhythm
     used for .habit-edit-form's group spacing below. */
  gap: var(--space-3);
  width: 100%;
  min-width: 0;
}
.task-edit-form .quick-add-grid { margin-top: 0; }
.task-edit-form-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
/* .btn-primary's own width:100% is exactly right as the lone action in
   a form (Quick Add, Settings Save), but here it shares a row with
   Cancel -- unmodified, its used width:100% flex-basis would claim the
   whole row and squeeze Cancel to nothing. .filter-form-actions
   already established the fix (width: auto) for the same reason; this
   is the second consumer, so the override belongs at this same
   "button inside a specific actions row" scope rather than as a
   change to .btn-primary itself. */
.task-edit-form-actions .btn-primary { width: auto; flex: 1 1 auto; }
.task-edit-form-actions .btn-quiet { flex: 0 0 auto; }

/* ---------- Recently completed / deleted (undo) ---------- */
.recent-activity {
  margin-top: var(--space-4);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.recent-activity > summary {
  cursor: pointer;
  padding: var(--space-2) 0;
}
.recent-activity-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.recent-activity-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
}
.recent-activity-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text);
}
.recent-activity-undo { margin: 0; flex-shrink: 0; }
.recent-activity[hidden] { display: none; }

/* ---------- Tags (management page) ---------- */
.tag-body { flex: 1; min-width: 0; }
.tag-name {
  display: block;
  font-size: 1rem;
  font-weight: 500;
  word-break: break-word;
}
.tag-count {
  display: block;
  margin-top: var(--space-1);
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* ---------- Tag chip picker (task form) ---------- */
.tag-select { grid-column: 1 / -1; }
.tag-select-label {
  display: block;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-1);
}
.tag-chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
/* A fixed height (not min-height) plus a second, direct centring rule
   on the text span itself — belt and suspenders. The chip's own
   inline-flex + align-items:center should be enough on paper, but the
   min-height version of this (relying on the browser to centre a
   flex item within space beyond its content's natural size) proved
   unreliable in the deployed build the same way an earlier icon-colour
   fix did; forcing the span to fill and centre within a fixed-height
   box removes that ambiguity rather than leaving it to inference.
   white-space:nowrap keeps a chip single-line at this fixed height —
   already the de facto behaviour, now made explicit; chips still wrap
   onto new rows via .tag-chip-list's own flex-wrap. */
.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0 var(--space-3);
  font-size: 0.85rem;
  color: var(--color-text-muted);
  cursor: pointer;
  height: 32px;
  white-space: nowrap;
  transition: border-color var(--transition-fast), background-color var(--transition-fast),
    color var(--transition-fast);
}
.tag-chip span {
  display: inline-flex;
  align-items: center;
  height: 100%;
  line-height: 1;
}
.tag-chip input {
  /* Still a real, keyboard-operable checkbox — just visually replaced
     by the chip's own checked styling below. */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.tag-chip:hover { border-color: var(--color-primary); color: var(--color-text); }
.tag-chip:has(input:checked) {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: white;
}
.tag-chip:has(input:focus-visible) {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.tag-chip-empty {
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

/* ---------- Compact Tag selector (per-user preference) ----------
   A plain <details> disclosure over the exact same checkboxes Buttons
   mode uses (same name="tags"/value=<id> pairs) — the two modes are
   just different presentations of identical form data, so no backend
   change was needed for this at all. Closed-state selections are
   never lost: that's inherent to how form controls inside a closed
   <details> behave (still submitted, just not rendered), the same
   mechanism this app already relies on for the collapsed recurring
   fields — not something this CSS has to implement. */
.tag-select-compact { grid-column: 1 / -1; }
.tag-select-compact-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  cursor: pointer;
  list-style: none;
  transition: border-color var(--transition-fast);
}
.tag-select-compact-toggle::-webkit-details-marker { display: none; }
.tag-select-compact[open] .tag-select-compact-toggle { border-color: var(--color-primary); }
.tag-select-compact-label { font-weight: 500; color: var(--color-text); }
.tag-select-compact-summary {
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 65%;
}
.tag-select-compact-list {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  max-height: 220px;
  overflow-y: auto;
  animation: reveal-fade-in 180ms ease;
}
.tag-select-compact-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  color: var(--color-text);
  cursor: pointer;
  border-bottom: 1px solid var(--color-border);
}
.tag-select-compact-option:last-child { border-bottom: none; }
.tag-select-compact-option:hover { background: var(--color-badge-bg); }
.tag-select-compact-option:has(input:checked) {
  background: var(--color-badge-bg);
  color: var(--color-primary);
  font-weight: 600;
}

/* ---------- Settings ---------- */
.settings-breadcrumb {
  display: inline-block;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.settings-breadcrumb:hover { color: var(--color-primary); }
.settings-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.settings-link {
  display: block;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  color: var(--color-text);
  box-shadow: var(--shadow-card);
  transition: border-color var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast);
}
.settings-link:hover { border-color: var(--color-primary); background: var(--color-badge-bg); box-shadow: var(--shadow-card-hover); }
.settings-link-title { display: block; font-weight: 500; }
.settings-link-hint { display: block; margin-top: var(--space-1); font-size: 0.85rem; color: var(--color-text-muted); }

.theme-toggle-inline { padding: var(--space-2) var(--space-4); font-size: 0.9rem; }

/* ---------- Shared field primitives ----------
   Generic label/control/hint building blocks for a single form field
   (.field-grid wraps one or more .field items with consistent spacing;
   .choice-row is one radio/checkbox option). Introduced for General
   (settings_general.html); Backup (settings_backup.html) is the second
   consumer, reusing .settings-field-group-legend/.field-hint for its
   one heading/hint pair even though it has no fieldset of its own --
   intended to widen to other forms over time rather than staying
   General-specific. */
.field-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 320px;
}
.field-label {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--color-text-muted);
}
/* Class-based, so it applies identically to <input>, <select>, and
   <textarea> alike -- Phase 3A's canonical control for both (see the
   Quick Add/Filter & Sort/Habits migrations below). A native <select>
   needs no extra appearance reset to match: browsers already box it
   the same way as a text input once border/height/padding/radius are
   set explicitly instead of left at browser default. */
.field-control,
.quick-add-input {
  width: 100%;
  min-width: 0;
  max-width: 100%;
  min-height: var(--touch-target);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-3);
  font-size: 1rem;
}
textarea.field-control { min-height: 60px; padding: var(--space-2) var(--space-3); }
.field-control:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background: var(--color-badge-bg);
}
.field-hint {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  margin: var(--space-2) 0 0;
  max-width: 480px;
}
.choice-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.9rem;
  padding: var(--space-1) 0;
  cursor: pointer;
}

/* ---------- Shared button system ----------
   The remaining two of the four canonical roles (PRODUCT_DESIGN.md
   Section 3) not already aliased onto an existing class above
   (.btn-primary, .btn-quiet).
   .btn-secondary is a genuine new rectangular bordered button --
   .preset-link already fills this role visually but is a deliberately
   pill-shaped, icon-slotted chip for the Tasks presets specifically
   (see its own rule below); this is the general-purpose shape for
   everything else. Neither role has a labelled-button instance in the
   three areas this phase touches (Quick Add, Filter & Sort, Habits
   need only primary/quiet there), but both are defined now so the
   system has all four roles named and ready. .btn-destructive is the
   labelled-button equivalent of the icon-only destructive convention
   already established by .task-icon-button.task-delete-icon. */
.btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow-button);
  transition: border-color var(--transition-fast), background-color var(--transition-fast),
    box-shadow var(--transition-fast), transform var(--transition-fast);
}
.btn-secondary:hover { border-color: var(--color-primary); background: var(--color-badge-bg); box-shadow: var(--shadow-card-hover); }
.btn-secondary:active { transform: scale(0.97); }
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }

.btn-destructive {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-4);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-danger);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--transition-fast), transform var(--transition-fast);
}
.btn-destructive:hover { background: var(--color-overdue-bg); }
.btn-destructive:active { transform: scale(0.97); }
.btn-destructive:disabled { opacity: 0.5; cursor: not-allowed; }

/* ---------- General / Backup (settings_general.html, settings_backup.html) ----------
   Structural vocabulary for a settings page's section/fieldset layout,
   rather than reusing .filter-group (built for the Tasks filter
   panel's two-column grid, with grid-specific rules that don't belong
   here). Both pages share this vocabulary now (Phase 2A: General;
   Phase 2B: Backup). */
.settings-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.settings-section {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--color-border);
}
.settings-section:last-of-type { border-bottom: none; padding-bottom: 0; margin-bottom: var(--space-4); }
.settings-section-heading {
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-4);
}
.settings-field-group { border: none; padding: 0; margin: 0; }
.settings-field-group-legend {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-text);
  padding: 0;
  margin-bottom: var(--space-2);
}
.settings-divider {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: var(--space-4) 0;
}
.settings-save-button { margin-top: var(--space-2); align-self: flex-start; }

/* .btn-primary assumes a <button> (inline-block by default) -- this is
   an <a> instead (a plain GET download, not a form submit), so it
   needs its own display/centring; the button's own colours/sizing are
   reused unmodified. */
.backup-download-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: var(--space-3);
  text-decoration: none;
}

/* ---------- Presets & Filter/Sort panel ---------- */
.preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.preset-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.85rem;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-text);
  background: var(--color-surface);
  box-shadow: var(--shadow-button);
  transition: border-color var(--transition-fast), background-color var(--transition-fast),
    box-shadow var(--transition-fast), transform var(--transition-fast);
}
.preset-link:hover { border-color: var(--color-primary); background: var(--color-badge-bg); box-shadow: var(--shadow-card-hover); }
.preset-link:active { transform: scale(0.97); }
/* Icon colour is set inline per instance (style="color: var(--...)"
   in tasks.html) rather than through a modifier class — a class-based
   descendant rule here proved unreliable in the deployed build, and an
   inline declaration on the element that actually carries
   stroke="currentColor" is the most direct, cache-proof mechanism CSS
   offers: nothing can shadow it. This wrapper only handles layout
   (self-centering the icon regardless of the SVG's own inline
   display type) — the label text after it keeps the link's own
   colour, untouched. */
.preset-link-icon {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 0;
}

.filter-panel-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.filter-panel {
  flex: 1;
  min-width: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  box-shadow: var(--shadow-card);
}
.filter-panel-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 600;
  list-style: none;
}
.filter-panel-toggle-icon {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 0;
}
.filter-panel-toggle::-webkit-details-marker { display: none; }
.filter-panel[open] .filter-panel-toggle { margin-bottom: var(--space-3); }

.filter-form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  animation: reveal-fade-in 180ms ease;
}
.filter-group {
  border: none;
  padding: 0;
  margin: 0;
  min-width: 0;
}
.filter-group legend {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-text-muted);
  padding: 0;
  margin-bottom: var(--space-1);
}
.filter-group-hint { font-weight: 400; text-transform: none; }
.filter-checkbox, .filter-radio {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.9rem;
  padding: 2px 0;
  cursor: pointer;
}
/* Select box styling now comes from .field-control (applied directly
   in tasks.html) -- this wrapper only handles the label/control stack
   layout. */
.filter-select-label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.filter-form-actions {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.filter-form-actions .btn-primary { width: auto; padding: 0 var(--space-5); }

@media (max-width: 420px) {
  .filter-form { grid-template-columns: 1fr; }
}

/* <details>/<summary> — same zero-JS disclosure pattern already used
   for the Filter & Sort panel and subtask-add, including the same
   marker suppression — but here the browser's own default marker is
   replaced with a small ▶/▼ ::before, not left off entirely, since
   this is the one disclosure users specifically asked to have a
   collapse/expand indicator on. It's still just the heading text (and
   now this glyph) that's clickable — no SVG/icon infrastructure — and
   the [open] attribute selector means the glyph swap is 100% native
   CSS reacting to the real <details> state, not JS tracking it.
   Expanded by default via the `open` attribute in the template.

   Group-to-group spacing (margin-top here) is intentionally a
   different, smaller value than heading-to-content spacing
   (margin-bottom below) — tightening the gap between stacked headings
   (especially collapsed ones) must never eat into the separation
   between an expanded heading and its first task card. */
.task-group { margin-top: var(--space-3); }
.task-group:first-of-type { margin-top: 0; }
.task-group-bulk-controls {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-3);
  margin-bottom: var(--space-2);
}
.task-group-bulk-controls:first-child { margin-top: 0; }
.task-group-bulk-controls + .task-group { margin-top: 0; }
.task-group-heading {
  /* Triangle mechanics (flex centring, the glyph, marker suppression)
     live in the shared .disclosure-caret class below — this rule only
     carries the heading's own, separate typography/spacing. */
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
  transition: color var(--transition-fast);
}
.task-group-heading:hover { color: var(--color-primary); }

.task-badge-recurring { background: var(--color-badge-bg); }
.task-badge-xp { background: var(--color-success-bg); color: var(--color-primary); font-weight: 600; }

/* ---------- XP status (level + progress bar) ----------
   One shared partial (_xp_status.html) renders both the normal
   in-page widget and the htmx OOB fragment returned by task
   completion — same #xp-status id either way, so htmx's out-of-band
   swap finds it automatically. The +XP toast and Level Up banner only
   render (and thus only animate) when the completion response actually
   includes them; a plain page load never shows either. */
.xp-status {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  margin-bottom: var(--space-4);
  box-shadow: var(--shadow-card);
}
.xp-status-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
/* Rank-badge emblem — four real layered elements (not one div with
   stacked gradients). Green outer rank frame + a visible gap + larger
   gold inner medallion + large prominent number:
     .xp-level-badge       — plain positioning wrapper; carries only
                              the restrained green ambient glow, so the
                              glow reads as coming from the badge as a
                              whole rather than just one layer
     .xp-level-badge-rim   — full-size hexagon, green — the outer rank
                              frame. Its own inset box-shadow (light
                              top edge, darker recessed bottom) gives
                              the frame itself some bevel/dimension
                              instead of being one flat green shape.
                              Kept thin so the face fills most of the
                              badge
     .xp-level-badge-gap   — a hexagon inset just inside the rim,
                              painted in the card's own surface colour
                              rather than a new one — this is what
                              makes the rim and face read as genuinely
                              separate nested pieces instead of two
                              gradients touching
     .xp-level-badge-face  — inset again inside the gap. Gold, with
                              real tonal variation (a multi-stop
                              gradient plus a radial highlight) and an
                              inset top-highlight/bottom-shadow bevel,
                              so it reads as a material rather than a
                              flat fill
     .xp-level-badge-number — the digit: large, bold, with a layered
                              text-shadow (light above, dark below,
                              plus a soft dark halo) so it reads as
                              embossed/carved metal rather than flat
                              printed text; centered both axes via the
                              face's own flex centering
   Just the level number is meaningful content; the rest is decorative,
   so the whole badge is aria-hidden and "Level N" text alongside it
   (below) remains the accessible, always-legible label. */
.xp-level-badge {
  position: relative;
  flex-shrink: 0;
  width: 58px;
  height: 68px;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--color-primary) 35%, transparent));
}
.xp-level-badge-rim {
  position: absolute;
  inset: 0;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  background: linear-gradient(
    155deg,
    color-mix(in srgb, var(--color-primary) 55%, white 25%) 0%,
    var(--color-primary) 55%,
    color-mix(in srgb, var(--color-primary-hover) 80%, black 20%) 100%
  );
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, 0.35),
    inset 0 -2px 3px rgba(0, 0, 0, 0.35);
}
.xp-level-badge-gap {
  position: absolute;
  inset: 2.5px;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  background: var(--color-surface);
}
.xp-level-badge-face {
  position: absolute;
  inset: 4.5px;
  display: flex;
  align-items: center;
  justify-content: center;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 40%),
    radial-gradient(
      circle at 50% 32%,
      var(--color-emblem-gold-highlight) 0%,
      var(--color-emblem-gold) 60%,
      var(--color-emblem-gold-shadow) 100%
    );
  box-shadow:
    inset 0 2px 3px rgba(255, 255, 255, 0.35),
    inset 0 -5px 7px rgba(0, 0, 0, 0.3);
}
.xp-level-badge-number {
  font-size: 2.7rem;
  font-weight: 800;
  line-height: 1;
  color: white;
  text-shadow:
    0 -1px 0 rgba(255, 255, 255, 0.3),
    0 2px 3px rgba(0, 0, 0, 0.55),
    0 0 6px rgba(0, 0, 0, 0.25);
  /* The box is centred correctly (via .xp-level-badge-face's own flex
     centring) -- this is a small optical correction for the digit
     glyphs' own visual weight/side bearings at this size/weight, which
     otherwise reads as sitting slightly right of centre. em-based so
     it scales down proportionally with the mobile font-size override
     below instead of needing a second, separate correction there. */
  transform: translateX(-0.03em);
}
.xp-status-main { flex: 1; min-width: 0; }
.xp-status-heading {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  font-size: 0.9rem;
  margin-bottom: var(--space-2);
}
.xp-level-name { font-weight: 700; color: var(--color-text); white-space: nowrap; }
.xp-progress-numbers { color: var(--color-text-muted); font-size: 0.85rem; white-space: nowrap; }
.xp-progress-track {
  height: 9px;
  border-radius: 999px;
  background: var(--color-badge-bg);
  overflow: hidden;
  box-shadow: var(--shadow-track-inset), inset 0 0 0 1px color-mix(in srgb, var(--color-border) 55%, transparent);
}
/* Base tier (< 75% through the level): a gloss highlight over the
   plain green gradient — same fraction/math as before, just more
   depth. The -near/-hot tiers (picked in the template purely from
   xp_fraction, no new derived number) fold in a restrained gold lean
   toward the leading edge as the level gets close, then a touch of
   glow right at the end — never red/orange, and the fraction itself
   never changes meaning. */
.xp-progress-fill {
  height: 100%;
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0) 50%),
    linear-gradient(90deg, var(--color-primary), var(--color-primary-hover));
  border-radius: 999px;
  transition: width 400ms ease;
}
.xp-progress-fill-near {
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0) 50%),
    linear-gradient(
      90deg,
      var(--color-primary),
      var(--color-primary-hover) 55%,
      color-mix(in srgb, var(--color-scale-3-text) 55%, var(--color-primary-hover)) 100%
    );
}
.xp-progress-fill-hot {
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 50%),
    linear-gradient(90deg, var(--color-primary), color-mix(in srgb, var(--color-scale-3-text) 75%, var(--color-primary-hover)));
  box-shadow: 0 0 6px color-mix(in srgb, var(--color-scale-3-text) 45%, transparent);
}
.xp-status-footer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-size: 0.8rem;
}
.xp-remaining {
  color: var(--color-scale-3-text);
  font-weight: 600;
  white-space: nowrap;
  background: color-mix(in srgb, var(--color-scale-3-text) 12%, transparent);
  padding: 1px var(--space-2);
  border-radius: 999px;
}
.xp-lifetime {
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 420px) {
  .xp-level-badge { width: 48px; height: 53px; }
  .xp-level-badge-gap { inset: 1.7px; }
  .xp-level-badge-face { inset: 3.5px; }
  .xp-level-badge-number { font-size: 2rem; }
  .xp-status-row { gap: var(--space-2); }
}
/* Animation starts automatically the moment this element is inserted
   (initial paint or an htmx OOB swap alike) — no JS needed, same
   principle as the existing completion checkmark/strikethrough
   animations. Respects prefers-reduced-motion via the global rule
   near the top of this file. */
.xp-gain-toast {
  margin-top: var(--space-2);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-primary);
  animation: xp-toast-fade 1800ms ease forwards;
}
@keyframes xp-toast-fade {
  0% { opacity: 0; transform: translateY(4px); }
  15% { opacity: 1; transform: translateY(0); }
  75% { opacity: 1; }
  100% { opacity: 0; }
}
/* Stronger than the toast (its own tinted panel, bigger title) but
   still restrained: no modal, no overlay, nothing to dismiss — it
   simply fades on its own after a few seconds. */
.xp-levelup-banner {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-success-bg);
  text-align: center;
  animation: xp-levelup-fade 3200ms ease forwards;
}
.xp-levelup-title {
  display: block;
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--color-primary);
}
.xp-levelup-detail {
  display: block;
  margin-top: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
@keyframes xp-levelup-fade {
  0% { opacity: 0; transform: scale(0.96); }
  10% { opacity: 1; transform: scale(1); }
  85% { opacity: 1; }
  100% { opacity: 0; }
}

/* ---------- Subtasks ----------
   Subtasks are full task-items (same badges, same htmx completion +
   undo), included recursively from this same template — but styled
   much lighter so a card full of them doesn't read as a stack of cards
   within a card. Nesting is a left rule + indent, not another border/
   background/shadow. */
.subtask-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0 0 0 var(--space-3);
  border-left: 2px solid color-mix(in srgb, var(--color-primary) 25%, var(--color-border));
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.task-item-subtask {
  background: none;
  border: none;
  border-radius: 0;
  padding: var(--space-1) 0;
  box-shadow: none;
  gap: var(--space-2);
}
.task-item-subtask:hover { box-shadow: none; }
.task-item-subtask { gap: var(--space-2); }
.task-item-subtask .task-checkbox { width: 18px; height: 18px; min-width: 18px; }
.task-item-subtask .task-title { font-size: 0.9rem; }
.task-item-subtask .task-badge { font-size: 0.7rem; height: 16px; padding: 0 var(--space-2); }
.task-item-subtask .task-badge svg { width: 12px; height: 12px; }
.task-item-subtask .task-quick-actions { gap: 0; }
/* Subtask icon buttons are already 28px, close enough to the ~17px
   subtask title's height that they never caused the oversized-gap
   problem the 44px buttons did -- reset the compensating margin from
   the base rule above so subtasks keep their existing compact layout
   unchanged. */
.task-item-subtask .task-icon-button { width: 28px; height: 28px; margin: 0; }
.task-item-subtask .task-icon-button svg { width: 12px; height: 12px; }
/* .task-body's base rule (min-width: 0) is exactly what lets flexbox
   shrink it to nothing rather than wrapping the actions row onto their
   own line when a nested, indented, further-cramped subtask card runs
   out of horizontal room — word-break then breaks mid-word as a last
   resort, which is how titles rendered one letter per line. A real
   floor here means the actions wrap below instead (task-item already
   has flex-wrap: wrap), which is a far more graceful degradation. Even
   with the tighter sizing above, deeply indented subtasks with a longer
   title can still wrap the action icons onto their own line on narrow
   phones — accepted as reasonable given how much padding/indent stacks
   up (app-main -> task-item -> subtask-list); it stays compact and
   readable either way. */
.task-item-subtask .task-body { min-width: 90px; }
/* One level of nesting only, so a subtask never renders its own
   subtask-list/add-subtask block (task_item.html already gates that on
   "not task.parent_task_id") — no need to further shrink beyond this. */

.subtask-add { margin-top: var(--space-2); }
.subtask-add-toggle {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  list-style: none;
}
.subtask-add-toggle::-webkit-details-marker { display: none; }
.subtask-add-toggle:hover { color: var(--color-primary); }
.subtask-add-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
/* Compact scale (36px, not the full 44px touch target) since this
   lives nested inside an already-dense task card — same idea as the
   existing subtask checkbox/icon-button scaling just above. Explicit
   equal min-height on both, plus flex-centred button text, is what
   keeps the button reading as a smaller sibling of the main Add
   control rather than an odd, near-square one — the input's flex:1
   is what keeps it the dominant width regardless of the button's own
   content-driven size. */
.subtask-add-form .quick-add-input {
  flex: 1 1 auto;
  min-width: 0;
  margin-bottom: 0;
  min-height: 36px;
  padding: 0 var(--space-3);
  font-size: 0.9rem;
}
.subtask-add-form .btn-primary {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  min-height: 36px;
  padding: 0 var(--space-4);
  font-size: 0.85rem;
}

/* ---------- Edit panel secondary actions ---------- */
.task-edit-secondary-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
}
.task-inline-action { margin: 0; }

/* ---------- Recurrence quick-add fields ---------- */
.quick-add-repeat { grid-column: 1 / -1; }
.quick-add-repeat-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.9rem;
  color: var(--color-text);
  cursor: pointer;
}
/* Progressive disclosure, no JS: the frequency/interval fields only
   take up space once Recurring is actually checked. */
.quick-add-repeat-fields {
  display: none;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
.quick-add-repeat:has(#recurring-checkbox:checked) .quick-add-repeat-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  animation: reveal-fade-in 180ms ease;
}
.quick-add-repeat-hint {
  flex-basis: 100%;
  margin: 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}
.quick-add-repeat-fields label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.quick-add-repeat-fields input,
.quick-add-repeat-fields select {
  min-height: var(--touch-target);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  font-size: 0.95rem;
}

/* ---------- Empty state ---------- */
.empty-state {
  text-align: center;
  color: var(--color-text-muted);
  padding: var(--space-6) var(--space-4);
}
.more-tasks-note {
  text-align: center;
  margin-top: var(--space-4);
  font-size: 0.9rem;
}

/* ---------- Desktop refinements ---------- */
@media (min-width: 640px) {
  .app-main { padding: var(--space-6) var(--space-4); }
  .quick-add-form { padding: var(--space-4); }
}

/* ---------- Habits ---------- */

/* -- Header: date + today's at-a-glance summary -- */
.habits-header { margin-bottom: var(--space-4); }
.habits-date-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}
.habits-date {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 0.9rem;
  text-align: center;
  min-width: 0;
}
.habits-date-arrow {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--color-text);
  font-size: 1.1rem;
  text-decoration: none;
  transition: background var(--transition-fast);
}
.habits-date-arrow:hover { background: var(--color-badge-bg); }
.habits-date-arrow-disabled {
  color: var(--color-border);
  cursor: default;
}
.habits-today-link {
  display: block;
  text-align: center;
  font-size: 0.85rem;
  margin-bottom: var(--space-3);
}
.habits-section-label {
  margin: var(--space-4) 0 var(--space-2);
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
}
.habits-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
/* Same track/fill shape as .xp-progress-track, just flat (no tier
   colour shift) — that nuance belongs to XP, not today's checklist. */
.habits-summary-track {
  height: 8px;
  border-radius: 999px;
  background: var(--color-badge-bg);
  overflow: hidden;
  box-shadow: var(--shadow-track-inset), inset 0 0 0 1px color-mix(in srgb, var(--color-border) 55%, transparent);
}
.habits-summary-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--color-primary), var(--color-primary-hover));
  transition: width 400ms ease;
}
.habits-summary-text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

/* -- Empty state -- */
.habits-empty-state {
  text-align: center;
  padding: var(--space-6) var(--space-4);
  color: var(--color-text-muted);
}
.habits-empty-emoji { font-size: 2rem; margin: 0 0 var(--space-2); }
.habits-empty-title {
  margin: 0 0 var(--space-1);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--color-text);
}
.habits-empty-copy { margin: 0; font-size: 0.9rem; }

/* -- The list, and each habit's card -- */
.habit-list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* One card anatomy for all three types: .habit-card-row is always
   [leading][body][options] -- every type now gets the same leading
   status circle (see habit_item.html's circle_action-driven markup),
   not just Simple. body is always flex:1/min-width:0 so a long title
   wraps *inside itself* rather than pushing the options trigger around
   or forcing the whole row to wrap. flex-direction:column here (not on
   the row) is what lets .habit-progress-controls/.habit-options-panel
   drop to their own full-width areas below without disturbing the
   row's own layout -- both now live as row-level siblings rather than
   inside .habit-card-body, which is what makes the row's own height
   independent of either disclosure's open/closed state, in turn making
   align-items:center below safe (no jump when either one opens). */
.habit-item {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  box-shadow: var(--shadow-card);
  transition: box-shadow var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
}
.habit-item:hover { box-shadow: var(--shadow-card-hover); }
/* Restrained completed-state treatment for Simple habits: a soft tint
   on the whole card (not a loud badge) plus a muted title — the habit
   stays fully visible and in place, just visually "done for today". */
.habit-item-complete {
  background: color-mix(in srgb, var(--color-success-bg) 55%, var(--color-surface));
  border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
}
.habit-item-complete .habit-name { color: var(--color-text-muted); }
/* Explicit failure gets its own restrained tint (the app's existing
   danger colour, not a new one) -- distinct from -complete's success
   tint, and never applied together since a habit can't be both. */
.habit-item-failed {
  background: color-mix(in srgb, var(--color-overdue-bg) 55%, var(--color-surface));
  border-color: color-mix(in srgb, var(--color-danger) 28%, var(--color-border));
}
.habit-card-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  position: relative; /* anchors .habit-xp-fly's absolute positioning */
}
/* Both revealable panels (.habit-progress-controls and
   .habit-options-panel) now live outside .habit-card-row entirely (see
   the anatomy comment above), so the row's own cross-axis height never
   changes with either disclosure's state -- align-items:center is safe
   and correct for every measurement type, no per-type override needed.
   .habit-card-body's own min-width:0 is what actually prevents the old
   wrap-the-options-trigger bug, not align-items.

   Reordering (drag handle + its keyboard fallback) is temporarily
   removed from the rendered UI -- see habit-reorder.js's own comment.
   Habit.position, /habits/{id}/move, and /habits/reorder are all still
   live; only the CSS/markup for this specific handle-based interaction
   was removed, since it's the one being redesigned, not reused as-is. */

.habit-card-leading { flex: 0 0 auto; }
.habit-card-body { flex: 1 1 auto; min-width: 0; }

.habit-name {
  font-size: 1.02rem;
  font-weight: 600;
  color: var(--color-text);
  overflow-wrap: break-word;
  transition: color var(--transition-fast);
}

/* -- Simple habits: the completion circle is the primary interaction,
   so it's larger than a Task checkbox and gets a real checkmark, not
   just a filled dot. -- */
.habit-circle {
  /* inline-flex, not the button-only default of inline-block: the
     non-interactive variant below is a bare <span>, and a plain inline
     element ignores width/height entirely -- without this, that span
     collapses to a near-zero-width box that only paints its border/
     background at line-height, rendering as a thin vertical pill
     instead of a circle (and shoving the title left into the space
     that should've stayed reserved). inline-flex also centres the
     ::after checkmark/× glyph without relying on inset:0 sizing
     against a collapsed parent. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 32px;
  height: 32px;
  min-width: 32px;
  border-radius: 50%;
  border: 2px solid var(--color-border);
  background: var(--color-surface);
  padding: 0;
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.habit-circle:hover { border-color: var(--color-primary); }
.habit-circle:active { transform: scale(0.9); }
/* Non-interactive variant (a <span>, not a <button>) -- target_reached/
   exceeded/exceeded_max, where there's nothing truthful to tap (see
   circle_action's own docstring). No hover/active affordance at all,
   since it isn't a control. */
.habit-circle-static { cursor: default; }
.habit-circle-static:hover { border-color: var(--color-border); }
.habit-circle-static:active { transform: none; }
.habit-circle-done {
  background: var(--color-primary);
  border-color: var(--color-primary);
}
/* Same Feather "check" glyph/centring approach as .task-checkbox::after
   — see that rule's comment for why this is a background-image rather
   than a rotated border. */
.habit-circle-done::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 55%;
}
.habit-circle-failed {
  background: var(--color-overdue-bg);
  border-color: var(--color-danger);
}
.habit-circle-failed::after {
  content: "×";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-danger);
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1;
}

/* -- Every habit type gets this same disclosure now: name (+ value +
   a slim progress bar, Count/Duration only) is always visible in the
   summary; every daily action for the viewed date -- adjust buttons,
   Set, At Most's Done/Undo Done, and ✕ Fail/Undo Fail -- lives in the
   one revealed tray below (see habit_progress_controls.html), kept
   out of the collapsed card and out of the ••• configuration panel
   alike. The htmx adjust response only ever re-renders
   #habit-progress-value-<id> (this wrapper) and the OOB controls
   fragment below — <details>/<summary> themselves are never part of
   that response, so the browser's native open state survives an
   adjustment untouched (Simple's own circle tap is the one exception:
   it still re-renders the whole row, so opening ✕ Fail and then
   tapping the circle closes it again -- a pre-existing tradeoff of
   Simple's "whole row is simple enough to redraw" htmx response). -- */
.habit-progress { display: block; }
.habit-progress-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  cursor: pointer;
  list-style: none;
}
.habit-progress-summary::-webkit-details-marker { display: none; }
.habit-progress-value-wrap {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.habit-progress-value {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}
/* Restrained status distinction — reuses the app's existing "success"
   colour (the same green as a completed Simple habit/Task) rather than
   introducing a new palette, and leans on weight rather than hue to
   separate "reached" from "exceeded" so this stays a text-colour
   nuance, not a heatmap. in_progress (a target-less habit with a
   logged value) intentionally gets no reached/exceeded treatment —
   there's no target to have reached. */
.habit-progress-value.habit-status-partial,
.habit-progress-value.habit-status-in_progress {
  color: var(--color-text);
}
.habit-progress-value.habit-status-target_reached {
  color: var(--color-primary);
}
.habit-progress-value.habit-status-exceeded {
  color: var(--color-primary);
  font-weight: 600;
}
/* Failed overrides the reached/exceeded/partial colouring above (see
   habit_status()'s own precedence) -- the real value/target stay
   printed exactly as logged, only the colour and a small tag change,
   since marking a day failed must never destroy that number. */
.habit-progress-value.habit-status-failed { color: var(--color-danger); }
/* At Most: within_max/no_data read the same as partial/in_progress
   (informational, not yet judged); exceeded_max reuses the danger
   colour (Failing Forward-bound, distinct from explicit "failed" only
   in the tag text below); completed reuses the same success colour as
   target_reached, since both are explicit "this day is a win" states. */
.habit-progress-value.habit-status-within_max,
.habit-progress-value.habit-status-no_data {
  color: var(--color-text);
}
.habit-progress-value.habit-status-exceeded_max { color: var(--color-danger); }
.habit-progress-value.habit-status-completed { color: var(--color-primary); }
.habit-failed-tag {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.habit-completed-tag {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--color-primary);
}
/* The bar itself is capped at 100% width — "exceeded" is communicated
   by the fill's colour/glow, not a second bar or an overflowing one. */
.habit-progress-bar-track {
  width: 100%;
  max-width: 220px;
  height: 6px;
  border-radius: 999px;
  background: var(--color-badge-bg);
  overflow: hidden;
  box-shadow: var(--shadow-track-inset);
}
.habit-progress-bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--color-border);
  transition: width 300ms ease;
}
.habit-progress-bar-fill.habit-status-partial { background: var(--color-primary); opacity: 0.55; }
.habit-progress-bar-fill.habit-status-target_reached { background: var(--color-primary); }
.habit-progress-bar-fill.habit-status-exceeded {
  background: var(--color-primary);
  box-shadow: 0 0 5px color-mix(in srgb, var(--color-primary) 55%, transparent);
}
.habit-progress-bar-fill.habit-status-failed { background: var(--color-danger); }
.habit-progress-bar-fill.habit-status-within_max { background: var(--color-primary); opacity: 0.55; }
.habit-progress-bar-fill.habit-status-exceeded_max { background: var(--color-danger); }
.habit-progress-bar-fill.habit-status-completed { background: var(--color-primary); }

/* Every daily action for the viewed date -- adjust buttons, Set,
   At Most's Done/Undo Done, ✕ Fail/Undo Fail -- stays behind the
   title/body disclosure, hidden by default and relocated into view
   via :has() exactly like .habit-options-panel below, so opening it
   never moves anything else in the row. This used to be two separate
   groups (an always-visible "quick actions" tray and a
   disclosure-gated "less common actions" tray); they're merged into
   one revealed tray now that none of it belongs on the collapsed
   card. */
.habit-progress-controls {
  display: none;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.habit-item:has(.habit-progress[open]) .habit-progress-controls {
  display: flex;
  animation: reveal-fade-in 180ms ease;
}
/* Compact chips — same pill shape as .task-badge, sized up just enough
   to stay comfortably tappable. Adapted from .btn-quiet's colour so
   they read as part of the same control family as Set/Archive, not a
   third button language. */
.habit-adjust-button {
  min-height: 36px;
  padding: 0 var(--space-3);
  background: var(--color-badge-bg);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-primary);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity var(--transition-fast), transform var(--transition-fast), background var(--transition-fast);
}
.habit-adjust-button:hover { opacity: 0.75; }
.habit-adjust-button:active { transform: scale(0.96); }
/* Set reads as "confirm", not another increment chip. */
.habit-set-submit {
  background: var(--color-primary);
  color: white;
}
.habit-set-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.habit-set-form input {
  width: 4.5rem;
  min-height: 36px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0 var(--space-3);
  text-align: center;
}

/* -- Options ("edit" + Archive), a discreet kebab affordance so it
   never competes visually with completing/logging the habit.
   .habit-options holds *only* the trigger -- it never grows, resizes,
   or repositions when opened, so the kebab stays anchored exactly
   where it was. The revealed content lives in the separate
   .habit-options-panel sibling below, shown via :has() watching the
   details' open state -- the same relocate-the-panel technique
   already used for Task's edit panel (.task-edit/.task-edit-panel),
   so opening it can never move the trigger itself. */
.habit-options { flex: 0 0 auto; }
.habit-options[open] > .habit-options-trigger { background: var(--color-badge-bg); color: var(--color-primary); }
.habit-options-panel {
  display: none;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  /* A lighter rule than a full border -- enough to separate
     configuration from the card's own name/value row without reading
     as a hard section break in what's still one card, not a form
     bolted underneath it. */
  border-top: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
}
.habit-item:has(.habit-options[open]) .habit-options-panel {
  display: flex;
  animation: reveal-fade-in 180ms ease;
}
.habit-edit-form {
  display: flex;
  flex-direction: column;
  /* space-4 between groups (Basics / Goal / Schedule / Options /
     Save) -- .habit-edit-group below keeps each group's own fields at
     the tighter space-2 rhythm they had before this split, so the only
     visible change is the added breathing room *between* concepts,
     not within one. Options (Personal Challenge) and Save stay direct
     children (a group of one gains nothing from its own wrapper). */
  gap: var(--space-4);
  width: 100%;
}
.habit-edit-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.habit-edit-form label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
/* :not(.field-control) keeps this scoped to whatever hasn't migrated
   to the shared control (weekday/personal-challenge checkboxes here
   are never given that class) -- Name/target/unit box styling now
   comes from .field-control directly instead of this duplicate. */
.habit-edit-form input:not(.field-control) {
  min-height: var(--touch-target);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-3);
  font-size: 0.95rem;
}
/* Archive is the one remaining action-link in an otherwise pure
   configuration panel (Move up/down moved to the always-in-DOM
   keyboard fallback near the drag handle; Done/Fail moved to the
   daily-actions disclosure) -- a little extra space keeps it from
   reading as one more form field directly under Save. */
.habit-archive-action { margin-top: var(--space-1); }
.habit-unit-hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* -- Add habit: a compact, prominent action; opening it reveals a
   real card, not a bare stack of inputs. -- */
.habit-add { margin-top: var(--space-4); }
.habit-add-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-target);
  padding: var(--space-3);
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-primary);
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  list-style: none;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.habit-add-toggle::-webkit-details-marker { display: none; }
.habit-add-toggle:hover { background: var(--color-success-bg); border-color: var(--color-primary); }
.habit-add[open] > .habit-add-toggle { border-style: solid; background: var(--color-success-bg); }
/* The empty state's own primary CTA — same control, filled instead of
   dashed since there's nothing else on the page competing for attention. */
.habit-add-toggle-primary {
  border-style: solid;
  background: linear-gradient(180deg, var(--color-primary), var(--color-primary-hover));
  color: white;
}
.habit-add-toggle-primary:hover { background: linear-gradient(180deg, var(--color-primary), var(--color-primary-hover)); opacity: 0.92; }

.habit-add-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  animation: reveal-fade-in 180ms ease;
}
.habit-add-form > label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
/* :not(.field-control) is a safety net for any future text/number
   field added here without the class -- Name/target/unit/schedule
   fields already carry .field-control directly and get their box
   styling from there instead. */
.habit-add-form input[type="text"]:not(.field-control),
.habit-add-form input[type="number"]:not(.field-control) {
  min-height: var(--touch-target);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-3);
  font-size: 0.95rem;
}
.habit-type-fieldset {
  border: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.habit-type-fieldset legend {
  padding: 0;
  margin-bottom: 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.habit-type-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
/* Segmented control built from real radios: the input stays in place
   and fully operable (native keyboard/arrow-key group behaviour,
   screen-reader semantics unchanged) but is visually hidden in favour
   of its sibling label, styled by :has() -- same technique already
   used for the Recurring-checkbox reveal elsewhere in this app. */
.habit-type-option {
  position: relative;
  flex: 1 1 88px;
  min-width: 88px;
}
.habit-type-option input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.habit-type-option-label {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-text-muted);
  background: var(--color-surface);
  text-align: center;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.habit-type-option:has(input:checked) .habit-type-option-label {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: white;
}
/* The input itself is opacity:0, so a box-shadow/outline painted on it
   would be invisible too -- the ring goes on the visible sibling
   instead, keeping keyboard focus genuinely visible. */
.habit-type-option input:focus-visible + .habit-type-option-label {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.habit-target-field, .habit-unit-field { display: none; }
.habit-add-form:has(#habit-type-count:checked) .habit-target-field,
.habit-add-form:has(#habit-type-duration:checked) .habit-target-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.habit-add-form:has(#habit-type-count:checked) .habit-unit-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
/* The Edit panel's own .habit-target-field has no live "choose type"
   radio to watch -- habit_item.html only ever renders the div at all
   for a Count/Duration habit's own edit panel (Jinja, server-side), so
   there's nothing to gate here beyond undoing the blanket display:none
   above. Missing this was a real bug: the field rendered in the DOM
   but stayed invisible, silently reproducing the exact "goal_direction
   is locked" limitation Stage 5 set out to remove. */
.habit-edit-form .habit-target-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
/* Compact "Daily target [ At least ▾ ] [ 10 ]" combined direction +
   target control -- goal_direction and target are shown/hidden
   together (both irrelevant for Simple) since they're now one visual
   row rather than two separate fields/fieldsets. */
.habit-target-field-label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.habit-target-field-controls {
  display: flex;
  gap: var(--space-2);
}
/* Box styling now comes from .field-control (applied alongside this
   class in the templates) -- only the flex-row-specific sizing stays
   here. */
.habit-direction-select {
  flex: 1 1 auto;
  min-width: 0;
}
.habit-target-input { width: 6rem; flex: 0 0 auto; }
.habit-target-hint {
  margin: calc(-1 * var(--space-2)) 0 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* Schedule: shared by the Add form and every row's Edit panel alike.
   Matched by input[name/value] rather than by id, since an id-based
   :has() (like the measurement-type reveal above) would collide once
   the same radio names repeat across many per-row Edit panels on one
   page -- an attribute selector scopes correctly to each .habit-add-
   form/.habit-edit-form's own subtree with no per-row id scheme
   needed. */
.habit-schedule-weekdays-field, .habit-schedule-interval-field { display: none; }
.habit-add-form:has(input[name="schedule_type"][value="weekdays"]:checked) .habit-schedule-weekdays-field,
.habit-edit-form:has(input[name="schedule_type"][value="weekdays"]:checked) .habit-schedule-weekdays-field {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.habit-add-form:has(input[name="schedule_type"][value="interval"]:checked) .habit-schedule-interval-field,
.habit-edit-form:has(input[name="schedule_type"][value="interval"]:checked) .habit-schedule-interval-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.habit-weekday-option {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 46px;
  font-size: 0.85rem;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
/* Generic fallback: still what the Add form's plain <label> (no
   .habit-schedule-interval-every/-start class -- that split is the
   Edit panel's own polish, see habit_item.html) relies on for its
   one combined "Every N days, starting" line + a bare date input
   below it. */
.habit-schedule-interval-field label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
/* Box styling for both now comes from .field-control (applied
   alongside in the templates) -- only the number field's narrower
   width is still page-specific. */
.habit-schedule-interval-field input[type="number"] { width: 4rem; }
/* Edit panel only: "Every [N] days" / "Starting [date]" as two
   aligned inline rows instead of one field-control-styled input
   attached to the tail of the first sentence and a second, full-width
   date box floating unlabeled underneath it -- the date now reads as
   its own clearly-labeled control the same width/shape as its
   sibling, not an orphaned full-width box. */
.habit-schedule-interval-field .habit-schedule-interval-every,
.habit-schedule-interval-field .habit-schedule-interval-start {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.habit-schedule-interval-input { width: 4rem; flex: 0 0 auto; }
.habit-schedule-interval-date { max-width: 160px; flex: 0 0 auto; }

/* Personal Challenge: a restrained inline marker on the name, not a
   redesign of the card -- same treatment in the Add form and every
   row's Edit panel. */
.habit-challenge-badge {
  font-size: 0.85em;
  color: var(--color-text-muted);
}
/* One compact, left-aligned row: checkbox beside "Personal Challenge"
   beside its own muted one-line explanation underneath -- not the
   generic small/muted field-label treatment every other .habit-edit-
   form/.habit-add-form label gets, and not that rule's column
   direction either (this needs the extra specificity to win over
   both -- .habit-edit-form label / .habit-add-form > label already
   set flex-direction: column at equal-or-higher specificity). The
   whole row is one <label>, so the entire thing -- checkbox, title,
   and hint -- is one generous tap target, not just the 20px box. */
.habit-edit-form .habit-challenge-field,
.habit-add-form .habit-challenge-field {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  font-size: 0.95rem;
  color: var(--color-text);
  cursor: pointer;
}
.habit-challenge-field input[type="checkbox"] {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 2px;
}
.habit-challenge-field-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.habit-challenge-hint {
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--color-text-muted);
}

/* -- Archived: available, but visually secondary and pushed to the
   bottom of the page. -- */
/* -- Failing Forward: explicit failures, moved out of the main list
   into their own collapsed disclosure rather than styled loudly in
   place. Always rendered (even at zero) so its inner id-targeted
   fragments always have somewhere to land via OOB after a crossing
   action -- see app/routers/habits.py's _group_sections_oob_html. -- */
.habit-failing-forward { margin-top: var(--space-3); }
.habit-failing-forward-toggle {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  cursor: pointer;
  list-style: none;
}
.habit-failing-forward-toggle::-webkit-details-marker { display: none; }
.habit-failing-forward-toggle:hover { color: var(--color-danger); }

.habit-archived { margin-top: var(--space-3); }
.habit-archived-toggle {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  cursor: pointer;
  list-style: none;
}
.habit-archived-toggle::-webkit-details-marker { display: none; }
.habit-archived-toggle:hover { color: var(--color-primary); }
.habit-archived-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.habit-archived-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  background: var(--color-badge-bg);
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

/* ---------- Settings: account / Switch user ----------
   Moved out of the header (base.html) into Settings -- same .logout-
   form/.btn-quiet markup and behavior (POST /logout), just a plain
   labelled row instead of the header's compact truncated name. */
.settings-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.settings-account-label {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}
.settings-account-label strong { color: var(--color-text); font-weight: 600; }
.settings-account .logout-form { margin: 0; }

/* ---------- Health ---------- */
.health-view { display: flex; flex-direction: column; }
.health-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: var(--shadow-card);
  margin-bottom: var(--space-4);
}
.health-card-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.health-card-heading {
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 var(--space-3);
}
.health-card-header .health-card-heading { margin-bottom: 0; }
.health-card-subtext {
  font-size: 0.85rem;
  color: var(--color-text-muted);
}
.health-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.health-measurements-grid,
.health-running-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--space-3);
}
.health-notes-field { max-width: none; }
.health-notes-field textarea { min-height: 60px; }
.health-save-button { width: auto; align-self: flex-start; padding: 0 var(--space-5); }

.health-bp-field { grid-column: 1 / -1; }
.health-bp-inputs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: var(--space-2);
}
.health-bp-note { margin-top: var(--space-2); }

.health-duration-field { grid-column: 1 / -1; }
.duration-input-group {
  display: flex;
  gap: var(--space-2);
  max-width: 320px;
}
.duration-input-part {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0 var(--space-2);
  background: var(--color-surface);
}
.duration-input-part .field-control.duration-input {
  border: none;
  padding: var(--space-2) 0;
  min-width: 0;
  text-align: right;
  background: transparent;
}
.duration-input-part .field-control.duration-input:focus {
  outline: none;
  box-shadow: none;
}
.duration-input-unit {
  color: var(--color-text-muted);
  font-size: 0.8rem;
  flex-shrink: 0;
}
@media (max-width: 420px) {
  .duration-input-group { max-width: none; }
  .duration-input-part { padding: 0 var(--space-1); gap: var(--space-1); }
}

.health-recent-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.health-recent-item-row:last-child .health-recent-item { border-bottom: none; }
.health-recent-item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.92rem;
}
.health-recent-label { font-weight: 600; flex: 0 0 auto; }
.health-recent-value { color: var(--color-text-muted); flex: 1 1 auto; }
.health-recent-date { color: var(--color-text-muted); font-size: 0.8rem; flex: 0 0 auto; }
.health-recent-note {
  margin: calc(-1 * var(--space-1)) 0 var(--space-1);
  padding: 0 0 var(--space-2);
  font-size: 0.82rem;
  color: var(--color-text-muted);
  font-style: italic;
  border-bottom: 1px solid var(--color-border);
}
.health-recent-delete,
.recent-activity-undo { margin: 0; flex-shrink: 0; }
.health-recent-item-deleted { color: var(--color-text-muted); font-style: italic; }

.health-trends-card {
  background: var(--color-badge-bg);
  border-style: dashed;
  text-align: center;
}
.health-trends-copy {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 0.9rem;
}

/* ---------- Finances ---------- */
.finances-view { display: flex; flex-direction: column; }
.finances-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.finances-header-row .page-heading { margin: 0; }

/* Same month/date-navigator shape as .habits-date-nav -- its own class
   names since Finances navigates calendar months, not days. */
.finances-month-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}
.finances-month-label {
  margin: 0;
  font-weight: 600;
  min-width: 160px;
  text-align: center;
}
.finances-month-arrow {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--color-text);
  font-size: 1.1rem;
  text-decoration: none;
  transition: background var(--transition-fast);
}
.finances-month-arrow:hover { background: var(--color-badge-bg); }
.finances-jump-current {
  display: block;
  text-align: center;
  font-size: 0.85rem;
  margin-bottom: var(--space-3);
}

.finances-summary-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.finances-summary-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.finances-summary-label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
}
.finances-summary-value {
  font-size: 1.15rem;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 420px) {
  .finances-summary-grid { grid-template-columns: 1fr; }
}

/* Shared incoming/outgoing colour + sign convention -- used on summary
   tiles, category breakdown, and every transaction row's amount.
   Colour is never the only signal: templates always pair these with a
   literal +/− sign or a text type badge (see .finance-type-badge). */
.finances-amount-in { color: var(--color-primary); }
.finances-amount-out { color: var(--color-danger); }
.finances-amount-neutral { color: var(--color-text-muted); }

.finances-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: var(--shadow-card);
  margin-bottom: var(--space-4);
}
.finances-card-heading {
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 var(--space-3);
}

/* Spending/Income-by-category headings as real <details>/<summary>
   disclosures (see .disclosure-caret above for the shared triangle
   mechanics) -- the whole row is the tap target, understated rather
   than a loud button, with the category total shown opposite the
   title so a collapsed section still communicates its figure. */
.finances-disclosure-summary {
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}
.finances-disclosure[open] > .finances-disclosure-summary { margin-bottom: var(--space-3); }
.finances-disclosure-title { display: flex; align-items: center; }
/* Keep the disclosure title aligned while pushing the total right. */
.finances-disclosure-total { flex-shrink: 0; margin-left: auto; padding-left: var(--space-3); font-weight: 600; }
.finances-breakdown-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.finances-breakdown-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
  transition: background-color var(--transition-fast);
}
.finances-breakdown-row:hover { background: var(--color-badge-bg); }
.finances-breakdown-row-active {
  background: var(--color-badge-bg);
  font-weight: 600;
}
.finances-breakdown-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.finances-breakdown-amount { flex-shrink: 0; color: var(--color-text-muted); }

/* Secondary detail area on the monthly summary -- explains the primary
   Incoming/Outgoing/Net tiles above (reimbursements, credit card
   spend/payments) as quiet label/value rows grouped under subheadings,
   deliberately lower visual weight than the primary KPI cards. */
.finances-detail-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.finances-detail-group {
  flex: 1 1 220px;
  min-width: 0;
}
.finances-detail-group-heading {
  margin: 0 0 var(--space-1);
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
}
.finances-detail-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  border-top: 1px solid var(--color-border);
  font-size: 0.88rem;
}
.finances-detail-row:first-of-type { border-top: none; }
.finances-detail-label { color: var(--color-text-muted); }
.finances-detail-value {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 420px) {
  .finances-detail-card { flex-direction: column; gap: var(--space-3); }
  /* flex-direction:column makes flex-basis apply to height, not width --
     the 220px basis inherited from the row layout above otherwise forces
     each group to a ~220px-tall box on phones regardless of its actual
     (much shorter) content, leaving huge empty gaps. auto lets each
     group size to its own content instead. */
  .finances-detail-group { flex: 1 1 auto; }
}

.finances-empty-state { }
.finances-empty-title {
  margin: 0 0 var(--space-1);
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--color-text);
}
.finances-empty-cta { display: inline-flex; width: auto; margin-top: var(--space-3); padding: 0 var(--space-5); }

.finances-categories-link { text-align: center; margin-top: var(--space-2); }

/* -- Transaction list -- */
.finance-txn-list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.finance-txn-row {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  box-shadow: var(--shadow-card);
}
.finance-txn-main {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-3);
}
.finance-txn-date {
  flex: 0 0 auto;
  width: 44px;
  color: var(--color-text-muted);
  font-size: 0.8rem;
  font-weight: 600;
  text-align: center;
  padding-top: 2px;
}
.finance-txn-body { flex: 1 1 auto; min-width: 0; }
.finance-txn-description {
  margin: 0 0 2px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.finance-txn-meta {
  margin: 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.finance-txn-amount {
  flex: 0 0 auto;
  order: 4;
  font-weight: 700;
  white-space: nowrap;
}
.finance-type-badge {
  display: inline-flex;
  align-items: center;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 1px var(--space-2);
  border-radius: 999px;
  background: var(--color-badge-bg);
  color: var(--color-text-muted);
}
.finance-type-badge-unclassified { font-style: italic; }
.finance-txn-note {
  margin: var(--space-1) 0 0;
  font-size: 0.82rem;
  color: var(--color-text-muted);
  font-style: italic;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.finance-txn-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
}
.finance-txn-controls .field-control {
  min-height: 34px;
  font-size: 0.82rem;
  width: auto;
  max-width: 100%;
  flex: 1 1 160px;
}
/* Category/Type auto-submit on change and must stay in their own
   <form>, separate from the note <form> -- sharing one <form> would
   mean changing Category/Type also submits (and silently saves)
   whatever unsaved draft is sitting in the note textarea. */
.finance-txn-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  flex: 1 1 auto;
  margin: 0;
}
/* The note form/editor lives in the header row (.finance-txn-main),
   immediately left of the amount, as a compact icon that opens a
   <details> disclosure -- not a permanent "+ Add note" line -- so a
   transaction without a note doesn't grow the card. When opened, the
   field editor breaks out to its own full-width line below the row
   via :has() rather than growing the icon's own flex item. */
.finance-txn-note-form { order: 3; flex: 0 0 auto; margin: 0; }
.finance-txn-note-form:has(> details[open]) {
  flex: 1 1 100%;
  order: 5;
  margin-top: var(--space-1);
}
.finance-txn-note-editor { list-style: none; }
.finance-txn-note-icon-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  list-style: none;
  transition: background-color var(--transition-fast), color var(--transition-fast);
}
.finance-txn-note-icon-button::-webkit-details-marker { display: none; }
.finance-txn-note-icon-button svg { width: 15px; height: 15px; }
.finance-txn-note-icon-button:hover { background: var(--color-badge-bg); color: var(--color-text); }
.finance-txn-note-icon-button.has-note { color: var(--color-primary); }
.finance-txn-note-editor[open] > .finance-txn-note-icon-button { background: var(--color-badge-bg); color: var(--color-primary); }
.finance-txn-note-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-1);
  align-items: flex-start;
}
.finance-txn-note-input {
  flex: 1 1 200px;
  height: auto;
  min-height: 44px;
  font-size: 0.85rem;
  padding: var(--space-2);
}
.finance-txn-note-save { flex: 0 0 auto; }
@media (max-width: 420px) {
  .finance-txn-main { gap: var(--space-2); }
  .finance-txn-date { width: 36px; font-size: 0.75rem; }
  .finance-txn-note-fields { flex-direction: column; align-items: stretch; }
}

/* -- Import flow -- */
.finances-import-dropzone {
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5) var(--space-4);
  text-align: center;
  background: var(--color-surface);
}
.finances-import-error {
  color: var(--color-danger);
  background: var(--color-overdue-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
  margin-bottom: var(--space-3);
}
.finances-preview-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.finances-preview-stat {
  background: var(--color-badge-bg);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  text-align: center;
}
.finances-preview-stat-value { display: block; font-size: 1.2rem; font-weight: 700; }
.finances-preview-stat-label {
  display: block;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.finances-preview-accounts,
.finances-preview-errors {
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
  font-size: 0.88rem;
}
.finances-preview-accounts li,
.finances-preview-errors li {
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--color-border);
}
.finances-preview-accounts li:last-child,
.finances-preview-errors li:last-child { border-bottom: none; }
.finances-new-account-badge {
  display: inline-block;
  margin-left: var(--space-2);
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--color-primary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.finances-import-actions { display: flex; gap: var(--space-3); margin-top: var(--space-3); }
.finances-import-actions .btn-primary { width: auto; padding: 0 var(--space-5); }

/* -- Category management -- */
.finances-category-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.finances-category-row:last-child { border-bottom: none; }
.finances-category-archived { color: var(--color-text-muted); font-style: italic; }
.finances-category-count { color: var(--color-text-muted); font-size: 0.85rem; margin-right: var(--space-2); }
.finances-category-actions { display: flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.finances-category-add-form {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
.finances-category-add-form .field-control { flex: 1 1 auto; }
.finances-category-add-form .btn-primary { width: auto; padding: 0 var(--space-4); }
