:root {
  --bg: #1b120c;
  --bg-elevated: #241812;
  --card: #2b1c14;
  --card-border: #4a3527;
  --gold: #c99a4b;
  --gold-bright: #e0b563;
  --cream: #f0e3ce;
  --muted: #b8a58d;
  --faint: #8a7863;
  --accent: #8f3a2a;
  --accent-bright: #b8503a;
  --success: #5f8a52;
  --success-bright: #9fd48b;
  --warn-bright: #e0a24b;
  /* Chart palette — deliberately separate from the semantic colors above
     (gold/success/accent/warn carry status meaning elsewhere) so a Stats
     chart legend is never visually confused with a badge. */
  --chart-1: #d9a441;
  --chart-2: #c1603a;
  --chart-3: #7a8f5a;
  --chart-4: #4f8a9c;
  --chart-5: #8a5a8f;
  --chart-6: #b08d5a;
  --chart-7: #b5544e;
  --chart-8: #6f9b8e;
  --chart-9: #9a8f4a;
  /* Selected filter chip. Its own pair rather than reusing --gold/--bg,
     because chip text is small (0.78rem) and the light theme's gold is not
     dark enough to clear 4.5:1 behind it — same reasoning as
     --success-bright below. */
  --chip-active-bg: var(--gold);
  --chip-active-fg: var(--bg);
  --radius: 14px;
  --tabbar-height: 66px;

  /* ---------- type scale (v2.89.0) ----------------------------------------
     Before this there were 105 font-size declarations across 30 distinct
     values, TWENTY of them packed between 0.64rem and 1.05rem — a 10-to-17px
     band with twenty steps in it. Neighbours differing by 0.02rem cannot read
     as different levels; they read as noise, and the result was that every
     element on a screen competed at the same emphasis. That is the larger half
     of why the app felt "busy": not the number of controls, but that nothing
     told the eye what to look at first.

     Seven steps, ~1.12 ratio at the bottom and wider at the top so headings
     genuinely lead. Everything is rem so the Settings text-size control (which
     sets the ROOT px size — see html[data-font-size] at the foot of this file)
     scales the whole scale uniformly. Do not add an eighth step without
     deleting one; the discipline is the point. */
  --text-2xs: 0.7rem;   /* uppercase micro-labels: field labels, badges */
  --text-xs: 0.78rem;   /* card sub-lines, chips, meta */
  --text-sm: 0.86rem;   /* secondary body: hints, descriptions */
  --text-md: 0.95rem;   /* body, card titles */
  --text-lg: 1.1rem;    /* section headings, the app-header wordmark */
  --text-xl: 1.35rem;   /* the page title — deliberately the largest thing in
                           the content column, see the h2 note below */
  --text-2xl: 1.6rem;   /* display numerals and glyphs (tile values, stars) */

  /* ---------- spacing scale (v2.89.0) -------------------------------------
     The other half of "busy". Consecutive blocks in the Humidor stack were
     separated by 16 / 18 / 18 / 20px — near-identical, so NOTHING GROUPED and
     the page read as one undifferentiated column. Grouping on a phone comes
     from spacing, not from borders: tight inside a group, wide between groups.

     --space-block is the between-groups gap and --space-tight the within-group
     one; the ratio between them is what does the work, so change them together
     or not at all. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-tight: var(--space-2);
  --space-block: var(--space-6);
}

/* Light theme — applied via [data-theme="light"] on <html>, set by
   settings-init.js (before paint) and kept in sync by app.js afterward.
   --success-bright in particular needs its own value per theme: the dark
   value (#9fd48b) is a light, readable-on-dark green that would fail
   contrast as text on a light card, so it's not just a straight invert. */
:root[data-theme="light"] {
  --bg: #f3e8d8;
  --bg-elevated: #ffffff;
  --card: #ffffff;
  --card-border: #dcc9a8;
  --gold: #9c7530;
  --gold-bright: #7c5a1e;
  --cream: #2b1c14;
  --muted: #6b5842;
  --faint: #8a7060;
  --accent: #8f3a2a;
  --accent-bright: #a84532;
  --success: #3f6a34;
  --success-bright: #2f5226;
  --warn-bright: #8a5a1a;
  --chip-active-bg: #6b4e19;
  --chip-active-fg: #ffffff;
  --chart-1: #a97a1e;
  --chart-2: #9c4a2a;
  --chart-3: #52672f;
  --chart-4: #2e6373;
  --chart-5: #6b3f70;
  --chart-6: #7a6650;
  --chart-7: #97423d;
  --chart-8: #4d7367;
  --chart-9: #756b34;
}

* { box-sizing: border-box; }

/* Respect the device's own reduced-motion accessibility setting — no
   in-app toggle needed, this is the standard way browsers expose it. */
@media (prefers-reduced-motion: reduce) {
  .view.active {
    animation: none;
  }
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--cream);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-tap-highlight-color: transparent;
}

body {
  /* .tabbar and .modal both add env(safe-area-inset-bottom); this reserve did
     not, so on a notched phone the tabbar grew by the inset while the body's
     reserve didn't and the last ~34px of every scrolling list sat under it.
     index.html opts into viewport-fit=cover, so the inset is live here. */
  padding-bottom: calc(var(--tabbar-height) + 12px + env(safe-area-inset-bottom));
  /* The dvh line is the fix; the vh line above it is the fallback for
     browsers without dvh (no @supports needed — an unknown unit is simply
     dropped). 100vh is the LARGE viewport on mobile, i.e. it ignores the
     browser chrome, which forces phantom scroll on first paint. */
  min-height: 100vh;
  min-height: 100dvh;
}

/* ⚠️ THE TOP INSET IS LOAD-BEARING ON iOS AND WAS THE ONE EDGE MISSING IT
   (v2.92.2). index.html has carried `viewport-fit=cover` and
   `apple-mobile-web-app-status-bar-style="black-translucent"` since v2.72.1,
   and together those draw page content UNDERNEATH the status bar — which is
   only correct if the topmost element reserves room for it. Every other edge
   already did: .tabbar, .modal, .app-toast and the age gate. This one had a
   flat 20px against a status bar / Dynamic Island that is ~47-59px on a modern
   iPhone, so the title and the ⚙️ button would have sat under it.
   max() keeps the existing 20px everywhere else — env() resolves to 0 off iOS —
   so this is free on Android and desktop. Fixed pre-emptively for the same
   reason v2.72.1 added the meta tags before anyone could test them: nobody here
   has an iPhone (COMMERCIAL_READINESS §3), and the checklist item stays open
   until someone confirms it on real hardware. */
