/* app.css — CheckIt's component library.
   Same design family as LinkIt / ScanIt: the same class vocabulary (.btn / .icon-btn / .input /
   .chip / .modal / .sheet / .toast) and the same token names, so a component lifted from either app
   lands here without a rewrite.

   ⚠️ NO HARD-CODED COLOURS IN THIS FILE, EVER. Every colour goes through var(). That rule is what
      keeps the skin swappable — it's what let ScanIt drop a blue skin onto LinkIt's components
      without forking them, and it's the reason theme-cyan.css is the only CheckIt-specific
      stylesheet. The only exceptions are alpha-only scrims, which are defined as tokens too.

   MOBILE FIRST throughout. Every base rule is the phone rule; the media queries only ever ADD
   desktop affordances. This app gets used one-handed at a car boot and walking round an aircraft —
   the phone layout is the real layout, not a fallback. */

/* ==========================================================================
   1. Base + app shell
   ========================================================================== */

html { height: 100%; }
html, body { background: var(--bg); color: var(--ink); }

/* THE APP FRAME IS FIXED; ONLY .app-scroll MOVES (#545).
   The header, the side nav and the tab bar are laid out as flex items of a viewport-height box that
   never scrolls, and the content region between them is its own scroll container. The document
   itself is therefore permanently unscrollable, which is what makes every fixed dialog immune to
   page scroll — see §9.

   100dvh, not 100vh: on a phone `vh` is the LARGE viewport, so with the URL bar showing, a 100vh
   frame is taller than the screen and the tab bar sits below the fold. `dvh` tracks the URL bar.
   The plain `vh` line above it is the fallback for anything that doesn't know `dvh`. */
body {
  font-family: var(--font-ui);
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

h1 { font-size: var(--font-size-h1); }
h2 { font-size: var(--font-size-h2); }
.muted { color: var(--ink-faint); }
.hidden { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* min-height:0 is load-bearing: without it a flex item refuses to shrink below its content, the
   shell grows past the frame, and the whole page scrolls again — exactly the bug being fixed. */
.app-shell { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.app-header {
  flex: none; position: relative; z-index: 40;
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--header-height);
  padding: var(--sp-2) var(--sp-4);
  padding-top: calc(var(--sp-2) + env(safe-area-inset-top));
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.brand { display: inline-flex; align-items: center; gap: var(--sp-2); font-family: var(--font-brand); font-weight: 800; font-size: var(--font-size-lg); color: var(--ink); }
.brand-logo { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: var(--r-md); background: var(--accent); color: var(--accent-ink); }
.brand-word b { color: var(--accent); }
.header-tools { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }

/* The scroll container. Wrapping .app-main rather than BEING it keeps the scrollbar at the true
   right edge of the window while the content stays centred inside --content-max, and leaves
   .app-main's padding box untouched — which matters because .page-head / .editor-head full-bleed
   themselves with negative margins measured against exactly that padding. */
.app-scroll {
  flex: 1; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
}

/* No tab-bar allowance in the padding any more — the tab bar is a sibling of this scroller now,
   not something floating over it, so it can't cover the last row. */
.app-main {
  width: 100%; max-width: var(--content-max); margin: 0 auto;
  padding: var(--sp-4);
  padding-left: max(var(--sp-4), env(safe-area-inset-left));
  padding-right: max(var(--sp-4), env(safe-area-inset-right));
  padding-bottom: var(--sp-6);
}

.view { display: flex; flex-direction: column; gap: var(--sp-3); }
.stack { display: grid; gap: var(--sp-3); }

/* ==========================================================================
   2. Navigation — bottom tab bar on phone, side nav at >=1200px
   ========================================================================== */

/* A real flex item of the fixed frame, not an overlay pinned to the viewport. Same pixels on
   screen, but the scroll region now genuinely ends where the tab bar begins, so no view has to
   reserve bottom padding to avoid being covered — and hiding it inside a run just gives that space
   straight back to the list with no maths. */
.tabbar {
  flex: none; position: relative; z-index: 45;
  display: flex;
  height: calc(var(--tabbar-height) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--surface);
  border-top: 1px solid var(--line);
}
.tab {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  color: var(--ink-faint); font-size: var(--font-size-xsm); font-weight: 700;
  transition: color var(--transition);
}
.tab svg { width: 22px; height: 22px; }
.tab.active { color: var(--accent); }
.sidenav { display: none; }

/* THE TAB BAR HIDES INSIDE A RUN (Maya §7.2.6). Once a run is open, the screen is the list: the tab
   bar is 56px of nothing useful sitting in the thumb zone, and one of those tabs navigates away
   mid-preflight. [<] in the run header is the way out, and it comes back on exit.
   The app header hides too — the run has its own, and two stacked sticky bars on a phone is a
   third of the screen gone before a single item is drawn.
   Both bars are flex items of the fixed frame, so `display: none` simply hands their space to the
   scroll region — there is no viewport arithmetic to keep in step. */
body.in-run .tabbar { display: none; }
body.in-run .app-header { display: none; }
body.in-run .app-main { padding: 0; padding-bottom: calc(env(safe-area-inset-bottom) + var(--sp-6)); }

/* THE APP HEADER HIDES INSIDE THE EDITOR TOO, on phone only — same reasoning as the run. The editor
   now carries its own sticky bar (back · title · Cancel · Save), and stacking it under the brand bar
   costs 61px of a 390-wide screen to show a logo you are not using. The tab bar stays: the editor is
   not a run, a mistap here does not lose a tick. Desktop keeps both — see §12. */
body.editing .app-header { display: none; }

/* ==========================================================================
   3. Buttons, chips, badges
   ========================================================================== */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: var(--button-height); padding: 8px var(--sp-4);
  border-radius: var(--r-md); border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); font-weight: 700;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.btn:active { transform: translateY(1px); }
.btn.small { min-height: var(--button-height-sm); padding: 5px 11px; font-size: var(--font-size-sm); }
.btn.block { width: 100%; }
.btn.tall { min-height: 52px; font-size: var(--font-size-lg); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); box-shadow: var(--shadow-sm); }
.btn.primary:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn.ghost { background: var(--surface); }
.btn.ghost:hover { background: var(--surface-2); }
.btn.danger { background: var(--danger); border-color: var(--danger); color: var(--accent-ink); }
/* danger-ghost — cool, not inviting. The override button (Maya §9.4) is deliberately styled so it
   never reads as the obvious next tap. */
.btn.danger-ghost { background: transparent; border-color: var(--danger); color: var(--danger); }
.btn.danger-ghost:hover { background: var(--surface-2); }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }
.btn svg { flex: none; }

