/*
 * `color-scheme` as well as the variables, because it is what native surfaces
 * read: scrollbars, form controls and the caret take no notice of `--bg`. It
 * is keyed on `data-theme` rather than on the media query for the same reason
 * the variables are — `apply()` has already resolved `system` against the
 * device by the time it sets the attribute, so this follows the ACCOUNT's
 * choice, which is the whole point of the setting.
 */
:root {
  color-scheme: light;
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #eef0f4;
  --border: #dcdfe6;
  --text: #14181f;
  --text-dim: #666e7d;
  --accent: #3b82f6;
  --danger: #dc2626;
  --grid-empty: #e6e9ef;
  --shadow: 0 1px 3px rgb(0 0 0 / 8%);
  --radius: 10px;
}

:root[data-theme='dark'] {
  color-scheme: dark;
  --bg: #0f1216;
  --surface: #171b21;
  --surface-2: #1f242c;
  --border: #2b313a;
  --text: #e8eaed;
  --text-dim: #9aa3af;
  --grid-empty: #232830;
  --shadow: 0 1px 3px rgb(0 0 0 / 40%);
}

* { box-sizing: border-box; }

/**
 * A `display` rule beats the `hidden` attribute, so any element given
 * `display: block/flex` below would stay visible when JS sets `hidden`.
 * This guarantees `hidden` always wins.
 */
[hidden] { display: none !important; }

html {
  /* Reserve the scrollbar's space permanently. A modal <dialog> locks body
     scrolling, which otherwise removes the scrollbar and shifts the whole
     page sideways underneath it — visible as the date header sliding out of
     line with the checkboxes it labels. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ---------- topbar ---------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
}

.brand {
  margin: 0;
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -0.02em;
  /* Give way before the action buttons do, and truncate rather than pushing
     them off a narrow screen. */
  min-width: 0;
  overflow: hidden;
}
/*
 * The title is a button (it navigates home) but must not look like one —
 * strip the control chrome and inherit the heading's type. The focus ring
 * stays: it is the only thing telling a keyboard user this is operable.
 */
.brand-link {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  cursor: pointer;
  border-radius: 4px;
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.brand-link:hover { color: var(--accent); }
.brand-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/*
 * The action row must be allowed to shrink, and the brand to give way before
 * it. Neither was true: both were rigid, so on a 360px screen the topbar
 * overflowed and took the whole page's horizontal scroll with it. It passed
 * on Windows and failed on CI's Linux runner purely because the font renders
 * wider there — a layout that depends on font metrics for a 30px margin is
 * broken whether or not the local machine happens to show it.
 */
.topbar-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-shrink: 0;
}

@media (max-width: 420px) {
  .topbar { padding: 12px 14px; gap: 8px; }
  .topbar-actions { gap: 6px; }
  /* "New habit" is the one label that can afford to lose its wording on a
     narrow screen; the icon buttons are already minimal. */
  .topbar .btn { padding: 7px 10px; }
}

/* ---------- buttons ---------- */

.btn {
  font: inherit;
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.btn:hover { background: var(--surface-2); }
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }
.btn-danger { color: var(--danger); border-color: var(--danger); }
.btn-icon { padding: 7px 10px; }
.btn-sm { padding: 4px 10px; font-size: 13px; }

/* ---------- layout ---------- */

.view { padding: 20px; max-width: 1100px; margin: 0 auto; }

/* The boot-error view rides these rules but not their class names — see the
   comment on `#view-error` in index.html for why it cannot share them. */
.empty,
.boot-error {
  text-align: center;
  color: var(--text-dim);
  padding: 48px 20px;
  max-width: 560px;
  margin: 0 auto;
}
.empty-title,
.boot-error-title {
  font-size: 19px;
  font-weight: 650;
  color: var(--text);
  margin: 0 0 6px;
  letter-spacing: -0.02em;
}
.empty-sub,
.boot-error-sub { margin: 0 0 20px; font-size: 14px; line-height: 1.5; }
.empty-note { margin: 14px 0 0; font-size: 12.5px; }
.empty-note code {
  background: var(--surface-2);
  padding: 1px 4px;
  border-radius: 4px;
}
.empty-actions,
.boot-error-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 18px;
}

