/* ============================================================
   BARELLO — Supper Club
   ------------------------------------------------------------
   Three faces, three jobs, no overlap:
     Archivo Condensed   the wordmark, and only the wordmark
     Cormorant Garamond  anything to do with wine — bottle names,
                         headlines, prices, years, cocktail names
     Jost                the interface — labels, meta, controls

   Two accents, and the discipline between them is the whole
   scheme: BRASS is money, ornament and actions. ACID is live
   state and nothing else. The moment acid also means "expensive"
   or "tap here", it stops meaning "open this tonight" — which is
   the single most useful thing this app says.
   ============================================================ */

:root {
  --room:    #12251c;   /* bottle green */
  --panel:   #0d1c15;   /* deeper — photo frames, tab bar, inputs */
  --ink:     #ece4d2;
  --ink-2:   #9db0a3;
  --ink-3:   #7f9384;
  --line:    #2a3f33;
  --line-2:  #1e3327;

  --brass:   #c9a227;
  --acid:    #ccff33;

  /* Past peak is its own state, not a variant of "hold". One says drink it before it fades,
     the other says don't open it yet — they must never look alike. Coral rather than red
     because red disappears against bottle green. */
  --coral:   #e8735c;
  --peak:    var(--acid);
  --hold:    var(--brass);

  --serif: "Cormorant Garamond", Georgia, serif;
  --sans:  "Jost", system-ui, -apple-system, sans-serif;

  --gutter: 20px;
  font-size: 16px;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--room);
  color: var(--ink);
  font-family: var(--sans);
  line-height: 1.5;
  padding-bottom: 150px;
  overflow-x: hidden;
}

button, input, select, textarea { font: inherit; color: inherit; }

/* Buttons carry a UA background and border unless told otherwise, and several things here
   are buttons for accessibility reasons rather than because they look like buttons — the
   Tonight cover story, every cellar row, the desktop tiles. Without this reset the cover
   story renders on a white slab. Anything that DOES want a filled look (.primary-btn, .fab,
   .chooser-option) sets its own background after this.

   Deliberately does NOT set text-align: doing so left-aligned every tab label and every
   full-width button. The few that need it (.hero, .tile, .drink-chip) say so themselves. */
button { background: none; border: none; padding: 0; cursor: pointer; }

/* ============================================================
   Masthead
   ============================================================ */

.topbar { max-width: 760px; margin: 0 auto; padding: 22px var(--gutter) 0; }

.mast-bar {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  border-bottom: 1.5px solid var(--brass);
  padding-bottom: 9px;
}

/* The one place Archivo Condensed appears. 70% width keeps it narrow enough never to crowd
   the bottle count. 23px chosen 20 Aug 2026 (Barello rebrand visual check, app/looks-wordmark.html)
   over 27/31/35/39px — sits politely beside the bottle count rather than owning the masthead. */
.wordmark {
  font-family: "Archivo", var(--sans);
  font-weight: 700;
  font-stretch: 70%;
  font-size: 23px;
  letter-spacing: .015em;
  text-transform: uppercase;
  line-height: 1;
  margin: 0;
  color: #f4eddc;
  white-space: nowrap;
}

.mast-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

.mast-count {
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--acid);
  border: 1px solid rgba(204,255,51,.4);
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}

.icon-btn {
  border: 1px solid var(--line);
  background: none;
  color: var(--ink-3);
  border-radius: 999px;
  width: 28px; height: 28px;
  font-size: 13px;
  cursor: pointer;
  flex-shrink: 0;
}
.icon-btn.active { border-color: var(--brass); color: var(--brass); }
/* The visible ring stays 28px — it sits in the masthead beside the bottle count and a bigger
   circle would compete with the wordmark. The TARGET is 44px, thrown out symmetrically by a
   pseudo-element. Safe here where an oversized hit area is safe: nothing else is within 44px
   of it, which is exactly the test the filter chips above failed. */
.icon-btn::after {
  content: "";
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}
.icon-btn { position: relative; }

.dateline {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3);
  padding: 8px 0 0;
  margin-bottom: 22px;
}

/* ============================================================
   Layout
   ============================================================ */

main { padding: 0 var(--gutter); max-width: 760px; margin: 0 auto; }

.rail { display: none; }

/* ============================================================
   Editorial primitives
   ============================================================ */

.kicker {
  font-size: 9.5px; letter-spacing: .24em; text-transform: uppercase;
  color: var(--brass); margin: 0 0 12px;
}

.headline {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(36px, 10.5vw, 48px);
  line-height: 1.02;
  margin: 0 0 24px;
  color: #f4eddc;
}
.headline i { font-style: italic; color: var(--acid); }

/* Department header: a label, then a hairline running to the edge. The rule is what makes
   it read as a magazine department rather than a heading in a form. */
.section-head, .dept {
  display: flex; align-items: center; gap: 10px;
  margin: 30px 0 14px;
  font-size: 9.5px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-2); font-weight: 400;
}
.section-head:after, .dept:after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}
.section-head em, .dept em {
  font-style: normal; font-size: 9.5px; letter-spacing: .12em; font-weight: 600;
  color: var(--acid); flex-shrink: 0;
}
.section-head span:first-child, .dept b { white-space: nowrap; font-weight: 400; }

.section-head.ready em { color: var(--acid); }
.section-head.almost em { color: var(--brass); }

@keyframes rise { from { opacity: 0; transform: translateY(12px); } }
.stagger > * { animation: rise .5s cubic-bezier(.2,.7,.3,1) backwards; }
.stagger > *:nth-child(1) { animation-delay: .02s; }
.stagger > *:nth-child(2) { animation-delay: .07s; }
.stagger > *:nth-child(3) { animation-delay: .12s; }
.stagger > *:nth-child(4) { animation-delay: .17s; }
.stagger > *:nth-child(5) { animation-delay: .22s; }
.stagger > *:nth-child(6) { animation-delay: .27s; }
@media (prefers-reduced-motion: reduce) { .stagger > * { animation: none; } }

/* ============================================================
   State badges
   ============================================================
   Loudness stays deliberately ranked, but the rank is carried by TREATMENT rather than by
   four competing colours:
     peaking    filled acid pill  — the recommendation, impossible to miss
     past peak  outlined coral    — the warning
     hold       brass text        — restraint, don't open it yet
     in window  muted text        — the quiet majority that asks nothing of you           */

.window-tag {
  display: inline-block;
  font-size: 9px; letter-spacing: .12em; text-transform: uppercase; font-weight: 600;
  white-space: nowrap;
}
/* ⚠️ THE LOUDEST OF THE FIVE, AND IT MUST OUT-SHOUT "PAST PEAK" WITHOUT A NEW COLOUR.
   FILLED coral where past-peak is outlined coral — same family, because both are warnings, but
   this one is about the PLACE rather than the bottle and is the only state where the app is
   telling you something is going wrong right now. It sorts above past peak (windowRank tier
   −1), so it has to read as more urgent than the pill directly beneath it or the sort and the
   colours would argue.
   Not acid: acid is live state, and this is the one state where there IS no live window. */
.window-tag.damage    { background: var(--coral); color: var(--room); padding: 3px 9px; border-radius: 999px; }
.window-tag.peaking   { background: var(--acid); color: var(--room); padding: 3px 9px; border-radius: 999px; }
.window-tag.past      { color: var(--coral); border: 1px solid rgba(232,115,92,.5); padding: 2px 8px; border-radius: 999px; }
.window-tag.hold      { color: var(--brass); letter-spacing: .16em; }
.window-tag.in-window { color: var(--ink-3); letter-spacing: .16em; }
/* Borrows .past's own coral-outline pill rather than inventing a colour — a bottle a year
   from closing needs the same "drink it before it fades" weight past-peak already carries,
   even though it hasn't crossed into that tier yet. Higher specificity than .in-window alone
   (3 classes vs 2) wins on its own; no !important needed. */
.window-tag.in-window.closing { color: var(--coral); border: 1px solid rgba(232,115,92,.5); padding: 2px 8px; border-radius: 999px; letter-spacing: normal; }

.band-tag {
  display: inline-block;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--brass); white-space: nowrap;
}
.band-tag.midweek { color: var(--ink-3); }

.tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }

/* ============================================================
   Controls
   ============================================================ */

.search { display: flex; gap: 10px; margin: 0 0 16px; }

/* Holds the input and its clear button on one baseline. The button is absolutely positioned
   over the right end of the field rather than sitting beside it, so adding it doesn't steal
   width from a search box that already has to share the row with the sort select. */
.search-field { position: relative; flex: 1; min-width: 0; display: flex; }
.search-clear {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px;
  background: none; border: none; cursor: pointer;
  color: var(--ink-3); font-size: 17px; line-height: 1;
}
.search-clear:hover { color: var(--brass); }
/* Room for the button, so a long search term never runs underneath it. */
.search-field input { padding-right: 30px; }

.reset-filters { display: block; padding: 0 0 14px; color: var(--brass); }

/* The add form's fold. A quiet brass line rather than a button that competes with Save —
   it opens something, it doesn't do anything. */
.more-toggle {
  width: 100%; text-align: left;
  background: none; border: none; border-top: 1px solid var(--line-2);
  margin: 6px 0 4px; padding: 16px 0 14px;
  color: var(--brass); cursor: pointer;
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
}
.more-toggle:hover { color: var(--ink); }
.more-caret { display: inline-block; width: 1em; }
.more-count { color: var(--ink-3); letter-spacing: .1em; }

/* "Makes" on a bottle page — what owning this lets you pour. A list of rows rather than the
   Bar's tile grid: this is a footnote on a bottle, and borrowing the grid would make it read
   as a second Bar. */
.mix-list { display: flex; flex-direction: column; }
.mix-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  background: none; border: none; border-bottom: 1px solid var(--line-2);
  padding: 10px 0; cursor: pointer;
}
.mix-row:last-child { border-bottom: none; }
.mix-art { width: 40px; flex-shrink: 0; }
.mix-art img, .mix-art svg { width: 100%; height: auto; display: block; border-radius: 3px; }
.mix-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mix-name { font-family: var(--serif); font-size: 16px; font-weight: 600; color: var(--ink); }
/* Coral, matching every other "you are one thing short" line in the app. */
.mix-need {
  font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--coral);
}
.mix-head {
  margin: 14px 0 2px;
  font-size: 9.5px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
}

/* The pointer to the staples checklist inside an empty lens. Brass, like every other "a way
   of looking / a way through" control — never acid, which is live state only. */
.empty-staples {
  display: inline-block; margin-top: 14px; padding: 10px 0;
  background: none; border: none; cursor: pointer;
  color: var(--brass);
  font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase;
}
.empty-staples:hover { color: var(--ink); }
.es-n { color: var(--ink-3); margin-left: 6px; }

.search input {
  flex: 1; min-width: 0;      /* lets the input shrink instead of shoving the select away */
  border: none; border-bottom: 1px solid var(--line);
  background: none;
  padding: 8px 2px;
  font-size: 14px; font-weight: 300;
  outline: none;
  color: var(--ink);
}
.search input::placeholder { color: var(--ink-3); }
.search input:focus { border-bottom-color: var(--brass); }

.sort-select {
  flex-shrink: 0; max-width: 46%;
  border: none; border-bottom: 1px solid var(--line);
  background: none;
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-2);
  padding: 8px 2px;
  outline: none; cursor: pointer;
}
.sort-select option { background: var(--panel); color: var(--ink); }

/* ⚠️ THE CHIPS CARRY THEIR OWN VERTICAL SPACE NOW, AND THE ROW GAP GAVE IT UP TO THEM
   (25 Aug 2026). They measured 21px tall against a 44pt minimum touch target — 13 of them on
   the cellar, 10 on Tasted — which on a phone is a row you aim at rather than tap.

   ⚠️ AND THE OBVIOUS FIX IS THE WRONG ONE: an invisible 44px hit area over a 21px chip
   OVERLAPS THE ROW BENEATH, because the pitch between these rows is ~37px. In the overlap the
   lower row wins, so a tap just under a type chip would select a price band — a mis-tap that
   performs the WRONG action, which is worse than a target that is merely small. The space has
   to be real, so the padding is real and the gap shrinks to pay for it.

   The look is unchanged: the active underline moves from `border-bottom` to a pseudo-element
   pinned near the text, so it stays tight under the label instead of drifting to the bottom of
   the new padding. */
.filters { display: flex; flex-wrap: wrap; gap: 0 16px; padding: 0 0 4px; }