.icon-btn {
  display: inline-grid; place-items: center; width: var(--button-height); height: var(--button-height);
  border-radius: var(--r-md); border: 1px solid transparent; background: transparent; color: var(--ink-soft);
  transition: background var(--transition), color var(--transition);
}
.icon-btn:hover { background: var(--surface-2); color: var(--ink); }
.install-pill { border-radius: 999px; }

.chip {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 34px; padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-soft);
  font-size: var(--font-size-sm); font-weight: 700; white-space: nowrap;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.chip:hover { background: var(--surface-2); color: var(--ink); }
.chip.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }

.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 999px;
  font-size: var(--font-size-xsm); font-weight: 800; letter-spacing: .02em;
  background: var(--surface-2); color: var(--ink-soft);
}
.badge-warn { color: var(--warn); background: var(--surface-2); }
.badge-ok { color: var(--ok); }
.badge-accent { color: var(--accent); background: var(--accent-soft); }

/* REQ — text alongside the amber stripe. Never colour alone. */
.tag { display: inline-flex; align-items: center; font-size: var(--font-size-xsm); font-weight: 800; letter-spacing: .04em; padding: 1px 7px; border-radius: var(--r-sm); background: var(--surface-2); color: var(--ink-soft); }
.tag.req { color: var(--warn); }
.tag.qty { color: var(--ink-soft); }
.tag.qty.short { color: var(--warn); }

/* Segmented control — Runs tab, theme picker, export scope. */
.segmented { display: flex; gap: 4px; padding: 4px; border-radius: 999px; background: var(--surface-2); }
.segmented button {
  flex: 1; min-height: 40px; padding: 0 var(--sp-3); border-radius: 999px;
  font-weight: 700; color: var(--ink-soft); white-space: nowrap;
  transition: background var(--transition), color var(--transition);
}
.segmented button.on { background: var(--surface); color: var(--accent); box-shadow: var(--shadow-sm); }

/* ==========================================================================
   4. Inputs
   ========================================================================== */

.input, select.input, textarea.input {
  width: 100%; min-height: var(--input-height); padding: 9px 12px;
  border-radius: var(--r-md); border: 1px solid var(--line);
  background: var(--surface); color: var(--ink);
  transition: border-color var(--transition), box-shadow var(--transition);
}
textarea.input { min-height: 84px; resize: vertical; line-height: 1.45; padding-top: 11px; }
/* The entry NAME is a two-line textarea, not a single-line input. Names carry the instruction now
   ("Fuel drain — left, check for water and debris"), so writing a longer one has to feel normal
   rather than like fighting a form (Maya §11.2). */
textarea.input.two-line { min-height: 64px; }
select.input { appearance: none; padding-right: 34px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }
.input[readonly] { background: var(--surface-2); color: var(--ink-faint); }

.field { display: grid; gap: 6px; }
.field-label { display: block; font-size: var(--font-size-sm); font-weight: 800; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .05em; }
.field-helper { font-size: var(--font-size-sm); color: var(--ink-faint); }

.search-wrap { position: relative; display: flex; align-items: center; }
.search-wrap svg { position: absolute; left: 12px; color: var(--ink-faint); pointer-events: none; }
.search-wrap .input { padding-left: 38px; }

/* Toggle switch */
.switch-row { display: flex; align-items: center; gap: var(--sp-3); min-height: var(--button-height); width: 100%; text-align: left; }
.switch-row .switch-text { flex: 1; min-width: 0; }
.switch-row .switch-title { font-weight: 700; }
.switch-row .switch-sub { font-size: var(--font-size-sm); color: var(--ink-faint); }
.switch {
  flex: none; width: 50px; height: 30px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--line); position: relative;
  transition: background var(--transition), border-color var(--transition);
}
.switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface); box-shadow: var(--shadow-sm); transition: transform var(--transition);
}
.switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }
.switch[aria-checked="true"]::after { transform: translateX(20px); }

/* Radio list — the four override reasons. 44px rows, big hit area, no aiming. */
.radio-row { display: flex; align-items: center; gap: var(--sp-3); width: 100%; min-height: var(--button-height); padding: 6px 10px; border-radius: var(--r-md); text-align: left; color: var(--ink); }
.radio-row:hover { background: var(--surface-2); }
.radio-mark { flex: none; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--ink-faint); display: grid; place-items: center; }
.radio-row[aria-checked="true"] .radio-mark { border-color: var(--accent); }
.radio-row[aria-checked="true"] .radio-mark::after { content: ""; width: 11px; height: 11px; border-radius: 50%; background: var(--accent); }

/* Stepper (quantity in the note modal) */
.stepper { display: flex; align-items: center; gap: var(--sp-3); }
.stepper .btn { width: var(--button-height); padding: 0; }
.stepper .stepper-value { min-width: 52px; text-align: center; font-weight: 800; font-size: var(--font-size-lg); }
/* Nothing recorded yet: the number on show is the TARGET, offered as a hint. It reads muted until
   the stepper is actually moved, so "the default is showing" and "the user set this" never look the
   same (#555). The value it carries is not saved unless it is touched. */
.stepper .stepper-value.is-hint { color: var(--ink-faint); font-weight: 600; }
.stepper .stepper-of { color: var(--ink-faint); font-size: var(--font-size-sm); }