.starters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px;
  text-align: left;
}
.starter {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: 'dot name' 'dot sub';
  gap: 0 9px;
  align-items: center;
  font: inherit;
  text-align: left;
  padding: 11px 13px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--text);
  transition: border-color 0.12s, transform 0.12s;
}
.starter:hover { border-color: var(--accent); transform: translateY(-1px); }
.starter:disabled { opacity: 0.5; cursor: default; transform: none; }
.starter .habit-dot { grid-area: dot; }
.starter-name { grid-area: name; font-weight: 600; font-size: 14px; }
.starter-sub { grid-area: sub; font-size: 11.5px; color: var(--text-dim); }

/* ---------- drag to reorder ---------- */

.drag-handle {
  border: none;
  background: none;
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1;
  padding: 6px 2px;
  cursor: grab;
  border-radius: 6px;
  opacity: 0.45;
  transition: opacity 0.12s, background 0.12s;
}
.habit-row:hover .drag-handle { opacity: 1; }
.drag-handle:focus-visible {
  opacity: 1;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.drag-handle:active { cursor: grabbing; }

.habit-row.dragging { opacity: 0.4; }
.habit-row.drop-above { box-shadow: inset 0 2px 0 var(--accent); }
.habit-row.drop-below { box-shadow: inset 0 -2px 0 var(--accent); }

/* ---------- toast action ---------- */

.toast-action {
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  margin-left: 12px;
  padding: 2px 8px;
  border: 1px solid currentColor;
  border-radius: 6px;
  background: none;
  color: inherit;
  cursor: pointer;
}
.toast-action:hover { background: rgb(255 255 255 / 15%); }

/* ---------- grid header (dates + navigation) ---------- */

/* The header must sit in exactly the same box as a habit row, or the date
   cells drift away from the checkboxes they label. Rather than hand-matching
   padding (which was wrong by 8px, then 24px), give it the row's border and
   padding with the border made invisible: identical geometry, by construction. */
.grid-head {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 10px;
  align-items: center;
  margin-bottom: 8px;
  padding: 0 14px;
  border: 1px solid transparent;
}
.grid-range {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.grid-nav { display: flex; gap: 6px; justify-self: end; }
.grid-nav .btn { padding: 3px 9px; line-height: 1.3; }
.grid-nav .btn:disabled { opacity: 0.35; cursor: default; }

/* The date row must line up with the checkbox columns in each habit row.
   Both are right-aligned strips of 44px cells with a 4px gap, so matching
   those three values keeps them registered regardless of the habit names'
   width. */
.grid-dates {
  grid-column: 1 / -1;
  display: flex;
  gap: 4px;
  justify-content: flex-end;
  /* The row reserves space for the drag handle to the left of the names;
     the header has none, so nothing else needs compensating here. */
}
/* flex: none — the strip is stretched to the full grid width, and without
   this the cells grow to fill it (44.8px instead of 44px), drifting out of
   line with the fixed-width checkboxes below. */
.grid-date {
  flex: none;
  width: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.1;
  color: var(--text-dim);
}
.grid-date-mon {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-height: 11px;
}
.grid-date-num { font-size: 12px; font-variant-numeric: tabular-nums; }

/* Today is called out in the header and in every row, so the column reads as
   a single highlighted stripe rather than one subtly coloured letter. */
.grid-date.is-today {
  color: var(--accent);
  font-weight: 700;
}
.grid-date.is-today .grid-date-num {
  background: var(--accent);
  color: #fff;
  border-radius: 999px;
  min-width: 20px;
  text-align: center;
}

/* ---------- habit grid ---------- */

.grid { display: flex; flex-direction: column; gap: 10px; }

/* One per category, drawn directly into `#grid` when `groupByCategory` is on
   (dashboard.js). Left border rather than a filled background, for the same
   reason the category chip's border is — a header has to stay legible
   whatever the category's own colour is. */
.category-section-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  margin-top: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
  border-left: 3px solid var(--chip-color, var(--accent));
}
.category-section-header:first-child { margin-top: 0; }
/* The trailing Uncategorised section has no colour of its own to border with. */
.category-section-header.uncategorised { border-left-color: var(--border); }
.category-section-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.category-section-count { flex: none; color: var(--text-dim); }

/* The mean + spread `dashboard.js` draws when `summarised` is true. `flex:
   none` on both, like `.category-section-count` above — the row is already
   `flex` with an ellipsising NAME, so at 360px it is the name that gives up
   width, never these figures. */
.category-section-figure {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.category-section-mean { font-weight: 700; color: var(--text); }
.category-section-spread { color: var(--text-dim); }

/* Flex rather than a fixed grid: the drag handle is absent in the archived
   view and when there is only one habit, and the row must still line up. */
.habit-row {
  display: flex;
  align-items: center;
  gap: 10px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
  box-shadow: var(--shadow);
}

.habit-meta { min-width: 0; flex: 1; cursor: pointer; }
.checks { flex: none; }
.habit-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: flex;
  align-items: center;
  gap: 8px;
}
.habit-dot {
  width: 10px; height: 10px;
  border-radius: 50%;
  flex: none;
}
/* The icon column is fixed-width and the name is what flexes — the reverse
   reintroduces the layout bug `responsive.mjs` was written to catch, where a
   14-column grid took 668px of a 698px row and left the name zero width. */
.habit-icon { flex: none; line-height: 1; }
.habit-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.habit-sub {
  font-size: 12.5px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.checks { display: flex; gap: 4px; }

.check {
  width: 44px;
  height: 44px;   /* 44px is the minimum comfortable touch target */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: none;
  background: none;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text-dim);
  font-size: 11px;
  padding: 0;
}
.check:hover { background: var(--surface-2); }
.check-box {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  background: var(--grid-empty);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: #fff;
  line-height: 1;
  /* An anchor for `.has-note::after` below, which positions absolutely
     inside the box. Adding this changes no geometry — nothing here sets an
     offset — so the box's own size and centring are untouched. */
  position: relative;
}
/* The checkbox medium's version of the calendar cell's own corner dot
   (charts.js's note mark): a small ring in the habit's own surface colour,
   mirroring the SVG mark so the two grids describe a note the same way.
   Positioned absolutely so it changes no box size — gridcheck.mjs pins
   `.check` centring at 1440 and 390px. */
.check-box.has-note::after {
  content: '';
  position: absolute;
  top: -1px;
  right: -1px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--text-dim);
}
.check-day { font-size: 9.5px; letter-spacing: 0.02em; }