.app-header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding: max(20px, env(safe-area-inset-top)) 16px 14px;
  background: linear-gradient(180deg, #241812 0%, #1b120c 100%);
  border-bottom: 1px solid var(--card-border);
}
:root[data-theme="light"] .app-header {
  background: linear-gradient(180deg, #fbf1e2 0%, #f3e8d8 100%);
}

.app-header-text {
  grid-column: 2;
  text-align: center;
}

/* 44px: the one control in the header, and it was 38. Above WCAG 2.2's 24px
   floor either way, but this is a thumb target at the top corner of a phone —
   the size that matters here is the comfortable one, not the legal minimum. */
.header-icon-btn {
  grid-column: 3;
  justify-self: end;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--card-border);
  color: var(--gold);
  font-size: var(--text-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* ⚠️ DELIBERATELY SMALLER THAN THE PAGE TITLE (v2.89.0), and this is the swap
   that creates the hierarchy rather than just tidying it. It used to be 1.4rem
   against an h2 of 1.15rem, so the one line that NEVER changes — the wordmark,
   present on every screen — shouted louder than the one that tells you where
   you are. Nobody needs reminding which app they opened. The header is
   persistent chrome and reads as chrome; the page title leads the column. */
.app-header h1 {
  margin: 0;
  font-size: var(--text-lg);
  color: var(--gold-bright);
  letter-spacing: 0.02em;
}

.tagline {
  margin: 4px 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

#views {
  padding: 16px;
  max-width: 640px;
  margin: 0 auto;
}

.view {
  display: none;
}
.view.active {
  display: block;
  animation: fadein 0.18s ease-out;
}

@keyframes fadein {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* The page title, and the largest thing in the content column by design. It was
   1.15rem against a 1.02rem card title — 13% larger, which is not a hierarchy,
   it is a rounding error. At --text-xl against --text-md it is 42% larger and
   the eye lands on it first. Pair it with the h1 note above; the two moved
   together and neither works alone. */
h2 {
  color: var(--gold-bright);
  font-size: var(--text-xl);
  margin: 0 0 var(--space-1);
}

.hint {
  color: var(--muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-4);
  line-height: 1.4;
}

label {
  display: block;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--faint);
  margin: 14px 0 6px;
}

.faint {
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--text-xs);
  color: var(--faint);
}

/* ⚠️ `input[type="password"]` WAS MISSING FROM THIS LIST AND EVERY PASSWORD
   FIELD IN THE APP WAS UNSTYLED — browser default, 21px tall, light box on a
   dark theme. All five of them: the lock screen's PIN box, both App Lock setup
   fields, and both backup-passphrase fields. So the control on the app's most
   important screen, the one a locked user sees before anything else, was the
   only unstyled input in the product.

   Found by accident at v2.107.4: a hit-test of the new reset control reported
   it at 46px next to a 21px `#lock-pin`, and the two are siblings. **A
   selector list is a hand-kept list** — the same failure mode as `SECRET_KEYS`
   or the repaint list, without a gate to catch it.

   ⚠️⚠️ AND IT HAPPENED AGAIN — `input[type="number"]`, found 2026-08-09. All
   four static number fields (tin quantity, bulk ounces, tin price, pipe price)
   plus six more built in app.js rendered at 21px, white on the dark theme,
   beside 48px siblings. v2.107.4 added the one type it had noticed and left
   this comment asking the next person to remember, which is precisely the
   instruction nobody executes.

   ⭐ THE FIX IS THE GATE, NOT THE SHAPE OF THIS SELECTOR — and inverting it was
   tried first and REJECTED on measurement, which is the part worth keeping.
   The obvious move is a denylist (`input:not([type="checkbox"])…`) so a new
   type is covered on arrival. It was written, and then its specificity was
   measured: six attribute selectors put it at (0,6,1) where this list is
   (0,1,1). That silently INVERTED two existing rules —
   `.hi-oz input[type="number"]` (0,2,1) lost its `width: 5.5em` to the shared
   `width: 100%`, and `.ch-rest-row input[type="date"]` (0,2,1) lost its
   `padding: 6px 8px` to the shared `padding: 12px`. Both are invisible in a
   diff and neither is covered by a test.

   **That is the same class of defect it was meant to prevent, moved somewhere
   harder to see**: every future `.some-class input[type="…"]` would silently
   lose to a selector nobody expects to be strong. `:where()` would flatten the
   specificity, but that is a Level 4 selector and an unsupported selector
   invalidates the WHOLE rule — the blast radius being every input in the
   product unstyled at once. Not worth it for a list that now has a gate.

   ⚠️ SO THIS STAYS A HAND-KEPT ALLOWLIST AT ITS ORIGINAL SPECIFICITY, AND
   `inputStyleLint()` IN tools/validate.js IS WHAT KEEPS IT HONEST. It fails the
   build if any `input[type="…"]` used in index.html or in a LINT_FILES template
   is neither listed here nor named in INTENTIONALLY_UNSTYLED with a reason.
   Adding a type to the markup and forgetting this line is now a red gate rather
   than a white box on a dark screen. Fault-injected three ways. */
select, input[type="text"], input[type="password"], input[type="date"], input[type="number"], textarea {
  width: 100%;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  color: var(--cream);
  padding: 12px;
  border-radius: 10px;
  font-size: var(--text-md);
  font-family: inherit;
}

textarea {
  resize: vertical;
}

/* The blanket `outline: none` here used to apply to every focus, including
   keyboard focus, leaving a 1px border-colour change as the only cue — and
   buttons had no rule at all, so the whole app relied on the UA default ring
   drawn over dark custom backgrounds. Narrowed to pointer/programmatic focus
   only; :focus-visible below restores a real ring for keyboard users. */
select:focus:not(:focus-visible),
input:focus:not(:focus-visible),
textarea:focus:not(:focus-visible) {
  outline: none;
}
select:focus, input:focus, textarea:focus {
  border-color: var(--gold);
}
:focus-visible {
  outline: 2px solid var(--gold-bright);
  outline-offset: 2px;
}

button {
  font-family: inherit;
  cursor: pointer;
  border: none;
  border-radius: 10px;
  font-size: var(--text-md);
}

/* The auto-fill buttons set .disabled = true for the duration of a scrape,
   which is two sequential cross-origin fetches behind a 12s timeout. Without
   this the button looked fully active the whole time. */
button:disabled {
  opacity: 0.55;
  cursor: default;
}

/* Not scoped to `button`, and not relying on the `button {}` reset above
   for cursor/border-radius/font either — the Backup & Restore "Import"
   control is a <label for="..."> styled to match (a real <button> can't
   trigger a file picker on click the way a label's implicit for-target
   activation does), so these need to be fully self-sufficient on whatever
   element wears the class. */
.primary {
  width: 100%;
  background: linear-gradient(180deg, var(--gold-bright), var(--gold));
  color: #241505;
  font-weight: 700;
  padding: 13px;
  /* Tight, because a primary action belongs to the hint that introduces it —
     it is the end of that group, not the start of a new one. */
  margin-top: var(--space-2);
  border-radius: var(--radius);
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}

.primary:active {
  transform: translateY(1px);
}

.secondary {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  cursor: pointer;
  padding: 11px 16px;
  /* The generic `label {}` rule further down (uppercase, small, letter-spaced
     — meant for form field labels like "Beverage") would otherwise leak
     through onto the Import <label> wearing this class, since nothing else
     here overrides them. Reset explicitly rather than relying on source
     order against a same-or-lower-specificity tag selector. */
  font-size: var(--text-md);
  text-transform: none;
  letter-spacing: normal;
}

/* An <a> wearing .secondary (the Settings feedback mailto, v2.69.0) — give it
   the block/centred/no-underline bits a <button> gets for free. */
.settings-link {
  display: block;
  width: 100%;
  box-sizing: border-box;
  text-align: center;
  text-decoration: none;
}

/* What's-New modal bullet list (v2.69.0, W3). */
.whatsnew-list {
  margin: 4px 0 0;
  padding-left: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.whatsnew-list li {
  line-height: 1.4;
}

/* Paste-the-page-text auto-fill fallback (v2.70.0, W4). A native <details> so
   the collapse needs no JS; only opened when the online auto-fill can't be
   used, so it stays out of the way by default. */
.paste-fallback {
  margin-top: 10px;
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 0 12px;
}
.paste-fallback > summary {
  cursor: pointer;
  padding: 11px 0;
  color: var(--muted);
  font-size: var(--text-md);
  list-style-position: inside;
}
.paste-fallback[open] > summary {
  border-bottom: 1px solid var(--card-border);
  margin-bottom: 10px;
}
.paste-fallback textarea {
  width: 100%;
  box-sizing: border-box;
}
.paste-fallback > button {
  margin-top: 8px;
  margin-bottom: 12px;
  width: 100%;
}

.results {
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.result-card {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 14px 16px;
}

.result-card .rc-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}

/* ⚠️ `overflow-wrap: anywhere` ON EVERY CARD TITLE, AND IT IS NOT COSMETIC.
   A name with no spaces in it — which the ORDER IMPORTER can produce, since it
   builds custom entries out of retailer product strings — has no break
   opportunity, so it sets a minimum width the flex row cannot go below and the
   whole PAGE scrolls sideways. Measured at v2.107.3: a 67-character token took
   document.scrollWidth to 677 against a 375 viewport, on Home, both humidor
   panes, the rack and the Bar — one missing rule, not five bugs, which is why
   the fix is here and on `.hi-name` rather than per list.

   `anywhere` rather than `break-word`: only `anywhere` also shrinks the
   element's intrinsic min-content width, which is the half that stops the row
   from forcing the page wide. `break-word` alone wraps the text and leaves the
   overflow. */
.result-card .rc-title {
  font-weight: 700;
  color: var(--gold-bright);
  font-size: var(--text-md);
  overflow-wrap: anywhere;
}

.result-card .rc-sub {
  color: var(--faint);
  font-size: var(--text-xs);
  margin-top: 1px;
  overflow-wrap: anywhere;
}

.result-card .rc-match {
  font-size: var(--text-xs);
  background: rgba(201,154,75,0.16);
  color: var(--gold-bright);
  padding: 3px 8px;
  border-radius: 20px;
  white-space: nowrap;
}

/* ⚠️ `.rc-desc` CARRIES USER TEXT TOO, AND IT IS THE ONE THE FIRST PASS MISSED.
   It is the card's body line, so it looks like catalog prose — but the pipe
   card puts "🔥 Last smoked <blend name>" through it, and that name is the
   user's. Fixing the titles alone left the rack overflowing at 513px against a
   375 viewport, with every other list already clean. Found by walking
   scrollWidth-vs-clientWidth down the tree rather than by re-reading the CSS:
   nothing's BOUNDING RECT exceeded the viewport, because the overflow was
   inside a container, so a rect-based probe reported no offenders at all. */
.result-card .rc-desc {
  font-size: var(--text-sm);
  color: var(--cream);
  margin-top: 8px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}

.result-card .rc-tags {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* ---- Shared badge system --------------------------------------------------
   One chip look for status/aging/stock cues across both humidors, Saved
   pairings, and future cards, so the app reads as one design rather than two.
   Three semantic variants by intent:
     --good  (green)  settled / ready / a healthy state
     --info  (gold)   aging readout / an achievement milestone
     --alert (amber)  needs attention (running low, out of stock)
   The legacy .humidor-item .hi-* badge rules below are kept visually identical
   to these on purpose; new markup should use `.badge .badge--*`. Phase 2 folds
   the .hi-* names out entirely when the pipe card is rebuilt. */
.badge {
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 6px;
  display: inline-block;
  margin-top: 6px;
  margin-right: 6px;
}
.badge--good { background: rgba(95,138,82,0.2); color: var(--success-bright); }
.badge--info { background: rgba(201,154,75,0.16); color: var(--gold-bright); }
.badge--alert { background: rgba(224,162,75,0.18); color: var(--warn-bright); }
/* "Your rating" badge on pairing cards (v2.65.0) — gold stars, display-only. */
.badge--rated { background: rgba(201,154,75,0.22); color: var(--gold-bright); letter-spacing: 0.02em; }
.badge--rated .rated-you { opacity: 0.72; font-size: 0.85em; }
.rc-rated-row { margin: 2px 0 6px; }

/* ---- Tonight's Pairing ----------------------------------------------------
   A view-level feature above the pipe/cigar panes: one owned smoke + the best
   owned drink for it. #tonight-result reuses .results (flex column, gapped) so
   the cards, the "enjoy with" divider and the re-roll button space themselves. */
.tonight { margin: var(--space-block) 0; }
.tonight-btn { width: 100%; }
#tonight-result { margin-top: 12px; }
.tonight-join {
  text-align: center;
  color: var(--faint);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.tonight-again { width: 100%; }

/* ---- Bar upgrade: per-bottle pairing panel --------------------------------
   Each bar row is a .bar-row wrapper holding the bottle card plus, when
   expanded, an inset "what smokes pair with this?" panel. The wrapper is the
   flex item in #bar-list (.results), so the panel hugs its own card instead of
   floating a full gap away from it. */
.bar-row { display: flex; flex-direction: column; gap: 8px; }
.bar-pairings,
.hi-pairings {
  margin-left: 12px;
  padding-left: 12px;
  border-left: 2px solid var(--card-border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* The humidor twin sits INSIDE the .result-card (the bar's is a sibling), so
   give it a little breathing room from the toggle button above it. */
.hi-pairings {
  margin-top: 10px;
}

/* ---- Saved Pairings journal ("tried on" row) ------------------------------ */
.fav-tried-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 8px;
  font-size: var(--text-sm);
  color: var(--muted);
}
.fav-tried-row input[type="date"] { margin: 0; }

/* Inline status / stock / date editors revealed under "▼ Details" on the
   humidor card (v2.54.0). Kept in the expanded section so collapsed cards stay
   scannable. Chips reuse .chip / .chip-row from the bar. */
.hi-edit-fields {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.hi-field { display: flex; flex-direction: column; gap: 4px; }
.hi-field-label {
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.hi-field-dates { display: flex; flex-wrap: wrap; gap: 10px; }
.hi-field-dates label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.hi-field-dates input[type="date"] { margin: 0; }
/* Bulk ounces field — a compact inline number + "oz" suffix where a sealed
   tin shows its Full/Half/Low chips (v2.61.0). */
.hi-oz { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--muted); }
.hi-oz input[type="number"] { width: 5.5em; margin: 0; }

/* ---------- Smoking Journal (v2.62.0) ------------------------------------- */
.journal-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.journal-head h2 { margin: 0; }
.journal-head .secondary { flex: 0 0 auto; }
/* A quiet text-link button (Home "View full journal", etc.). */
.link-btn {
  background: none; border: none; padding: 6px 0 0; margin: 0;
  color: var(--accent-bright); font: inherit; font-size: var(--text-sm);
  cursor: pointer; text-align: left;
}
.link-btn:hover { text-decoration: underline; }
.journal-filter { margin-bottom: 10px; }
.journal-summary { margin: 4px 2px 12px; }
/* v2.77.0 — the insight cards sit between the summary line and the heatmap.
   They reuse .result-card via statCard(), so this only needs the spacing. */
.journal-insights { margin-bottom: 14px; }

.journal-heatmap {
  background: var(--card); border: 1px solid var(--card-border);
  border-radius: 10px; padding: 12px; margin-bottom: 16px;
}
.jhm-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.jhm-title { font-size: var(--text-md); font-weight: 600; }
.jhm-arrow { padding: 4px 12px; }
.jhm-dow, .jhm-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.jhm-dow { margin-bottom: 4px; }
.jhm-dow span { text-align: center; font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.jhm-cell {
  aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); border-radius: 5px; color: var(--muted);
  background: var(--bg-elevated); border: 1px solid var(--card-border);
}
.jhm-pad { background: transparent; border: none; }
/* Single-hue ramp off the accent, so it reads in both themes. */
.jhm-l0 { opacity: 0.55; }
.jhm-l1 { background: color-mix(in srgb, var(--accent) 30%, var(--bg-elevated)); border-color: transparent; }
.jhm-l2 { background: color-mix(in srgb, var(--accent) 55%, var(--bg-elevated)); border-color: transparent; color: #fff; }
.jhm-l3 { background: color-mix(in srgb, var(--accent) 80%, var(--bg-elevated)); border-color: transparent; color: #fff; }
.jhm-l4 { background: var(--accent-bright); border-color: transparent; color: #fff; }

.journal-day { margin-bottom: 14px; }
.journal-day-head {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); margin-bottom: 6px; padding-bottom: 3px;
  border-bottom: 1px solid var(--card-border);
}
.journal-entry {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 5px 2px; font-size: var(--text-md);
}
.journal-kind { flex: 0 0 auto; }
.journal-label { flex: 1 1 auto; }
.journal-bev { flex: 0 0 auto; font-size: var(--text-xs); color: var(--muted); }

/* ---------- Share + toast (v2.67.0) --------------------------------------- */
.tonight-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.tonight-actions .secondary { flex: 1 1 auto; }
.app-toast {
  position: fixed; left: 50%; bottom: 84px; transform: translateX(-50%) translateY(12px);
  background: var(--bg-elevated); color: var(--cream); border: 1px solid var(--card-border);
  border-radius: 8px; padding: 10px 16px; font-size: var(--text-sm); box-shadow: 0 4px 16px rgba(0,0,0,0.35);
  opacity: 0; pointer-events: none; transition: opacity 0.18s ease, transform 0.18s ease; z-index: 60;
  max-width: 90vw; text-align: center;
}
.app-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---------- Wishlist (v2.64.0) -------------------------------------------- */
.wishlist-add { margin-bottom: 12px; flex-wrap: wrap; }
.wishlist-row .rc-title { font-size: var(--text-md); }
.wishlist-got { margin-top: 8px; width: 100%; }

/* ---------- 21+ age gate (v2.86.0) ----------------------------------------
   Driven entirely by `data-age-gate` on <html>, stamped by settings-init.js
   before first paint. CSS does the hiding rather than JS so the app cannot
   render for a frame before the gate covers it.

   ⚠️ THE DEFAULT IS HIDDEN AND THAT IS THE SAFE DIRECTION HERE, because the
   attribute is only ever ADDED when the gate is needed — never removed to
   reveal the app on a device that hasn't answered. A confirmed user has no
   attribute at all and pays nothing for these rules. */
#age-gate { display: none; }
html[data-age-gate] #age-gate {
  display: flex; align-items: center; justify-content: center;
  position: fixed; inset: 0; z-index: 200;
  background: var(--bg); padding: 24px;
  /* Cover the notch/home-indicator area too — the app sets viewport-fit=cover,
     so without this the gate leaves live app pixels visible at the edges. */
  padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right))
           max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
  overflow-y: auto;
}
/* Hide the app itself, not just cover it: a fixed overlay still leaves the
   content behind it reachable by Tab and readable by a screen reader. */
html[data-age-gate] main,
html[data-age-gate] .tabbar,
html[data-age-gate] .app-header { display: none; }

/* ---------- app lock (v2.107.0) -------------------------------------------
   Deliberately a near-clone of the age gate above, sharing .age-gate-card, so
   the two pre-paint gates cannot drift apart visually or in what they hide.
   Same default-hidden direction and the same reason: the attribute is only
   ever ADDED, never removed to reveal the app on a device that has not been
   unlocked. */
#lock-screen { display: none; }
html[data-app-lock] #lock-screen {
  display: flex; align-items: center; justify-content: center;
  position: fixed; inset: 0; z-index: 201;
  background: var(--bg);
  padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right))
           max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
  overflow-y: auto;
}
/* Hide the app itself rather than covering it — a fixed overlay still leaves
   the content behind it reachable by Tab and readable by a screen reader, and
   a lock that a screen reader can read straight through is not a lock. */
html[data-app-lock] main,
html[data-app-lock] .tabbar,
html[data-app-lock] .app-header { display: none; }

/* ⚠️ THE AGE GATE WINS WHEN BOTH ARE PENDING. It is a compliance gate answered
   by whoever is holding the phone, it costs one tap, and stacking two
   full-screen dialogs would leave whichever lost sitting invisibly behind the
   other. z-index alone would not do it — the loser must be DISPLAY-hidden or
   it stays tabbable, which is the same mistake this file warns about above. */
html[data-age-gate] #lock-screen { display: none; }

#lock-screen input { width: 100%; margin-bottom: 10px; }
#lock-screen .primary, #lock-screen .secondary { width: 100%; }
.lock-recovery { margin-top: 18px; text-align: left; }
.lock-recovery summary { cursor: pointer; color: var(--muted); font-size: var(--text-sm); }
.lock-recovery p { margin: 10px 0; }
.lock-recovery input { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.04em; }

.age-gate-card { max-width: 30rem; text-align: center; }
.age-gate-card h2 { margin: 0 0 12px; }
.age-gate-card p { color: var(--muted); line-height: 1.5; margin: 0 0 18px; }
.age-gate-actions { display: flex; flex-direction: column; gap: 10px; }
.age-gate-actions button { width: 100%; }
.age-gate-fineprint { font-size: var(--text-xs); margin: 18px 0 0; }

/* ---------- Feature guide (v2.86.0) --------------------------------------- */
.guide-section { margin-bottom: 22px; }
.guide-section h3 {
  font-size: var(--text-md); margin: 0 0 4px;
  padding-bottom: 4px; border-bottom: 1px solid var(--card-border);
}
.guide-section p { margin: 8px 0; line-height: 1.5; font-size: var(--text-md); }
.guide-section ul { margin: 8px 0; padding-left: 20px; }
.guide-section li { margin-bottom: 6px; line-height: 1.45; font-size: var(--text-sm); }
.guide-lead { color: var(--muted); }
.guide-toc { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
.guide-toc a {
  font-size: var(--text-xs); padding: 4px 10px; border-radius: 999px;
  background: var(--bg-elevated); border: 1px solid var(--card-border);
  color: var(--accent-bright); text-decoration: none;
}

/* ---------- Import Order History (v2.81.0) -------------------------------- */
#orders-paste { width: 100%; font-family: inherit; font-size: var(--text-sm); margin-bottom: 8px; }
.orders-actions { margin-bottom: 10px; }

.oi-summary {
  background: var(--card); border: 1px solid var(--card-border);
  border-radius: 10px; padding: 12px; margin-bottom: 16px;
}
.oi-counts { font-size: var(--text-md); margin-bottom: 8px; }
.oi-summary button { width: 100%; margin-top: 4px; }
.oi-summary button[disabled] { opacity: 0.5; cursor: default; }
/* Both of these carry meaning that must survive a monochrome or colour-blind
   reading, which is why each also leads with a word or a glyph rather than
   relying on the tint alone. */
.oi-warn, .oi-ok {
  font-size: var(--text-sm); line-height: 1.4; border-radius: 8px;
  padding: 8px 10px; margin-bottom: 8px;
}
.oi-warn { background: color-mix(in srgb, var(--warn, #b4762a) 18%, transparent); }
.oi-ok { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--muted); }

.oi-section { margin-bottom: 18px; }
.oi-head {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); margin: 0 0 6px; padding-bottom: 3px;
  border-bottom: 1px solid var(--card-border);
}
.oi-note { margin: 0 0 8px; font-size: var(--text-sm); }
.oi-row { padding: 6px 2px; border-bottom: 1px solid color-mix(in srgb, var(--card-border) 50%, transparent); }
.oi-row:last-child { border-bottom: none; }
.oi-main { font-size: var(--text-md); display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.oi-name { flex: 0 1 auto; }
.oi-meta { font-size: var(--text-xs); color: var(--muted); margin-top: 2px; }
.oi-inferred { font-style: italic; }
.oi-flag {
  flex: 0 0 auto; font-size: var(--text-2xs); text-transform: uppercase;
  letter-spacing: 0.03em; padding: 1px 6px; border-radius: 4px;
  background: var(--bg-elevated); border: 1px solid var(--card-border); color: var(--muted);
}
.oi-flag-warn { background: color-mix(in srgb, var(--warn, #b4762a) 30%, var(--bg-elevated)); border-color: transparent; }
.oi-flag-info { background: color-mix(in srgb, var(--accent) 28%, var(--bg-elevated)); border-color: transparent; }

/* Inline "log a past smoke" form on the humidor card (v2.55.1). */
.hi-session-form {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: 8px;
}
.hi-session-form input[type="date"] { margin: 0; flex: 0 0 auto; }
.hi-session-form button { padding: 6px 10px; font-size: var(--text-sm); }

/* Blend Family breakdown chart (Humidor Stats) — hand-rolled with plain
   CSS flex segments, no charting library/CDN needed. */
.family-chart-bar {
  display: flex;
  overflow: hidden;
  border-radius: 8px;
  height: 14px;
  margin-top: 10px;
  background: var(--bg-elevated);
}
.family-chart-segment {
  min-width: 2px;
}
.family-chart-legend {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.family-chart-legend-item {
  font-size: var(--text-xs);
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 5px;
}
.family-chart-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
}

.tag {
  font-size: var(--text-xs);
  background: rgba(240,227,206,0.07);
  color: var(--muted);
  padding: 3px 9px;
  border-radius: 20px;
  border: 1px solid var(--card-border);
}

.tag.match {
  color: var(--cream);
  background: rgba(143,58,42,0.35);
  border-color: var(--accent-bright);
}

.rc-badge {
  display: inline-block;
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--bg);
  background: var(--gold);
  padding: 2px 8px;
  border-radius: 6px;
  margin-top: 8px;
}

.strength-bar {
  display: flex;
  gap: 3px;
  margin-top: 8px;
}
.strength-bar span {
  width: 16px;
  height: 5px;
  border-radius: 3px;
  background: var(--card-border);
}
.strength-bar span.filled {
  background: var(--accent-bright);
}

.rc-source {
  margin-top: 8px;
  font-size: var(--text-xs);
}
.rc-source a {
  color: var(--faint);
}

.rc-match-summary {
  margin-top: 8px;
  font-size: var(--text-xs);
  color: var(--gold-bright);
  font-weight: 600;
}

.rc-save {
  margin-top: 12px;
  background: transparent;
  border: 1px solid var(--card-border);
  color: var(--muted);
  padding: 7px 14px;
  font-size: var(--text-xs);
  border-radius: 20px;
}
.rc-save.saved {
  color: var(--gold-bright);
  border-color: var(--gold);
  background: rgba(201,154,75,0.14);
}

/* Flavor Finder — "show what pairs with this" button and its expanded panel.
   The panel nests result cards inside a result card, so it's inset with a gold
   rule on the left to keep the hierarchy readable rather than looking like a
   flat run of siblings. */
.rc-pair-btn {
  margin-top: 12px;
  display: block;
  width: 100%;
  background: rgba(201,154,75,0.12);
  border: 1px solid var(--gold);
  color: var(--gold-bright);
  padding: 9px 14px;
  font-size: var(--text-xs);
  border-radius: 20px;
}

.rc-pair-panel {
  display: none;
}
.rc-pair-panel.open {
  display: block;
  margin-top: 12px;
  padding-left: 12px;
  border-left: 2px solid var(--gold);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.rc-pair-panel-title {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

/* Cigar humidor — quantity stepper and rest row. Count-based rather than
   one row per stick, so the controls are inline on the card. */
.ch-qty-row,
.ch-rest-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  flex-wrap: wrap;
}

.ch-step {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--gold);
  background: rgba(201,154,75,0.12);
  color: var(--gold-bright);
  font-size: var(--text-lg);
  line-height: 1;
  padding: 0;
}

.ch-qty {
  font-size: var(--text-md);
  color: var(--cream);
  min-width: 92px;
}
.ch-qty strong {
  color: var(--gold-bright);
  font-size: var(--text-lg);
}

.ch-boxes {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}
.ch-boxes label,
.ch-rest-row label {
  font-size: var(--text-xs);
  color: var(--faint);
}
.ch-boxes input {
  width: 62px;
  padding: 6px 8px;
  margin: 0;
}
.ch-rest-row input[type="date"] {
  padding: 6px 8px;
  margin: 0;
  flex: 1;
  min-width: 140px;
}
/* Subtle price control (v2.72.0). Plain tappable text — "💰 $X" when set, a
   faint "＋ price" when not — with the number input hidden until tapped. No chip
   background, muted, so it stays quiet on a browsing card. Replaced the old
   always-on .bar-price-row / .ch-price boxes that read as cheap/cluttered. */
.price-row { display: flex; align-items: baseline; gap: 5px; margin: 2px 0 8px; }
.price-control { display: inline-flex; align-items: center; gap: 6px; }
.price-display {
  background: none;
  border: none;
  padding: 3px 0;
  margin: 0;
  color: var(--muted);
  font-size: var(--text-sm);
  cursor: pointer;
  line-height: 1.2;
}
.price-display--empty { opacity: 0.55; }
.price-display:active { opacity: 0.5; }
.price-unit { color: var(--muted); font-size: var(--text-xs); opacity: 0.7; }

/* The number input, unified across bar/cigar/tin. `appearance` strips the
   native up/down spinner arrows — the biggest "cheap" tell on Android Chrome. */
.price-field {
  width: 6em;
  margin: 0;
  -moz-appearance: textfield;
  appearance: textfield;
}
.price-field::-webkit-outer-spin-button,
.price-field::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* The small "💰 value" summary card shown atop the Bar / Pipes lists. */
.bar-value-card { padding: 10px 12px; }
.bar-value-card .rc-desc { margin: 0; }

/* Size field — free text, since the catalog stores no size range. */
.ch-size {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 100%;
}
.ch-size input {
  margin: 0;
  padding: 6px 8px;
  flex: 1;
  min-width: 0;
}

/* Zero on hand: dim the card but keep it listed, so a blend you've smoked
   through stays visible for re-stocking rather than vanishing. */
.result-card.ch-empty {
  opacity: 0.55;
}

/* Rating + notes, shared per cigar line (not per stick) via blendProfiles. */
.ch-profile {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--card-border);
}
.ch-stars {
  margin-bottom: 8px;
}

/* Box photo for the line. The whole .ch-photo wrapper starts hidden and is
   revealed by loadCigarThumbnails() once a blob actually loads, which is
   also what keeps the ✕ from appearing when there's nothing to remove. */
.ch-photo-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.ch-photo {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}
.ch-photo img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--card-border);
  background: var(--bg-elevated);
  display: block;
}
.ch-photo-del {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 22px;
  height: 22px;
  padding: 0;
  line-height: 1;
  font-size: var(--text-2xs);
  border-radius: 50%;
  border: 1px solid var(--card-border);
  background: var(--bg-elevated);
  color: var(--muted);
}
.ch-photo-add {
  padding: 7px 12px;
  font-size: var(--text-sm);
}

/* ---------- Pipe collection cards (v2.57.0) ---------- */
/* The pipe photo is shown LARGER than a tin/cigar thumbnail — showing the pipe
   off is the point — and stacks above the controls at full card width, since a
   pipe is long. Reuses the .ch-photo-row / .ch-photo-del machinery. */
.pipe-card .ch-photo-row {
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
.pipe-photo {
  position: relative;
  display: block;
  width: 100%;
}
.pipe-photo img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--bg-elevated);
  display: block;
}
.pipe-card-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
.pipe-card .hi-edit {
  background: transparent;
  border: none;
  color: var(--faint);
  font-size: var(--text-md);
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
}
.pipe-card .hi-edit:active {
  color: var(--gold-bright);
}
.pipe-smoke-btn {
  width: 100%;
  margin: 8px 0 10px;
  padding: 8px 12px;
  font-size: var(--text-sm);
}
/* "From your rack: …" — the personalised pipe hint on a tobacco card. */
.rc-owned-pipe {
  color: var(--gold-bright);
}
/* The two import buttons (Replace / Merge) sit side by side. */
.backup-import-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 10px;
}
.backup-import-row button {
  width: 100%;
}
.ch-notes {
  width: 100%;
  margin: 0;
  font-size: var(--text-sm);
  min-height: 46px;
  resize: vertical;
}

/* Humidor list */
.humidor-item {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}
.humidor-item .hi-info { flex: 1; }
/* Same rule, same reason, for the pipe humidor's own title class — see
   `.rc-title`. Card markup differs by list (HANDOFF §5) and that is exactly how
   a fix like this gets applied to three of four places. */
.humidor-item .hi-name {
  font-weight: 700;
  color: var(--gold-bright);
  font-size: var(--text-md);
  overflow-wrap: anywhere;
}
.humidor-item .hi-meta {
  font-size: var(--text-xs);
  color: var(--muted);
  margin-top: 3px;
  line-height: 1.4;
}
.humidor-item .hi-status {
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 6px;
  background: rgba(95,138,82,0.2);
  color: var(--success-bright);
  display: inline-block;
  margin-top: 6px;
  margin-right: 6px;
}
.humidor-item .hi-milestone {
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 6px;
  /* Gold (info), matching the cigar humidor's milestone chip — an achievement
     badge reads the same on both sides now, not green here and gold there. */
  background: rgba(201,154,75,0.16);
  color: var(--gold-bright);
  display: inline-block;
  margin-top: 6px;
}
/* Grouped humidor cards — tins sharing the same brand, name & status
   collapse into one card with a ×N pill; expanding reveals the individual
   tin cards underneath, unchanged from how they'd render standalone.
   .humidor-group-header also carries the .humidor-item class so it gets
   that card's background/border/padding/flex-row layout and all its
   .hi-* badge styling for free — .humidor-group itself stays unstyled so
   the header doesn't end up with a double border. */
.humidor-group-items {
  margin: 8px 0 0 16px;
  padding-left: 12px;
  border-left: 2px solid var(--card-border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.humidor-group-toggle {
  background: var(--bg-elevated);
  color: var(--gold);
  border: 1px solid var(--card-border);
  padding: 8px 12px;
  border-radius: 8px;
  font-size: var(--text-sm);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Per-tin card collapse/expand toggle ("▼ Details" / "▲ Less") — styled as
   a plain text link like .star-picker-clear, not a bordered button, so it
   reads as secondary to the actual Edit/🔥 actions. */
.hi-details-toggle {
  display: block;
  background: none;
  border: none;
  color: var(--gold);
  font-size: var(--text-xs);
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
  margin-top: 6px;
}

.humidor-item .hi-edit {
  background: var(--bg-elevated);
  color: var(--gold);
  border: 1px solid var(--card-border);
  padding: 8px 12px;
  border-radius: 8px;
  font-size: var(--text-sm);
}
.humidor-item .hi-smoke {
  background: var(--bg-elevated);
  color: var(--gold-bright);
  border: 1px solid var(--card-border);
  padding: 8px 10px;
  border-radius: 8px;
  font-size: var(--text-md);
}

/* Tin photo upload (modal) */
.photo-preview-wrap {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 8px 0 10px;
}
.photo-preview-wrap img {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--bg-elevated);
}
.photo-preview-wrap button {
  padding: 8px 12px;
  font-size: var(--text-sm);
}

/* Take Photo / Choose from Library — two label-triggered file inputs side
   by side (the actual <input type="file"> elements are visually hidden;
   the camera one adds `capture="environment"` so mobile browsers open the
   camera directly instead of the full picker). */
.photo-source-row {
  display: flex;
  gap: 8px;
  margin-top: 6px;
}
.photo-source-row label {
  flex: 1;
  margin: 0;
  text-align: center;
}

/* Smoke History list (Add/Edit Humidor Item modal) */
.session-list {
  max-height: 160px;
  overflow-y: auto;
  border: 1px solid var(--card-border);
  border-radius: 10px;
  margin-top: 6px;
  background: var(--bg-elevated);
}
.session-list .empty-state {
  padding: 14px 4px;
}
.session-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--card-border);
  font-size: var(--text-sm);
}
.session-row:last-child {
  border-bottom: none;
}
.session-date {
  color: var(--cream);
  white-space: nowrap;
}
.session-beverage {
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-remove {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--faint);
  font-size: var(--text-lg);
  line-height: 1;
  padding: 2px 6px;
  cursor: pointer;
}
.session-add-row {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}
.session-add-row input[type="date"] {
  flex: 1;
  min-width: 0;
}
/* The beverage field is a picker button, not a <select> — it has to flex like
   one and truncate rather than push "+ Add" off a 320px row. */
.session-add-row #btn-session-beverage {
  flex: 1.4;
  min-width: 0;
  padding: 8px 10px;
  font-size: var(--text-sm);
}
.session-add-row #session-beverage-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-add-row button {
  flex-shrink: 0;
  padding: 8px 12px;
  font-size: var(--text-sm);
}
/* Cigar smoke log. The 🔥 sits next to the remove ✕ in the card header, so
   the two top-right controls need to line up rather than stack. */
.ch-top-actions {
  display: flex;
  gap: 4px;
  align-items: flex-start;
  flex-shrink: 0;
}
.ch-log {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--card-border);
  font-size: var(--text-sm);
}
.ch-log-empty {
  color: var(--muted);
}
.ch-log-head {
  font-weight: 600;
  margin-bottom: 4px;
}
.ch-log-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.ch-log-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 0;
  min-width: 0;
}
.ch-log-list li > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ch-log-del {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 2px 6px;
  font-size: var(--text-xs);
  line-height: 1;
}
.ch-log-del:hover {
  color: var(--cream);
}
.ch-log-more {
  margin-top: 2px;
}