/* Focus — LinkIt's ring, unchanged. */
.btn:focus-visible, .icon-btn:focus-visible, .chip:focus-visible, .input:focus, textarea.input:focus,
select.input:focus, .tick-row:focus-visible, .note-btn:focus-visible, .row:focus-visible,
.tab:focus-visible, .sheet-row:focus-visible, .radio-row:focus-visible, .switch-row:focus-visible,
.page-back:focus-visible, .page-kebab:focus-visible,
.group-head:focus-visible, .run-card:focus-visible, .template-row:focus-visible, .start-btn:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ==========================================================================
   5. Generic rows, cards, notices
   ========================================================================== */

/* ---- PAGE HEAD — the bar on every screen that isn't a tab root (#543 / #545) ------------------
   Shaun's words about the old version were "the controls at the top appear to be an afterthought —
   back button, checklist, list menu?", and they were: .page-head had NO CSS anywhere in the
   project, so it rendered as a plain block with two inline-grid buttons and a bare inline span all
   jammed against the left edge, 16px / 60px / 123px. Three loose widgets, not a bar.

   It is now built the same way as .run-head and .editor-head, which are the family's existing top
   bars: full-bleed across .app-main's padding, one hairline underneath, sticky to the top of the
   scroll region. Three deliberate choices, all aimed straight at Shaun's confusion:
     • the back control NAMES ITS DESTINATION ("< Checklists"), so you know where it goes before you
       tap. Maya's rule (#547): a backLabel names the SCREEN you are going back to, never the record
       you came from — one short word that never competes with the title for width;
     • the kebab carries the visible word "Actions", so it reads as something that opens AND says
       what it opens. "More" meant "more of what you can already see", and there is no more;
     • they sit at opposite edges with the title centred between them, which is what makes it read
       as one bar instead of three controls that happen to be adjacent.
   The 1fr/auto/1fr grid centres the title on the BAR rather than on the space left over, so it
   stays centred whether or not a screen has a kebab.

   ⚠️ THE FLOORS ON THE TWO SIDE TRACKS ARE LOAD-BEARING, and `minmax(0, 1fr)` was wrong. An `fr`
   track with a 0 floor will shrink to nothing to give the middle track its content width, so the
   title — which is sized to its content — took whatever it wanted and the controls paid for it.
   Measured in Chrome at 390px: "‹ Checklists" was cut to 70px of the 74px it needs, and a long
   checklist name overlapped the kebab by 65px. `min-content` floors the side tracks at exactly the
   width their (nowrap) labels need, so the TITLE is what yields — which is Maya's stated priority,
   and the title is the one thing here that can truncate honestly with an ellipsis.
   The cost is up to ~10px of optical off-centre on a phone when the two sides differ in width. The
   alternative that keeps it perfectly centred (three equal tracks) truncates names that would
   otherwise have fitted — 119px of title instead of 154px. Measured both; this is the better trade. */
.page-head {
  position: sticky; top: 0; z-index: 25;
  display: grid;
  grid-template-columns: minmax(min-content, 1fr) minmax(0, auto) minmax(min-content, 1fr);
  align-items: center; gap: var(--sp-2);
  min-height: var(--head-bar-height);
  margin-top: calc(var(--sp-4) * -1);
  margin-left: calc(-1 * max(var(--sp-4), env(safe-area-inset-left)));
  margin-right: calc(-1 * max(var(--sp-4), env(safe-area-inset-right)));
  padding-left: max(var(--sp-2), env(safe-area-inset-left));
  padding-right: max(var(--sp-2), env(safe-area-inset-right));
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.page-back, .page-kebab {
  display: inline-flex; align-items: center; gap: 2px;
  min-height: var(--button-height); min-width: var(--button-height); max-width: 100%;
  padding: 0 var(--sp-2); border-radius: var(--r-md);
  font-weight: 700;
  transition: background var(--transition), color var(--transition);
}
/* ONE accent in the bar. The back control is the primary navigation affordance, so it keeps the
   accent; the kebab drops to --ink-soft/600 rather than pulling at the opposite edge in the same
   colour and weight with an 800-weight title between them — three competing emphases (Maya §3). */
.page-back { color: var(--accent); justify-self: start; }
.page-kebab { color: var(--ink-soft); font-weight: 600; }
.page-back:hover, .page-kebab:hover { background: var(--surface-2); }
.page-kebab:hover { color: var(--ink); }
.page-back svg, .page-kebab svg { flex: none; }
.page-back-label, .page-kebab-label {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Belt and braces: no caller can blow the bar up by passing a long backLabel. 11ch clears the
   longest label the app ships ("Checklists") without eating into the title's width budget, which
   on a 390px phone is only about 200px to begin with. */
.page-back-label { max-width: 11ch; }
/* NO justify-self here, deliberately. `justify-self: center` sizes a grid item to its CONTENT, so
   the span was always exactly as wide as the name — which meant `text-overflow: ellipsis` could
   never fire, and a long title simply took the width it wanted and starved the two 1fr columns
   either side of it. That is what clipped "‹ Checklists" down to "‹ Ch…" on a 390px phone. Left to
   stretch, the span is exactly its track, the ellipsis works, and `text-align: center` keeps the
   text centred where the old rule kept the box centred. */
.page-head-title {
  min-width: 0; text-align: center;
  font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: opacity var(--transition);
}
/* Held back while the screen's own <h1> is still visible — showing the same name twice, 40px
   apart, is the redundancy that made the old middle label look like filler. shared.js reveals it
   the moment the h1 scrolls under the bar. */
.page-head-title.is-deferred { opacity: 0; }
.page-head-actions { justify-self: end; display: flex; align-items: center; gap: var(--sp-1); min-width: 0; }

.section-label {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  font-size: var(--font-size-xsm); text-transform: uppercase; letter-spacing: .07em; font-weight: 800;
  color: var(--ink-faint); margin-top: var(--sp-3);
}
.section-label a, .section-label button { color: var(--accent); font-weight: 800; }

.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md); }
.card-pad { padding: var(--sp-4); }

.row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
}
.row-title { font-weight: 700; }
.row-meta { font-size: var(--font-size-sm); color: var(--ink-soft); }