/* Today reads as a continuous tinted column down the whole list, which is
   far easier to find than one coloured weekday letter was. */
.check.today {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);
}
.check.today .check-day { color: var(--accent); font-weight: 700; }

/* ---------- detail view ---------- */

.detail-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 20px;
}
.detail-head h2 { margin: 0 0 2px; font-size: 22px; letter-spacing: -0.02em; }
.detail-head .habit-sub { white-space: normal; }

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin-bottom: 18px;
}
.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
  box-shadow: var(--shadow);
}
.stat-value {
  font-size: 25px;
  font-weight: 650;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 2px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 16px;
  box-shadow: var(--shadow);
}
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
.card-title { font-weight: 600; font-size: 15px; }

.seg { display: flex; gap: 2px; background: var(--surface-2); padding: 2px; border-radius: 8px; }
.seg button {
  font: inherit;
  font-size: 12.5px;
  padding: 4px 10px;
  border: none;
  background: none;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text-dim);
}
.seg button[aria-pressed='true'] {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}

/*
 * The day strip on a habit's own page — the same cells as the dashboard grid,
 * in a card instead of a list row.
 *
 * Three overrides, and each undoes something `.grid-dates` / `.checks` carry
 * only because of where they normally sit. `grid-column: 1 / -1` is inert
 * outside `#grid-head`'s grid, so it needs nothing; `justify-content:
 * flex-end` is NOT — it right-aligns the captions while `.checks` (a plain
 * flex row) left-aligns the boxes, so above 640px the dates drifted off the
 * squares they label by however much slack the card had. And `.checks`'
 * narrow-width `flex: 1 1 100%` means "grow to the row's full width" inside
 * `.habit-row`; inside a flex COLUMN it means grow vertically.
 *
 * Below 640px both become `grid-auto-columns: 1fr` with the same gap, so they
 * line up by construction and nothing here has to intervene. Measured at
 * 360 / 390 / 768 / 1440 in `responsive.mjs` rather than argued.
 */
.day-strip { display: flex; flex-direction: column; gap: 6px; }
.day-strip .grid-dates { justify-content: flex-start; }
.day-strip .checks { flex: none; }

.chart-scroll { overflow-x: auto; }
svg.chart { display: block; max-width: 100%; height: auto; }

/*
 * Inside a scroll container a chart keeps its natural size and the container
 * scrolls. `max-width: 100%` there would shrink the SVG to fit instead — which
 * is not "responsive" for a calendar: it scales every square down uniformly,
 * so on a 320px screen the cells rendered at 7.6px and the grid became
 * unreadable rather than scrollable.
 */
.chart-scroll > svg.chart { max-width: none; }