.chip {
  position: relative;
  border: none; background: none;
  font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
  /* 14px of type plus 15px either side = a 44px target, which is the number this exists to
     hit. The underline offset below is tied to it: change one, change the other, or the rule
     drifts away from the word it belongs to. */
  padding: 15px 0;
  cursor: pointer;
  transition: color .15s ease;
}
.chip::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 12px;
  height: 1.5px; background: transparent;
  transition: background .15s ease;
}
.chip:hover { color: var(--ink-2); }
.chip.active { color: var(--ink); }
.chip.active::after { background: var(--acid); }

.filters.bands { padding-bottom: 8px; }

/* ============================================================
   The index — hairline rows, not a stack of cards
   ============================================================ */

.list { border-top: 1px solid var(--line); }

.bottle-card {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  padding: 12px 2px;
  border-bottom: 1px solid var(--line-2);
  cursor: pointer;
  transition: background .16s ease, padding-left .16s ease;
}
.bottle-card:hover { background: rgba(255,255,255,.025); padding-left: 7px; }

.bottle-thumb {
  flex: 0 0 44px; height: 58px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 7px;
  display: flex; align-items: center; justify-content: center;
  padding: 3px;
  overflow: hidden;
  font-size: 17px;
}

/* Contained, never cropped — a bottle photo is portrait and the label is the whole point. */
.bottle-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }

.bottle-info { flex: 1; min-width: 0; }

.bottle-name {
  font-family: var(--serif); font-size: 19px; font-weight: 600; line-height: 1.1;
  margin: 0 0 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.bottle-meta {
  font-size: 11px; color: var(--ink-3); font-weight: 300;
  margin: 0 0 5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bottle-meta.missing { color: var(--brass); }

.qty-badge { font-size: 9px; letter-spacing: .1em; color: var(--ink-3); font-family: var(--sans); }

.fav-star {
  flex-shrink: 0;
  font-size: 15px;
  color: var(--line);
  cursor: pointer;
  padding: 4px;
}
.fav-star.active { color: var(--brass); }
.fav-star.big { font-size: 22px; }

.empty-state {
  text-align: center; color: var(--ink-3);
  padding: 50px 20px; font-size: 13px; font-weight: 300;
}

/* ============================================================
   Cover story — the Tonight pick, and every detail hero
   ============================================================ */

/* text-align is explicit because on Tonight this is a <button>, and a button centres its
   text by default. */
.hero {
  display: flex; gap: 15px; align-items: flex-start;
  width: 100%; text-align: left;
  margin-bottom: 4px;
}

.hero-photo {
  flex: 0 0 40%; max-width: 190px;
  aspect-ratio: 3 / 4.2;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  padding: 8px;
  overflow: hidden;
}
.hero-photo img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.hero-glyph { font-size: 38px; opacity: .5; }

.hero-info { flex: 1; min-width: 0; }

/* The Tonight cover is a <button>, so its contents must be spans — and spans are inline,
   which stacks nothing. This puts the block layout back. */
.hero-info > span { display: block; }
.hero-info > .tags, .hero-info > .hero-value { display: flex; }

.hero-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.hero-titles { min-width: 0; }

.hero-producer {
  font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-2); margin: 0 0 6px;
}

.hero-name {
  font-family: var(--serif);
  font-size: clamp(25px, 7vw, 31px);
  font-weight: 600; line-height: 1.05;
  margin: 0 0 8px;
}

.hero-meta {
  font-size: 12px; color: var(--ink-2); font-weight: 300;
  margin: 0 0 12px; line-height: 1.45;
}

.hero-tags { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; }

.hero-value { display: flex; align-items: baseline; gap: 8px; margin-top: 13px; flex-wrap: wrap; }
.hero-price { font-family: var(--serif); font-size: 27px; font-weight: 600; line-height: 1; }
.hero-price-src { font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); }

/* ============================================================
   Detail
   ============================================================ */

.detail { padding: 0 0 40px; }

.back-btn {
  background: none; border: none; cursor: pointer;
  font-size: 9.5px; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3);
  padding: 0 0 18px;
}
.back-btn:hover { color: var(--brass); }

/* A "card" is now a block separated by a hairline, not a floating panel. */
.card { padding: 22px 0; border-bottom: 1px solid var(--line-2); }
.card:last-of-type { border-bottom: none; }

.card h3 {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 12px;
  font-size: 9.5px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-2); font-weight: 400;
}
.card h3:after { content: ""; flex: 1; height: 1px; background: var(--line); }

.ai-card { border-bottom: 1px solid var(--line-2); }

/* The AI note is somebody else's voice, so it is set apart the way a magazine sets a quote
   apart rather than boxed in a dashed card. */
.ai-card p {
  font-family: var(--serif); font-size: 18px; line-height: 1.45; font-style: italic;
  margin: 0; padding-left: 16px; border-left: 2px solid var(--brass); color: var(--ink);
}

/* The outline button. TWO NAMES, ONE DEFINITION — .edit-btn is the original ("Edit details"),
   .ghost-btn is the neutral name for anywhere it isn't an edit. A second block that merely
   looked the same is exactly how windowRank and windowTag drifted; the only thing .edit-btn
   keeps to itself is its margin, which is specific to where it sits on the detail page. */
.edit-btn, .ghost-btn {
  display: block; width: 100%; padding: 11px;
  border-radius: 999px;
  border: 1px solid var(--brass);
  background: none; color: var(--brass);
  font-size: 10px; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
  cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.edit-btn { margin: 14px 0 20px; }
.edit-btn:hover, .ghost-btn:hover { background: var(--brass); color: var(--room); }

/* ============================================================
   Docked detail actions
   ============================================================

   ⚠️ `position: sticky` WITH A BOTTOM OFFSET, NOT A SCROLL LISTENER. The element pins to the
   bottom of the viewport for as long as its natural position is below the fold, and settles
   into the flow when you actually reach it — which is the behaviour we want and costs no JS.

   Three details are load-bearing:
     • The offset clears the fixed tab bar, or the buttons dock UNDER it and the primary
       action is unreachable on exactly the screens this was built for.
     • It needs an opaque background and a top hairline. Without one the page's own text
       scrolls visibly through the docked buttons.
     • No ancestor may set `overflow: hidden` — sticky dies silently inside one, with nothing
       in the console. `.detail` doesn't; check before nesting this deeper. */
.detail-actions {
  position: sticky;
  bottom: calc(56px + env(safe-area-inset-bottom));
  z-index: 20;
  margin-top: 24px;
  padding: 10px 0 12px;
  background: var(--room);
  border-top: 1px solid var(--line-2);
}
.detail-actions .primary-btn { margin-top: 0; }
.detail-actions .danger-btn { margin-top: 8px; }

/* The rail replaces the tab bar above 900px, so there is nothing at the bottom to clear. */
@media (min-width: 900px) {
  .detail-actions { bottom: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .detail-actions { transition: none; }
}

.danger-btn {
  width: 100%; margin-top: 24px; padding: 13px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: none; color: var(--ink-3);
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  cursor: pointer;
}
.danger-btn:hover { border-color: var(--coral); color: var(--coral); }

/* The edit sheet's delete. A .text-btn, not a .danger-btn: it should read as the quiet way
   out of a mistake rather than compete with Save, and the confirm dialog is what actually
   guards it. */
.danger-text { display: block; width: 100%; margin-top: 18px; color: var(--coral); }
.danger-text:hover { color: var(--coral); text-decoration: underline; }

/* ---------- Drinking window ---------- */

/* FOUR LAYERS, and which one is which is the whole design:
     track     the time axis — always contains today, with a year of padding either side
     span      the drinkable window, a segment ON that axis with two visible ends
     band      the best years (peak ±1), soft-edged because it is an estimate
     playhead  today

   The track used to BE the window, and both of the bar's worst faults came from that single
   decision: a past-peak bottle's "today" fell off the end of its own chart and had to be
   pinned to the edge, so one year past and ten years past drew the same picture — and a wine
   in its final year sat hard against the right edge, reading as a cliff rather than as an
   estimate running out. Don't collapse the axis back onto the window. */
/* ⚠️ THE TOP PADDING IS THE RIDGE'S ROOM. It was 26px, which was headroom for the "now"
   label alone; the curve now stands in that space, so it has to be tall enough for a
   silhouette to read as one. Shrink it and the shapes flatten into each other — a decline
   and a plateau stop being distinguishable, which is the entire point of drawing them. */
.window-bar { padding: 104px 0 4px; }

.window-track {
  position: relative; height: 1px; margin: 0 0 22px;
  background: var(--line-2);
}

/* The ridge. Sits ON the track line and rises into the padding above it, sharing the same
   0–100 horizontal space as the span, band and playhead, so nothing can drift out of step.

   ⚠️ NO GRADIENT, NO FILTER, AND THE REASON IS ids. Both would need a <defs> id, and the one
   thing guaranteed about an id in a template rendered per bottle is that it will eventually
   be rendered twice. A flat fill plus a weighted crest says the same thing with nothing to
   collide. */
.window-ridge {
  position: absolute; left: 0; bottom: 0; width: 100%; height: 86px;
  overflow: visible; pointer-events: none;
}
.ridge-fill { fill: var(--brass); fill-opacity: .13; }
.ridge-crest { fill: none; stroke-linejoin: round; stroke-linecap: round; }

/* State by HUE, mirroring .window-band exactly — acid never appears on a wine whose best
   years are behind it, here either. */
.ridge-crest.live { stroke: var(--acid); }
.ridge-crest.ahead { stroke: var(--brass); }
.ridge-crest.gone { stroke: #55665c; }

/* Confidence by WEIGHT. A grape-rule window is wide and soft; a researched one is thin and
   crisp. CLAUDE.md flagged this as a deliberate call against the locked "opacity must not
   carry status" rule — it holds because state is still carried by the colours above, and
   these two classes only ever say how sure the number is. */
.window-ridge.feathered .ridge-crest { stroke-width: 3; stroke-opacity: .42; }
.window-ridge.feathered .ridge-fill { fill-opacity: .10; }
.window-ridge.crisp .ridge-crest { stroke-width: 1.75; stroke-opacity: 1; }
.window-span {
  position: absolute; top: -1.5px; height: 4px; border-radius: 2px;
  background: var(--line);
}
/* The peak is a BAND, not a tick. Two reasons, and the second is the important one: the badge
   already claims a three-year state ("peaking now" runs peak ±1), so a 1px tick made the
   graphic and the words disagree — and 16 of the 17 windowed wines get their numbers from a
   grape-variety lookup, so a knife edge asserted a precision that has never existed. The
   gradient's soft ends are the honest part: they say "somewhere around here". Don't square
   them off.

   It carries STATE so acid never appears on a wine whose best years are behind it. That is the
   same contradiction that got the old acid "peak YYYY" label deleted — when the label went, it
   simply moved into the band. */
.window-band {
  position: absolute; top: -2.5px; height: 6px; border-radius: 3px;
}
.window-band.live {
  background: linear-gradient(90deg,
    rgba(204,255,51,0) 0%, var(--acid) 24%, var(--acid) 76%, rgba(204,255,51,0) 100%);
}
.window-band.ahead {
  background: linear-gradient(90deg,
    rgba(201,162,39,0) 0%, var(--brass) 24%, var(--brass) 76%, rgba(201,162,39,0) 100%);
  opacity: .75;
}
.window-band.gone {
  background: linear-gradient(90deg,
    rgba(127,147,132,0) 0%, #55665c 24%, #55665c 76%, rgba(127,147,132,0) 100%);
}

/* Today is a POSITION, not a state — so it is cream, not acid. Sharing acid with the band is
   exactly why the two were unreadable together: on a short window the marker was an acid
   circle sitting on an acid bar. A vertical playhead also reads at a different angle from a
   horizontal band, which is the other half of telling them apart.
   No .late/.early variants any more — those existed only to compensate for an axis that could
   not contain today. It contains it now, so position carries the meaning. */
.window-now {
  position: absolute; top: -9px; width: 1.5px; height: 19px; margin-left: -.75px;
  background: var(--ink);
}
.window-now::after {
  content: ""; position: absolute; top: -3px; left: -2.75px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--ink);
}

.window-year {
  position: absolute; top: 11px; transform: translateX(-50%);
  font-family: var(--serif); font-size: 13px; color: var(--ink-3); white-space: nowrap;
}
.window-nowlab {
  position: absolute; top: -26px; transform: translateX(-50%);
  font-size: 8.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink); white-space: nowrap;
}

/* The sentence. The end year was always in the record and always on the bar as grey 13px
   type nobody read; saying it out loud is what separates a Rosé with months left from a
   St Henri with nine years when both carry the same acid pill. Only "a year or less" earns
   coral — acid still means live state, coral still means the warning, no new colour. */
.window-say {
  margin: 13px 0 0; font-family: var(--serif); font-size: 17px;
  line-height: 1.35; color: var(--ink);
}
.window-say em { font-style: normal; }
.window-say .urgent { color: var(--coral); font-style: italic; }
.window-say .easy { color: var(--ink-3); }

.source-note { margin: 10px 0 0; font-size: 12px; color: var(--ink-3); font-weight: 300; line-height: 1.55; }
.source-note em { font-style: normal; opacity: .85; }

/* ---------- Value ---------- */

.value-row { display: flex; gap: 22px 30px; flex-wrap: wrap; }
.value-label { display: block; font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); }
.value-figure { font-family: var(--serif); font-size: 24px; font-weight: 600; line-height: 1.1; }

/* ---------- Notes ---------- */

.notes-input {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  padding: 11px 12px;
  font-family: var(--sans); font-size: 14px; font-weight: 300;
  resize: vertical;
  outline: none;
}
.notes-input:focus { border-color: var(--brass); }

.save-hint {
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3);
  display: inline-block; margin-top: 8px; min-height: 12px;
}