.notice { display: flex; gap: var(--sp-3); padding: var(--sp-3); border-radius: var(--r-md); background: var(--surface-2); color: var(--ink-soft); }
.notice svg { flex: none; margin-top: 2px; }
.notice.warn { color: var(--warn); border-left: 3px solid var(--warn); }
.notice.danger { color: var(--danger); border-left: 3px solid var(--danger); }
.notice.accent { background: var(--accent-soft); color: var(--accent); }
.notice-strong { font-weight: 700; }

/* Progress bar — the glance. 4px, and the counter next to it carries the detail. */
.progress { height: 4px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.progress-fill { display: block; height: 100%; background: var(--accent); transition: width var(--transition); }

/* ==========================================================================
   6. Home — in-progress strip, categories, template rows
   ========================================================================== */

.run-card {
  display: grid; gap: 4px; width: 100%; text-align: left;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
  transition: background var(--transition);
}
.run-card:hover { background: var(--surface-2); }
.run-card-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.run-card-title { font-weight: 700; }
.run-card-version { font-size: var(--font-size-sm); color: var(--ink-faint); font-weight: 700; }
.run-card-label { color: var(--accent); font-weight: 700; font-size: var(--font-size-sm); }
.run-card-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: var(--font-size-sm); color: var(--ink-soft); }
.run-card .progress { margin-top: 4px; }
.req-left { color: var(--warn); font-weight: 700; }

/* Category accordion. Flattening categories to accordions on one screen (rather than a drill-in)
   keeps the whole library visible, which is how people actually recognise the list they want. */
.cat-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-2) var(--sp-1);
  font-weight: 800; text-transform: uppercase; letter-spacing: .05em; font-size: var(--font-size-sm);
  color: var(--ink); margin-top: var(--sp-2);
}
.cat-head .twist { color: var(--ink-faint); transition: transform var(--transition); }
.cat-head[aria-expanded="false"] .twist { transform: rotate(-90deg); }
.cat-head .cat-count { margin-left: auto; color: var(--ink-faint); font-weight: 700; }
.cat-body { display: grid; gap: var(--sp-2); }

/* A template row has TWO tap targets on purpose: the body opens the checklist, the [▶] starts a run.
   That's what makes cold-start-to-ticking two taps. The hairline between them is not decoration. */
.template-row-wrap {
  display: flex; align-items: stretch;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); overflow: hidden;
}
.template-row {
  flex: 1; min-width: 0; display: grid; gap: 2px; align-content: center;
  text-align: left; padding: var(--sp-3) var(--sp-4); min-height: var(--start-btn); color: var(--ink);
  transition: background var(--transition);
}
.template-row:hover { background: var(--surface-2); }
.template-name { font-weight: 700; overflow-wrap: anywhere; }
.template-meta { font-size: var(--font-size-sm); color: var(--ink-soft); }
.start-btn {
  flex: none; width: var(--start-btn); display: grid; place-items: center;
  border-left: 1px solid var(--line); color: var(--accent);
  transition: background var(--transition);
}
.start-btn:hover { background: var(--accent-soft); }
/* MED-1: .start-btn isn't a .btn, so it never got the disabled look and an empty list's play
   button read as live. Same dimming as .btn:disabled. */
.start-btn:disabled { opacity: .45; color: var(--ink-faint); cursor: not-allowed; }
.start-btn:disabled:hover { background: none; }
.ghost-row {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-2) var(--sp-4);
  border: 1px dashed var(--line); border-radius: var(--r-md);
  color: var(--ink-faint); font-weight: 700;
}
.ghost-row:hover { color: var(--accent); border-color: var(--accent); }

/* The one sync prompt, shown once after the first checklist is saved (Maya §12.3). Never at first
   run — asking for a folder before the user owns anything is asking them to protect nothing. */
.promo-strip {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md);
  background: var(--accent-soft); color: var(--accent);
}
.promo-strip .promo-text { flex: 1; min-width: 0; font-size: var(--font-size-sm); }
.promo-strip .promo-title { font-weight: 800; display: block; }

/* The quiet receipt after a sync pull changed something. Not a toast: this isn't an event, it's a
   state (Maya §12.6 case 2). */
.sync-receipt { padding: var(--sp-2) var(--sp-4); border-radius: var(--r-md); background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: var(--font-size-sm); }

/* ==========================================================================
   7. THE RUN VIEW — the screen that matters
   ========================================================================== */

.run-view { display: flex; flex-direction: column; }

.run-head {
  position: sticky; top: 0; z-index: 30;
  background: var(--surface); border-bottom: 1px solid var(--line);
  padding-top: env(safe-area-inset-top);
}
/* Same height as .page-head — 48/52 was two numbers for one job (Maya §5). --run-head-h is measured
   at runtime, so the sticky group heads follow the new number on their own. */
.run-head-bar { display: flex; align-items: center; gap: var(--sp-1); min-height: var(--head-bar-height); padding: 0 var(--sp-2); }
.run-head-title { flex: 1; min-width: 0; font-weight: 800; font-size: var(--font-size-lg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.run-head-version { color: var(--ink-faint); font-size: var(--font-size-sm); font-weight: 700; }
.run-head .progress { border-radius: 0; }
.run-counter {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: 6px var(--sp-4) 0; font-size: var(--font-size-sm); color: var(--ink-soft);
}
/* "2 required left" is a TAP TARGET that jumps to the first outstanding required item. On a 60-item
   preflight, "jump me to what's blocking" is the entire interaction (Maya §9.3). */
.run-jump-req { min-height: 34px; padding: 0 8px; margin: -4px 0; border-radius: var(--r-sm); color: var(--warn); font-weight: 800; }
.run-jump-req:hover { background: var(--surface-2); }
.run-filters { display: flex; gap: 6px; padding: var(--sp-2) var(--sp-4); overflow-x: auto; scrollbar-width: none; }
.run-filters::-webkit-scrollbar { display: none; }

.run-banner { display: flex; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); font-size: var(--font-size-sm); }
.run-banner.readonly { background: var(--surface-2); color: var(--ink-soft); }
.run-banner.override { background: var(--surface-2); color: var(--warn); border-left: 3px solid var(--warn); }
.run-banner-body { flex: 1; min-width: 0; display: grid; gap: 2px; }
.run-banner-title { font-weight: 800; }

/* Tight side padding on purpose: every pixel here comes straight off the tick target. At 390px this
   keeps the row's tap zone around 320px wide by 64+ tall. */
.run-list { width: 100%; max-width: var(--run-max); margin: 0 auto; padding: var(--sp-2) var(--sp-2) var(--sp-6); }
@media (min-width: 640px) { .run-list { padding-left: var(--sp-4); padding-right: var(--sp-4); } }
.run-group { margin-bottom: var(--sp-3); }

/* Sticky group head, pinned directly under the run header. --run-head-h is measured and written by
   run.js so the two can never drift apart when the header wraps to two lines. */
.group-head {
  position: sticky; top: var(--run-head-h, 132px); z-index: 20;
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-2) var(--sp-2);
  background: var(--bg); color: var(--ink);
  font-weight: 800; letter-spacing: .03em; text-transform: uppercase; font-size: var(--font-size-sm);
}
.group-head .twist { color: var(--ink-faint); transition: transform var(--transition); }
.group-head[aria-expanded="false"] .twist { transform: rotate(-90deg); }
.group-head .group-count { margin-left: auto; color: var(--ink-faint); font-weight: 700; letter-spacing: 0; text-transform: none; }
.group-head .group-req { color: var(--warn); font-weight: 800; letter-spacing: 0; }
.group-head .group-done { color: var(--ok); display: inline-grid; place-items: center; }
.group-items { display: grid; gap: var(--sp-2); }
.run-group.collapsed .group-items { display: none; }