/* ---------- calendar cell hover ---------- */

/*
 * `transform-box: fill-box` makes the transform origin relative to the rect's
 * own geometry, so `center` is the square's middle. Without it the origin is
 * the SVG's origin and a hovered cell flies across the grid.
 */
.cal-cell {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 120ms cubic-bezier(0.2, 0, 0, 1);
}

/*
 * Suppress the browser's own tooltip. `<title>` stays in the markup for
 * assistive tech, but the native bubble takes about a second to appear,
 * cannot be styled, and would sit on top of the popover.
 */
.cal-cell > title { display: none; }

/*
 * `.is-active` is set by the script on focus. `:focus-visible` alone would
 * miss it: focus moved programmatically (arrow keys call .focus()) does not
 * always match, so the keyboard path grew nothing while the mouse path did.
 */
.cal-cell:hover,
.cal-cell:focus,
.cal-cell.is-active {
  transform: scale(1.45);
}
.cal-cell:focus { outline: none; }

.cal-pop {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  padding: 5px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font-size: 12px;
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;   /* never steals the hover that summoned it */
  box-shadow: 0 4px 14px rgb(0 0 0 / 0.28);

  /* The JS sets `transform` for position, so the entrance animates opacity
     and nothing else — mixing the two would fight over the same property. */
  opacity: 0;
  transition: opacity 120ms ease-out;
}
.cal-pop.is-open { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .cal-cell { transition: none; }
  /* The popover still appears — it carries the information. Only the motion
     goes, which is what the preference actually asks for. */
  .cal-cell:hover, .cal-cell:focus, .cal-cell.is-active { transform: none; }
  .cal-pop { transition: none; }
}

/*
 * The awards row.
 *
 * `auto-fit` with a minimum rather than a fixed count, for the reason the day
 * grid's column count is a function of width: five badges at 360px in one row
 * squeeze the label to nothing, and one per line at 1440px is a column of
 * whitespace. `minmax(0, 1fr)` as the second half, or a long detail sentence
 * makes the track wider than the card and the whole card scrolls sideways.
 */
