/* Dark-mode-first: the app is opened at night. Colors live as custom
   properties on :root so there's one place to change them; the light
   scheme is an override, not the default. */
:root {
  --bg: #111111;
  --bg-elevated: #1c1c1e;
  --fg: #f2f2f2;
  --fg-muted: #9a9a9a;
  --accent: #3478f6;
  --border: #2e2e30;
  --nav-height: 56px;
  --danger: #ff6b6b;
  /* Step 2.1b: "good"/"bad" verdict colours (CONTRACT-2.1b.md §5). --bad
     is an alias for --danger, not a duplicate literal, so the two colours
     can never drift apart. Neutral reuses the existing --fg-muted — no
     new token needed for it. */
  --good: #34c759;
  --bad: var(--danger);
  --good-bg: rgba(52, 199, 89, 0.18);
  --bad-bg: rgba(255, 107, 107, 0.18);
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f5f5f7;
    --bg-elevated: #ffffff;
    --fg: #111111;
    --fg-muted: #6b6b6f;
    --accent: #3478f6;
    --border: #dcdcdf;
    --danger: #d9362e;
    --good: #1f9c46;
    --good-bg: rgba(31, 156, 70, 0.14);
    --bad-bg: rgba(217, 54, 46, 0.14);
  }
}

* {
  box-sizing: border-box;
}

html, body {
  height: 100%;
  margin: 0;
}

/* Fix (device-reported 2026-08-22): installed standalone PWA on iOS showed
   a visible band of mismatched color below the fixed bottom nav. `html` had
   no background of its own, so `body`'s color was propagating to the canvas
   underneath it; and `height: 100%` resolves against the layout viewport,
   which under `viewport-fit=cover` can be shorter than the physical screen,
   leaving a gap beyond it that paints as canvas color. Giving `html` the
   *elevated* color (matching #nav's background) makes that gap invisible
   instead of a seam. This is a hypothesis pending device re-verification —
   #nav's own rules (fixed position, safe-area padding) are already correct
   and were ruled out as the cause. If banding persists after this, look at
   the layout-viewport height itself next (e.g. `100dvh` or
   `-webkit-fill-available` on `html, body`), not the nav rules. */
html {
  background: var(--bg-elevated);
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: -apple-system, system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

header {
  padding: 16px;
  padding-top: calc(16px + env(safe-area-inset-top, 0px));
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
  border-bottom: 1px solid var(--border);
}

header h1 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
}

/* Step D.6 — global "unsent writes" indicator. Sits under the title in the
   shell so it is visible from every route, including the detail screens
   where an offline log is most likely to be made.
   `[hidden]` needs the explicit display:none because the rule below would
   otherwise win on specificity. */
.outbox-status {
  margin: 6px 0 0;
  font-size: 13px;
  font-weight: 500;
  /* Amber, not red: nothing is lost — the write is queued and persisted, it
     just has not reached the server yet. Red would read as an error and
     invite the user to re-log a value that is already safely captured. */
  color: #b26a00;
}

.outbox-status[hidden] {
  display: none;
}

@media (prefers-color-scheme: dark) {
  .outbox-status {
    color: #ffb340;
  }
}

/* Step 5.1 — offline indicator, same placement/shape rules as
   .outbox-status above (it sits directly below it in the shell). Muted
   rather than alarming: it states a fact ("showing last saved data"), not
   an error — nothing is broken and nothing is lost. Uses the existing
   --fg-muted token (already flips for the light-scheme override above),
   so no new custom property or dark-mode block is needed. */
.net-status {
  margin: 6px 0 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--fg-muted);
}

.net-status[hidden] {
  display: none;
}

#app {
  flex: 1;
  padding: 16px;
  padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px) + 16px);
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
}

#app h1 {
  font-size: 22px;
  margin: 0 0 12px;
}

#app a {
  color: var(--accent);
}

/* Bottom nav: fixed to the viewport bottom, padded for the iPhone home
   indicator (safe-area-inset-bottom) so content isn't hidden under it. */
#nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* Step D.7: an element given its own explicit `display` (the `display:
   flex` above) loses the user-agent's `[hidden] { display: none }` rule —
   the author rule wins on specificity, so `#nav.hidden = true` (main.js's
   sign-out gate) would otherwise set the attribute while the bar stays
   visibly on screen. This rule wins the fight back and is what keeps the
   gate honest. */
#nav[hidden] {
  display: none;
}

#nav a {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  color: var(--fg-muted);
  text-decoration: none;
  font-size: 13px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

#nav a[aria-current="page"] {
  color: var(--accent);
  font-weight: 600;
}