/* ---------- Pairings + facts ---------- */

.pair-list { margin: 0; padding: 0; list-style: none; }
.pair-list li { padding: 9px 0; border-bottom: 1px solid var(--line-2); font-size: 13px; font-weight: 300; }
.pair-list li:last-child { border-bottom: none; }
.pair-list strong { font-family: var(--serif); font-weight: 600; font-size: 16px; }

.facts { display: grid; grid-template-columns: 96px 1fr; gap: 9px 18px; margin: 0; font-size: 13.5px; }
.facts dt { font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); padding-top: 4px; }
.facts dd { margin: 0; font-weight: 300; }

/* ---------- Critic score ---------- */

.critic-line {
  display: flex; align-items: center; gap: 8px;
  margin: 14px 0 0 18px; padding-top: 12px;
  border-top: 1px solid var(--line-2);
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
}
.critic-score { font-family: var(--serif); font-size: 17px; font-weight: 600; color: var(--brass); letter-spacing: 0; }
.critic-who { flex: 1; min-width: 0; }
.critic-line a { color: var(--brass); flex-shrink: 0; }

/* ---------- Quantity ---------- */

.qty-stepper { display: inline-flex; align-items: center; gap: 12px; }
.qty-btn {
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: none; color: var(--ink);
  font-size: 14px; line-height: 1; cursor: pointer;
}
.qty-btn:hover:not(:disabled) { border-color: var(--brass); color: var(--brass); }
.qty-btn:disabled { opacity: .35; cursor: default; }
.qty-value { min-width: 1.5em; text-align: center; font-family: var(--serif); font-size: 18px; font-weight: 600; }

/* ============================================================
   Tonight — food + cocktail scrollers
   ============================================================ */

.scroller {
  display: flex; gap: 8px; overflow-x: auto; min-width: 0;
  scrollbar-width: none; padding-bottom: 3px;
  margin: 0 calc(var(--gutter) * -1);
  padding-left: var(--gutter); padding-right: var(--gutter);
}
.scroller::-webkit-scrollbar { display: none; }

.food-chip {
  flex-shrink: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: none;
  padding: 8px 14px;
  font-size: 11.5px; font-weight: 300;
  color: var(--ink);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .15s ease;
}
.food-chip:hover { border-color: var(--brass); }
.food-chip.active { border-color: var(--acid); color: var(--acid); }

.food-name { }
.food-count { font-family: var(--serif); font-size: 13px; color: var(--acid); margin-left: 7px; }

/* Foods with nothing to offer stay on screen but recede — "we have nothing for oysters" is
   an answer, and hiding it would read as the app not knowing what oysters are. */
.food-chip.empty { color: var(--ink-3); border-style: dashed; }
.food-chip.empty .food-count { color: var(--ink-3); }
.food-chip.active .food-count { color: var(--acid); }

/* The "all foods" toggle — same doctrine as the Bar's book chips: brass, never acid, because
   browsing everything is a way of looking, not a live state competing with the real picks. */
.food-chip.book { border-style: dashed; }
.food-chip.book .food-name { color: var(--ink-2); }
.food-chip.book.active { border-style: solid; border-color: var(--brass); }
.food-chip.book.active .food-name { color: var(--brass); }

.drink-chip {
  flex-shrink: 0; width: 128px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: none;
  padding: 12px; text-align: left;
  cursor: pointer;
  transition: border-color .15s ease;
}
.drink-chip:hover { border-color: var(--brass); }
.drink-name { display: block; font-family: var(--serif); font-size: 18px; font-weight: 600; line-height: 1.1; margin-bottom: 6px; }
.drink-meta { display: block; font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); }

.pair-hint { margin: 0; font-size: 13px; line-height: 1.6; color: var(--ink-3); font-weight: 300; }

/* ---------- Serve — how to drink this bottle ---------- */

.serve-line { margin: 0 0 4px; font-family: var(--serif); font-size: 19px; line-height: 1.25; }
/* The price of the pour, brass because it is money — same register as the sip tax on a
   recipe, which is the same fact said from the other side. */
.serve-nip { font-family: var(--sans); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--brass); white-space: nowrap; margin-left: 6px; }
.serve-picker { display: flex; gap: 6px; margin-top: 12px; }
.serve-opt {
  flex: 1; border: 1px solid var(--line); border-radius: 999px; background: none;
  padding: 8px 4px; font-size: 11px; font-weight: 300; color: var(--ink);
  text-align: center; cursor: pointer; transition: border-color .15s ease;
}
.serve-opt:hover { border-color: var(--brass); }
/* Brass, not acid — a stored preference, not a live state. Same rule as .day-chip. */
.serve-opt.active { border-color: var(--brass); color: var(--brass); }
.pair-why { margin: -2px 0 0; padding: 0 2px 8px; font-size: 11.5px; font-style: italic; color: var(--ink-3); }

/* ============================================================
   Bar
   ============================================================ */

.staples { margin: 0 0 12px; }

.staples-head {
  width: 100%;
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 10px 16px;
  color: var(--ink-2);
  font-size: 11px; font-weight: 300;
  text-align: left; cursor: pointer;
}
.staples-head strong { font-weight: 500; color: var(--ink); }
.staples-caret { color: var(--brass); font-size: 9px; letter-spacing: .16em; text-transform: uppercase; flex-shrink: 0; }

.staples-body { padding: 16px 2px 4px; }
.staples-hint { margin: 0 0 14px; font-size: 12px; line-height: 1.55; color: var(--ink-3); font-weight: 300; }

.staple-group + .staple-group { margin-top: 14px; }
.staple-group h4 {
  margin: 0 0 8px;
  font-size: 9px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3); font-weight: 400;
}

.staple-chips { display: flex; flex-wrap: wrap; gap: 6px; }

.staple-chip {
  border: 1px solid var(--line);
  border-radius: 999px;
  background: none;
  color: var(--ink-3);
  padding: 6px 12px;
  font-size: 11px; font-weight: 300;
  cursor: pointer;
}
.staple-chip.on { border-color: var(--acid); color: var(--acid); }

.unlock-strip { padding: 0 0 14px; }
.unlock-lead {
  display: block;
  font-size: 9.5px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-2); margin-bottom: 9px;
}
.unlock-chips { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.unlock-chips::-webkit-scrollbar { display: none; }

.unlock-chip {
  flex-shrink: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  border: 1px dashed var(--brass);
  border-radius: 12px;
  background: none;
  color: var(--brass);
  padding: 8px 14px;
  cursor: pointer; text-align: left;
}
.unlock-chip strong { font-family: var(--serif); font-size: 16px; font-weight: 600; }
.unlock-chip span { font-size: 9px; letter-spacing: .14em; text-transform: uppercase; opacity: .8; }

.bar-section { margin-bottom: 20px; }

.section-toggle {
  width: 100%;
  display: flex; justify-content: space-between; align-items: center;
  background: none; border: none;
  border-top: 1px solid var(--line);
  padding: 14px 0 0; margin: 0 0 10px;
  color: var(--ink-3);
  font-size: 9.5px; letter-spacing: .2em; text-transform: uppercase;
  cursor: pointer;
}
.section-toggle em { font-style: normal; color: var(--brass); margin-left: 8px; }

/* ---------- Glassware marks ----------
   The fallback when a drink has no illustration yet. Outline only: ice and garnish were
   built, tried and rejected — computed from coordinates they come out identical on every
   drink, and a garnish never is. That detail has to be drawn, which is what image_url is
   for. When an image exists it replaces this entirely. */

.glass-mark { display: block; width: 58px; height: 66px; margin: 0 auto 6px; color: var(--ink-2); }
.glass-mark .gm-body,
.glass-mark .gm-rim {
  fill: none; stroke: currentColor; stroke-width: 1.45;
  stroke-linecap: round; stroke-linejoin: round;
}
.glass-mark.big { width: 104px; height: 118px; margin-bottom: 10px; }
.glass-mark.chip { width: 34px; height: 39px; margin: 0 0 6px; }

/* The illustrations arrive on warm cream paper, which against bottle green reads as a lit
   panel cut into the screen rather than an object sitting in the room. Two fixes, doing
   two different jobs, and NEITHER dims the drawing:

     the wash   a 16% multiply of --room over the art, pulling the paper toward the
                app's palette while leaving the ink line and the liquid colour alone
     the frame  a hairline in --line, which gives the square a deliberate edge. Most of
                the "popping" is the hard boundary, not the brightness

   Deliberately NOT used: a brightness filter. It was tried (see app/art-treatment.html,
   option D) and it is the only ingredient that costs you the artwork itself. If the pop
   still bothers you on an OLED phone in a dim room, raise the wash before reaching for
   brightness.

   The wash lives on ::after, which is why cocktailArt() wraps the <img> in a span —
   replaced elements don't render pseudo-elements. */
.cocktail-art {
  display: block; position: relative; overflow: hidden;
  width: 100%; max-width: 92px; margin: 0 auto 6px;
  border: 1px solid var(--line);
}
.cocktail-art img { display: block; width: 100%; height: auto; }
.cocktail-art::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: var(--room); opacity: .16; mix-blend-mode: multiply;
}
.cocktail-art.big { max-width: 168px; margin-bottom: 10px; }
.cocktail-art.chip { max-width: 42px; margin: 0 0 6px; }

/* ---------- Mood lenses ---------- */

/* position: relative makes this the offsetParent for its .lens children — without it,
   offsetLeft on a chip is measured from the page origin (the nearest positioned ancestor,
   or the body), not from this scroller, and the active-chip centering math below picks up a
   huge bogus number instead of a small one. That's what clipped the first chip's own left
   edge on load whenever it happened to be the active lens. */
.lenses { position: relative; display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; }
.lenses::-webkit-scrollbar { display: none; }

.lens {
  flex-shrink: 0; display: flex; flex-direction: column; justify-content: center;
  border: 1px solid var(--line); background: none; color: inherit;
  padding: 9px 13px; min-height: 48px; white-space: nowrap; cursor: pointer;
  text-align: left; transition: border-color .18s;
}
.lens-name { display: block; font-family: var(--serif); font-size: 16px; font-weight: 600; line-height: 1.1; }
.lens-count { display: block; font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
.lens.active { border-color: var(--acid); }
.lens.active .lens-name, .lens.active .lens-count { color: var(--acid); }
/* A lens with nothing in it stays on screen, dimmed — same rule as the food chips. "We
   have nothing sour" is a real answer; hiding it reads as never having heard of sour. */
.lens.empty { opacity: .42; }

/* The two books sit at the end of the moods and are deliberately not moods: brass rather
   than acid, because a book is a way of looking rather than a live state. Acid stays
   reserved for "pour this now" — a permanent chip in it would compete with every window
   pill on the screen, which is the same trap the brass FAB avoided. */
.lens.book { border-style: dashed; border-color: var(--line); }
.lens.book .lens-name { color: var(--ink-2); }
.lens.book.active { border-style: solid; border-color: var(--brass); }
.lens.book.active .lens-name, .lens.book.active .lens-count { color: var(--brass); }

/* The house book carries the same star the tiles do, so the mark means one thing everywhere.
   It stays brass even when the chip is inactive — that hairline of gold is the only thing
   separating the two book chips at a glance in a row you scroll sideways. */
.lens.house .lens-name::before { content: "★ "; font-size: 10px; color: var(--brass); }

.book-note {
  margin: 16px 0 4px; font-family: var(--serif); font-style: italic; font-size: 16px;
  line-height: 1.5; color: var(--ink-2);
  border-left: 2px solid var(--brass); padding-left: 14px;
}

/* ---------- House pick ---------- */

.house-pick {
  display: block; width: 100%; text-align: center; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel);
  padding: 22px 18px 20px; margin-top: 16px; position: relative; overflow: hidden;
}
.house-pick::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(75% 60% at 50% 0%, rgba(201, 162, 39, .12), transparent 70%);
}
.house-pick.empty { cursor: default; }
.hp-art { display: block; position: relative; }
.hp-eyebrow {
  display: block; position: relative; margin-bottom: 7px;
  font-size: 9px; letter-spacing: .2em; text-transform: uppercase; color: var(--acid);
}
.hp-eyebrow.none { color: var(--coral); }
.hp-name {
  display: block; position: relative; margin-bottom: 8px;
  font-family: var(--serif); font-size: 34px; font-weight: 600; line-height: 1;
}
.hp-why {
  display: block; position: relative;
  font-family: var(--serif); font-style: italic; font-size: 17px;
  color: var(--ink-2); line-height: 1.45;
}
.hp-from {
  display: block; position: relative; margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--line-2);
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
}
/* The label stays a small-caps tag; the bottle names do not. "VERMOUTH DE CHAMBERY ROUGE"
   in full caps shouts, and there can be three of them. */