.award-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px;
}
.award {
  background: var(--surface-2);
  border: 1px solid var(--border);
  /* The habit's own colour, set by detail.js as --award-accent. */
  border-left: 3px solid var(--award-accent, var(--accent));
  border-radius: 8px;
  padding: 10px 12px;
  min-width: 0;
}
.award-label {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.award-detail {
  margin-top: 3px;
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.4;
}
/*
 * The "New" pill, and it deliberately does NOT take the habit's colour.
 *
 * This is the one element here with text ON a fill, and `--award-accent` is a
 * colour the user picked — nothing constrains its lightness, so a pale habit
 * gave white-on-white and an unreadable pill. Every other coloured surface in
 * this app either carries no text or mixes against a theme variable
 * (`color-mix` in charts.js). `--accent` is the theme's own, and `#fff` on it
 * is the pairing `.btn-primary` already ships in both themes. The habit's
 * colour is on the chip's left edge, where it needs no contrast ratio.
 */
.award-fresh {
  display: inline-block;
  vertical-align: 1px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
}

/*
 * The notes card. Each row is a `<button>` styled like `.starter` above — a
 * list row disguised as a control, not the other way round — because it is
 * the one way into the day editor here that is keyboard-reachable by
 * construction rather than by a shortcut.
 */
.notes-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.note-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 12px;
  align-items: baseline;
  width: 100%;
  font: inherit;
  text-align: left;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.12s;
}
.note-row:hover, .note-row:focus-visible { border-color: var(--accent); }
.note-date {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
}
.note-text {
  font-size: 13px;
  line-height: 1.4;
  min-width: 0;
  /* A note can be up to 500 characters; this is a summary row, not the
     editor, so it clips to one line rather than growing indefinitely. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.legend {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 10px;
}
.legend-swatch { width: 11px; height: 11px; border-radius: 3px; }

/* ---------- dialog ---------- */

dialog {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  padding: 20px;
  width: min(440px, 92vw);
  box-shadow: 0 12px 40px rgb(0 0 0 / 22%);
}
dialog::backdrop { background: rgb(0 0 0 / 45%); }
dialog h2 { margin: 0 0 14px; font-size: 18px; }

dialog label {
  display: block;
  margin-bottom: 11px;
  font-size: 13px;
  color: var(--text-dim);
}
dialog input,
dialog select {
  display: block;
  width: 100%;
  margin-top: 4px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 9px;
}
dialog input[type='color'] { height: 38px; padding: 3px; cursor: pointer; }

/* The habit dialog's target box says why it was refused (#156). `.field-hint`
   is the quiet grey every field's explanatory line takes, and the `.error`
   colour is scoped per hint — `.timefield`, `.countfield`, `#category-hint` —
   so a refusal here reads as ordinary help text without this line. */
#target-hint.error { color: var(--danger); }

/* ---------- icon field (habit dialog) ---------- */

/* Wraps the label, the row and the hint as one field-height group — the same
   shape `.category-field` uses below for the same reason: the `<label>` here
   names only the input (`for`, not wrapping it), so `dialog label`'s own
   `margin-bottom: 11px` would otherwise land between "Icon" and the row
   instead of after the whole field. */
.icon-field { margin-bottom: 11px; }
.icon-field label { margin-bottom: 4px; }

.icon-field-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* The icon input sits in this row beside the preview and the toggle, so it
   needs the same override `.category-new input[type='text']` uses below —
   `dialog input` otherwise forces `display: block; width: 100%`. */
.icon-field-row input {
  flex: 1;
  min-width: 0;
  margin-top: 0;
}
.icon-preview {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* aria-hidden — see icon-field.js's `updatePreview`. Sized up from the rest
   of the row so the glyph itself, not just the caption beside it, is what a
   sighted user actually checks. */
.icon-preview-glyph {
  font-size: 20px;
  line-height: 1;
  min-width: 1.2em;
  text-align: center;
}
.icon-preview-caption { margin-top: 0; }

/* Inline in the dialog's own flow, not a fixed-position popover — that is
   what keeps a narrow viewport honest (responsive.mjs checks it at 360px). */
.icon-picker {
  margin-top: 8px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
#icon-search { margin-bottom: 8px; }
/* `.starters`' own tile-grid shape (`auto-fill` + a minimum), scoped to a
   scrolling panel the way `.category-manage` scopes its own list. The track
   minimum and the cell's own `min-height` are both 44px — `MIN_TOUCH` in
   `responsive.mjs`, the same figure `.check` already meets above 640px — and
   the two must move together: a cell is measured as `min(width, height)`, so
   raising one alone leaves it as small as it ever was. At 360px this resolves
   to five columns inside the dialog rather than seven, which is the trade. */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 4px;
  max-height: 200px;
  overflow-y: auto;
}
.icon-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  font-size: 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
}
.icon-cell:hover { border-color: var(--accent); }
.icon-cell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* ---------- category field (habit dialog) ---------- */

.category-field { margin-bottom: 11px; }
.category-field label { margin-bottom: 4px; }

.category-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin: 8px 0;
}
/* A coloured left edge, not a filled chip — a label has to stay legible
   whatever the suggestion's own colour is, the same reason the "New" pill in
   detail.js takes --accent rather than a per-habit colour. */
.category-chip { border-left: 3px solid var(--chip-color, var(--accent)); }

.category-new {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
}
.category-new input[type='text'] { flex: 1; min-width: 0; margin-top: 0; }
.category-new input[type='color'] { margin-top: 0; flex: none; width: 44px; }

.category-manage {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 160px;
  overflow-y: auto;
}
.category-manage-row { display: flex; align-items: center; gap: 6px; padding: 4px 2px; }
.category-swatch { width: 12px; height: 12px; border-radius: 3px; flex: none; }
.category-manage-name {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.category-manage-row input[type='text'] { flex: 1; min-width: 0; margin-top: 0; }
.category-manage-row input[type='color'] {
  margin-top: 0;
  flex: none;
  width: 36px;
  height: 32px;
}
/* Four buttons now share this row with the swatch and the name — `flex: none`
   keeps them at their own intrinsic size so `.category-manage-name`'s
   `flex: 1; min-width: 0` is what absorbs a narrow screen, not them. Tighter
   padding than the ordinary `.btn-icon` (declared for every icon button in
   this row, not only the two new ones, so ✎ and ✕ shrink by the same amount
   rather than sitting wider than their new neighbours) is what keeps the row
   fitting at 360px with four of them in play. */
.category-manage-row .btn-icon {
  flex: none;
  padding: 5px 7px;
}
#category-hint.error { color: var(--danger); }

/* ---------- category comparison (#/categories) ---------- */

/*
 * `auto-fit` with a minimum rather than a fixed count, the same shape the
 * awards row uses and for the same reason: an account may have one category
 * or thirty, and a fixed two-column grid is a column of whitespace at 360px
 * and a wasted half-screen at 1440. The minimum is 360 rather than the awards
 * row's 190 because each cell holds a chart, and `ui/categories.js` measures
 * one cell to size that chart — below ~300px of card the axis labels start to
 * meet in the middle. `minmax(0, 1fr)` as the second half, or a long category
 * name makes the track wider than the grid and the page scrolls sideways.
 */
.compare-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: 0 16px;
  align-items: start;
}