/* Add Tin catalog field: picker button + a clear control that returns to
   "custom / not listed". Same shape as .session-add-row. */
.catalog-pick-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}
.catalog-pick-row #btn-item-catalog {
  flex: 1;
  min-width: 0;
}
.catalog-pick-row #item-catalog-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#btn-clear-item-catalog {
  flex-shrink: 0;
  padding: 8px 10px;
}
#btn-clear-item-catalog.hidden {
  display: none;
}

/* Mirrors .star-picker-clear: only shown once a beverage is actually set,
   since the picker itself has no "none" row to pick. */
#btn-clear-session-beverage {
  flex-shrink: 0;
  padding: 8px 10px;
}
#btn-clear-session-beverage.hidden {
  display: none;
}

/* My Rating star picker (Add/Edit Humidor Item modal) */
.star-picker {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
}
.star-picker .star-btn {
  background: none;
  border: none;
  font-size: var(--text-2xl);
  line-height: 1;
  padding: 2px 1px;
  color: var(--card-border);
  cursor: pointer;
}
.star-picker .star-btn.filled {
  color: var(--gold-bright);
}
.star-picker .star-picker-clear {
  background: none;
  border: none;
  color: var(--muted);
  font-size: var(--text-xs);
  text-decoration: underline;
  margin-left: 6px;
  cursor: pointer;
}