/* ---- THE TICK ROW ----------------------------------------------------------
   The single most important thing in the app. The WHOLE ROW is the tick target — roughly
   338 x 64px on a 390px phone — so you never have to aim. The 52px note button at the right edge
   is the row's only other job, separated by a visible hairline. Everything else bends around this.
--------------------------------------------------------------------------- */
.tick-row-wrap {
  display: flex; align-items: stretch;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); overflow: hidden;
  transition: background var(--transition), border-color var(--transition);
}
.tick-row {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--tick-row-height);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-4);
  text-align: left; color: var(--ink);
}
.tick-box {
  flex: 0 0 auto; width: var(--tick-box); height: var(--tick-box);
  display: grid; place-items: center;
  /* 2px, in --ink-faint, NEVER --line. --line is a hairline for a calm desktop canvas; in sunlight
     it disappears, and an unticked checkbox you can't see is a broken app. */
  border: var(--tick-box-border) solid var(--ink-faint);
  border-radius: var(--r-sm);
  color: var(--accent-ink);
  transition: background var(--transition), border-color var(--transition);
}
.tick-box svg { opacity: 0; transition: opacity var(--transition); }
.tick-main { flex: 1; min-width: 0; display: grid; gap: 3px; }
/* Names WRAP rather than truncate. Item names carry all the instruction now, so "Fuel drain — left,
   check for water and debris" must be fully readable without tapping anything (Maya §7.3). */
.tick-name { font-weight: 700; font-size: 1.0625rem; line-height: 1.3; overflow-wrap: anywhere; white-space: pre-line; } /* COS-1: keep the editor's line breaks */
.tick-note {
  font-size: var(--font-size-sm); color: var(--ink-soft);
  background: var(--surface-2); border-radius: var(--r-sm); padding: 4px 8px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.tick-note.expanded { -webkit-line-clamp: unset; display: block; }
.tick-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.tick-skip { font-size: var(--font-size-sm); color: var(--warn); display: grid; gap: 1px; }
.tick-skip-when { color: var(--ink-faint); font-size: var(--font-size-xsm); }

/* Ticked uses THREE signals, not one: the box fills, the name softens, the row tints. Three signals
   survives sunlight, colour blindness and a dirty screen. NO strikethrough — on an aviation list you
   re-read what you already ticked, and struck-through text fights that. */
.tick-row-wrap.is-ticked { background: var(--accent-soft); border-color: var(--accent-soft); }
.tick-row-wrap.is-ticked .tick-box { background: var(--accent); border-color: var(--accent); }
.tick-row-wrap.is-ticked .tick-box svg { opacity: 1; }
.tick-row-wrap.is-ticked .tick-name { color: var(--ink-soft); }

/* Required + unticked: a 3px amber left stripe AND the REQ tag. Text plus colour. The stripe
   disappears on tick. */
.tick-row-wrap.is-required { border-left: 3px solid var(--warn); }
.tick-row-wrap.is-ticked.is-required { border-left-color: var(--accent-soft); }
.tick-row-wrap.is-skipped { border-left: 3px solid var(--warn); background: var(--surface-2); }

/* The pulse when "go to the first one" lands on a row. */
@keyframes rowPulse { 0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); } 40% { box-shadow: 0 0 0 5px var(--accent-soft); } }
.tick-row-wrap.pulse { animation: rowPulse 1.1s ease 2; }

/* The note button — the row's SECOND job. A note glyph, not a chevron: a chevron says "there's more
   in here somewhere", a note glyph says "this is where you write something". */
.note-btn {
  flex: none; width: var(--row-note-zone);
  display: grid; place-items: center;
  border-left: 1px solid var(--line);
  color: var(--ink-faint);
  transition: background var(--transition), color var(--transition);
}
.note-btn:hover { background: var(--surface-2); color: var(--ink); }
.note-btn.has-note { color: var(--accent); }
.tick-row-wrap.is-ticked .note-btn { border-left-color: var(--line-soft); }

/* Jump-to-group pill, floating bottom-right. The header bar gives the feel; this sheet gives the map. */
.jump-pill {
  position: fixed; right: max(var(--sp-4), env(safe-area-inset-right));
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom)); z-index: 35;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--button-height); padding: 0 var(--sp-4); border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line); color: var(--ink);
  font-weight: 700; box-shadow: var(--shadow);
}
body.has-finish-bar .jump-pill { bottom: calc(var(--button-height) + var(--sp-6) + env(safe-area-inset-bottom)); }

/* Finish. While required items are outstanding there is NO fixed button — Finish exists only at the
   very bottom of the list, which you reach by scrolling to the end, which is exactly when finishing
   makes sense. The commit button becomes ambient only at the moment committing is the obvious next
   thing (Maya §9.1). */