/* The card already carries its own bottom margin; the grid's row gap would
   double it. */
.compare-card { margin-bottom: 16px; }
.compare-card .card-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* Uncategorised is a state a habit is in and has no colour of its own, so its
   swatch is an outline rather than a fill — the same distinction the grouped
   dashboard's trailing section header draws with its border. */
.compare-card.uncategorised .category-swatch {
  box-shadow: inset 0 0 0 1px var(--border);
}

.compare-figure { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.compare-mean {
  font-size: 27px;
  font-weight: 650;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.compare-note { font-size: 12px; color: var(--text-dim); line-height: 1.4; }

.compare-spread {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 10px 0 8px;
  font-size: 13px;
}
/* Best and worst are TEXT and link nowhere — see the comment in
   `ui/categories.js` for the back-stack reason. Styled as a row of plain
   spans rather than anything that reads as tappable, deliberately. */
.compare-member { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.compare-member-label {
  flex: none;
  width: 56px;
  font-size: 12px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.compare-member-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.compare-member-score { flex: none; font-variant-numeric: tabular-nums; color: var(--text-dim); }

@media (max-width: 400px) {
  /* One column below the minimum above, which `auto-fit` already does — what
     this buys is the track being allowed to be NARROWER than 360px, or the
     grid overflows a 360px viewport by the difference. */
  .compare-grid { grid-template-columns: minmax(0, 1fr); }
}

.freq {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 11px 11px;
  margin: 0 0 11px;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--text-dim);
}
.freq legend { font-size: 12px; padding: 0 4px; }
.freq input { width: 62px; margin-top: 0; }

.dialog-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
.spacer { flex: 1; }

/* ---------- offline banner ---------- */

.offline-bar {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  padding: 9px 20px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  color: var(--text-dim);
  position: sticky;
  top: 0;
  z-index: 9;
}
/* The dot lives inside the message now, so it disappears with it when the
   strip is up only to report a queue. inline-flex, because a bare span gives
   the dot no box to be 8px in. */
.offline-message { display: inline-flex; align-items: center; gap: 9px; }
.offline-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #f59e0b;
  flex: none;
}
.pending-count { color: var(--text); font-weight: 600; }
.offline-bar .btn { margin-left: auto; }

/* Respect the notch / home indicator when installed as an app. */
@supports (padding: env(safe-area-inset-top)) {
  .topbar { padding-top: calc(14px + env(safe-area-inset-top)); }
  body { padding-bottom: env(safe-area-inset-bottom); }
}

/* ---------- sign in ---------- */

.signin {
  max-width: 420px;
  margin: 64px auto;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 36px 28px;
  box-shadow: var(--shadow);
}
.signin-title {
  margin: 0 0 8px;
  font-size: 20px;
  letter-spacing: -0.02em;
}
.signin-sub {
  margin: 0 0 22px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.5;
}
.btn-signin {
  display: inline-block;
  text-decoration: none;
  padding: 10px 24px;
  font-size: 15px;
}

/*
  The password form, shown instead of the sign-in link when the server reports
  a `password` or `setup` mode. Left-aligned inside a centred card: labels above
  inputs read as a column, and centring them makes a two-field form look like a
  ransom note.

  These mirror the `dialog label` / `dialog input` rules above rather than
  reusing them, because the sign-in view is a <main>, not a <dialog> — it is the
  one form in the app that has to render before there is a session.
*/
.signin-form { text-align: left; }
.signin-form label {
  display: block;
  margin-bottom: 11px;
  font-size: 13px;
  color: var(--text-dim);
}
.signin-form input {
  display: block;
  width: 100%;
  margin-top: 4px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 9px;
}
.signin-form .btn-signin {
  width: 100%;
  margin-top: 6px;
}
.signin-error {
  margin: 0 0 11px;
  color: var(--danger);
  font-size: 13px;
  line-height: 1.4;
}

.user-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-left: 10px;
  margin-left: 2px;
  border-left: 1px solid var(--border);
  font-size: 13px;
  color: var(--text-dim);
}
#user-name {
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 640px) {
  #user-name { display: none; }
  .user-chip { padding-left: 6px; }
}