/* ⚠️ --muted, NOT --faint (v2.89.0). This is the one message a new user most
   needs to read — every list in the app teaches itself through its empty state,
   and there are 48 of them — and it was painted in the lowest-emphasis colour
   in the palette. Whispering the instructions to the person who needs them most
   is exactly backwards. */
.empty-state {
  text-align: center;
  color: var(--muted);
  padding: 30px 10px;
  font-size: var(--text-md);
}

/* The "Pipe tobaccos" / "Cigars" headings between the two rankings a pairing
   returns. Lifted out of an inline style in app.js's sectionHeading() at
   v2.89.0 so it sits on the type scale like every other heading. --text-lg,
   one step above a card title, which is what makes two stacked rankings read
   as two sections rather than ten cards. */
.rc-section {
  color: var(--gold-bright);
  font-size: var(--text-lg);
  margin: var(--space-block) 0 var(--space-2);
}

/* ---------- bulk select (v2.90.3) ------------------------------------------
   A selection row is deliberately much plainer than a result card: identity,
   the field you are about to change, and a checkbox. Anything else would be a
   mis-tap risk on a surface whose whole purpose is tapping quickly down a list. */
#bulk-bar {
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.bulk-bar-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2);
}
.bulk-bar-actions {
  display: flex;
  gap: var(--space-3);
}
.bulk-bar-ops {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.bulk-delete {
  width: 100%;
  color: var(--accent-bright);
}
#bulk-status:empty {
  display: none;
}
.bulk-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  text-align: left;
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: var(--space-3);
  cursor: pointer;
}
.bulk-row.selected {
  border-color: var(--gold);
  background: var(--bg-elevated);
}
.bulk-check {
  font-size: var(--text-lg);
  flex: 0 0 auto;
}
.bulk-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* Checkbox rows ("only from my humidor" toggles) */
.checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--text-sm);
  color: var(--muted);
  cursor: pointer;
}
.checkbox-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--gold);
  flex-shrink: 0;
}