.hp-from b { color: var(--ink-2); font-weight: 400; text-transform: none; letter-spacing: .01em; font-size: 12px; }

/* ---------- Drink tiles ---------- */

.drink-grid {
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
  background: var(--line-2); border: 1px solid var(--line-2);
}
.drink-tile {
  background: var(--room); border: none; color: inherit; cursor: pointer;
  padding: 14px 6px 12px; text-align: center; position: relative;
}
.tile-name { display: block; font-family: var(--serif); font-size: 14.5px; font-weight: 600; line-height: 1.16; }
.tile-meta {
  display: block; margin-top: 3px;
  font-size: 8px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
}
.tile-meta.missing { color: var(--coral); }
/* An un-makeable drink is NOT faded. The illustrations are the best thing on this screen
   and dimming them to carry state was paying for the state twice: every grid is already
   segregated by a section heading ("Ready to pour" / "One thing short" / "Everything
   else"), so no tile is ever ambiguous about its own status. What state it does need is
   carried explicitly by the coral missing-ingredient line, which also says WHICH
   ingredient — something an opacity value never could. */
.drink-tile.short { opacity: 1; }
.tile-star { position: absolute; top: 6px; right: 8px; font-size: 11px; color: var(--brass); }

/* A neat pour sitting in the same grid as the cocktails. ⚠️ DELIBERATELY NOT .cocktail-art:
   `contain` rather than `cover` because the label is the point and cropping it loses the one
   thing that identifies the bottle, and NO multiply wash because the wash exists to seat
   cream-paper illustrations into the bottle-green room — a photographed bottle on a dark
   ground already sits there, which is the entire reason the room is green.
   No border either: the illustrations are framed prints, a bottle is an object. That contrast
   IS how the two tile kinds tell themselves apart, so it needs no heading or badge. */
.neat-art {
  display: flex; align-items: flex-end; justify-content: center;
  height: 92px; margin: 0 auto 6px;
}
.neat-art img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
.neat-glyph { font-size: 34px; line-height: 1; opacity: .55; }
/* Brass, because a neat pour is the good bottle — the same thing the sip tax is about,
   said from the other side. */
.drink-tile.neat .tile-meta { color: var(--brass); }

@media (min-width: 900px) {
  .drink-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ---------- Shopping, demoted into the lens that needs it ---------- */

.lens-shop {
  display: flex; align-items: baseline; gap: 11px; width: 100%; text-align: left;
  border: 1px solid var(--line-2); background: none; color: inherit;
  padding: 13px 14px; margin-top: 14px; cursor: pointer;
}
.ls-n { font-family: var(--serif); font-size: 26px; font-weight: 600; color: var(--brass); line-height: 1; }
.ls-body { flex: 1; min-width: 0; }
.ls-item { display: block; font-family: var(--serif); font-size: 16px; font-weight: 500; }
.ls-names { display: block; margin-top: 2px; font-size: 10.5px; color: var(--ink-3); line-height: 1.4; }
.ls-go { font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--brass); flex-shrink: 0; }

.bar-search-late { margin-top: 22px; }
.clear-search {
  border: none; background: none; color: var(--brass); cursor: pointer;
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  padding: 12px 0; min-height: 44px;
}
.setup-banner.subtle { border-style: dashed; opacity: .8; margin-bottom: 16px; }

/* ---------- Auth ---------- */

/* Nobody's signed in (or nobody's added them to a household yet), so none of Tonight,
   Cellar, Bar, Tasted, Add, or Settings actually work — the rail, mobile masthead and tab
   bar are static chrome outside #app and don't get cleared by a normal render(), so the
   auth screens toggle this on body instead. */
body.auth-gate .topbar,
body.auth-gate .rail,
body.auth-gate .tabbar,
body.auth-gate .fab { display: none; }

.auth-screen {
  max-width: 340px; margin: 60px auto 0;
  padding: 0 20px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 14px;
}
.auth-screen .wordmark { margin-bottom: 4px; }
.auth-lede { font-family: var(--serif); font-size: 19px; color: var(--ink); margin: 0; }
.auth-form { width: 100%; display: flex; flex-direction: column; gap: 12px; }
.auth-form input {
  width: 100%; padding: 12px 14px; box-sizing: border-box;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--panel); color: var(--ink);
  font-family: var(--sans); font-size: 14px; font-weight: 300;
}
.auth-form input:focus { outline: none; border-color: var(--brass); }
.auth-note { font-size: 12.5px; color: var(--ink-3); font-weight: 300; line-height: 1.5; margin: 0; }
.auth-note.auth-error { color: var(--coral); }
.auth-note code {
  background: var(--panel); padding: 2px 6px; border-radius: 5px;
  font-size: 11.5px; color: var(--brass);
}

/* ---------- Cocktail detail ---------- */

.cocktail-hero {
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 26px 20px;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.cocktail-hero.ready { border-color: var(--acid); }

/* The detail hero draws the same mark as the tiles, one size up. Dimmed until you can
   actually make it, which is the one place opacity still carries state on this screen. */
/* Same on the detail page, where the art is biggest and dimming cost the most. The verdict
   line underneath already says "You can make this now" or "3 ingredients short" in words,
   and the acid border says it again. */
.cocktail-hero .glass-mark, .cocktail-hero .cocktail-art { opacity: 1; }

.cocktail-verdict {
  font-size: 9.5px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3);
}
.cocktail-hero.ready .cocktail-verdict { color: var(--acid); }

/* ---------- Cocktail pager ---------- */
/* Keeps you in the recipe book. Naming the drink either side is the point: "Next" alone
   makes you tap to find out, which is the thing that sends people back to the list. */
.pager {
  display: flex; align-items: stretch; gap: 10px;
  margin: 22px 0 4px; padding-top: 16px; border-top: 1px solid var(--line);
}
.pager-btn {
  flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 3px;
  border: 1px solid var(--line); background: none; color: inherit;
  padding: 11px 13px; min-height: 56px; cursor: pointer;
  transition: border-color .18s;
}
.pager-btn.prev { text-align: left; }
.pager-btn.next { text-align: right; }
.pager-btn:hover { border-color: var(--brass); }
.pager-btn.empty { border: none; cursor: default; }
.pager-dir {
  font-size: 8.5px; letter-spacing: .18em; text-transform: uppercase; color: var(--brass);
}
.pager-name {
  font-family: var(--serif); font-size: 17px; font-weight: 600; line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pager-count {
  align-self: center; flex-shrink: 0;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3);
}

.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin: 20px 0 4px; }
.detail-head h2 { margin: 0; font-family: var(--serif); font-size: 28px; font-weight: 600; line-height: 1.1; }
.detail-meta { margin: 6px 0 0; color: var(--ink-3); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; }

.ingredient-list { list-style: none; margin: 0; padding: 0; }
.ingredient-list li {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-2);
  font-size: 13.5px; font-weight: 300;
}
.ingredient-list li:last-child { border-bottom: none; }
.ingredient-list .tick { flex-shrink: 0; width: 14px; color: var(--acid); font-size: 11px; }
.ingredient-list li.missing .tick { color: var(--brass); }
.ingredient-list li.missing .ing-name { color: var(--ink-3); }
.ingredient-list li.optional .ing-name em {
  font-style: normal; font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-3); margin-left: 6px;
}

.ing-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ing-name { font-family: var(--serif); font-size: 16px; font-weight: 600; }
.ing-via { font-size: 10.5px; color: var(--ink-3); }
/* The sip tax. Brass because it is about money — coral means missing, acid means live state,
   and this is neither. No opacity anywhere near it: the drink is fully makeable and dimming
   it would say the opposite. */
.ing-tax { font-size: 10.5px; color: var(--brass); font-style: italic; }
/* The recipe's preference, when you don't own what it would rather have. Muted ink, not brass:
   this costs nothing and blocks nothing, so giving it the money colour would over-state it and
   giving it coral would say the drink is short of something, which it isn't. Italic so it sits
   apart from the factual "via" line directly above it. */
.ing-style { font-size: 10.5px; color: var(--ink-3); font-style: italic; }
.ing-amount { flex-shrink: 0; color: var(--ink-3); font-size: 11.5px; }

.method-text { margin: 0; font-size: 13.5px; line-height: 1.65; font-weight: 300; color: var(--ink-2); }

/* ============================================================
   Tasted
   ============================================================ */

.tasted-count { margin: 0 0 12px; font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); }
.tasted-card { cursor: pointer; }
.tasted-thumb img { object-fit: contain; }

.tasted-rating {
  flex-shrink: 0; text-align: right;
  color: var(--brass); font-size: 11px; line-height: 1.2;
  max-width: 78px;
}
.tasted-rating em { display: block; font-style: normal; font-family: var(--serif); font-size: 15px; color: var(--ink-2); }
.tasted-rating.none { color: var(--line); }
.tasted-rating.big { font-size: 13px; max-width: 100px; }
.tasted-rating.big em { font-size: 18px; }

/* ---------- The passport ---------- */
/* Brass only, never acid — a level is a way of looking back, not a live state, and an acid
   "you're an Enthusiast" would compete with every "peaking now" pill on Tonight. No badge
   art either: the garnish lesson applies double here, and 200 badges would fight the whole
   supper-club look. This is typography carrying the whole thing. */

.passport { margin-top: 6px; }

/* ---------- summary stat row ---------- */
/* The stat row won over a prose sentence — Pat's call, tasted-refined.html. Scannable every
   time you open the tab beats read-once-and-forgotten. */