/* ---------- list header / archive toggle ---------- */

#list-head { display: flex; justify-content: flex-end; margin-bottom: 10px; }

/*
 * The habit search. Full width on a phone, where the grid below it is already
 * the whole screen; the count sits beside it rather than under, so a filtered
 * list does not shift vertically as you type.
 */
#search-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
#habit-search {
  flex: 1;
  min-width: 0;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 9px;
  /* 44px is the touch minimum every day cell already clears. The padding alone
     left this at 39, and nothing had measured it: `responsive.mjs` runs on four
     fixture habits and the box only appears at six, so the one control that
     comes and goes with the size of an account was the one control never laid
     out on a phone. */
  min-height: 44px;
}
#habit-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.search-count {
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}

/* For a label a screen reader needs and the eye does not. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
#toggle-archived[aria-pressed='true'] {
  border-color: var(--accent);
  color: var(--accent);
}

/* An archived habit reads as dimmed but still legible. */
.habit-row.archived { opacity: 0.62; }
.habit-row.archived .habit-name::after {
  content: 'archived';
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  margin-left: 2px;
}

/* ---------- checkbox rows ---------- */

.checkbox {
  display: flex !important;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text);
  margin-bottom: 11px;
}
.checkbox input {
  width: auto !important;
  margin: 0 !important;
  flex: none;
}
.muted { color: var(--text-dim); font-weight: 400; }

/* ---------- calendar navigation ---------- */

.cal-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cal-range {
  font-size: 12px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  padding: 0 4px;
}

/* ---------- day editor ---------- */

#day-dialog { width: min(360px, 92vw); }
#day-dialog h2 { margin-bottom: 2px; }
#day-dialog .hint { margin-bottom: 14px; }

.day-choices { display: flex; gap: 8px; margin-bottom: 12px; }
.day-choice { flex: 1; padding: 12px 8px; font-size: 14px; }
.day-choice[aria-pressed='true'] {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
#day-skip[aria-pressed='true'] {
  border-color: var(--accent);
  color: var(--accent);
}

/* ---------- reminder time picker ---------- */

.timefield {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px 11px 9px;
  margin: 0 0 11px;
  /* A fieldset defaults to `min-width: min-content`, which ignores its
     container — without this the row below pushes the dialog past a 360px
     screen instead of wrapping. */
  min-width: 0;
}
.timefield legend { padding: 0 5px; font-size: 13px; color: var(--text-dim); }

.timefield-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
/* The selects size to their content and the typed box takes what is left, so a
   narrow screen wraps the Clear button rather than crushing the hour list. */