.finish-inline { margin-top: var(--sp-5); }
.finish-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 36;
  padding: var(--sp-2) var(--sp-4);
  padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom));
  background: var(--surface); border-top: 1px solid var(--line);
  animation: slideUp .18s ease;
}
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.finish-bar .btn { max-width: var(--run-max); margin: 0 auto; }

.run-note-well { padding: var(--sp-3) var(--sp-4); background: var(--surface-2); border-radius: var(--r-md); color: var(--ink-soft); font-size: var(--font-size-sm); }

/* ==========================================================================
   8. Template editor
   ========================================================================== */

/* The editor's own header bar. Sticky, full-bleed across .app-main's padding, and on phone it is the
   ONLY bar at the top (body.editing hides the app header). Save/Cancel live here rather than in a
   bottom bar: the bottom of a phone is the thumb rest and where the keyboard opens, so a bar pinned
   there sits on top of the rows being edited. */
.editor-head {
  position: sticky; top: 0; z-index: 32;
  margin-top: calc(var(--sp-4) * -1);
  margin-left: calc(-1 * max(var(--sp-4), env(safe-area-inset-left)));
  margin-right: calc(-1 * max(var(--sp-4), env(safe-area-inset-right)));
  padding-top: env(safe-area-inset-top);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.editor-head-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: calc(var(--editor-head-height) - 1px);
  padding-left: max(var(--sp-4), env(safe-area-inset-left));
  padding-right: max(var(--sp-4), env(safe-area-inset-right));
}
/* The title yields first — Cancel and Save are the reason the bar exists and must never be clipped. */
.editor-head-title {
  flex: 1; min-width: 0; font-weight: 800; font-size: var(--font-size-lg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.editor-head-bar .icon-btn, .editor-head-bar .btn, .editor-head-bar .page-back { flex: none; }

.editor-strip {
  min-height: var(--editor-strip-height); display: flex; align-items: center; gap: var(--sp-2);
  padding: 6px max(var(--sp-4), env(safe-area-inset-left)); background: var(--accent-soft); color: var(--accent);
  font-size: var(--font-size-sm); font-weight: 700;
}
.editor-group { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
.editor-group-head { display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left; min-height: var(--button-height); padding: var(--sp-2) var(--sp-3); background: var(--surface-2); font-weight: 800; }
.editor-group-head .twist { color: var(--ink-faint); transition: transform var(--transition); }
.editor-group.collapsed .twist,
.editor-group-head[aria-expanded="false"] .twist { transform: rotate(-90deg); }
.editor-group.collapsed .editor-group-body { display: none; }
.editor-group-count { margin-left: auto; color: var(--ink-faint); font-weight: 700; }
.editor-group-body { display: grid; }
.entry-row {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--line-soft); color: var(--ink);
}
.entry-row:hover { background: var(--surface-2); }
.entry-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.entry-row .chev { color: var(--ink-faint); }
.entry-add { color: var(--accent); font-weight: 700; border-top: 1px dashed var(--line); }
.reorder-btns { display: flex; gap: 4px; }
.reorder-btns .icon-btn { width: 38px; height: 38px; border: 1px solid var(--line); }
body.reordering .entry-row, body.reordering .editor-group-head { cursor: grab; }
.drag-over { outline: 2px dashed var(--accent); outline-offset: -2px; }

/* ==========================================================================
   9. Modals, sheets, toasts
   ========================================================================== */

/* ⚠️ ONE POSITIONING RULE FOR EVERY DIALOG IN THE APP (#545). Do not re-declare position, inset or
   z-index on any individual dialog — a new dialog type gets its geometry by being a .modal-overlay
   or a .sheet-overlay, and inherits this. That is what stops one dialog quietly drifting away from
   the rest, which is how this got raised four times.

   Both overlays are fixed against the VIEWPORT and are appended to <body>, which puts them outside
   .app-scroll entirely. They cannot ride the content's scroll position because they are not in it.

   Modals: horizontally centred, TOP-ANCHORED at --overlay-top (never vertically centred — no
   eye-jump), internal scroll if too tall. Sheets: bottom-anchored, because a bottom sheet is a list
   of choices with no text entry (Maya §3.2) — that is a different anchor on purpose, and it is just
   as fixed. */
.modal-overlay, .sheet-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: grid;
  background: var(--scrim);
  overscroll-behavior: contain;
}
.modal-overlay {
  place-items: start center;
  padding: var(--overlay-top) var(--sp-4) var(--sp-4);
  overflow-y: auto;
}
/* #1037 — THE MODAL STANDARD. The flex column was already here; what was missing is that
   the CARD was the scroller (`overflow-y:auto`) while .modal-body had no flex or overflow
   of its own. So on a long dialog the head scrolled away and the action bar sat ~2,460px
   below the bottom of the card — measured, at every viewport, completely unreachable.
   Now the card clips and only .modal-body scrolls. */
.modal {
  max-width: 440px; width: 100%; max-height: 90dvh; overflow: hidden;
  display: flex; flex-direction: column; gap: var(--sp-3); min-height: 0;
  padding: var(--sp-5); border-radius: var(--r-lg);
  border: 1px solid var(--line); background: var(--surface); box-shadow: var(--shadow-lg);
}
.modal.wide { max-width: 620px; }
.modal.danger { border-color: var(--danger); }

/* FREEZE THE BACKGROUND — and note what is NOT here. The old rule was
   `html:has(.modal-overlay) { overflow: hidden }`, and it was the actual bug behind "the modals
   jump around": setting overflow:hidden on the ROOT element clamps the document's scroll to 0, so
   opening a dialog 600px down a run threw the list back to the top, and closing it left you there.
   The frame no longer scrolls at all, so the root needs no rule; freezing the inner scroller is
   enough, and a nested scroll container keeps its scrollTop when it is hidden. */
body:has(.modal-overlay) .app-scroll,
body:has(.sheet-overlay) .app-scroll { overflow: hidden; }
.modal-head { flex: 0 0 auto; display: flex; align-items: flex-start; gap: var(--sp-2); }
.modal-head-text { flex: 1; min-width: 0; }
.modal-title { font-size: var(--font-size-lg); }
.modal-title.danger-title { color: var(--danger); }
.modal-sub { font-size: var(--font-size-sm); color: var(--ink-faint); }
/* min-height:0 lets the flex child shrink below its content; without it the body will not
   scroll and the action bar is pushed out through the bottom of the card instead. */
.modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.modal-message { color: var(--ink-soft); white-space: pre-line; }
.modal-detail { color: var(--ink-faint); font-size: var(--font-size-sm); white-space: pre-line; }
.modal-actions { flex: 0 0 auto; display: flex; gap: var(--sp-2); justify-content: flex-end; flex-wrap: wrap; }
.modal-actions .btn.block { flex-basis: 100%; }
.modal-rule { border: none; border-top: 1px solid var(--line); margin: var(--sp-1) 0; }
.modal-list { display: grid; gap: 6px; }
.modal-list-item { display: flex; align-items: flex-start; gap: var(--sp-2); color: var(--warn); font-weight: 700; }
.modal-list-item svg { flex: none; margin-top: 3px; }
.modal-linkbtn { color: var(--accent); font-weight: 700; text-align: left; min-height: var(--button-height); }

/* Bottom sheet — a list of choices, no text entry. Geometry comes from the shared rule above. */
.sheet-overlay { place-items: end center; }
/* #1037: same treatment, and the BOTTOM ANCHOR IS UNCHANGED — a sheet is a list of
   choices with no text entry (Maya §3.2) and anchoring it low is deliberate. What changes
   is only that the head no longer scrolls away with the rows: the sheet used to be the
   scroller, so a 30-row menu took its own title off the top.
   The row list is wrapped in .sheet-body by sheet.js. */
.sheet {
  width: 100%; max-width: 520px; max-height: 82dvh; overflow: hidden;
  display: flex; flex-direction: column; min-height: 0;
  padding: var(--sp-3) var(--sp-4) 0;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: var(--surface); box-shadow: var(--shadow-lg);
  animation: sheetUp .18s ease;
}
/* There is no footer on a sheet, so the BODY is what ends against the home indicator and
   the safe-area inset belongs to it, not to the card. On the card it would pad below the
   scroller, where nothing can be reached. */
.sheet-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom));
}
@keyframes sheetUp { from { transform: translateY(14px); opacity: .6; } to { transform: translateY(0); opacity: 1; } }
.sheet-head { flex: 0 0 auto; display: flex; align-items: flex-start; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.sheet-title { font-size: var(--font-size-lg); flex: 1; min-width: 0; }
.sheet-sub { font-size: var(--font-size-sm); color: var(--ink-faint); }
.sheet-row {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%; text-align: left;
  min-height: var(--button-height); padding: var(--sp-2) var(--sp-2);
  border-radius: var(--r-md); color: var(--ink); font-weight: 700;
}
.sheet-row:hover { background: var(--surface-2); }
.sheet-row svg { flex: none; color: var(--accent); }
.sheet-row-main { flex: 1; min-width: 0; display: grid; gap: 1px; }
.sheet-row-sub { font-size: var(--font-size-sm); font-weight: 400; color: var(--ink-faint); }
.sheet-row-trailing { color: var(--ink-faint); font-weight: 700; font-size: var(--font-size-sm); }
/* Destructive rows are always last, always after a rule, always --danger. */
.sheet-row.danger { color: var(--danger); }
.sheet-row.danger svg { color: var(--danger); }
.sheet-row:disabled { opacity: .45; }
.sheet hr { border: none; border-top: 1px solid var(--line); margin: var(--sp-2) 0; }

.toast-stack {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--tabbar-height) + env(safe-area-inset-bottom) + var(--sp-3));
  z-index: 300; display: flex; flex-direction: column; gap: var(--sp-2);
  width: min(92vw, 420px); align-items: stretch;
}
.toast {
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-left-width: 3px; border-left-color: var(--line);
  box-shadow: var(--shadow-lg); opacity: 0; transform: translateY(8px);
  transition: opacity .2s ease, transform .2s ease;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* A toast must never eat a tap (PaneKit #1364). On a phone the stack sits over a sheet's bottom rows
   and the list underneath, so toasts are click-through; only a whole-toast action ("Update available")
   and a toast's own button (Undo) take the pointer. */
.toast-stack, .toast-stack > .toast { pointer-events: none; }
.toast-stack > .toast.toast-action, .toast-stack .toast-btn { pointer-events: auto; }
.toast-action { cursor: pointer; font-weight: 700; }
/* A toast carrying its own button ("Undo", MED-10). Negative margins keep the toast's height while
   giving the button a finger-sized target. */
.toast-has-btn { display: flex; align-items: center; gap: var(--sp-3); }
.toast-has-btn .toast-text { flex: 1; min-width: 0; }
.toast-btn {
  flex: none; min-height: 40px; margin: calc(var(--sp-2) * -1) calc(var(--sp-2) * -1) calc(var(--sp-2) * -1) 0;
  padding: 0 var(--sp-3); border: 0; border-radius: var(--r-md); background: none;
  color: var(--accent); font: inherit; font-weight: 800; cursor: pointer;
}
.toast-btn:hover { background: var(--accent-soft); }
.toast-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.toast-success { border-left-color: var(--ok); }
.toast-warn { border-left-color: var(--warn); }
.toast-error { border-left-color: var(--danger); }
.toast-info { border-left-color: var(--accent); }

/* ==========================================================================
   10. Sync indicator
   ========================================================================== */

/* A 7px dot is about as close to zero visual cost as a status indicator gets — and the two states
   that DON'T fade are exactly the two you must not miss. LinkIt hides its indicator below 720px;
   for CheckIt that's the wrong call, because phone<->desktop convergence IS the feature and the
   phone is where you most need to know whether your ticks made it out (Maya §12.4). */
.sync-dot-btn { display: inline-grid; place-items: center; width: var(--button-height); height: var(--button-height); border-radius: var(--r-md); color: var(--ink-faint); }
.sync-dot { width: 7px; height: 7px; border-radius: 50%; background: transparent; flex: none; }
.sync-dot.syncing { background: var(--accent); animation: syncPulse 1s infinite; }
.sync-dot.ok { background: var(--ok); }
.sync-dot.warn { background: var(--warn); }
.sync-dot.danger { background: var(--danger); }
.sync-dot-btn .sync-label { display: none; }
@keyframes syncPulse { 50% { opacity: .3; } }

.sync-status-line { display: flex; align-items: center; gap: var(--sp-2); font-weight: 700; }

/* ==========================================================================
   11. Guide, settings, empty / loading / error
   ========================================================================== */

.guide-item { border-bottom: 1px solid var(--line); }
.guide-item > summary { list-style: none; cursor: pointer; padding: 14px 4px; font-weight: 800; display: flex; align-items: center; gap: var(--sp-2); min-height: var(--button-height); }
.guide-item > summary::-webkit-details-marker { display: none; }
.guide-item > summary::before { content: "▸"; color: var(--accent); transition: transform var(--transition); }
.guide-item[open] > summary::before { transform: rotate(90deg); }
.guide-body { padding: 0 4px 16px; color: var(--ink-soft); display: grid; gap: var(--sp-3); }
.guide-body strong { color: var(--ink); }
.guide-body ul { padding-left: 20px; display: grid; gap: 6px; }
.guide-body li { list-style: disc; }

.settings-section { display: grid; gap: var(--sp-3); padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }
.settings-actions { display: grid; gap: var(--sp-2); }
.meter { height: 8px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.meter > span { display: block; height: 100%; background: var(--accent); }

.empty-state { display: grid; gap: var(--sp-3); place-items: center; text-align: center; padding: var(--sp-6) var(--sp-4); color: var(--ink-soft); }
.empty-glyph { color: var(--accent); opacity: .8; }
.empty-title { font-size: var(--font-size-lg); color: var(--ink); }
.empty-message { max-width: 32ch; }
.empty-actions { display: grid; gap: var(--sp-2); width: min(100%, 320px); }
.empty-actions .btn { width: 100%; }

.skeleton-stack { display: grid; gap: var(--sp-2); }
.skeleton-row { height: var(--tick-row-height); border-radius: var(--r-md); background: var(--surface-2); animation: shimmer 1.2s ease-in-out infinite; }
@keyframes shimmer { 50% { opacity: .5; } }

.view-error { display: grid; gap: var(--sp-3); place-items: center; text-align: center; padding: var(--sp-6) var(--sp-4); color: var(--danger); }
.view-error code { color: var(--ink-faint); font-size: var(--font-size-sm); }
/* An error screen that offers no way out is a trap, and on a phone inside a run there is no browser
   chrome to fall back on. Every error panel carries its own doors (#557). */
.view-error-actions { display: grid; gap: var(--sp-2); justify-content: center; margin-top: var(--sp-3); }
.view-error-actions .btn { min-width: 200px; }
.run-error { display: grid; gap: 0; padding-top: var(--sp-4); }
.run-error .empty-actions { max-width: 22rem; margin: 0 auto; padding: 0 var(--sp-4); }

/* ==========================================================================
   12. Desktop
   ========================================================================== */

@media (min-width: 1200px) {
  body { flex-direction: row; }
  .tabbar { display: none; }
  /* A flex item of the fixed frame — no sticky, no 100vh. It is exactly as tall as the frame
     because the frame is the height of the viewport, and it scrolls itself if the roster ever
     outgrows a short window. */
  .sidenav {
    display: flex; flex-direction: column; gap: 4px;
    width: var(--sidenav-width); padding: var(--sp-4) var(--sp-3);
    background: var(--surface); border-right: 1px solid var(--line);
    flex: none; min-height: 0; overflow-y: auto;
  }
  .sidenav .tab { flex: none; flex-direction: row; justify-content: flex-start; gap: 10px; padding: 10px 12px; border-radius: var(--r-md); font-size: 1rem; }
  .sidenav .tab.active { background: var(--accent-soft); }
  .toast-stack { bottom: var(--sp-4); }

  /* The desktop sync indicator earns its label — there's room for it. */
  .sync-dot-btn { width: auto; gap: 6px; padding: 0 10px; }
  .sync-dot-btn .sync-label { display: inline; font-size: var(--font-size-sm); font-weight: 700; }

  /* Templates become a card grid inside each open category. */
  .cat-body { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
  .in-progress-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--sp-3); }

  /* Inside a run the side nav stays — there's no thumb-zone mistap risk on a desktop pointer, and
     losing all navigation on a large screen is worse than the 190px it costs. The bottom tab bar
     (the one that IS in the tap zone) still hides. */
  body.in-run .app-header { display: flex; }
  body.in-run .app-main { padding: var(--sp-4); }
  /* No `.run-head { top: --header-height }` any more, at any width. The app header is OUTSIDE the
     scroll container now, so a sticky bar inside it pins at 0 whether the header is showing or
     not. The old offset also quietly broke the desktop group heads: they stick at --run-head-h,
     which is measured from the run header's own box, so pushing the run header down 56px parked
     every group head underneath it. */
}

/* The editor is the one screen where desktop is the likely device, so it earns a real two-column
   layout: group/item list left, entry config panel right instead of a modal. */
@media (min-width: 1200px) {
  .editor-two-col { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: var(--sp-5); align-items: start; }
  .editor-side { position: sticky; top: calc(var(--editor-head-height) + var(--sp-4)); display: grid; gap: var(--sp-3); padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }

  /* Desktop keeps the app header while editing — there's room for it, and losing every bit of app
     chrome on a big screen is worse than the 56px it costs. So the editor bar parks underneath. */
  body.editing .app-header { display: flex; }
  /* When the fork strip rides inside the head, the head is taller — the side panel has to clear it. */
  .view.has-fork-strip .editor-side { top: calc(var(--editor-head-height) + var(--editor-strip-height) + var(--sp-4)); }
}
@media (max-width: 1199px) {
  .editor-side { display: none; }
}