.passport-stats { margin-bottom: 28px; }
.stat-line {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 10px 0; border-bottom: 1px solid var(--line-2);
}
.stat-line:last-child { border-bottom: none; }
.stat-k { font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.stat-v { font-family: var(--serif); font-size: 17px; color: var(--ink); text-align: right; }
.stat-v em { font-style: normal; color: var(--brass); font-weight: 600; }

/* ---------- ranking: styles / grapes / countries ---------- */
.passport-axis-title {
  font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 400; margin: 20px 0 4px;
}
.passport-axis-title em {
  font-style: normal; color: var(--brass); margin-left: 6px;
}

.rank-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
.rank-tab {
  font-family: var(--sans); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  padding: 7px 15px; border: 1px solid var(--line-2); border-radius: 999px;
  color: var(--ink-3); background: none; cursor: pointer;
}
.rank-tab.active { border-color: var(--brass); color: var(--brass); background: rgba(201,162,39,.08); }

.rank-row {
  display: flex; align-items: baseline; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--line-2); cursor: pointer;
}
.rank-row:last-child { border-bottom: none; }
.rank-n { font-family: var(--serif); font-size: 19px; color: var(--line-2); width: 24px; flex-shrink: 0; }
.rank-row.top .rank-n { color: var(--brass); font-weight: 600; }
.rank-name {
  font-family: var(--serif); font-size: 17px; color: var(--ink); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rank-row.top .rank-name { font-weight: 600; }
.rank-avg { font-size: 10.5px; color: var(--ink-3); flex-shrink: 0; }
.rank-count {
  font-family: var(--serif); font-size: 17px; color: var(--brass);
  flex-shrink: 0; min-width: 22px; text-align: right; font-variant-numeric: tabular-nums;
}

/* Both of these are real navigation set as 14px-tall text ("Show all 82", "Show earlier years
   — 217 more wines"). Padded to a ~40px target; the text itself is untouched. */
.passport-toggle { display: block; margin: 4px 0 0; padding: 13px 0; }
.diary-toggle { display: block; padding: 13px 0; }

.passport-note {
  font-size: 12px; color: var(--ink-3); line-height: 1.5;
  margin: 4px 0 24px; font-weight: 300;
}

/* Tap a ranking row to see the actual bottles behind the number. */
.rank-expand { display: flex; gap: 8px; flex-wrap: wrap; padding: 4px 0 14px; }
.rank-photo {
  width: 56px; aspect-ratio: 3/4; background: var(--panel);  /* was var(--panel-2), a token that is defined nowhere — the frame rendered with no background at all */ border: 1px solid var(--line-2);
  border-radius: 6px; display: flex; align-items: center; justify-content: center;
  overflow: hidden; padding: 4px;
}
.rank-photo img { max-width: 100%; max-height: 100%; object-fit: contain; }

/* ---------- chronological (diary) — the underneath view ---------- */
.diary-thread { position: relative; padding-left: 20px; }
.diary-thread:before {
  content: ""; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 1px; background: var(--line-2);
}
.diary-year { position: relative; margin: 26px 0 12px; }
.diary-year:first-child { margin-top: 0; }
.diary-year:before {
  content: ""; position: absolute; left: -20px; top: 5px; width: 10px; height: 10px;
  border-radius: 50%; background: var(--room); border: 2px solid var(--brass);
}
.diary-year .big { font-family: var(--serif); font-size: 28px; font-weight: 600; color: var(--ink); line-height: 1; }
.diary-year .cnt { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); margin-left: 8px; }
.diary-row { display: flex; gap: 12px; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--line-2); cursor: pointer; }
.diary-row:last-child { border-bottom: none; }
.diary-thumb {
  flex-shrink: 0; width: 36px; height: 46px; background: var(--panel);  /* was var(--panel-2), a token that is defined nowhere — the frame rendered with no background at all */ border: 1px solid var(--line-2);
  border-radius: 5px; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 2px;
}
.diary-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }
.diary-body { flex: 1; min-width: 0; }
.diary-name {
  font-family: var(--serif); font-size: 15px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.diary-meta { font-size: 10.5px; color: var(--ink-3); font-weight: 300; }
.diary-rating { font-family: var(--serif); font-size: 15px; color: var(--brass); flex-shrink: 0; }

.diary-toggle { display: block; margin: 16px 0 0; }
.diary-hidden-n { color: var(--ink-3); margin-left: 6px; }

.promote-btn { margin-top: 16px; }
.vivino-link { text-align: center; margin-top: 20px; }
.vivino-link a { color: var(--brass); }

/* ============================================================
   Settings
   ============================================================ */

.settings-title { margin: 0 0 6px; font-family: var(--serif); font-size: 34px; font-weight: 400; }
.settings-intro, .settings-note { font-size: 12.5px; color: var(--ink-3); line-height: 1.6; margin: 0 0 14px; font-weight: 300; }

/* A second (or third) topic living inside the SAME card, not a card of its own. First pass
   at this redesign gave every topic its own bordered card plus a brass group label above a
   cluster of them — two captions of almost the same weight back to back ("STORAGE" then
   "WHERE YOU KEEP IT"), and a spacing tier (34px group margin stacked on the card's own 22px
   padding) that matched nothing else in the app. One card per real topic, closely related
   topics nested inside it as a subsection, is the actual fix — fewer seams, one spacing
   rule (22px, the card's own rhythm) reused rather than a second number invented for it. */
.card-subsection { margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--line-2); }
.card-subsection h4 {
  margin: 0 0 10px; font-family: var(--sans); font-size: 13px; font-weight: 500; color: var(--ink-2);
}

.band-editor { display: flex; flex-direction: column; gap: 10px; }
.band-row { display: flex; align-items: center; gap: 8px; }

.band-name, .band-max {
  border: none; border-bottom: 1px solid var(--line);
  background: none; color: var(--ink);
  padding: 7px 2px; font-size: 13px; font-weight: 300;
  outline: none;
}
.band-name { flex: 1; min-width: 0; }
.band-name:focus, .band-max:focus { border-bottom-color: var(--brass); }
.band-max { width: 70px; flex-shrink: 0; font-family: var(--serif); font-size: 16px; }

.band-under { font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); flex-shrink: 0; }
.band-any { width: 70px; flex-shrink: 0; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); text-align: center; }
/* Brass, not acid — a band's bottle count is a static tally, not live state. It read as a
   "peaking now" pill by accident, the same drift .food-chip.book was written to avoid. */
.band-count { flex-shrink: 0; min-width: 28px; text-align: right; font-family: var(--serif); font-size: 17px; color: var(--brass); }

.settings-reset { display: block; margin: 14px auto 0; }

/* A fixed set of 7, always visible at once — not a scrolling strip. The old version reused
   .food-chip + .scroller (the horizontal food-picker component), which brought in overflow
   and edge-bleed behaviour built for a list that doesn't fit on screen; these always do, so
   it's a plain 7-column grid instead. */
.day-picker { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.day-chip {
  border: 1px solid var(--line); border-radius: 999px; background: none;
  padding: 8px 2px; font-size: 11px; font-weight: 300; color: var(--ink);
  text-align: center; cursor: pointer; transition: border-color .15s ease;
}
.day-chip:hover { border-color: var(--brass); }
/* Brass, not acid — this is a stored preference, not a live state. */
.day-chip.active { border-color: var(--brass); color: var(--brass); }

/* The toggle that opens a subsection's own body on demand — the bulk-storage tool, which is
   a lot of checkbox rows to put in front of someone who opened Settings to flip one price.
   Same weight as .card-subsection h4 (it stands in for one), just clickable, with a brass
   caret — the same colour the Bar staples disclosure already uses for "more here, tap it." */
.subsection-toggle {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 0 0 10px; text-align: left;
  font-family: var(--sans); font-size: 13px; font-weight: 500; color: var(--ink-2);
}
.subsection-toggle:hover { color: var(--brass); }
.settings-disclosure-caret {
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--brass); flex-shrink: 0;
}

/* .ghost-btn carries no margin of its own (by design — see its definition), so whoever places
   it adds the gap. Sits right under a .facts dl, which has none. */
.storage-change-btn { margin-top: 14px; }

.bulk-storage-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.bulk-storage-actions select {
  border: none; border-bottom: 1px solid var(--line); background: none; color: var(--ink);
  padding: 7px 2px; font-size: 13px; font-weight: 300; outline: none;
}
.bulk-storage-actions select:focus { border-bottom-color: var(--brass); }

/* .text-btn on its own is deliberately quiet everywhere else (delete, skip, dismiss) — no
   border, no background. Here it's the primary way to select bottles, sitting next to a
   dropdown with nothing else marking it as tappable, so it needs the same outline-pill
   affordance .ghost-btn uses elsewhere, just compact and inline rather than full-width. */
.bulk-storage-actions .text-btn {
  border: 1px solid var(--brass); border-radius: 999px;
  padding: 6px 12px; color: var(--brass);
  transition: background .18s ease, color .18s ease;
}
.bulk-storage-actions .text-btn:hover { background: var(--brass); color: var(--room); }

.bulk-storage-list {
  display: flex; flex-direction: column; max-height: 340px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 8px; padding: 2px 10px; margin-bottom: 14px;
}
.bulk-storage-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 0;
  border-bottom: 1px solid var(--line-2); cursor: pointer;
}
.bulk-storage-row:last-child { border-bottom: none; }
/* Brass, not acid. Acid is live state and nothing else — "open this tonight" — and these
   checkboxes are a selection for a pending "Set place for N bottles", which is an action.
   Same misuse the Settings pass caught twice already (.band-count and the weekend-night
   picker); every one of them dilutes the one signal the scheme exists to protect. */
.bulk-storage-row input[type="checkbox"] { flex-shrink: 0; accent-color: var(--brass); }
.bulk-storage-glyph { flex-shrink: 0; font-size: 13px; }
.bulk-storage-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 300; }
.bulk-storage-place { flex-shrink: 0; font-size: 11px; color: var(--ink-3); text-align: right; }
.bulk-storage-row.set .bulk-storage-place { color: var(--brass); }

/* ============================================================
   Add / edit sheet
   ============================================================ */

.sheet {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.55);
  z-index: 40;
  display: flex; align-items: flex-end;
}
.sheet.hidden { display: none; }

/* The add/confirm sheet is the only sheet ever launched FROM another one (tapping a bottle
   in the upload queue), so it must win the stack. Without this, every .sheet shares z-index
   40 and the winner is just whichever sits later in index.html — which is #uploadSheet, so
   the confirm form opened invisibly BEHIND the list that launched it. openReviewSheet()
   also hides the queue outright; this is the belt to that pair of braces. */
#addSheet { z-index: 41; }

.sheet-inner {
  background: var(--room);
  width: 100%; max-height: 92vh; overflow-y: auto;
  /* `vh` measures the viewport WITHOUT the browser's own toolbars, which on a phone appear
     and disappear as you scroll — so a 92vh sheet can be taller than what you can actually
     see. `dvh` measures the real, current viewport. Plain vh stays above it as the fallback
     for anything that doesn't know dvh; the later rule wins where it does. */
  max-height: 92dvh;
  /* ⚠️ This is a PAINT fix, not a layout one, and it can only be confirmed on a real phone.
     A scrolling box inside a position:fixed overlay is painted lazily on iOS, and a fast
     flick can outrun it — leaving the part you have just scrolled past as a blank slab of
     background, borders and all. Reported 22 Aug from real use ("the top third of the
     screen disappears" while confirming a bottle, screenshot on file): the header, the
     review note, the label photo and two fields all rendered as empty green, then reappeared.
     Promoting the box to its own compositing layer is what keeps it painted; the scroll
     containment stops a flick that reaches the end of the form from carrying on into the
     page underneath, which is the motion that triggers it. */
  transform: translateZ(0);
  overscroll-behavior: contain;
  border-radius: 20px 20px 0 0;
  border-top: 1.5px solid var(--brass);
  padding: 22px var(--gutter) 30px;
}

.sheet-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 18px; }
.sheet-header h2 { margin: 0; font-family: var(--serif); font-size: 26px; font-weight: 600; }
.sheet-header-actions { display: flex; align-items: baseline; gap: 16px; }

.text-btn {
  background: none; border: none; cursor: pointer;
  color: var(--ink-3);
  font-size: 9.5px; letter-spacing: .18em; text-transform: uppercase;
}
.text-btn:hover { color: var(--brass); }

/* Shared by the bottle form and the tasted-wine form. These were ID-scoped to #addForm, so the
   tasted form initially rendered with unstyled inline labels — the labels sat beside their inputs
   instead of above them, and two fields ran onto one line. Extending the selector rather than
   copying the block: a second copy of a form's entire visual language is how two forms in one app
   start looking like two apps. */
#addForm label,
#tastedForm label {
  display: block;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 16px;
}

#addForm input, #addForm select,
#tastedForm input, #tastedForm select {
  display: block; width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  font-size: 14px; font-weight: 300;
  letter-spacing: 0; text-transform: none;
  outline: none;
}
#addForm input:focus, #addForm select:focus,
#tastedForm input:focus, #tastedForm select:focus { border-color: var(--brass); }
#addForm select option,
#tastedForm select option { background: var(--panel); color: var(--ink); }

.row { display: flex; gap: 12px; }
.row label { flex: 1; }

.hint { font-weight: 300; text-transform: none; letter-spacing: 0; font-size: 11px; color: var(--ink-3); }

.checkbox-row { display: flex; align-items: center; gap: 10px; }
/* ⚠️ ID-scoped on purpose, and this is specificity rather than taste: `#addForm input`
   (1-0-1) beats a plain `.checkbox-row input` (0-1-1), so the block/100%-wide field rule
   won and the Favourite tick box rendered as a full-width block with its own label pushed
   underneath it. A form whose element rules are ID-scoped needs its exceptions ID-scoped
   too — same trap as `.auto-filled` (input only) and the `.hidden` drift before it. */
#addForm .checkbox-row input,
#tastedForm .checkbox-row input,
.checkbox-row input { display: inline-block; width: auto; margin: 0; accent-color: var(--acid); }

.photo-picker { cursor: pointer; display: block; }

/* The upload sheet's picker. Reads as the primary action it is (brass, same as
   .primary-btn) rather than as an empty preview slot — see the note in index.html. */