.timefield-row select {
  width: auto;
  margin-top: 0;
  flex: 0 0 auto;
}
.timefield-row input[type='text'] {
  width: auto;
  margin-top: 0;
  flex: 1 1 5.5em;
  min-width: 5.5em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.timefield-sep { color: var(--text-dim); }

.timefield-presets {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.timefield .field-hint { display: block; margin-top: 7px; }
.timefield .field-hint.error { color: var(--danger); }

/* ---------- amount field ---------- */

/* The same shape as .timefield above, and for the same reason: several
   controls over one value, with the typed box as the one that is read. */
.countfield {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px 11px 9px;
  margin: 0 0 11px;
  /* See .timefield — a fieldset ignores its container without this, and the
     row below would push a dialog past a 360px screen instead of wrapping. */
  min-width: 0;
}
.countfield legend { padding: 0 5px; font-size: 13px; color: var(--text-dim); }

.countfield-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
/* The steppers size to their content and the box takes what is left. They are
   deliberately wide enough to be a comfortable target on a phone — this is the
   control people use every day on a measurable habit. */
.countfield-step {
  flex: 0 0 auto;
  min-width: 44px;
  font-size: 17px;
  line-height: 1;
}
.countfield-typed {
  width: auto;
  margin-top: 0;
  flex: 1 1 5.5em;
  min-width: 5.5em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.countfield-presets {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.countfield-presets:empty { display: none; }
.countfield .field-hint { display: block; margin-top: 7px; }
.countfield .field-hint.error { color: var(--danger); }

#count-dialog { width: min(380px, 92vw); }

/* ---------- settings dialog ---------- */

#settings-dialog { width: min(460px, 92vw); }
#settings-dialog .data-section:first-of-type { margin-top: 8px; }
.setting-help { margin: -6px 0 12px; font-size: 12px; }
#settings-dialog label:not(.checkbox) { margin-bottom: 4px; }

/* "Your last reminder was not delivered." Sits above the controls it is about,
   because it is the answer to the question that brought the user here. Loud
   enough not to read as another hint — the whole failure mode this fixes is a
   destination that stopped working while everything on screen looked fine. */
.setting-problem {
  margin: 0 0 12px;
  padding: 8px 10px;
  border: 1px solid var(--danger);
  border-radius: 8px;
  color: var(--danger);
}

/* A group of checkboxes that together are one setting (notification
   destinations). The fieldset carries the group's name, so the rows inside it
   need no label of their own. */
.setting-multi {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px 11px 0;
  margin: 0 0 4px;
  /* A fieldset's default `min-width: min-content` ignores its container, so a
     long destination label would push the dialog past a 360px screen. Every
     other block here shrinks; this one has to be told to. */
  min-width: 0;
}
.setting-multi legend {
  padding: 0 5px;
  font-size: 13px;
  color: var(--text-dim);
}
/* The rows are the only labels in the dialog that must not stretch, or each
   checkbox row becomes a full-width click target over empty space. */
.setting-multi label.checkbox { margin-bottom: 9px; }
/* A long webhook URL must not widen the dialog on a phone. */
#settings-dialog input[type='text'] { text-overflow: ellipsis; }

/* `ordered-multi` (detailCards, #163): the checkbox plus a pair of buttons
   that swap the row's position, so the label must not claim the whole row
   the way a plain `.checkbox` does. */
.setting-ordered-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 9px;
}
.setting-ordered-row label.checkbox { flex: 1; min-width: 0; margin-bottom: 0; }
.setting-ordered-row button { flex: none; padding: 3px 9px; }

/* ---------- backup / restore dialog ---------- */

#data-dialog { width: min(500px, 92vw); }

.data-section {
  border-top: 1px solid var(--border);
  padding-top: 14px;
  margin-top: 14px;
}
.data-section:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }
.data-section h3 { margin: 0 0 4px; font-size: 14px; }

.hint {
  margin: 0 0 10px;
  font-size: 12.5px;
  color: var(--text-dim);
  line-height: 1.45;
}
/* Explanatory text under a form field, quieter than the label above it. */
.field-hint {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  font-weight: 400;
  color: var(--text-dim);
  line-height: 1.4;
}

/* Section label inside a card that holds more than one chart. */
.card-subhead {
  margin: 16px 0 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.card-subhead:first-of-type { margin-top: 8px; }

.hint code {
  background: var(--surface-2);
  padding: 1px 4px;
  border-radius: 4px;
  font-size: 11.5px;
}

.data-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

.import-mode {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 11px 10px;
  margin: 0 0 11px;
}
.import-mode legend { font-size: 12px; color: var(--text-dim); padding: 0 4px; }
.radio {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: var(--text);
  margin-bottom: 4px;
}
.radio input { width: auto; margin: 0; }

#import-file {
  display: block;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 9px;
}

.import-result {
  margin-top: 11px;
  padding: 9px 11px;
  border-radius: 8px;
  font-size: 13px;
  background: var(--surface-2);
  line-height: 1.5;
}
.import-result.error { color: var(--danger); }
.import-result ul { margin: 6px 0 0; padding-left: 18px; }
.import-result li { font-size: 12px; color: var(--text-dim); }

/* ---------- toast ---------- */

.toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--text);
  color: var(--bg);
  padding: 9px 16px;
  border-radius: 8px;
  font-size: 13.5px;
  z-index: 100;
  box-shadow: 0 4px 16px rgb(0 0 0 / 25%);
}

@media (max-width: 640px) {
  .view { padding: 12px; }
  /* Stack the name above the day strip, keeping the handle beside the name. */
  .habit-row { flex-wrap: wrap; gap: 8px 10px; }
  .habit-meta { flex: 1 1 60%; }
  .checks { flex: 1 1 100%; }

  /* Fewer, larger day cells that share the row width evenly, so the strip
     never scrolls sideways and every target clears 44px. */
  .checks { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px; }
  .check { width: auto; min-width: 0; height: 46px; }
  .check-box { width: 26px; height: 26px; }

  /* The header must use the same track sizing as .checks above, or the dates
     stop lining up with the boxes they label. */
  .grid-head { padding: 0 14px; }
  .grid-dates {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 3px;
  }
  .grid-date { width: auto; min-width: 0; }
}