/* Flavor Finder tappable chips */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.chip {
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  color: var(--muted);
  padding: 6px 12px;
  border-radius: 20px;
  font-size: var(--text-xs);
}
.chip:active {
  transform: translateY(1px);
}
/* Selected state for chips used as filters (tobacco picker) rather than as
   one-shot flavor suggestions. */
.chip.active {
  background: var(--chip-active-bg);
  border-color: var(--chip-active-bg);
  color: var(--chip-active-fg);
  font-weight: 600;
}

/* Tobacco picker (Pair by Tobacco) */
.picker-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  color: var(--cream);
  padding: 12px;
  border-radius: 10px;
  font-size: var(--text-md);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.picker-caret {
  color: var(--muted);
  flex-shrink: 0;
}
/* Pin the search box and filter chips while only the list scrolls — the base
   .modal scrolls as one block, which would push them out of reach. */
.picker-modal {
  display: flex;
  flex-direction: column;
  /* dvh matters most here: this sheet is overflow:hidden with only
     .picker-list scrolling, so if 88vh makes it taller than the visible
     viewport, the Cancel row goes off-screen and CANNOT be scrolled back. */
  height: 88vh;
  height: 88dvh;
  overflow: hidden;
}
.picker-list {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 -4px;
}
.picker-row {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--card-border);
  color: var(--cream);
  padding: 12px 4px;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}