.photo-picker-btn { cursor: pointer; display: block; margin-bottom: 18px; }
.photo-picker-btn input { display: none; }
.photo-picker-btn span {
  display: block; width: 100%; padding: 14px;
  border-radius: 999px; background: var(--brass); color: var(--room);
  font-size: 10px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  text-align: center;
}
.photo-picker-btn:hover span { background: #d8b23d; }

/* Scoped to #addForm on purpose. A bare `.photo-picker input` loses to `#addForm input`
   below — an ID beats a class — and the raw "Choose File / No file chosen" control appears
   on top of the styled drop zone. */
#addForm .photo-picker input,
.photo-picker input { display: none; }

/* Two states, and they look nothing alike on purpose — see setPhotoPreview() in app.js.
   There is no third "empty frame" state any more: a preview slot that has nothing to
   preview is just a slab, which is exactly what the upload sheet's picker was fixed for
   in August and what the add form kept doing for another day. */
.photo-preview {
  display: flex; align-items: center; justify-content: center;
  border-radius: 12px;
  overflow: hidden;
  margin-bottom: 18px;
}
.photo-actions { display: flex; justify-content: flex-end; margin: -12px 0 16px; }
.photo-actions .text-btn { color: var(--brass); }

/* Nothing picked yet: the same brass action as .photo-picker-btn, because that is what a
   tap does here. Deliberately NOT a copy of that rule — this one lives inside a form whose
   element rules are ID-scoped, and it is the label, not a span, that carries it. */
.photo-preview.is-empty {
  padding: 14px;
  border-radius: 999px;
  background: var(--brass); color: var(--room);
  font-size: 10px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  text-align: center;
}
.photo-picker:hover .photo-preview.is-empty { background: #d8b23d; }
/* A photo: shown at a size you can actually read a label off, since on the confirm screen
   it is the evidence for every highlighted guess above it. 240px is a judgement call
   between two real complaints from the same week — the old 150px frame letterboxed a
   portrait label into a strip, and this sheet is already the one Pat found too scrolly. At
   240 the label and the first two guessed fields sit on screen together, which is the
   actual job. `contain`, not `cover`: cropping to fill would eventually cut the label off
   the one photo whose whole purpose is being read. */
.photo-preview.has-photo {
  border: 1px solid var(--line);
  background: var(--panel);
}
.photo-preview.has-photo img {
  display: block; width: 100%; max-height: 240px; object-fit: contain;
}

.pending-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 8px; margin-top: 12px; }
.pending-grid img {
  width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line);
}

/* The primary "something's waiting on you" surface — brass, the same "decide something"
   treatment as .barcode-match (amber because it's a prompt, not an error or a success).
   Sits at the top of the Cellar list, not behind the + button — checking on something you
   already tried to add is a different intent from starting something new. */
.review-banner {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  border: 1px solid var(--brass); border-radius: 12px;
  padding: 12px 14px; margin: 0 0 22px;
  background: rgba(201,162,39,.06);
}
.review-banner img {
  width: 44px; height: 44px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line); flex-shrink: 0;
}
.review-banner-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.review-banner-text strong {
  font-family: var(--serif); font-size: 15px; font-weight: 600; color: var(--brass);
}

/* AI-recognized / failed uploads waiting for a one-tap confirm. Full strength for
   'recognized' — it's the common, good-news case; 'failed' dims (review-row-failed),
   same doctrine as an un-makeable cocktail or an empty pair-food chip: state is carried by
   treatment and words, not by hiding the row. */
#reviewQueue { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.review-row {
  display: flex; align-items: center; gap: 12px;
  width: 100%; text-align: left;
  padding: 8px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--panel);
}
.review-row img {
  width: 48px; height: 48px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--line); flex-shrink: 0;
}
.review-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.review-row-text strong {
  font-family: var(--serif); font-size: 14.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.review-row-failed { opacity: .55; }

.primary-btn {
  width: 100%; padding: 14px;
  border-radius: 999px; border: none;
  background: var(--brass); color: var(--room);
  font-size: 10px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  cursor: pointer;
}
.primary-btn:hover { background: #d8b23d; }

/* ---------- Scan + barcode match ---------- */

.scan-status {
  font-size: 11px; color: var(--ink-3); font-weight: 300;
  margin: -8px 0 16px;
  display: flex; align-items: center; gap: 8px;
}
.scan-status.hidden { display: none; }

.scan-status .spinner {
  width: 12px; height: 12px;
  border: 1.5px solid var(--line);
  border-top-color: var(--brass);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.scan-text { margin: -8px 0 16px; font-size: 11px; color: var(--ink-3); }
.scan-text.hidden { display: none; }

/* Amber because it is a "decide something" prompt, not an error or a success. */
.barcode-match {
  border: 1px solid var(--brass);
  border-radius: 12px;
  padding: 12px 14px;
  margin: -8px 0 16px;
  font-size: 12.5px; font-weight: 300;
}
.barcode-match.hidden { display: none; }
.barcode-match p { margin: 0 0 10px; }
.barcode-match strong { font-family: var(--serif); font-size: 15px; font-weight: 600; }
.barcode-match-actions { display: flex; gap: 18px; }
.barcode-match-actions .text-btn { color: var(--brass); }

/* BOTH halves, not just input. Every other field rule in this form pairs `input, select`
   on the same line (see the base block above); this one didn't, so an AI-guessed Type or
   Spirit Type got the class and showed nothing — the two dropdowns the reader guesses most
   confidently were the two it never marked. Same shape as the `.hidden` drift: a rule
   written for one element type, in a form built out of two. */
#addForm input.auto-filled,
#addForm select.auto-filled { border-color: var(--acid); }

.setup-banner {
  margin: 20px 0;
  padding: 16px;
  border: 1px solid var(--brass);
  border-radius: 12px;
  color: var(--ink-2);
  font-size: 13px; line-height: 1.6; font-weight: 300;
}
.setup-banner strong { color: var(--ink); font-weight: 500; }
.setup-banner code {
  background: var(--panel);
  padding: 2px 6px; border-radius: 5px;
  font-size: 12px; color: var(--brass);
}

/* ============================================================
   Floating add + chooser
   ============================================================ */

/* Brass, not acid. Acid is reserved for live state — an acid add button would sit
   permanently on top of a screen full of acid "peaking now" pills and compete with every
   one of them. */
.fab {
  position: fixed; right: 20px; bottom: 82px;
  width: 50px; height: 50px;
  border-radius: 50%;
  background: var(--brass);
  color: var(--room);
  border: none;
  font-size: 24px; line-height: 1;
  box-shadow: 0 8px 22px rgba(0,0,0,.4);
  cursor: pointer;
  z-index: 31;
}
.fab:active { background: #b08d1c; }

.fab-badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 19px; height: 19px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--acid);
  color: var(--room);
  font-size: 10px; font-weight: 600;
  line-height: 19px; text-align: center;
  box-shadow: 0 0 0 2px var(--room);
}

/* These two rules did not exist before, which is why the chooser floated over every screen
   permanently and the FAB showed a "0" badge with nothing queued. `hidden` is not a
   browser-styled class — only the `hidden` attribute is — so it does nothing unless the
   rule is written. `.sheet`, `.barcode-match` and `.scan-status` had one; these did not. */
.fab-badge.hidden { display: none; }
.chooser.hidden { display: none; }

/* ⚠️ THE CHOOSER IS ITS OWN BACKDROP (25 Aug 2026). It used to be a small floating menu at
   right: 20px with nothing behind it, so two brass pills sat over a live cellar list —
   the only overlay in the app that didn't dim what it covered, and the only one you could
   mis-tap through. It now fills the viewport and dims, with the options held in the same
   spot by the flex alignment rather than by their own offsets.
   The dismiss handler in app.js keys on `e.target === chooser` for exactly this reason:
   the element that IS the scrim can't be treated as "inside the menu". */
.chooser {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.55);
  z-index: 32;
  display: flex; flex-direction: column; gap: 8px;
  justify-content: flex-end; align-items: flex-end;
  padding: 0 20px 150px;
}

.chooser-option {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px;
  border-radius: 999px;
  border: 1px solid var(--brass);
  background: var(--panel);
  color: var(--ink);
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  box-shadow: 0 8px 20px rgba(0,0,0,.4);
  cursor: pointer;
  white-space: nowrap;
}
.chooser-icon { font-size: 15px; }
.chooser-count { margin-left: 6px; color: var(--acid); }
.chooser-count.hidden { display: none; }

/* ============================================================
   Tab bar
   ============================================================ */

.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: 30;
  display: flex;
  background: var(--panel);
  border-top: 1px solid var(--brass);
  padding-bottom: env(safe-area-inset-bottom);
}

.tab {
  flex: 1;
  border: none; background: none;
  color: var(--ink-3);
  font-size: 10px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase;
  padding: 14px 0;
  cursor: pointer;
}
.tab.active { color: var(--acid); }

/* The emoji tab icons are gone — small caps labels carry it, and four emoji at the bottom of
   an otherwise typographic screen were the loudest thing on it. */
.tab-icon { display: none; }

/* ============================================================
   Desktop — a rail and a real grid, not a stretched phone
   ============================================================ */

@media (min-width: 900px) {
  body { padding-bottom: 0; }

  .shell {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 50px;
    max-width: 1220px; margin: 0 auto; padding: 0 40px;
    align-items: start;
  }

  .topbar, .tabbar, .fab, .chooser { display: none; }

  .rail {
    display: block;
    position: sticky; top: 0; height: 100vh;
    padding: 42px 0 30px;
    border-right: 1px solid var(--line);
  }
  .rail .wordmark { display: block; margin-bottom: 8px; }
  .rail-meta {
    font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3);
    padding-bottom: 24px; border-bottom: 1.5px solid var(--brass); margin-bottom: 24px;
    line-height: 1.9;
  }
  .rail nav { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
  .rail nav .tab {
    flex: none;
    font-family: var(--serif); font-size: 24px; font-weight: 500; line-height: 1.35;
    letter-spacing: 0; text-transform: none;
    color: var(--ink-3);
    padding: 0; position: relative;
    transition: color .15s ease;
  }
  .rail nav .tab:hover { color: var(--ink-2); }
  .rail nav .tab.active { color: var(--ink); }
  .rail nav .tab.active:before {
    content: ""; position: absolute; left: -16px; top: 50%;
    width: 9px; height: 1.5px; background: var(--acid);
  }

  .rail-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; margin-top: 28px; }
  /* ⚠️ BRASS, NOT ACID (changed 22 Aug 2026). This was outlined in --acid, byte-identical to
     the "peaking now" pills it sits beside — and the desktop cellar is a photo grid full of
     them, so the loudest colour in the app was a permanent Add button competing with every
     bottle worth opening tonight.
     That is the exact case the FAB decision was argued out on: "an acid FAB sat permanently
     on top of a screen full of acid 'peaking now' pills and competed with all of them",
     which is why the phone's + is brass. The rail was built later (session 4's desktop
     layout) and the reasoning didn't travel with it — a locked decision recorded in prose
     doesn't apply itself when the same problem reappears at another breakpoint.
     Weight and shape are deliberately unchanged; only the token moved. Acid elsewhere in the
     rail (the active-nav dash) is SELECTION state and matches .tab.active on the phone —
     that is a different usage and is left alone. */
  .rail-btn {
    border: 1px solid var(--brass); color: var(--brass);
    background: none; border-radius: 999px;
    padding: 11px 22px;
    font-size: 10px; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
    cursor: pointer;
    transition: background .18s ease, color .18s ease;
  }
  .rail-btn:hover { background: var(--brass); color: var(--room); }
  .rail-settings {
    background: none; border: none; cursor: pointer;
    font-size: 9px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-3);
    padding: 0;
  }
  .rail-settings:hover { color: var(--brass); }

  main { padding: 42px 0 80px; max-width: none; margin: 0; }

  .headline { font-size: clamp(46px, 4.6vw, 62px); }
  .hero { gap: 28px; }
  .hero-photo { max-width: 240px; }
  .hero-name { font-size: clamp(29px, 3vw, 38px); }
  .bottle-thumb { flex-basis: 50px; height: 66px; }
  .bottle-name { font-size: 21px; }
  .scroller { margin: 0; padding-left: 0; padding-right: 0; }
  .facts { grid-template-columns: 120px 1fr; }
  .detail { max-width: 720px; }
  .sheet-inner { max-width: 640px; margin: 0 auto; border-radius: 20px; }
  .sheet { align-items: center; padding: 20px; }
}

/* Two-column spread for Tonight on the widest screens. */
@media (min-width: 1160px) {
  .spread { display: grid; grid-template-columns: 1.3fr 1fr; gap: 48px; align-items: start; }
  .spread > section { min-width: 0; }
}

/* ---------- Cellar grid (desktop only) ---------- */

.grid { display: grid; gap: 24px 20px; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); }

.tile {
  display: flex; flex-direction: column; height: 100%;
  text-align: left; width: 100%;
  background: none; border: none; padding: 0;
  cursor: pointer;
  color: inherit;
}