button {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  font-size: 16px;
  padding: 12px 20px;
  border-radius: 12px;
  border: none;
  background: var(--accent);
  color: #fff;
  min-height: 44px;
}

/* Home: trackable list + quick-log (Step 2.1) --------------------------- */

.home {
  display: block;
  width: 100%;
}

.home-offline,
.home-empty {
  margin: 0 0 12px;
  color: var(--fg-muted);
  font-size: 14px;
}

.home-empty a {
  color: var(--accent);
}

.tlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Direct children only — the DOM shape here is fixed by the interface
   contract (no extra wrapper elements), so the row is laid out purely
   with flex-wrap + `order` on siblings rather than nested containers. */
.trow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 10px;
  row-gap: 4px;
  width: 100%;
  min-width: 0;
  padding: 12px 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  /* The verdict border-left below adds its own visible edge; a slightly
     smaller base radius on that side keeps the tint from poking past the
     rounded corner. */
  border-left-width: 4px;
  border-radius: 12px;
}

.trow[data-state="pending"] {
  opacity: 0.65;
}

.trow[data-state="failed"] {
  border-color: var(--danger);
}

/* Step 2.1b good/bad/neutral verdict (CONTRACT-2.1b.md §5). A green check
   means "today is good", not "logged" — see home-model.js verdict(). The
   coloured left edge plus a background tint is the "substantially more
   prominent" fix for the user's explicit complaint that checked/unchecked
   was too subtle before; colour is never the only cue (WCAG 1.4.1) —
   .trow-value's word and .trow-symbol's shape carry the same meaning. */
.trow[data-verdict="good"] {
  border-left-color: var(--good);
  background: linear-gradient(0deg, var(--good-bg), var(--good-bg)), var(--bg-elevated);
}

.trow[data-verdict="bad"] {
  border-left-color: var(--bad);
  background: linear-gradient(0deg, var(--bad-bg), var(--bad-bg)), var(--bg-elevated);
}

.trow[data-verdict="neutral"] {
  border-left-color: var(--border);
}

/* Failed state still wins visually over a stale verdict tint. */
.trow[data-state="failed"] {
  border-left-color: var(--danger);
}

/* Step 2.5: the trackable's identity colour (which thing is this),
   carried ONLY by this icon's tint — never by data-verdict, which colours
   the row border/background and .trow-symbol instead. See
   CONTRACT-2.5.md §0 "the channel rule". color is set inline per-row from
   the trackable's stored colour (home.js); when absent this falls back to
   the same muted tone the symbol ring uses, so an uncoloured trackable
   still reads as a coherent (if neutral) icon. */
.trow-icon {
  order: -1;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg-muted);
}

.trow-icon svg {
  width: 22px;
  height: 22px;
}

.trow-symbol {
  order: 0;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 2px solid var(--fg-muted);
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  color: transparent;
}

/* Glyphs are plain Unicode text characters rendered via CSS `content` on
   a pseudo-element, styled with `color`/`border` custom properties — NOT
   an emoji font, which renders inconsistently across iOS versions. */
.trow-symbol[data-symbol="check"] {
  border-color: var(--good);
  background: var(--good-bg);
  color: var(--good);
}

.trow-symbol[data-symbol="check"]::before {
  content: '\2713'; /* U+2713 CHECK MARK */
}

.trow-symbol[data-symbol="cross"] {
  border-color: var(--bad);
  background: var(--bad-bg);
  color: var(--bad);
}

.trow-symbol[data-symbol="cross"]::before {
  content: '\2715'; /* U+2715 MULTIPLICATION X */
}

/* "empty" renders no glyph at all — the plain outline ring (border +
   border-radius: 50% above) IS the "empty outline circle" the contract
   asks for. */
.trow-symbol[data-symbol="empty"] {
  border-color: var(--fg-muted);
  background: transparent;
}

.trow-name {
  order: 1;
  flex: 1 1 55%;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--fg);
  font-weight: 600;
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.trow-value {
  order: 2;
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
}

/* A logged numeric value keeps the existing accent tint on .trow-value
   itself — Step 2.4 defect 5's good/bad verdict for a bounded numeric is
   carried by the row's border/background and by .trow-status below, not
   by recolouring this span (most numerics still have no target/bounds
   and stay 'neutral', see home-model.js verdict()). Boolean rows are
   coloured by verdict directly instead, since 'logged' no longer implies
   'good' there. */
.trow[data-shape="numeric"][data-logged="true"] .trow-value {
  color: var(--accent);
}