.picker-row:active {
  background: var(--card);
}
.picker-row.selected {
  background: var(--card);
  box-shadow: inset 3px 0 0 var(--gold);
}
.picker-row-title {
  font-size: var(--text-md);
}
.picker-row-sub {
  font-size: var(--text-xs);
  color: var(--muted);
}

/* Truncation notice at the foot of a long picker list (PICKER_MAX_ROWS).
   Styled as a note, not a row — it must not read as something tappable. */
.picker-more {
  margin: 0;
  padding: 0.75rem 0.9rem;
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--muted);
  text-align: center;
  border-top: 1px solid var(--card-border);
}

/* Rows carrying an edit affordance (custom cigars only). The action button is
   a sibling of .picker-row rather than a child — nested buttons are invalid
   HTML — so the wrapper is what puts them on one line. .picker-row already
   sets width:100%, which makes it take the free space on its own. */
.picker-row-wrap {
  display: flex;
  align-items: stretch;
  gap: 4px;
}
.picker-row-edit {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--card-border);
  color: var(--muted);
  padding: 0 12px;
  font-family: inherit;
  font-size: var(--text-md);
  cursor: pointer;
}
.picker-row-edit:active {
  background: var(--card);
  color: var(--gold);
}

/* Sits under the scrolling list, above the Cancel row. */
.picker-add-row {
  width: 100%;
  margin-top: 10px;
}