.tile .tile-photo {
  width: 100%;
  /* Squarer than the detail frame on purpose: phone photos are a mix of portrait and
     landscape, and a 3/4 cell letterboxes the landscape ones into a thin strip. */
  aspect-ratio: 4 / 5;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  padding: 8px; overflow: hidden;
  margin-bottom: 10px;
  font-size: 32px;
  transition: transform .25s cubic-bezier(.2,.7,.3,1), border-color .25s ease;
}
.tile:hover .tile-photo { transform: translateY(-4px); border-color: var(--brass); }
.tile .tile-photo img { max-width: 100%; max-height: 100%; object-fit: contain; }

.tile-name { display: block; font-family: var(--serif); font-size: 18px; font-weight: 600; line-height: 1.12; margin: 0 0 3px; }
.tile-meta { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 300; margin: 0 0 6px; }

/* Grid rows size to their tallest item, so one two-line bottle name in a row of one-line
   names drags every neighbour's tags out of line. Reserving two lines on the name fixes the
   alignment but leaves a visible hole under short names, which reads as a bug. Pushing the
   tags to the bottom of the tile aligns them across the row with no hole.

   Bottom-alignment alone was not enough: the two tags side by side needed 130-160px inside
   a 158px tile, so some wrapped and some didn't, and a wrapped tile lifted its first badge
   a line above the rest of the row. Each tag now gets its OWN reserved row, always present
   even when empty, so a window tag is at the same height on every tile regardless of how
   long its label or its neighbour's happens to be. */
.tile .tags {
  margin-top: auto; padding-top: 4px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
}
.tile .tag-row {
  display: flex; align-items: center;
  min-height: 17px;      /* one pill, so an empty row still holds its place */
}

/* ============================================================
   The drink log
   ------------------------------------------------------------
   Brass throughout, never acid: a score is an opinion about the
   past, and acid is reserved for live state ("open this tonight").
   A four-star Negroni is not urgent.
   ============================================================ */

/* ---------- The rating input ---------- */

/* Two hit areas per star — left half scores x.5, right half x.0 — so the scale matches the
   Vivino ratings already in tasted_wines. The glyph underneath is the empty star and
   .star-fill is the same glyph clipped to a width, which is how halves render without
   needing a half-star character. */
.star-input { display: flex; gap: 2px; margin-top: 8px; }

.star-slot {
  position: relative;
  width: 34px; height: 34px;
  font-size: 28px; line-height: 34px;
}
.star-glyph { color: var(--line); }
.star-fill {
  position: absolute; inset: 0;
  overflow: hidden; white-space: nowrap;
  color: var(--brass);
}
.star-hit {
  position: absolute; top: 0; height: 100%; width: 50%;
  background: none; border: none; padding: 0;
  cursor: pointer;
}
.star-hit.left { left: 0; }
.star-hit.right { right: 0; }

.star-readout {
  display: block; margin-top: 6px;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3);
}

/* ---------- The log sheet ---------- */

.log-subject {
  margin: -8px 0 20px;
  font-family: var(--serif); font-size: 21px; color: var(--ink);
}

/* .log-field is a <div> that looks exactly like a label, for the two fields whose control is
   several buttons rather than one input — see the warning in index.html. Same rule, so they are
   indistinguishable on screen; different element, so the browser doesn't forward their clicks. */
#logForm label,
#logForm .log-field {
  display: block;
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 18px;
}
#logForm input[type="date"],
#logForm input[type="text"] {
  display: block; width: 100%;
  margin-top: 6px; padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel); color: var(--ink);
  font-size: 14px; font-weight: 300;
  letter-spacing: 0; text-transform: none;
  outline: none;
}
#logForm input:focus { border-color: var(--brass); }
#logForm .notes-input { margin-top: 6px; }

/* The escape hatch under "save and finish". Full width so it reads as the second of two
   choices rather than a stray link. */
.text-btn.wide {
  display: block; width: 100%;
  margin-top: 14px; padding: 10px;
}
.text-btn.wide.hidden { display: none; }

/* ---------- "Again": repeat intent ---------- */

/* `hidden` is NOT a browser-styled class — only the hidden ATTRIBUTE is. Missing rules for
   .chooser.hidden and .fab-badge.hidden are why the add-chooser floated over every screen for
   three sessions with correct JS behind it. This is that rule for the again field. */
#againField.hidden { display: none; }

.again-input { display: flex; gap: 8px; margin-top: 8px; }

/* Brass, not acid. Acid is live state and nothing else — "would we buy this again" is a
   standing opinion, which is exactly what it is not. */
.again-chip {
  flex: 1; border: 1px solid var(--line); background: none; color: var(--ink-2);
  font-family: var(--serif); font-size: 16px; font-weight: 600;
  padding: 10px 14px; cursor: pointer; transition: border-color .18s, color .18s;
}
.again-chip.on { border-color: var(--brass); color: var(--brass); }

/* A sentence on the detail page, deliberately not a badge — a badge is what a star already was,
   and being unable to say what it meant is why this feature exists. */
.again-line {
  margin: 6px 0 0; font-family: var(--serif); font-style: italic; font-size: 15px;
  color: var(--ink-2);
}
.again-line.yes { color: var(--brass); }

/* ---------- The re-buy list on Tasted ---------- */

/* Breathing room before the "234 of 234 wines" count, which otherwise butts straight up against
   the last re-buy row and reads as part of the same list. */
.list.rebuy { margin-bottom: 26px; }

.rebuy-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
/* contain, never cover: a bottle photo is portrait and its label is the entire point. Same
   reason the Tasted thumbnails and the detail hero contain. */
.rebuy-thumb {
  width: 34px; height: 46px; flex-shrink: 0; object-fit: contain;
  background: var(--panel);
}
.rebuy-thumb.none { background: var(--panel); }
.rebuy-body { flex: 1; min-width: 0; }
.rebuy-name {
  display: block; font-family: var(--serif); font-size: 17px; font-weight: 600;
  color: var(--ink); line-height: 1.2;
}
.rebuy-meta {
  display: block; margin-top: 3px; font-size: 11px; letter-spacing: .04em;
  color: var(--ink-3);
}
.rebuy-tag {
  flex-shrink: 0; font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase;
}
.rebuy-tag.have { color: var(--brass); }
.rebuy-tag.gone { color: var(--ink-3); }

/* ---------- Entry points on the detail pages ---------- */

.primary-btn.wide { margin-top: 24px; }

/* ---------- The standing verdict, under the name ---------- */

.our-rating {
  margin: 6px 0 0;
  display: flex; align-items: baseline; gap: 8px;
  color: var(--brass); font-size: 13px; letter-spacing: .08em;
}
.our-rating em {
  font-style: normal; font-family: var(--serif); font-size: 17px; color: var(--ink);
}
.our-rating-count {
  font-size: 8px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
}

/* ---------- Whose rating (24 Aug 2026) — attributed pair + "you both loved this" ---------- */
.our-rating { flex-wrap: wrap; }
.our-rating.both-love {
  display: block; color: var(--brass);
  font-family: var(--serif); font-style: italic; font-size: 16px; letter-spacing: 0;
}
.rating-who { display: inline-flex; align-items: baseline; gap: 5px; }
.rating-who .who {
  font-family: var(--sans); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3);
}
.rating-sep { color: var(--line); }
.again-line.no, .again-line.mixed { color: var(--ink-2); }
.log-stars .who {
  font-family: var(--sans); font-size: 9px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3); margin-left: 6px;
}

/* ---------- Household card ---------- */
.household-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--line-2);
}
.household-row:last-child { border-bottom: none; }
.household-name { font-size: 14.5px; font-weight: 300; }
.household-you {
  flex-shrink: 0; font-family: var(--sans); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--brass);
  border: 1px solid var(--brass); border-radius: 999px; padding: 3px 9px;
}

/* ---------- You & sharing card ---------- */
.sharing-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--line-2);
}
.sharing-row:last-of-type { border-bottom: none; }
.sharing-label { color: var(--ink); font-size: 15px; }
.sharing-name {
  flex: 1; max-width: 210px; text-align: right;
  background: none; border: none; border-bottom: 1px solid var(--line);
  color: var(--ink); font-family: var(--serif); font-size: 16px; padding: 4px 0;
}
.sharing-name:focus { outline: none; border-bottom-color: var(--brass); }
.share-toggle {
  flex-shrink: 0; width: 46px; height: 26px; border-radius: 999px;
  background: var(--line); border: 1px solid var(--line); position: relative;
  cursor: pointer; transition: background .15s, border-color .15s; padding: 0;
}
.share-toggle .knob {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: var(--ink-3); transition: transform .15s, background .15s;
}
.share-toggle.on { background: var(--brass); border-color: var(--brass); }
.share-toggle.on .knob { transform: translateX(20px); background: var(--room); }
.share-toggle:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

/* ---------- The history list ---------- */

.log-list { margin: 0; padding: 0; list-style: none; }
.log-list li {
  padding: 11px 0;
  border-bottom: 1px solid var(--line-2);
}
.log-list li:last-child { border-bottom: none; }

.log-line { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.log-date {
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3);
}
.log-stars { color: var(--brass); font-size: 12px; letter-spacing: .06em; }
.log-stars em {
  font-style: normal; font-family: var(--serif); font-size: 15px; color: var(--ink);
  margin-left: 6px;
}

.log-occasion {
  margin: 5px 0 0;
  font-family: var(--serif); font-size: 16px; color: var(--ink);
}
.log-note {
  margin: 4px 0 0;
  font-size: 13px; font-weight: 300; line-height: 1.55; color: var(--ink-2);
}

/* A score on a drink tile replaces the base line rather than joining it — two pieces of
   meta on a 158px tile is one too many. */
.tile-meta.rated { color: var(--brass); letter-spacing: .06em; }

/* ---------- The empty Bar's way out ----------
   Shown only when the household owns no bottles and has ticked no staples, so the Bar is a
   screen full of drinks you can't have with no control on it. See barGettingStartedHtml().
   Styled as a panel rather than a .setup-banner: nothing is broken here, it's a new bar. */
.bar-start {
  margin: 18px 0 22px; padding: 20px 18px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
}
.bar-start-lead {
  margin: 0 0 16px;
  font-family: "Cormorant Garamond", Georgia, serif;
  font-size: 18px; line-height: 1.4; color: var(--ink); font-weight: 400;
}
.bar-start-actions { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.bar-start-note {
  margin: 16px 0 0; font-size: 12px; line-height: 1.55;
  color: var(--ink-3); font-weight: 300;
}

/* Review-mode note on the add sheet — "these fields are a guess". Sits under the header,
   above the form, so it is read before anything is corrected. */
.review-note {
  margin: 0 0 16px; padding: 10px 12px;
  background: rgba(201, 162, 39, .09);
  border-left: 2px solid var(--brass);
  font-size: 12px; line-height: 1.55; color: var(--ink-2); font-weight: 300;
}
.review-note.hidden { display: none; }

/* Inline field warning — currently the grape/drinking-window mismatch. Coral because it is
   the "you're losing something" colour everywhere else (past-peak bottles, the missing-
   ingredient line), and deliberately not acid: acid is live state only. */
.field-warn {
  display: block; margin-top: 6px;
  font-size: 11px; line-height: 1.5; color: var(--coral); font-weight: 300;
  text-transform: none; letter-spacing: 0;
}
.field-warn.hidden { display: none; }

@media (min-width: 900px) {
  /* The rail's secondary action (photo recognition). Deliberately NOT acid like .rail-btn:
     acid is live state and nothing else, and two acid buttons stacked in the rail would
     shout louder than the "peaking now" pills they sit beside. Brass outline = an action,
     which is the scheme's own rule. */
  .rail-btn.quiet {
    border-color: var(--line);
    color: var(--ink-2);
  }
  .rail-btn.quiet:hover { background: var(--brass); border-color: var(--brass); color: var(--room); }
}

/* Window legend — the four states, shown on the cellar only while it is sorted by window.
   Uses the real .window-tag classes, so it can never disagree with the pills it explains. */
/* The legend collapses now — see the note at its render site in app.js. The summary line is
   the permanent half (it names what the ORDER means, which is the thing a sort actually needs
   to say); the colour key is the half behind the tap. */
.window-legend { margin: 0 0 14px; }
.legend-toggle {
  width: 100%; display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  background: none; border: none; cursor: pointer;
  padding: 10px 0; text-align: left;
  font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3);
}
.legend-toggle:hover { color: var(--ink-2); }
.legend-more { color: var(--brass); white-space: nowrap; }
.legend-key {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  margin: 2px 0 6px; padding: 0;
}
.wl-item { display: inline-flex; align-items: center; gap: 6px; }
.wl-gloss {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 300;
}