.trow[data-shape="boolean"][data-verdict="good"] .trow-value {
  color: var(--good);
}

.trow[data-shape="boolean"][data-verdict="bad"] .trow-value {
  color: var(--bad);
}

/* Step 2.4 defect 5: the non-colour cue (WCAG 1.4.1) for a bounded
   numeric's good/bad verdict. Same order as .trow-value so the two sit
   together — ties in flex `order` keep document order, and this span is
   always appended right after .trow-value in the DOM (home.js). The
   coloured border/background from `.trow[data-verdict]` above already
   applies to numeric rows now that verdict() can return good/bad for
   them; this adds the required non-colour word on top of it. */
.trow-status {
  order: 2;
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 600;
}

.trow[data-verdict="good"] .trow-status {
  color: var(--good);
}

.trow[data-verdict="bad"] .trow-status {
  color: var(--bad);
}

.trow-direction {
  order: 3;
  flex: 0 0 auto;
  color: var(--fg-muted);
  font-size: 12px;
  font-style: italic;
  white-space: nowrap;
}

.trow-hint {
  order: 4;
  flex: 1 1 100%;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--fg-muted);
  font-size: 13px;
}

.trow-log {
  order: 5;
  margin-left: auto;
  min-height: 44px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.trow-editor {
  order: 6;
  flex: 1 1 100%;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

.trow-input {
  flex: 1 1 100px;
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  font-size: 16px; /* keeps iOS Safari from auto-zooming on focus */
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.trow-save {
  min-height: 44px;
}

.trow-cancel {
  min-height: 44px;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
}

.trow-error {
  order: 7;
  flex: 1 1 100%;
  min-width: 0;
  overflow-wrap: anywhere;
  margin: 2px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.home-new {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 12px;
  min-height: 44px;
  padding: 0 16px;
  border-radius: 12px;
  border: 1px solid var(--border);
  color: var(--accent);
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Create / edit a trackable (Step 2.2) ----------------------------------- */

.tform {
  display: block;
  width: 100%;
}

.tform-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

.tform-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  min-width: 0;
}

/* DEFECT 1 fix (CONTRACT-2.4.md §3): an AUTHOR stylesheet rule (the
   `.tform-field { display: flex }` above) always beats the browser's
   built-in `[hidden] { display: none }`, regardless of specificity — so
   without this rule, setting `wrap.hidden = true` sets the attribute but
   the field stays visibly on screen (and a "hidden" numeric input can
   still raise the iOS keyboard on tap). This rule exists ONLY to win back
   that fight. Removing it silently un-hides every progressively-disclosed
   field in the trackable form. See trackable.js buildField() for the
   second half of this fix (disabling the controls inside a hidden
   wrapper, so input is blocked even if this rule is ever deleted). */
.tform-field[hidden] {
  display: none;
}

.tform-field > label,
.tform-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
}

.tform-input,
.tform-select {
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  font-size: 16px; /* keeps iOS Safari from auto-zooming on focus */
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.tform-select {
  /* Native select affordance; padding matches .tform-input so both line
     up when they appear back-to-back in a numeric trackable's form. */
  padding-right: 8px;
}

/* DEFECT 2 fix (CONTRACT-2.4.md §4): without align-items, this defaults
   to stretch, so the fixed-height radio lands at the top of the 44px row
   while the label's own align-items: center centres its text — the two
   read at different heights. */
.tform-radio-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 20px;
}

/* Each radio + its label is one flex item (see trackable.js radioField())
   so a wrap can never split a radio from its own label, and the pair is
   centred against itself directly rather than relying on the group's
   cross-axis alignment. */
.tform-radio-pair {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.tform-radio-group input[type="radio"] {
  width: 22px;
  height: 22px;
  accent-color: var(--accent);
}

.tform-field input[type="checkbox"] {
  width: 22px;
  height: 22px;
  margin: 0 8px 0 0;
  vertical-align: middle;
  accent-color: var(--accent);
}

.tform-radio-group label {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--fg);
  font-size: 15px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.tform-field[data-field="bounds_enabled"] {
  flex-direction: row;
  align-items: center;
}

.tform-field[data-field="bounds_enabled"] label {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--fg);
  font-size: 15px;
}

/* Step 2.5: the icon picker. Same visually-collapsed-radio pattern as
   .tform-color-group below. The grid's own `color` is set inline from the
   currently-selected colour swatch (trackable.js iconField()) so every
   glyph in it previews the icon+colour combination together before
   saving — see CONTRACT-2.5.md §3.3. Bounded height + vertical scroll so
   ~54 options don't push the rest of the form far down the page; the page
   itself must never scroll horizontally at 390px, which this grid (flex
   + wrap, no fixed widths wider than the viewport) does not risk. */
.tform-icon-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  max-height: 220px;
  overflow-y: auto;
  padding: 6px;
  border-radius: 12px;
  border: 1px solid var(--border);
}

.tform-icon-grid input[type="radio"] {
  /* Same rationale as .tform-color-group input[type="radio"] below: kept
     native (not display:none) for keyboard/AT focusability, just visually
     collapsed — the label carries the visible affordance. */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.tform-icon-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-width: 44px;
  min-height: 44px;
  border-radius: 10px;
  border: 2px solid transparent;
  background: var(--bg-elevated);
  color: inherit;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.tform-icon-glyph {
  width: 24px;
  height: 24px;
  display: flex;
}

.tform-icon-glyph svg {
  width: 100%;
  height: 100%;
}

/* Same unmistakable-selection treatment as the colour swatch fix in Step
   2.4 (CONTRACT-2.4.md §8) — an outline clearly separated from the option,
   not colour alone (WCAG 1.4.1). outline-offset is 2px here (vs 3px for
   the colour swatch) per CONTRACT-2.5.md §3.3. */
.tform-icon-grid input[type="radio"]:checked + .tform-icon-option {
  outline: 3px solid var(--fg);
  outline-offset: 2px;
}

.tform-icon-grid input[type="radio"]:focus-visible + .tform-icon-option {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.tform-color-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.tform-color-group input[type="radio"] {
  /* The swatch label carries the visible affordance; the radio itself is
     kept native (not display:none) for keyboard/AT focusability, just
     visually collapsed. */
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.tform-color-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-width: 44px;
  min-height: 44px;
  border-radius: 50%;
  border: 2px solid transparent;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* DEFECT 6 fix (CONTRACT-2.4.md §8): the selection WAS tracked correctly
   already — the problem was purely that a 2px border flush on the
   circle's own edge is too weak an affordance to notice on a phone. The
   selected swatch must now differ from the unselected ones by more than
   colour: a ring clearly separated from the swatch (outline +
   outline-offset, not a border sitting on the edge) plus a check glyph
   centred inside it, drawn via CSS `content` — matching how
   .trow-symbol draws its check — NOT emoji. */
.tform-color-group input[type="radio"]:checked + .tform-color-swatch {
  outline: 3px solid var(--fg);
  outline-offset: 3px;
}

.tform-color-group input[type="radio"]:checked + .tform-color-swatch::after {
  content: '\2713'; /* U+2713 CHECK MARK */
  color: #fff;
  font-size: 18px;
  font-weight: 700;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
}

/* The radio itself stays focusable-but-visually-collapsed (see the
   comment on `.tform-color-group input[type="radio"]` above); the
   keyboard focus ring is drawn on the swatch it labels instead. */
.tform-color-group input[type="radio"]:focus-visible + .tform-color-swatch {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.tform-save,
.tform-archive,
.tform-archive-confirm,
.tform-archive-cancel {
  min-height: 44px;
}

.tform-cancel {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  color: var(--fg-muted);
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.tform-archive {
  margin-top: 16px;
  background: transparent;
  color: var(--danger);
  border: 1px solid var(--danger);
}

.tform-confirm-archive {
  margin-top: 16px;
  padding: 12px;
  border-radius: 12px;
  border: 1px solid var(--danger);
  background: var(--bad-bg);
  color: var(--fg);
  font-size: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.tform-confirm-archive .tform-archive-confirm {
  background: var(--danger);
  color: #fff;
}

.tform-confirm-archive .tform-archive-cancel {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
}

.tform-error {
  margin: 4px 0 0;
  color: var(--danger);
  font-size: 13px;
}

/* Trackable detail screen shell (Step 2.3) ------------------------------- */

.detail {
  display: block;
  width: 100%;
}

.detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 10px;
  row-gap: 6px;
  margin: 0 0 16px;
}

/* Step 2.5: same tinted-icon treatment as .trow-icon, at a larger size
   for the detail header. Tinted by the trackable's colour only — this
   screen doesn't even compute a verdict, so there is nothing for it to
   conflict with. No explicit `order` needed: this is appended first in
   detail.js and detail-head's other children carry no order of their own,
   so DOM order already puts it first. */
.detail-icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg-muted);
}

.detail-icon svg {
  width: 32px;
  height: 32px;
}

.detail-name {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 20px;
  font-weight: 700;
}

.detail-unit,
.detail-direction {
  flex: 0 0 auto;
  color: var(--fg-muted);
  font-size: 13px;
  white-space: nowrap;
}

.detail-edit {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  color: var(--accent);
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.detail-ranges {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
}

.detail-range {
  min-height: 44px;
  min-width: 44px;
  padding: 0 16px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  font-size: 14px;
  font-weight: 500;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.detail-range[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

.detail-range:disabled {
  opacity: 0.6;
}

.detail-count {
  margin: 0 0 16px;
  color: var(--fg-muted);
  font-size: 13px;
}

.chart-slot {
  width: 100%;
  min-width: 0;
  margin: 0 0 14px;
  padding: 16px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
}

.chart-slot-title {
  margin: 0 0 8px;
  font-size: 15px;
  font-weight: 600;
}

.chart-slot-placeholder {
  margin: 0;
  color: var(--fg-muted);
  font-size: 13px;
}

/* Step 3.2b (CONTRACT-3.2b.md §5, fixing U1): shown instead of a slot's
   chart/placeholder until the first entries load settles. min-height is
   set per-slot via the data-slot attribute already on .chart-slot, close
   to that slot's real rendered height (heatmap .hm-grid + .hm-head/
   .hm-weekdays ≈ 300px; .weekly-canvas-wrap is a fixed 200px), so the
   page does not jump when real content replaces this. */
.chart-slot-loading {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  color: var(--fg-muted);
  font-size: 13px;
}

.chart-slot[data-slot="heatmap"] .chart-slot-loading {
  min-height: 300px;
}

.chart-slot[data-slot="weekly"] .chart-slot-loading {
  min-height: 200px;
}

.chart-slot[data-slot="bounds"] .chart-slot-loading {
  min-height: 200px;
}

/* Step 3.4: the overlay picker is a row of chips, much shorter than a
   chart canvas — a tall shared min-height here would leave a big empty
   gap on first load. */
.chart-slot[data-slot="overlay"] .chart-slot-loading {
  min-height: 60px;
}

.detail-offline {
  margin: 0 0 12px;
  color: var(--fg-muted);
  font-size: 14px;
}

.detail-error {
  margin: 4px 0 0;
  color: var(--danger);
  font-size: 13px;
}

/* Calendar heatmap (Step 3.1) --------------------------------------------
   Cell hue comes from the verdict (good/bad) where there is an honest
   verdict, and from the trackable's own identity colour (--hm-identity)
   where verdict is neutral (an unbounded numeric day) — see
   CONTRACT-3.1.md §0. Magnitude is a second, independent channel: alpha
   on the nested .hm-fill element, never on .hm-cell itself — fading the
   cell would fade the day number too and destroy legibility. A nested
   fill element with opacity is used precisely so no color-mix() or other
   modern colour function is needed. */

.heatmap {
  width: 100%;
}

.hm-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 10px;
}

.hm-nav {
  min-height: 44px;
  min-width: 44px;
  padding: 0 10px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  font-size: 18px;
  line-height: 1;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.hm-nav:disabled {
  opacity: 0.4;
}

.hm-month {
  flex: 1 1 auto;
  text-align: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--fg);
}

.hm-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  margin: 0 0 4px;
}

.hm-weekday {
  text-align: center;
  font-size: 11px;
  color: var(--fg-muted);
}

.hm-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.hm-cell {
  position: relative;
  aspect-ratio: 1;
  min-height: 40px;
  border: none;
  border-radius: 8px;
  padding: 0;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

button.hm-cell {
  cursor: pointer;
}

.hm-fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--hm-fill-color);
  opacity: var(--hm-alpha, 0);
}

.hm-day {
  position: relative; /* above the fill */
  font-size: 12px;
  color: var(--fg);
}

.hm-cell[data-verdict="good"] {
  --hm-fill-color: var(--good);
}

.hm-cell[data-verdict="bad"] {
  --hm-fill-color: var(--bad);
}

.hm-cell[data-verdict="neutral"] {
  --hm-fill-color: var(--hm-identity, var(--accent));
}

/* Non-colour cue (WCAG 1.4.1): a 'bad' cell must differ from 'good' by
   more than hue. The day number stays visible either way, and the full
   reading is always in the cell's aria-label. */
.hm-cell[data-verdict="bad"] .hm-fill {
  background-image: repeating-linear-gradient(
    45deg,
    rgba(0, 0, 0, 0.22) 0,
    rgba(0, 0, 0, 0.22) 2px,
    transparent 2px,
    transparent 6px
  );
}

.hm-cell[data-cell-state="outside"],
.hm-cell[data-cell-state="future"],
.hm-cell[data-cell-state="before"] {
  background: transparent;
}

.hm-cell[data-cell-state="outside"] .hm-day,
.hm-cell[data-cell-state="future"] .hm-day,
.hm-cell[data-cell-state="before"] .hm-day {
  color: var(--fg-muted);
  opacity: 0.5;
}

/* Day editor (Step 3.1) --------------------------------------------------
   Deliberately NOT optimistic (contrast home.js's .trow-editor): a
   correction of a specific past day waits for the write to settle, so
   every control here is disabled while the write is in flight. */

.day-editor {
  margin: 12px 0 0;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg);
}

.day-editor-date {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
}

.day-form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.day-input {
  flex: 1 1 100px;
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  font-size: 16px; /* keeps iOS Safari from auto-zooming on focus */
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.day-mark,
.day-save,
.day-clear,
.day-cancel {
  min-height: 44px;
  padding: 0 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  font-size: 14px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.day-mark,
.day-save {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.day-mark:disabled,
.day-save:disabled,
.day-clear:disabled,
.day-cancel:disabled {
  opacity: 0.6;
}

.day-error {
  margin: 8px 0 0;
  color: var(--danger);
  font-size: 13px;
}

/* Weekly trend chart (Step 3.2) -------------------------------------------
   Bars are colored by target verdict (--good/--bad), falling back to the
   trackable's own identity colour when there is no honest verdict for that
   week (no target, or a gap week) — same two-channel convention as the
   heatmap above, applied by js/charts/weekly.js at render time via
   getComputedStyle rather than hardcoded here. .weekly-canvas-wrap needs an
   explicit height: maintainAspectRatio:false makes the canvas fill its
   parent, and a parent with no height collapses to zero. */

.weekly {
  width: 100%;
}

.weekly-meaning {
  margin: 0 0 10px;
  font-size: 13px;
  color: var(--fg-muted);
}

.weekly-canvas-wrap {
  position: relative;
  width: 100%;
  height: 200px;
}

.weekly-canvas {
  width: 100%;
  height: 100%;
}

.weekly-empty,
.weekly-unavailable {
  margin: 0;
  color: var(--fg-muted);
  font-size: 13px;
}

/* Trend granularity control (Step 3.2c) ---------------------------------
   Sits ON the trend chart. .detail-ranges now sits between the calendar
   and the trend chart (Step D.6b follow-up, 2026-09-04) rather than at
   the top of the screen, because it governs only the trend and range
   charts, not the heatmap — see js/views/detail.js's comment above where
   rangesDiv is built. Same visual idiom as .detail-range so the two read
   as controls of the same family. */

.trend-periods {
  display: flex;
  gap: 8px;
  margin: 0 0 10px;
}

.trend-period {
  flex: 1 1 0;
  min-height: 44px;
  padding: 0 10px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg-muted);
  font-size: 13px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.trend-period[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.trend-period:disabled {
  opacity: 0.4;
}

/* Two-bars threshold chart (Step 3.3) -------------------------------------
   .bounds-summary is the non-colour cue (WCAG 1.4.1): the zone word is
   readable with no colour perception at all, and data-zone below is only
   a supplementary tint. Zone shading itself (the box/line annotations) is
   drawn by chartjs-plugin-annotation using the existing --good-bg/--bad-bg
   tokens, so nothing about the shading needs its own CSS here — only the
   summary line's tint and the canvas-wrap sizing do. Same "wrap needs an
   explicit height" note as .weekly-canvas-wrap. */

.bounds {
  width: 100%;
}

.bounds-summary {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
}

.bounds-summary[data-zone="in"] {
  color: var(--good);
}

.bounds-summary[data-zone="below"],
.bounds-summary[data-zone="above"] {
  color: var(--bad);
}

.bounds-canvas-wrap {
  position: relative;
  width: 100%;
  height: 200px;
}

.bounds-canvas {
  width: 100%;
  height: 100%;
}

.bounds-unavailable {
  margin: 0;
  color: var(--fg-muted);
  font-size: 13px;
}

/* Step 3.3b — the Range chart's granularity control reuses the trend
   chart's .trend-periods / .trend-period rules verbatim (it carries both
   classes), so the two controls cannot drift apart visually. Only the
   meaning line below is new. */

.bounds-meaning {
  margin: 0 0 8px;
  font-size: 12px;
  color: var(--fg-muted);
}

/* Correlation overlay picker (Step 3.4) -----------------------------------
   Rendered in the 'overlay' chart slot (js/views/detail.js) — this is NOT
   a chart, it's the control that adds/removes marker rows on the Range
   chart above. .overlay-chip reuses the .trend-period/.bounds-period pill
   idiom (same border/background/pressed treatment) so every button group
   on this screen reads as one family; the ::before dot is the one thing
   unique to this control — it lets a chip carry its own trackable's
   colour, matching the marker colour drawn on the chart. */

.overlay-picker {
  width: 100%;
}

.overlay-hint {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--fg-muted);
}

.overlay-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.overlay-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg-muted);
  font-size: 13px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.overlay-chip::before {
  content: '';
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--chip-color, var(--fg-muted));
}

.overlay-chip[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.overlay-chip:disabled {
  opacity: 0.4;
}

/* Compare screen (Step 3.5, CONTRACT-3.5.md) -------------------------------
   A top-level view at #/compare, not a chart slot inside .detail — so it
   gets its own top-level block rather than living inside .chart-slot.
   .compare-picker/.compare-hint/.compare-chips mirror the overlay picker's
   own .overlay-picker/.overlay-hint/.overlay-chips rules (the chip itself
   already reuses .overlay-chip's visual treatment via the shared class on
   the button — see js/views/compare.js). .compare-meaning mirrors
   .bounds-meaning's small muted line. The range/period controls below
   reuse .detail-ranges/.trend-periods UNTOUCHED (js/views/compare.js
   builds identical markup), so nothing new is needed for them here. */

.compare-view {
  display: block;
  width: 100%;
}

.compare-picker {
  width: 100%;
  margin: 0 0 16px;
}

.compare-hint {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--fg-muted);
}

.compare-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.compare-meaning {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--fg-muted);
}

.compare-canvas-wrap {
  position: relative;
  width: 100%;
  height: 260px;
}

.compare-canvas {
  width: 100%;
  height: 100%;
}

.compare-empty,
.compare-unavailable,
.compare-loading {
  margin: 0;
  color: var(--fg-muted);
  font-size: 13px;
}

/* Soft series-count warning (§0 rule 5: no hard cap, only this) — uses
   --bad the same way .bounds-summary's out-of-range state does, but at
   body-text weight/size since this is advisory, not a verdict. */
.compare-warning {
  margin: 10px 0 0;
  color: var(--bad);
  font-size: 12px;
}

.compare-skipped {
  margin: 10px 0 0;
  color: var(--fg-muted);
  font-size: 12px;
}

.compare-key {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  font-size: 12px;
}

.compare-key-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 0;
}

.compare-key-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: 0 0 auto;
}

.compare-key-name {
  font-weight: 600;
}

.compare-key-range {
  color: var(--fg-muted);
}

/* Sign-in gate (Step D.7) -------------------------------------------------
   Rendered by js/views/signin.js in place of every route's own view while
   there is no session (see main.js's render()). Inputs follow the same
   44px-min-height / 16px-font sizing every other form in this app uses
   (.tform-input, .trow-input) so iOS never auto-zooms on focus. */

.signin {
  display: block;
  width: 100%;
}

.signin-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

.signin-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
}

.signin-field input {
  min-height: 44px;
  padding: 0 12px;
  font-size: 16px; /* keeps iOS Safari from auto-zooming on focus */
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Show/Hide toggle (2026-09-13, device feedback) — sits under the password
   field, before the submit button (see js/views/signin.js). Secondary
   look, same treatment as .signout below, so it reads as an auxiliary
   action rather than competing with .signin-submit. */
.signin-toggle {
  width: 100%;
  min-height: 44px;
  background: transparent;
  color: var(--fg-muted);
  border: 1px solid var(--fg-muted);
}

.signin-submit {
  width: 100%;
  min-height: 44px;
}

.signin-error {
  margin: 4px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.signin-error[hidden] {
  display: none;
}

/* Settings: signed-in-as + sign-out (Step D.7) ---------------------------- */

.signin-as {
  margin: 0 0 12px;
  color: var(--fg-muted);
  font-size: 14px;
}

.signin-email {
  color: var(--fg);
  font-weight: 600;
}

.signout {
  background: transparent;
  color: var(--fg-muted);
  border: 1px solid var(--fg-muted);
}

.signout-warning {
  margin: 0 0 12px;
  color: var(--danger);
  font-size: 13px;
}

.signout-warning[hidden] {
  display: none;
}

/* Lock screen (Step 5.2, CONTRACT-5.2 §5) ---------------------------------
   Rendered by js/views/lock.js in place of every route's own view while
   signed in with the app lock enabled and this session not yet unlocked
   (see main.js's render()). Styled like .signin* just above — both are
   full-screen gates with nothing else on the page until they resolve —
   and .lock-signout reuses .signout's transparent/muted "secondary text
   button" treatment for the same reason .signout itself uses it: this is
   an escape hatch, not the primary action on the screen. */

.lock {
  display: block;
  width: 100%;
}

.lock-title {
  margin: 0 0 8px;
  font-size: 18px;
  font-weight: 600;
}

.lock-help {
  margin: 0 0 16px;
  color: var(--fg-muted);
  font-size: 13px;
}

.lock-unlock {
  width: 100%;
}

.lock-error {
  margin: 12px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.lock-error[hidden] {
  display: none;
}

.lock-alt {
  margin: 16px 0 0;
}

.lock-signout {
  background: transparent;
  color: var(--fg-muted);
  border: 1px solid var(--fg-muted);
}

/* Settings screen (Step 4.1, CONTRACT-4.1.md §6) ---------------------------
   .settings-block groups (window / order / archived / account) get the same
   card-ish spacing rhythm as .chart-slot on the detail screen, but without
   a border — this screen is a list of preferences, not a set of charts.
   .settings-order / .settings-archived reuse the flex-row-with-growing-name
   idiom already established by .compare-key-item. .settings-move /
   .settings-unarchive reuse the 44px pill treatment of .trend-period /
   .bounds-period so every button group across the app reads as one family. */

.settings {
  display: block;
  width: 100%;
}

.settings-block {
  margin: 0 0 24px;
}

.settings-block:last-of-type {
  margin-bottom: 0;
}

.settings-title {
  margin: 0 0 8px;
  font-size: 15px;
  font-weight: 600;
}

.settings-help {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--fg-muted);
}

.settings-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
}

.settings-window-form {
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

.settings-window-input {
  width: 8ch;
  min-height: 44px;
  padding: 0 10px;
  font-size: 16px; /* keeps iOS Safari from auto-zooming on focus */
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.settings-window-save {
  min-height: 44px;
}

.settings-window-status {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--fg-muted);
}

.settings-window-error {
  margin: 4px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.settings-window-error[hidden] {
  display: none;
}

.settings-order,
.settings-archived {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.settings-order-item,
.settings-archived-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.settings-order-name,
.settings-archived-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--fg);
}

.settings-order-empty,
.settings-archived-empty {
  margin: 0;
  color: var(--fg-muted);
  font-size: 13px;
}

.settings-move,
.settings-unarchive {
  flex: 0 0 auto;
  min-height: 44px;
  min-width: 44px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  font-size: 14px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.settings-move:disabled,
.settings-unarchive:disabled {
  opacity: 0.4;
}

.settings-error {
  margin: 12px 0 0;
  color: var(--danger);
  font-size: 13px;
}

/* Export (Step 4.2, CONTRACT-4.2.md §3) -----------------------------------
   .settings-export-list/-item/-name mirror .settings-archived's own rules
   exactly (same row-with-growing-name idiom); .settings-export-one/-select
   reuse the .settings-unarchive pill; .settings-export-all is a primary,
   full-width action like .signin-submit, since it is the one button this
   whole block exists for. */

.settings-export-all {
  width: 100%;
  min-height: 44px;
  margin: 0 0 12px;
}

.settings-export-list {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.settings-export-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.settings-export-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--fg);
}

.settings-export-one,
.settings-export-select {
  flex: 0 0 auto;
  min-height: 44px;
  min-width: 44px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
  font-size: 14px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.settings-export-one:disabled,
.settings-export-all:disabled {
  opacity: 0.4;
}

.settings-export-status {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--fg-muted);
}

.settings-export-error {
  margin: 4px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.settings-export-error[hidden] {
  display: none;
}

.settings-export-fallback {
  margin: 10px 0 0;
}

.settings-export-fallback[hidden] {
  display: none;
}

.settings-export-text {
  display: block;
  width: 100%;
  min-height: 120px;
  margin: 8px 0;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--fg);
}

/* App lock (Step 5.2, CONTRACT-5.2 §5) -------------------------------------
   Sits between Export and Account (js/views/settings.js's render()).
   .settings-applock-toggle takes the plain base `button` sizing with no
   extra overrides, same as .settings-window-save; status/error lines
   mirror .settings-window-status/-error exactly. */

.settings-applock-status {
  margin: 0 0 10px;
  font-size: 13px;
  color: var(--fg-muted);
}

.settings-applock-toggle {
  margin: 0 0 4px;
}

.settings-applock-error {
  margin: 8px 0 0;
  color: var(--danger);
  font-size: 13px;
}

.settings-applock-error[hidden] {
  display: none;
}