/* My Bar — remove button and the Full/Half/Low stock chips on each row */
.rc-remove {
  background: transparent;
  border: none;
  color: var(--faint);
  font-size: var(--text-md);
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
  flex-shrink: 0;
}
.rc-remove:active {
  color: var(--accent-bright);
}
.bar-stock-row {
  margin-top: 10px;
  margin-bottom: 0;
}

/* Onboarding banner (first launch) */
.onboarding-banner {
  background: var(--card);
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: var(--space-block);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.onboarding-banner p {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--cream);
  line-height: 1.45;
}
.onboarding-banner button {
  align-self: flex-end;
}
/* Install banner's two-button row (Install now / Got it) — override the single
   flex-end button rule so they sit side by side, right-aligned. */
.banner-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  flex-wrap: wrap;
}
.banner-actions button {
  align-self: auto;
}

/* "Open a box" button on a cigar line — compact, left-aligned, not full width. */
.ch-openbox {
  align-self: flex-start;
  margin-top: 8px;
  font-size: var(--text-md);
  padding: 8px 14px;
}

/* Humidor toolbar (search / sort / filter) */
.humidor-toolbar {
  margin: var(--space-block) 0 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.humidor-toolbar-row {
  display: flex;
  gap: 8px;
}
.humidor-toolbar-row select {
  flex: 1;
}

/* Three-across accordion buttons (Stats / Surprise Me / Saved Pairings) —
   deliberately more compact than .secondary since three now share one row
   instead of each being a separate full-width button. */
/* The pane is a group of its own and must not touch the view-level tools row
   above it. Its first child is a .hint with no top margin, so without this the
   "Journal / Wishlist / Import Orders" row ran straight into "Track what you
   have on hand…" and the two read as one block. */
#humidor-pane-tobacco,
#humidor-pane-cigar,
#humidor-pane-pipes,
#smoke-pane-pipe,
#smoke-pane-cigar {
  margin-top: var(--space-block);
}

/* Four across at 375px gives each button ~85px against a 0.82rem label and
   9px/4px padding — labels like "📓 Smoking Journal" would wrap mid-word or
   clip. Wrapping to 2x2 keeps them readable and keeps the four view-level
   destinations visually one group. */
.humidor-tools-row--wrap {
  flex-wrap: wrap;
}
.humidor-tools-row--wrap .tool-btn {
  flex: 1 1 45%;
}

.humidor-tools-row {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-block);
}
.tool-btn {
  flex: 1;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  cursor: pointer;
  padding: 9px 4px;
  font-size: var(--text-sm);
  text-transform: none;
  letter-spacing: normal;
}

/* Tab bar */
.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: var(--tabbar-height);
  background: var(--bg-elevated);
  border-top: 1px solid var(--card-border);
  display: flex;
  padding-bottom: env(safe-area-inset-bottom);
  z-index: 20;
}

.tab {
  flex: 1;
  min-width: 0; /* four tabs since v2.91.0 (six before it) — let labels shrink, not overflow */
  background: none;
  color: var(--faint);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-size: var(--text-2xs);
  padding: 6px 2px;
}

.tab > span:not(.icon) {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tab .icon {
  font-size: var(--text-xl);
  line-height: 1;
}

.tab.active {
  color: var(--gold-bright);
}

/* ---------- Home dashboard (v2.56.0) ---------- */
.home-tiles {
  display: flex;
  gap: 8px;
  margin: 14px 0;
}
.home-tile {
  flex: 1;
  min-width: 0;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 12px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  cursor: pointer;
}
.home-tile-value {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--gold-bright);
  line-height: 1;
}
.home-tile-label {
  font-size: var(--text-xs);
  color: var(--faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* ⚠️ THESE ARE BUTTONS WEARING A BADGE'S CLOTHES, AND `.badge` IS SIZED FOR
   TEXT. Home's attention chips inherit `.badge`'s 2px padding and 2xs type,
   which rendered them **19px tall** — under WCAG 2.2 AA's 24x24 floor for a
   target, on the one surface built to be tapped rather than read. The badge
   rule itself is left alone: it is mostly non-interactive labels, where target
   size does not apply and a taller pill would just look wrong.

   28px rather than 44px deliberately. 44 is the comfortable touch size and
   these would stop being chips at that height — they sit in a wrapping row
   under a card and reading as a row of pills is the point. 28 clears the
   normative floor with margin; the row's `gap` keeps neighbouring targets from
   crowding, which is the other half of what 2.5.8 is about. */
.home-chip {
  font-family: inherit;
  border: none;
  cursor: pointer;
  min-height: 28px;
  padding-top: 5px;
  padding-bottom: 5px;
  /* Home's chips carry BLEND NAMES ("🔥 Peter Stokkebye — PS6"), so they are
     the one badge that holds user-supplied text and the one that can be forced
     wide by a name with no spaces in it. `.badge` is inline-block, so without
     these the chip sets the page's minimum width. See `.rc-title`. */
  max-width: 100%;
  overflow-wrap: anywhere;
}
.home-shortcuts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 16px;
}
.home-shortcuts button {
  width: 100%;
}
/* The four Pair shortcuts sit 2x2; My Humidor is a different kind of
   destination and gets its own full-width row below them (v2.92.1). The
   tighter margin keeps the two rows reading as one block rather than two. */
.home-shortcuts--single {
  grid-template-columns: 1fr;
  margin-top: 8px;
}

/* Modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: none;
  align-items: flex-end;
  z-index: 30;
}
.modal-backdrop.active {
  display: flex;
}

/* A picker can be opened from INSIDE another modal — the smoke-session
   beverage field lives in the Edit Tin modal and borrows beveragePicker via
   open(onPick). All backdrops share z-index 30, so this only worked because
   the picker backdrops happen to sit later in index.html than #modal-backdrop.
   That's a source-order accident: reordering the markup would silently drop
   the picker behind the modal it was opened from. Make it explicit. */
#beverage-picker-backdrop.active,
#tobacco-picker-backdrop.active,
#cigar-picker-backdrop.active {
  z-index: 35;
}

/* Add-a-custom-cigar / add-a-custom-tobacco are opened from inside their
   pickers (35) and the picker deliberately stays open behind them, so they
   have to outrank the sheet they were launched from. Below the update popup
   at 40, which outranks all. */
#cigar-custom-modal-backdrop.active,
#tobacco-custom-modal-backdrop.active {
  z-index: 36;
}

/* Checking for updates from inside the Settings modal can trigger this
   popup while Settings is still open — stack it above any other modal
   (same base z-index otherwise) so it's never hidden behind one. */
#update-modal-backdrop.active {
  z-index: 40;
}

/* The passphrase dialog is opened from INSIDE Settings, so it has the same
   problem the pickers have above — and worse, because #settings-modal-backdrop
   is the LAST backdrop in index.html, so source order puts it on top of
   everything that shares a z-index. Without this the dialog renders but every
   tap lands on the Settings backdrop behind it.

   ⚠️ FOUND BY PLAYWRIGHT, NOT BY THE BROWSER PROBE THAT "VERIFIED" THE FEATURE.
   A programmatic .click() does not hit-test, so the manual probe drove the
   whole export/import round trip successfully while a real finger could not
   have pressed the button (HANDOFF §5 warns about exactly this asymmetry).

   Above the update popup deliberately: this dialog holds a pending promise for
   an operation the user just started, where the update popup is background
   news that can wait its turn. */
#passphrase-backdrop.active,
#lock-setup-backdrop.active {
  z-index: 41;
}

/* ⚠️ THE LOCK-SETUP BACKDROP WAS ADDED TO THIS RULE ONE RELEASE LATER, AFTER
   REPEATING THE EXACT BUG THE NOTE ABOVE DESCRIBES. The warning was written,
   sat here, and the next dialog opened from Settings still shipped without a
   z-index — because the person adding a modal reads the MODAL's markup, not
   this file. **Any new `.modal-backdrop` opened from inside Settings belongs
   in this selector.** If that happens a third time, the fix is not another
   comment: give `.modal-backdrop` a stacking rule that does not depend on
   source order at all. */