/* Tonight's cover story now says WHY that bottle — windowSentence(), which was detail-page
   only. Matches .window-say's voice but sized for the hero. */
.hero-say {
  display: block; margin-top: 8px;
  font-family: var(--serif); font-size: 15px; line-height: 1.4;
  color: var(--ink-2); font-weight: 400;
}
.hero-say em { font-style: normal; }
.hero-say .urgent { color: var(--coral); font-style: italic; }
.hero-say .easy { color: var(--ink-3); }

/* ============================================================
   The location wizard — WINDOW-BUILD-PLAN.md slice 2.

   A screen type this app didn't have. It inherits the Supper Club system rather than
   inventing a second one: Cormorant for anything about wine, Jost for interface, hairline-
   under-small-caps department headers (.dept) instead of cards, brass for actions.

   ⚠️ ACID APPEARS EXACTLY ONCE IN HERE — on step 4's adjusted-window pill, which reuses
   .window-tag.peaking. That is not decoration and not an exception to the rule: a drinking
   window state is precisely what acid is reserved for, and the whole point of the payoff
   screen is that those dates are now live state rather than a static assumption. Everything
   else that wants emphasis uses brass.

   It borrows body.auth-gate to hide the topbar, rail, tabbar and FAB, so the wizard is the
   only thing on screen. Same width and rhythm as .auth-screen, which is the other full-page
   takeover in the app — they should feel like the same room.
   ============================================================ */

.wizard {
  max-width: 420px; margin: 40px auto 0;
  padding: 0 20px 40px;
  display: flex; flex-direction: column; align-items: stretch; gap: 12px;
}
.wizard-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.wizard-steps { display: flex; gap: 6px; flex-shrink: 0; }

/* Progress as four pips rather than "Step 2 of 4". The count is small enough to read at a
   glance, and a wizard that names its own length invites you to weigh it up before starting. */
.wizard-pip {
  width: 18px; height: 2px; border-radius: 2px;
  background: var(--line); transition: background .25s;
}
.wizard-pip.done { background: var(--ink-3); }
.wizard-pip.on   { background: var(--brass); }

.wizard-q {
  font-family: var(--serif); font-weight: 500;
  font-size: 27px; line-height: 1.15; margin: 2px 0 0; color: var(--ink);
}
.wizard-lede {
  font-size: 13.5px; line-height: 1.6; font-weight: 300;
  color: var(--ink-2); margin: 0;
}
.wizard-lede.small { font-size: 12.5px; margin-top: -6px; }
.wizard-lede strong { color: var(--ink); font-weight: 500; }
.wizard-hint { font-size: 12px; color: var(--ink-3); font-weight: 300; line-height: 1.55; margin: 0; }

/* Coral, matching "past peak" — the one place in the flow where something is actually wrong
   and the answer is don't-do-this rather than here-are-your-dates. */
.wizard-warn {
  font-size: 13px; line-height: 1.6; font-weight: 300;
  color: var(--ink); margin: 0;
  border-left: 2px solid var(--coral); padding: 2px 0 2px 12px;
}

/* ---------- Step 1: the town search ---------- */

.wizard-results { display: flex; flex-direction: column; gap: 1px; }
.wizard-result {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 11px 2px; background: none; border: none;
  border-bottom: 1px solid var(--line-2); color: var(--ink);
}
.wizard-result:hover { border-bottom-color: var(--brass); }
.wizard-town  { font-family: var(--serif); font-size: 17px; }
/* Region and country are never optional here. "Melbourne" matches Florida and a search for
   "Byron" returns Minnesota first — this line is what makes the list answerable. */
.wizard-where {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); text-align: right; flex-shrink: 0;
}

/* ---------- Steps 2 and 3: the places ---------- */

.wizard-places { display: flex; flex-direction: column; gap: 8px; margin: 2px 0 4px; }
.wizard-place {
  display: flex; flex-direction: column; gap: 3px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 12px 14px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--panel); color: var(--ink);
}
.wizard-place:hover { border-color: var(--ink-3); }
/* Brass, not acid: choosing a place is an ACTION, and nothing here is live state yet. */
.wizard-place.on { border-color: var(--brass); }
.wizard-place-label { font-size: 14px; font-weight: 400; }
.wizard-place.on .wizard-place-label { color: var(--brass); }
.wizard-place-note { font-size: 11.5px; line-height: 1.5; font-weight: 300; color: var(--ink-3); }
/* ⚠️ BRASS, OVERRIDING .food-chip.active, WHICH IS ACID. Not a stylistic preference — it is
   the acid rule at the top of this block. Elsewhere that component marks a live filter (the
   Tonight food picker, the weekend-days editor), which is a state. Here it is one more
   selection in a form, sitting directly beneath place cards that are already brass, so
   inheriting acid would both break the rule and make two halves of one question look like
   two different kinds of control. 0-3-0 beats the base rule's 0-2-0. */
.wizard-primary { margin: -4px 0 4px; }
.wizard-primary .food-chip.active { border-color: var(--brass); color: var(--brass); }

/* ---------- Step 4: the payoff ---------- */

.wizard-examples { display: flex; flex-direction: column; gap: 1px; margin-bottom: 2px; }
.wizard-example {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 10px 2px; border-bottom: 1px solid var(--line-2);
}
.wizard-ex-name {
  font-family: var(--serif); font-size: 15.5px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Struck-through original beside the new one. The movement IS the message, so both numbers
   have to be on screen at once — a single adjusted date proves nothing to someone who never
   saw the old one. */
.wizard-ex-move { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.wizard-ex-move s { font-size: 11.5px; color: var(--ink-3); font-weight: 300; }
.wizard-ex-move .window-tag { font-size: 11px; }

/* ---------- Step 5: the first bottle (empty cellars only) ---------- */

/* Two routes to the same place, stacked rather than side by side: at 390px a pair of pill
   buttons in a row gives each about 160px, and "Type one in instead" at 10px/.18em tracking
   does not fit in that. The gap is tighter than the wizard's usual rhythm because these are
   two halves of one choice, not two sections. */
.wizard-actions { display: flex; flex-direction: column; gap: 10px; margin: 22px 0 20px; }

/* ---------- The foot ---------- */

/* "Set this up later" sits on every step, deliberately quiet and deliberately always there.
   The wizard is skippable by decision — the app works without it — so the exit must never be
   hunted for, and must never compete with the brass action above it. */
.wizard-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-2);
}
.wizard-skip { color: var(--ink-3); }

/* The ageing-tier threshold in Settings. Reuses .band-under and the .band-max field styling
   deliberately — it is the same kind of control asking a different question, and a second
   visual language for one number would read as a different feature. */
.tier-editor { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.tier-editor input {
  width: 90px; flex-shrink: 0;
  font-family: var(--serif); font-size: 16px;
  background: none; border: none; border-bottom: 1px solid var(--line);
  color: var(--ink); padding: 4px 2px;
}
.tier-editor input:focus { border-bottom-color: var(--brass); outline: none; }

/* ============================================================
   .hidden — the generic rule, and why it is here at the bottom with !important
   ============================================================
   ⚠️ `hidden` is NOT a browser-styled class. Only the `hidden` ATTRIBUTE is. This file
   carried ten element-specific `.X.hidden { display: none }` rules instead, written one at a
   time as each bug was found — .chooser and .fab-badge after the chooser floated over every
   screen for weeks, .review-note and .field-warn as recently as today.

   That approach fails silently and had failed again: `#skipReview` is `class="text-btn
   hidden"`, there is no `.text-btn.hidden` rule, so the "Not a bottle" button was VISIBLE on
   every ordinary Add-a-bottle and Edit-bottle form. (Its handler guards on
   reviewingUploadId, so it did nothing when tapped — a dead button rather than a
   destructive one, but on the first form a new user ever sees.)

   Two things about the placement, both load-bearing:

     • AT THE END OF THE FILE. A generic `.hidden` declared near the top would LOSE the
       source-order tie to any later single-class rule that sets display — `.chooser` sets
       `display: flex` at line ~1420, so an early `.hidden` would leave the chooser
       permanently on screen. That ordering trap is precisely why the per-element rules
       existed.
     • !important. It also has to beat the two-class rules (`.sheet.hidden`, 0-2-0) and
       anything ID-scoped that might set display later. Every existing .hidden rule resolves
       to `display: none` anyway, so there is nothing for this to contradict.

   With this in place, toggling `.hidden` on any new element just works. The per-element
   rules above are now redundant but harmless, and are left alone rather than swept out in
   the same change. */
.hidden { display: none !important; }

/* ============================================================
   Figures — lining where a number is DATA, old-style where it is display
   ============================================================

   ⚠️ CORMORANT'S DEFAULT FIGURES ARE OLD-STYLE, which is why the wordmark-adjacent numbers
   look expensive and why small ones are unreadable: the 1 has no more height than an x, so
   "14 wines" reads as "I4", "11" as "I I" and "0 pourable now" as "o pourable now". Reported
   25 Aug 2026 from a UX walkthrough of the passport and the house book.

   Pat's call was to split it rather than fix it globally: **a number that carries information
   gets lining figures; a number that is doing display work keeps the old-style ones.** So the
   hero price, the headlines and the drinking-window years are deliberately NOT in this list —
   they are large, they are the look, and they read fine at that size.

   `lining-nums` is a font FEATURE, not a font swap: everything below stays Cormorant, so the
   texture of the page is unchanged and only the digit shapes move. Kept as one list at the end
   of the file rather than a property sprinkled through fifty rules, because the question
   "which numbers are data?" is a single design decision and should be answerable by reading
   one place.

   ⚠️ ADD TO THIS LIST WHEN YOU ADD A SERIF NUMBER. A new count set in var(--serif) and left
   off here fails the way every silent-styling bug in this file fails: it just quietly looks a
   bit wrong, and nothing reports it. */
.stat-v,
.rank-n, .rank-avg, .rank-count,          /* the passport's ranked styles/grapes/countries */
.food-count,                               /* "Hard cheese 19" on Tonight and the Pair picker */
.section-head em, .dept em,                /* "ALSO BITTER 2", "ONE THING SHORT 6" */
.ls-n,                                     /* the shopping prompt's count */
.pager-count,                              /* "1 of 49" */
.qty-value,                                /* the +/- stepper on a bottle */
.critic-score,
.tasted-rating em,                         /* the score beside a wine in your history */
.tile-meta.rated,                          /* a cocktail tile's own rating */
.band-count,                               /* "36 of 49 bottles have a price to band" */
.unlock-chip strong {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "lnum" 1, "tnum" 1;
}

/* Ranked lists and score columns line up vertically, so their digits must be the same width.
   Everything else above takes tabular figures too — harmless inline, and it keeps the rule to
   one declaration rather than two lists that would drift. */

/* ============================================================
   iOS auto-zoom — 16px is a threshold, not a design choice
   ============================================================

   ⚠️ MOBILE SAFARI ZOOMS THE WHOLE PAGE WHEN A TEXT INPUT UNDER 16px TAKES FOCUS, and in an
   installed home-screen app there is no address bar to tap to get back, so the app is simply
   left looking wrong. Reported 26 Aug 2026 from the real device, on the sign-in screen.

   ⚠️ THE SIGN-IN SCREEN IS THE WORST CASE BECAUSE IT FOCUSES AN INPUT ON YOUR BEHALF.
   renderLogin() calls .focus() on the code box the moment the email is sent, so the zoom fires
   without the user having tapped anything — it reads as the app breaking rather than as a
   keyboard appearing. Every other input in the app has the same fault, just less visibly.

   The fix has to be the font size. The alternative everyone reaches for first is
   `maximum-scale=1, user-scalable=no` on the viewport, which disables pinch-zoom for people who
   need it — and recent iOS ignores it anyway, so it is an accessibility regression that does
   not even work.

   Scoped to `pointer: coarse` rather than a width breakpoint, because the trigger is a touch
   device, not a narrow window: a phone in landscape is still a phone, and a small desktop
   window is not. Desktop keeps the 14px the design was drawn at.

   ⚠️ THE ID-SCOPED SELECTORS STAY ID-SCOPED. `#addForm input` is specificity 1-0-1, so a plain
   `.some-class` override would lose to it silently — the exact trap this file already records
   from the `.auto-filled` and `.checkbox-row` bugs.

   `<select>` is deliberately NOT included: iOS opens a picker wheel rather than a keyboard and
   never zooms, so .sort-select can keep its 10px. */
@media (pointer: coarse) {
  .auth-form input,
  .search input,
  .notes-input,
  #addForm input,
  #tastedForm input,
  #logForm input[type="date"],
  #logForm input[type="text"] {
    font-size: 16px;
  }
}