.modal {
  background: var(--bg-elevated);
  width: 100%;
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  border-radius: 20px 20px 0 0;
  padding: 20px 18px calc(20px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--card-border);
}
.modal h3 {
  margin: 0 0 4px;
  color: var(--gold-bright);
}
/* The passphrase dialog (v2.106.0). The warning is styled as an alert rather
   than a .hint on purpose: a hint is skimmable and this one is the difference
   between a recoverable collection and a dead file. Same palette pair as
   .badge--alert, so "this is a warning" reads consistently across the app. */
.passphrase-warning {
  background: rgba(224, 162, 75, 0.15);
  color: var(--warn-bright);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin: 12px 0;
  font-size: var(--text-sm);
  line-height: 1.45;
}
.passphrase-error {
  color: var(--accent-bright);
  font-size: var(--text-sm);
  margin: 8px 0 0;
}
.modal-actions {
  display: flex;
  gap: 8px;
  margin-top: 20px;
  align-items: center;
}
.modal-actions .spacer { flex: 1; }
.modal-actions .primary { margin-top: 0; width: auto; padding: 11px 20px; }

.hidden { display: none !important; }

/* Settings modal — segmented (radio-like) button groups for theme/text size */
/* Binds to the page title above it — a segment IS the title's sub-navigation,
   so it sits tight against it rather than floating between two groups. */
.segmented {
  display: flex;
  gap: 6px;
  margin-top: var(--space-2);
}
.seg-btn {
  flex: 1;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  color: var(--muted);
  padding: 10px 4px;
  border-radius: 8px;
  font-size: var(--text-sm);
}
.seg-btn.active {
  background: rgba(201,154,75,0.16);
  border-color: var(--gold);
  color: var(--gold-bright);
  font-weight: 700;
}

/* Separates the distinct feature groups now bundled into the Settings
   modal (theme/text size, backup & restore, updates). */
.settings-divider {
  border: none;
  border-top: 1px solid var(--card-border);
  margin: 22px 0 4px;
}

/* Text-size setting — everything else is authored in rem, so scaling the
   root font-size cascades through headings/body/labels/etc. uniformly.
   Paddings/gaps stay in px on purpose: at "large" that keeps tap targets
   comfortable rather than blowing up the whole layout's spacing too. */
html[data-font-size="small"] { font-size: 14px; }
html[data-font-size="large"] { font-size: 19px; }

/* Visually hidden but still announced. Not display:none and not
   visibility:hidden — both remove the element from the accessibility tree,
   which would defeat the entire point of a screen-reader-only label. */
.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;
}

/* My Bar — the optional "which bottle is this" field on each row. Sits between
   the intensity bar and the stock chips, quiet until focused so a shelf of
   unnamed rows doesn't read as a wall of empty inputs. */
.bar-label-row {
  display: block;
  margin-top: 10px;
}
.bar-label-input {
  width: 100%;
  background: var(--bg);
  border: 1px dashed var(--card-border);
  color: var(--cream);
  border-radius: 8px;
  padding: 9px 10px;
  font-size: var(--text-sm);
  font-family: inherit;
  min-height: 44px;
}
.bar-label-input::placeholder {
  color: var(--faint);
  font-size: var(--text-xs);
}
.bar-label-input:focus {
  border-style: solid;
  border-color: var(--gold);
}

/* ---------- tap targets --------------------------------------------------
   The smallest controls in the app were also the destructive ones: the photo
   ✕ was 22x22, the smoke-log ✕ ~17px. Both sit next to something you very
   much do not want to hit by accident.

   The hit area is grown with a centred ::after overlay rather than by
   resizing the buttons, because several of these are deliberately positioned
   — .ch-photo-del is offset OUTSIDE its 56px thumbnail on purpose — and
   resizing them would turn an accessibility fix into a visual redesign.
   min-width/min-height on the overlay (rather than a fixed negative inset)
   guarantees 44px in both axes whatever size the button itself is. */
.ch-photo-del,
.ch-log-del,
.session-remove,
.rc-remove,
.hi-details-toggle {
  position: relative;
}
.ch-photo-del::after,
.ch-log-del::after,
.session-remove::after,
.rc-remove::after,
.hi-details-toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  height: 100%;
  min-width: 44px;
  min-height: 44px;
}
/* .ch-photo-del is position:absolute for its own offset; the rule above would
   override that, so restore it. It is still a containing block for ::after. */
.ch-photo-del {
  position: absolute;
}
/* The stars have horizontal room, so grow them honestly — an overlay here
   would make adjacent stars' hit areas overlap each other. */
.star-picker .star-btn {
  min-width: 44px;
  min-height: 44px;
}

/* Storage-failure banner, created on demand by reportStorageFailure().
   Fixed above the tabbar rather than inline in a view, because a failed
   write can happen from any tab and from a module-load migration, before
   any view has rendered. z-index sits above the tabbar (20) but below
   every modal (30+) — a modal that is open when the write fails is the
   thing the user is looking at, and covering it would hide the controls
   they need to retry. */
.storage-error-banner {
  position: fixed;
  left: 8px;
  right: 8px;
  bottom: calc(var(--tabbar-height) + 8px + env(safe-area-inset-bottom));
  z-index: 25;
  background: var(--bg-elevated);
  border: 1px solid var(--accent-bright);
  border-left: 4px solid var(--accent-bright);
  border-radius: 10px;
  padding: 12px 40px 12px 14px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
.storage-error-msg {
  margin: 0;
  color: var(--cream);
  font-size: var(--text-sm);
  font-weight: 700;
}
.storage-error-count {
  font-weight: 400;
  color: var(--muted);
}
.storage-error-hint {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: var(--text-xs);
}
.storage-error-dismiss {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  color: var(--muted);
  font-size: var(--text-md);
  line-height: 1;
}

@media (min-width: 640px) {
  #views { padding: 24px; }
}

/* ---------- Customise Home (v2.105.0) --------------------------------------
   The editor list. Deliberately plain: it is a settings surface, not a
   dashboard, and it is read once and then left alone. */
.home-customise-row { margin: 18px 0 4px; text-align: center; }

.home-edit-list { list-style: none; margin: 10px 0 0; padding: 0; }

.home-edit-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 4px;
  border-bottom: 1px solid var(--border, #333);
}
.home-edit-row:last-child { border-bottom: 0; }

/* A hidden card stays legible rather than being greyed into unreadability —
   you have to be able to find it to turn it back on. */
.home-edit-row.is-hidden .home-edit-label { opacity: 0.55; text-decoration: line-through; }

.home-edit-label { flex: 1; min-width: 0; }

/* ⚠️ touch-action: none is scoped to the GRIP, never the row. On the row it
   would kill scrolling inside the modal on every touch device. */
.home-edit-grip {
  cursor: grab;
  touch-action: none;
  opacity: 0.6;
  font-size: 1.1rem;
  padding: 0 2px;
  user-select: none;
}
.home-edit-row.is-dragging { opacity: 0.6; background: var(--panel, #241811); }

.home-edit-moves { display: flex; gap: 4px; }
.home-edit-move {
  min-width: 34px;
  min-height: 34px;           /* a real tap target, per the v2.89.0 type work */
  border-radius: 8px;
}
.home-edit-move:disabled { opacity: 0.3; }

.home-edit-hide { min-height: 34px; border-radius: 8px; font-size: var(--text-sm, 0.85rem); }
.home-edit-fixed { font-size: var(--text-sm, 0.85rem); opacity: 0.7; white-space: nowrap; }

/* The one-time recovery key (v2.107.0, P4 phase D). Monospaced and spaced out
   because it is READ OFF A SCREEN AND WRITTEN ONTO PAPER — the one string in
   this app where a misread character costs someone their collection. */
.lock-recovery-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-lg);
  letter-spacing: 0.08em;
  text-align: center;
  word-break: break-all;
  background: var(--bg-elevated);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  padding: 12px;
  margin: 12px 0;
  user-select: all;
}
#lock-setup-backdrop input { width: 100%; margin-bottom: 10px; }
#lock-setup-backdrop .secondary,
#lock-setup-backdrop .primary { width: auto; }
#btn-lock-backup-first { width: 100%; }
