/* ============================================================
   Wayfinder — design tokens
   Source of truth: "Vietnam Wayfinder - Styles.dc.html"
   Light = default (:root).  Dark = [data-theme="dark"] (Đêm).
   Việt Red (#DA251D) is the single primary red in both modes.
   ============================================================ */

:root {
  /* Type families */
  --font: 'Space Grotesk', system-ui, -apple-system, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, Menlo, monospace;
  --display: 'Bricolage Grotesque', 'Space Grotesk', sans-serif;

  /* Radii (Styles · Shape & rhythm) */
  --r-pill: 999px;   /* buttons, header bar, chips, toggles */
  --r-card: 16px;    /* cards & option rows */
  --r-input: 14px;   /* text inputs */
  --r-chip: 11px;    /* small chips */
  --r-sheet: 20px;   /* sheets / overlays */
  --r-tile: 14px;    /* section tiles */
  --r-photo: 16px;   /* place photos */
  --r-thumb: 12px;   /* feed thumbnails */

  /* ---- LIGHT (default) ---- */
  --bg: #F1F0F3;
  --gold-wash: #F5E3B5;      /* secondary / flow pages in light */
  --card: #FFFFFF;
  --tile-fill: #E4E3E8;      /* solid chip fill for the back button — a step DOWN from --bg in light */
  --tile-sheen: rgba(255,255,255,.95);   /* light on the tile's curved top. Stronger in light: a white sheen over a grey chip has far less to work with than over a near-black one. */
  --ink: #191A22;
  --ink-2: rgba(25,26,34,.65);   /* body-secondary */
  --ink-3: rgba(25,26,34,.55);   /* captions */
  --ink-4: rgba(25,26,34,.5);
  --ink-5: rgba(25,26,34,.45);
  --line: rgba(25,26,34,.12);
  --line-soft: rgba(25,26,34,.08);
  --line-strong: rgba(25,26,34,.25);
  --line-mid: rgba(25,26,34,.18);

  --primary: #DA251D;            /* Việt Red */
  --on-primary: #FFFFFF;
  --primary-ring: rgba(218,37,29,.08);
  --primary-soft: rgba(218,37,29,.15);

  --gold: #F5C242;              /* Star Gold */
  --gold-ink: #9A6B00;         /* gold text on light */
  --teal: #23A69D;
  --nav-hover: var(--teal);     /* header-nav hover: teal in light, gold in dark */
  --nav-active: #0F6F69;                     /* active pill ring + label — 6.00:1 on #FFF (WCAG 1.4.11) */
  --nav-active-fill: rgba(35,166,157,.14);   /* teal wash; label on it = 5.22:1 (WCAG 1.4.3) */
  --eyebrow: #23A69D;          /* mono section eyebrows on light */
  --explore: #1F56AB;          /* Explore seat & compass — light scheme */

  --field-bg: #FFFFFF;
  /* OPAQUE twin of --field-bg, used only by the :-webkit-autofill inset-shadow fix (an alpha fill
     would let Chrome's blue show through). Light: --field-bg is already opaque, so identical. */
  --field-fill: #FFFFFF;
  --field-line: rgba(25,26,34,.14);
  --header-flat: #FFFFFF;
  --header-flat-line: rgba(25,26,34,.10);
  --sheet: rgba(255,255,255,.96);
  --sheet-line: rgba(25,26,34,.12);

  --shadow-card: 0 1px 3px rgba(25,26,34,.06);
  --shadow-pill: 0 2px 10px rgba(25,26,34,.06);
  --shadow-pop: 0 12px 40px rgba(25,26,34,.08);

  /* header pill over imagery (both themes) */
  --header-glass: rgba(12,13,18,.55);
  --header-glass-line: rgba(255,255,255,.12);
  --on-glass: #ECEBF2;
}

/* ---- DARK — Đêm ---- */
[data-theme="dark"] {
  --bg: #0C0D12;
  --card: #171A26;
  --tile-fill: #1F2230;      /* and a step UP from --bg in dark, so the tile lifts either way */
  --tile-sheen: rgba(255,255,255,.16);
  --ink: #ECEBF2;
  --ink-2: rgba(236,235,242,.6);
  --ink-3: rgba(236,235,242,.45);
  --ink-4: rgba(236,235,242,.45);
  --ink-5: rgba(236,235,242,.4);
  --line: rgba(255,255,255,.14);
  --line-soft: rgba(255,255,255,.10);
  --line-strong: rgba(255,255,255,.25);
  --line-mid: rgba(255,255,255,.20);

  --primary: #DA251D;           /* Việt Red — single red, both modes */
  --on-primary: #FFFFFF;
  --primary-ring: rgba(218,37,29,.14);
  --primary-soft: rgba(255,255,255,.12);

  --gold: #F5C242;
  --gold-ink: #F5C242;         /* gold text on dark = bright gold */
  --teal: #23A69D;
  --nav-hover: var(--gold);     /* header-nav hover: gold in dark */
  --nav-active: #F5C242;                     /* active pill ring + label — 10.46:1 on #171A26 (WCAG 1.4.11) */
  --nav-active-fill: rgba(245,194,66,.16);   /* gold wash; label on it = 7.33:1 (WCAG 1.4.3) */
  --eyebrow: #F5C242;          /* mono section eyebrows on dark */
  --explore: #3E7DC4;          /* Explore seat & compass — dark scheme */

  --field-bg: rgba(255,255,255,.06);
  /* the flattened result of --field-bg over --bg (#0C0D12 + 6% white) — matches an unfilled field
     exactly, where --card (#171A26) would have read as a slightly different, bluer shade. */
  --field-fill: #1B1C21;
  --field-line: rgba(255,255,255,.16);
  --header-flat: #171A26;
  --header-flat-line: rgba(255,255,255,.14);
  --sheet: rgba(23,26,38,.92);
  --sheet-line: rgba(255,255,255,.14);

  --shadow-card: 0 1px 3px rgba(0,0,0,.4);
  --shadow-pill: 0 2px 12px rgba(0,0,0,.5);
  --shadow-pop: 0 16px 48px rgba(0,0,0,.55);
}

/* Flow / secondary pages take the gold wash in LIGHT mode only.
   Browse pages (landing, section, place) keep the standard surface. */
[data-theme="light"] body[data-wash] { --bg: var(--gold-wash); }
:root body[data-wash] { --bg: var(--gold-wash); }
[data-theme="dark"] body[data-wash] { --bg: #0C0D12; }
/* ============================================================
   Wayfinder — app styles
   All styling lives here. No CSS in any HTML document.
   Reproduces "Pass 1" + "Pass 2" as one responsive app.
   Mobile-first; desktop layout kicks in at 760px.
   ============================================================ */

/* ---------- reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; max-width: 100%; overflow-x: clip; }  /* clip stray horizontal scroll; clip (not hidden) keeps sticky headers working */
html { -webkit-text-size-adjust: 100%; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  line-height: 1.5;
  min-height: 100vh;
  transition: background .25s ease, color .25s ease;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
img { max-width: 100%; display: block; }
h1, h2, h3, p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 4px; }

/* ---------- layout shell ---------- */
.app { min-height: 100vh; display: flex; flex-direction: column; }
.view { flex: 1 0 auto; }
/* header slot is a semantics-free wrapper — display:contents removes its box so the
   sticky .site-header inside it belongs to the tall .app (its real containing block).
   Without this the sticky header sits in a wrapper only as tall as itself and scrolls
   away the instant you scroll past its own height. */
[data-slot="header"] { display: contents; }
.wrap { width: 100%; max-width: 1128px; margin: 0 auto; padding: 0 20px; }
@media (min-width: 760px) { .wrap { padding: 0 40px; } }
.stack > * + * { margin-top: 18px; }
.spacer { flex: 1; }

/* mono eyebrow / label */
.eyebrow {
  font: 500 9.5px var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--eyebrow);
}
.eyebrow--muted { color: var(--ink-4); }
.mono { font-family: var(--mono); }

/* ---------- wordmark ---------- */
.wordmark { font: 500 16px var(--font); letter-spacing: -.01em; white-space: nowrap; }
.wordmark .star { color: var(--primary); }
.wordmark--lg { font-size: 24px; font-weight: 500; }
/* primary logo (Four Tiles) locked up left of the wordmark. On a light chip so the
   dark tile keeps its backing on the dark header (brand rule: marks need solid backing). */
.brand-lock { display: inline-flex; align-items: center; gap: 9px; }
.brand-logo { width: 26px; height: 26px; flex: none; display: inline-flex; background: #F1F0F3; border-radius: 7px; padding: 2px; box-sizing: border-box; }
.brand-logo svg { width: 100%; height: 100%; display: block; }

/* header brand lockup — first-visit chooser only (no-city header, "Landing Final" design). Same
   VIETNAM/badge/WAYFINDER pattern as .brand-lockup (signin page, footer) but sized for the header
   pill. Absolutely centred on the header-bar itself (NOT flex-centred in the remaining space beside
   the hamburger/avatar) — flex-centring left it visually off-center, since the hamburger's own width
   biased the "remaining space" left of true center. */
.header-bar--nonav { position: relative; }
.header-bar--nonav .header-brand-lockup { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }
/* Chooser page only: NO HAMBURGER, and the toggle takes its slot. ui.js:167 records that the
   original mock for this page was a bare pill, and the hamburger was kept solely because the
   flyout was the only route to theme, language and sign-in for a signed-out visitor with no city.
   Language now lives in this header and sign-in has its own buttons on the page, so the flyout was
   guarding a menu with no section tiles and no city name. Theme moves one tap later, after a city
   is picked, which is the only thing this page asks anyone to do. */
.header-bar--nonav .hamburger { display: none; }
/* ...and the toggle then sits almost on the phone's edge. The lockup is centred across the FULL
   bar, so every pixel the toggle gives back is a pixel it stops covering "WAYFINDER". Two-class
   selector so it beats the mobile .header-bar padding regardless of source order, and capped below
   760px because the toggle does not exist above it. */
@media (max-width: 759px) {
  .header-bar.header-bar--nonav { padding-right: 2.5px; }
  .header-bar--nonav .header-lang { margin-right: 0; }
}
.header-brand-lockup { display: flex; align-items: center; justify-content: center; gap: 8px; text-decoration: none; min-width: 0; }
.header-brand-lockup__word { font-family: 'Aboreto', serif; letter-spacing: .3em; text-indent: .3em; text-transform: uppercase; font-size: 12px; color: var(--ink); white-space: nowrap; }
.header-brand-lockup__icon svg { width: 34px; height: 34px; border-radius: 8px; display: block; flex: none; }
@media (min-width: 760px) {
  .header-brand-lockup__word { font-size: 14px; letter-spacing: .34em; text-indent: .34em; }
  .header-brand-lockup__icon svg { width: 40px; height: 40px; }
}

/* ---------- header ---------- */
.site-header { position: sticky; top: 0; z-index: 40; padding: 12px 0 0; }
.header-bar {
  display: flex; align-items: center; gap: 14px;
  border-radius: var(--r-pill);
  padding: 2.5px 16px 2.5px 20px;   /* vertical halved 5→2.5; left 20 (more room before the pin), right 16 (desktop band; mobile overrides below) */
  background: var(--header-flat);
  border: 1px solid var(--header-flat-line);
  box-shadow: var(--shadow-pill);
}
.header-nav { display: none; gap: 4px; margin-left: auto; }
/* transparent ring by default → the active pill adds no width, so nothing shifts on state change */
.header-nav a {
  font: 500 13.5px var(--font); color: var(--ink);
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px; border-radius: var(--r-pill);
  border: 1.5px solid transparent;
  transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}
/* Nav tiles, SITE-WIDE (owner): bare colour tiles — NO gold glow, NO white backing card; +15% size.
   (Was scoped to .header-nav; lifted to the base so the mobile tab bar + nav drawer match.) */
.nav-ico { width: 25.3px; height: 25.3px; flex: none; }   /* 22 +15% */
.nav-ico > rect:first-of-type { fill: transparent; }       /* drop the white backing card behind the colour chip */
.header-nav .nav-ico { width: 40px; height: 40px; }  /* desktop header tiles — matched to the city icon (owner: all 40px, even) */
/* Desktop nav: hover shows the gold text colour; the ACTIVE/selected item gets NO pill (owner's
   call — removed the wash + ring + bold). aria-current="true" still marks it for screen readers,
   so the visual "where I am" pill is gone but the SR state remains. Desktop only — .header-nav is
   display:none under 760px, so the mobile tab bar keeps its own dim/undim mechanic. */
.header-nav a:hover { color: var(--nav-hover); }
.header-nav a.is-active { color: var(--nav-hover); }   /* current section = same gold/teal as hover, colour only (no pill) */
/* destination tile is icons-only (owner's call — matches the mobile bar), so it needs a squarer
   pad than the icon+label items or it reads as a stretched lozenge */
.header-nav__dest { padding-left: 8px; padding-right: 8px; }
.header-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.hamburger {
  display: inline-flex; flex-direction: column; justify-content: center; gap: 4px;
  width: 30px; height: 30px; flex: none; color: inherit; padding: 6px;
}
.hamburger span { display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor; }
/* EN/VI in the header bar — MOBILE ONLY, hidden at the same breakpoint that hides the hamburger so
   the two can never disagree about what mobile means. ⚠ NOT a filled segmented pill: at this size
   the active fill became a squashed circle overlapping its neighbour. Plain text with a hairline
   between, active in the site red, which also keeps the control the same height as the bar — the
   header is sticky sitewide, so anything taller shifts every page down. */
.header-lang {
  position: relative; flex: none; display: inline-flex; align-items: center;
  height: 29px; padding: 2px;            /* 29px sits inside the hamburger's 30px, bar unchanged */
  margin-right: 6px;                     /* a little air before the hamburger */
  background: var(--primary-soft); border: 0; border-radius: var(--r-pill);
  font: 500 12px/1 var(--font); letter-spacing: .02em; cursor: pointer;
}
/* the sliding indicator. One tap anywhere on the button flips the class and the pill travels,
   which is the whole point of the control — the labels never move, only the fill does. */
.header-lang__pill {
  position: absolute; top: 2px; left: 2px; width: 31px; height: 25px;
  border-radius: var(--r-pill); background: var(--primary);
  transition: transform .18s ease;
}
.header-lang.is-vi .header-lang__pill { transform: translateX(31px); }
.header-lang__opt {
  position: relative; z-index: 1; width: 31px; text-align: center;
  color: var(--ink-2); transition: color .18s ease;
}
.header-lang:not(.is-vi) .header-lang__opt[data-lang="en"],
.header-lang.is-vi .header-lang__opt[data-lang="vi"] { color: var(--on-primary); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .header-lang__pill, .header-lang__opt { transition: none; }
}
@media (min-width: 760px) {
  .header-nav { display: flex; }
  .hamburger { display: none; }    /* desktop uses the inline nav instead */
  .header-lang { display: none; }  /* desktop reaches language through Preferences */
  /* Anchor the desktop bar as a fixed 3-zone grid so the nav sits DEAD-CENTRE and stops sliding:
     equal minmax(0,1fr) side rails mean the city-name length (left) and avatar (right) can't move
     the centre. Only when a city is chosen (3 real zones exist) — the first-visit chooser keeps the
     flex layout. Replaces the old margin-left:auto float that made the nav position depend on
     neighbour widths (city name → shift #1; Stay/Live label width → shift #2). */
  .header-bar--hascity { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
  .header-bar--hascity .header-city { justify-self: start; min-width: 0; }
  .header-bar--hascity .header-nav { margin-left: 0; justify-self: center; }
  .header-bar--hascity .header-actions { margin-left: 0; justify-self: end; }
  /* long city names ellipsize inside the left rail instead of spilling toward the nav */
  .header-city .city-dd__trigger { min-width: 0; }
  .header-city .city-dd__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  /* reserve a constant width for the stay↔live seat label so the persona swap can't resize the nav
     (kills the residual shift — "Stay" is wider than "Live" in this proportional font). inline-block
     so min-width + centring apply to the span. */
  .header-nav a[data-nav="stay"] span,
  .header-nav a[data-nav="live"] span { display: inline-block; min-width: 2.6em; text-align: center; }
}
@media (max-width: 759px) {
  /* folder-tab header: a full-width band fused to the iOS status-bar frame, with rounded-bottom
     "shoulders". Extends up under the status bar via safe-area-inset-top; colour follows the theme
     so it keeps matching the status-bar backing. (Capacitor-ready: same env() the native WebView uses.) */
  .site-header { padding: 0; }
  .site-header .wrap { padding: 0; max-width: none; }
  .header-bar {
    border-radius: 0;
    border: none;
    /* left inset 14px (was 10 — still a touch tight against the pin); right stays 20 for the hamburger */
    padding: calc(9px + env(safe-area-inset-top, 0px)) 20px 12px 14px;
    box-shadow: 0 4px 12px rgba(0,0,0,.14);
  }
  /* the no-city brand lockup (VIETNAM [logo] WAYFINDER) is absolutely centred on .header-bar's full
     box (see the rule above) — fine when top/bottom padding are equal, but on a standalone-install
     iPhone (camera-pill/notch) the top padding balloons via env(safe-area-inset-top) while bottom
     stays 12px, so true 50% lands the lockup well above the hamburger's actual row (which still
     centers on the padding BOX, i.e. lower, closer to the smaller bottom pad). Nudge down by half
     that top/bottom padding delta so it lines up with the hamburger regardless of notch size —
     evaluates to ~0 on a browser tab (env() is 0 there) and to the right offset on any notched
     device without hardcoding a specific inset value. */
  .header-bar--nonav .header-brand-lockup {
    top: calc(50% + (env(safe-area-inset-top, 0px) - 3px) / 2);
  }
  /* landing: let the hero photo be the background — no tinted glass band */
  .hero .header-bar {
    box-shadow: none; background: transparent;
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  /* content/image directly below the menu should be square, not rounded */
  .hero__media { border-radius: 0; }
  /* mobile: profile moves into the flyout, so the header avatar is gone — the hamburger (now inside
     header-actions) takes the far-right slot the avatar used to hold */
  .header-actions .avatar { display: none; }
}

/* Header city switcher — the building tile + shared city dropdown, lifted out of the profile sheet
   for one-tap change. Shown on BOTH mobile and desktop (owner's call), taking the top-left slot the
   brand used to occupy; the brand moves to the footer (desktop) / drops (mobile — logo lives on the
   app icon). Only rendered when a city is chosen, so it never appears on the first-visit chooser. */
/* pin RIGHT of the dropdown → ( City ⌄ ) [pin] on BOTH mobile & desktop (owner, 2026-08-07) —
   supersedes the earlier "pin LEFT of the dropdown" order below. */
.header-city { display: inline-flex; align-items: center; gap: 12px; min-width: 0; }
/* WAYFINDER text-tag on desktop RETIRED (owner: wasn't supposed to be there) — desktop now matches
   mobile's [pin] City⌄, no separate desktop-only branch needed. .header-brand-tag kept in the DOM
   (app.js still emits it) but permanently hidden so nothing depends on removing the markup. */
.header-brand-tag { display: none; }
/* Pin back on the LEFT of the dropdown pill (2026-08-08), both breakpoints — reverts the v325
   order-swap. DOM order (pin, then dropdown — see ui.js headerBar) is what we want, so no `order`
   override needed at all; explicit order:1/2 kept anyway (rather than deleting the rule) so a
   future markup reorder can't silently flip this again. */
.header-city__pin { order: 1; }
.header-city .city-dd { order: 2; }
.header-city .city-dd__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.header-city .city-dd__trigger { min-width: 0; }
.header-city__pin { display: inline-flex; align-items: center; }   /* decorative — not clickable */
.header-city .nav-ico { width: 40px; height: 40px; }   /* the map-pin beside the dropdown */
/* tile removed → the bare pin lifts off the screen with a soft drop shadow (overflow visible so the
   shadow isn't clipped by the svg box) */
/* hug the glyph — the pin is tall/narrow, so a 40px-wide box left ~10px dead space on each side that
   pushed it off the edge; 24px width trims that to the glyph */
/* pin with the TIGHT ground-ring: viewBox hugs it (24×47.3, slim) → box 18×36 to match that aspect;
   overrides .nav-ico's 40×40. Tunable if the owner wants it bigger/smaller. */
.header-city .city-pin { width: 16.2px; height: 32.4px; overflow: visible; filter: drop-shadow(0 3px 3px rgba(0,0,0,.32)); }
.header-city .city-dd { margin-top: 0; align-self: center; }   /* neutralise the settings-sheet spacing */
/* chevron BACK to the RIGHT of the city name (owner reverted, 2026-08-07) — the brief "chevron LEFT"
   row-reverse experiment above is gone; natural DOM order (value, chev), same as pre-v325. */
.header-city .city-dd__trigger { padding: 6px 12px; font-size: 14px; border-radius: var(--r-pill); }
/* light mode: reverse the desktop header pill → dark fill + light text (dark mode already reads that way) */
[data-theme="light"] .header-city .city-dd__trigger { background: var(--ink); border-color: var(--ink); color: #FFFFFF; }
[data-theme="light"] .header-city .city-dd__trigger:hover { border-color: var(--ink); }
[data-theme="light"] .header-city .city-dd__chev { color: rgba(255,255,255,.6); }
/* brand moves to the footer (desktop) / drops (mobile) — ONLY when a city control replaces it
   (the first-visit chooser has no city → brand stays). Applies at every width now. */
.header-bar--hascity .brand-lock { display: none; }

/* profile avatar button */
.avatar { width: 30px; height: 30px; border-radius: 50%; border: 1.5px solid var(--ink); position: relative; flex: none; background: none; }
.avatar svg { width: 100%; height: 100%; display: block; }
.avatar svg path, .avatar svg circle { fill: var(--ink); }
.avatar--filled { background: var(--primary); border-color: var(--primary); }
.avatar--filled svg path, .avatar--filled svg circle { fill: var(--on-primary); }
.avatar__initials { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font: 700 12px var(--font); letter-spacing: .01em; color: var(--on-primary); }

.iconbtn { font: 400 15px var(--font); color: var(--ink-3); padding: 4px; line-height: 1; }

/* header pill floating over a hero image */
.hero .header-bar {
  background: var(--header-glass);
  border-color: var(--header-glass-line);
  backdrop-filter: blur(11px);
  -webkit-backdrop-filter: blur(11px);
  color: var(--on-glass);
  box-shadow: none;
}
.hero .header-nav a { color: #fff; }
.hero .header-nav a:hover, .hero .header-nav a.is-active { color: var(--nav-hover); }
.hero .wordmark { color: var(--on-glass); }
.hero .avatar { border-color: var(--on-glass); }
.hero .avatar svg path, .hero .avatar svg circle { fill: var(--on-glass); }

/* ---------- footer ---------- */
.site-footer { flex: none; }
/* 3-zone grid so the wordmark is dead-centre regardless of the side groups' widths (equal 1fr
   rails flank an auto centre). Left group left-justified, CTA right-justified. */
.footer-bar {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 16px;   /* minmax(0,…) rails stay equal so the centre wordmark can't drift off true centre */
  border-top: 1px solid var(--line);
  padding: 16px 0 26px;
  margin-top: 24px;
}
.footer-bar__left { display: flex; align-items: center; gap: 16px; justify-self: start; }
.footer-bar__right { justify-self: end; }
/* Suggest page: the visitor already clicked "Suggest a place" to get here — the footer's
   duplicate CTA is redundant. Now hidden on both mobile and desktop. */
body[data-page="suggest"] .footer-bar__right { display: none; }
.footer-wordmark { justify-self: center; }
.header-logo__img { width: 30px; height: 30px; border-radius: 8px; display: block; }   /* header brand-lock (no-city state): app-icon logo — unrelated to the footer lockup below */
.footer-bar a { font: 400 11.5px var(--font); color: var(--ink-4); }
.footer-bar a:hover { color: var(--ink); }
/* About/Contact ride at full brightness (the brand-page white = --ink #ECEBF2 on dark), not the
   muted --ink-4 — owner request. */
.footer-bar__left a { color: var(--ink); }
.footer-bar a.btn--pill-cta, .footer-bar a.btn--pill-cta:hover { color: #fff; }  /* CTA text stays bright white on coral */

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  border-radius: var(--r-pill);
  font: 700 14px var(--font);
  padding: 13px 26px;
  white-space: nowrap;
  transition: transform .04s ease, filter .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn--primary { background: var(--primary); color: var(--on-primary); }
.btn--primary:hover { filter: brightness(1.05); }
.btn--ghost { border: 1px solid var(--line-strong); color: var(--ink-2); font-weight: 500; font-size: 13px; padding: 12px 20px; }
.btn--back { background: var(--teal); color: #0C0D12; border: 0; }
.btn--back:hover { filter: brightness(1.05); }
.btn--block { display: flex; width: 100%; }
.btn--sm { padding: 9px 17px; font-size: 12px; }
.btn--pill-cta { padding: 9px 17px; font: 700 12px var(--font); color: #fff; }

/* ---------- chips / tags ---------- */
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  padding: 3px 9px;
  font: 400 10px var(--font);
  color: var(--ink-2);
  white-space: nowrap;
}
.chip--price { font-family: var(--mono); font-weight: 500; color: var(--gold-ink); }
/* Michelin flags only, filled in the site red rather than outlined like every other chip */
.chip--michelin { background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 500; }
.chip--toggle { cursor: pointer; }
.chip.is-on { border-color: var(--primary); color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
/* wizard-only: selected flag chips read dark ink on light's pale red wash, white on dark's solid wash — not red-on-red */
.wizard__body .chip.is-on { color: var(--ink); }
[data-theme="dark"] .wizard__body .chip.is-on { color: #fff; }

/* price marker in listings */
.price { font: 500 10px var(--mono); color: var(--gold-ink); flex: none; }

/* price meter — repeated đồng signs (₫, no $, we're in Việt Nam). tier 1–4 = that many
   filled ₫ to four; filled slots use currentColor, empty slots fade; null → nothing (unknown
   ≠ cheap); Free places show a tag instead. The venue icon moved to the name (.type-ico). */
.price-meter { display: inline-flex; align-items: center; gap: 1px; flex: none; vertical-align: middle; color: var(--teal); }
[data-theme="dark"] .price-meter { color: var(--gold-ink); }   /* gold in dark, teal in light — like the old $ */
.price-meter__slot { display: inline-flex; line-height: 0; opacity: .22; }
.price-meter__slot.is-on { opacity: 1; }
/* ₫ at its natural glyph width (NOT a fixed box) — a centred ₫ in a 15px box left big gaps. */
.price-meter__dong { font: 400 14px/1 var(--font); display: block; }
/* Free is the word FOLLOWED BY a full faded meter (25 Aug), so every place shows a meter of the
   same width and free reads as the bottom of one scale rather than a different kind of answer. */
/* ⚠ The gap belongs on the TAG, not the row. `.price-meter` is a flex row with `gap: 1px`
   holding the đồng together, so widening the gap here pushed every đồng apart and the free
   meter came out visibly looser than every other meter on the same list. */
.price-meter--free .tag-free { margin-right: 6px; }
.tag-free {
  display: inline-flex; align-items: center; flex: none;
  font: 700 9px var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--teal); border: 1px solid var(--teal); border-radius: var(--r-pill); padding: 2px 7px;
}
/* compact inside the feed meta line (inherits its teal/ink colour via currentColor) */
.feed-row__meta .price-meter__dong { font-size: 10px; }
.feed-row__meta .tag-free { font-size: 8px; padding: 1px 5px; }
/* place-detail eyebrow: sit the meter on the text baseline */
.detail__eyebrow-price { display: inline-flex; vertical-align: middle; }
.detail__eyebrow-price .price-meter__dong { font-size: 13px; }
/* wizard live preview under the tier buttons */
/* The tiers are a SCALE, so wrapping the top one onto a row of its own breaks the one thing the
   control is saying. They share the width instead of wrapping.
   ⚠ SIX buttons since the meter went to five đồng (25 Aug), so the type had to come down a step
   to keep them on one line on a narrow phone. Verify on a small device before trusting it. */
.seg--tier { flex-wrap: nowrap; }
/* ⚠ `flex: 1 1 0` + `min-width: 0` + `nowrap` is what made Treat look like it was sitting on
   Everyday: every cell was forced to the same width, and a label longer than its share simply
   OVERFLOWED into the neighbour, where the selected pill's fill then painted over it. It read
   as crowding and was really an overflow. `min-width: max-content` is the floor that stops it —
   a cell can still give away spare width, it just can never be narrower than its own word. */
.seg--tier button { flex: 0 1 auto; min-width: max-content; padding: 8px 5px;
  font-size: 10px; letter-spacing: -.02em; white-space: nowrap; }
.seg--tier { justify-content: space-between; }
/* The meter sits under whichever tier is selected — a transform, no layout. It does NOT animate
   across: owner's call, it just appears under the new tier. The travel was tried and read as
   fussy for something you tap five times in a row. */
.tier-preview { position: relative; margin-top: 12px; min-height: 22px; color: var(--gold-ink); }
/* centred on a phone, where placeEditMeter deliberately leaves the transform alone */
@media (max-width: 759px) { .tier-preview { display: flex; justify-content: center; } }
@media (prefers-reduced-motion: reduce) { .switch, .switch__knob { transition: none; } }
.tier-preview .price-meter__dong { font-size: 20px; }

/* name-prefix venue icons (up to 2) — lead the place name at a larger-than-text size.
   Sized in em so they scale with each surface (card, feed, detail title). Colour follows
   the old venue-glyph treatment: teal in light, gold in dark. Inline before the name, so
   they stay on line 1 when a long name wraps. */
.type-ico { display: inline-flex; align-items: center; gap: 2px; margin-right: 7px; vertical-align: -.12em; color: var(--teal); }
[data-theme="dark"] .type-ico { color: var(--gold-ink); }
.type-ico__svg { width: 1em; height: 1em; display: block; flex: none; }
/* two-icon places (e.g. cafe + work_spot): the glyphs carry ~0.2em of baked-in edge padding
   that scales with font size, so the inter-icon gap looked fine on small cards but loose on
   large ones. A negative em margin between adjacent icons cancels that padding proportionally.
   Only applies when there IS a second icon — single-icon places + icon→text spacing untouched. */
.type-ico__svg + .type-ico__svg { margin-left: -.1em; }
/* Per-glyph sizing hook, kept but UNUSED. ⚠ Growing a glyph's BOX misaligns it: .type-ico centres
   the two svgs on each other, so a taller box overhangs its neighbour top AND bottom. Size the
   ARTWORK in the transform instead, against the neighbour's real extent. See beer_tap in ui.js. */
/* editor/wizard venue-type picker: order badge on a selected chip (1 = lead) + a live
   name-icon preview so the owner sees the real result on-device (no self-screenshot). */
.vtype-chip__ord { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin-right: 5px; border-radius: 999px; background: var(--gold); color: #1b1300; font: 700 9px var(--mono); }
/* Name then icons, centred — it is a sample of what the tags will draw on the card, so it reads
   left to right as the card does. The glyphs are sized in `em`, so raising the font raises them
   with it; ⚠ .type-ico's margin-RIGHT is for leading icons and has to flip here. */
/* Name on its own line, the glyphs under it. A long name wrapped to two lines and left the
   icons stranded beside the second one, which read as if they belonged to the last word. */
.vtype-preview { display: flex; flex-direction: column; align-items: center; gap: 8px;
  margin-top: 14px; min-height: 34px; font: 600 22px var(--font); color: var(--ink); }
.vtype-preview__name { letter-spacing: -.01em; text-align: center; }
/* the leading margin is wrong in both directions once the glyphs are on a line of their own */
.vtype-preview .type-ico { margin-right: 0; margin-left: 0; }

/* ---------- first-visit city chooser ("Landing Final" design, rebuilt 2026-08-06; mobile region
   layout REVERTED 2026-08-07 to the pre-rebuild horizontal-scroll rails + dot indicators) ----------
   Single centered column at both breakpoints: hero → pick-line/account (order swaps by breakpoint,
   see .chooser__pick below) → the regions. Mobile: all three regions always visible, each a
   horizontal-scroll rail of city cards with a dot row that tracks scroll position (see
   wireChooserDots() in app.js). Desktop: all three as static columns, unchanged. */
.chooser { display: flex; flex-direction: column; padding: 8px 0 44px; }
/* mobile order (2026-08-07, owner-specified): title → sub1 → divider → sub2 → pick-line →
   regions → account question → account CTAs. Desktop restores the original grouped layout — see
   the @media override below. */
.chooser__title { order: 1; margin: 0; padding-top: 16px; font: 700 27px/1.12 var(--font); letter-spacing: -.02em; text-wrap: balance; text-align: center; }
.chooser__sub { order: 2; margin: 10px 0 0; font: 400 13px/1.5 var(--font); color: var(--gold-ink); text-align: center; }
.chooser__sub2 { order: 3; margin: 2px 0 0; font: 400 13px/1.5 var(--font); color: var(--gold-ink); text-align: center; }
[data-theme="dark"] .chooser__sub, [data-theme="dark"] .chooser__sub2 { color: var(--gold); }
.chooser__divider { order: 4; height: 0; margin: 14px 0 0; border: 0; border-top: 1px solid var(--line-strong); }
.chooser__pick { order: 5; margin-top: 14px; font: 600 15px var(--font); color: var(--ink); text-align: center; }

.chooser__account-q { order: 7; margin: 18px 0 0; font: 400 13px/1.5 var(--font); color: var(--ink); text-align: center; }
.chooser__account-btns { order: 8; display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 12px; }
.chooser__account-or { font: 600 11.5px var(--font); color: var(--teal); }
.pill-btn { border-radius: var(--r-pill); font: 700 13.5px var(--font); padding: 13px 26px; white-space: nowrap; text-decoration: none; }
.pill-btn--red { background: var(--primary); color: var(--on-primary); }
.pill-btn--ghost { border: 1.5px solid var(--line-strong); color: inherit; font-weight: 600; padding: 11.5px 24.5px; }

.chooser__regions { order: 6; margin-top: 18px; }

.region { display: block; margin-top: 14px; }
/* mobile: label left + dot row right, plain (no border/pill) — restored from the pre-rebuild design */
.region__head { display: flex; align-items: center; gap: 8px; }
.region__head-dt { font: 700 15px/1 var(--font); letter-spacing: -.01em; text-align: left; white-space: nowrap; color: var(--gold-ink); }
[data-theme="dark"] .region__head-dt { color: var(--gold); }
.region__dots { display: flex; gap: 4px; margin-left: auto; }
.region__dot { width: 5px; height: 5px; border-radius: 50%; background: rgba(35,166,157,.5); }
.region__dot.is-on { background: var(--primary); }
/* horizontal-scroll rail, peeking next-card bleed to the screen edge */
.region__rail {
  display: flex; gap: 9px; margin-top: 8px;
  overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; padding-right: 20px; margin-right: -20px;
}
.region__rail::-webkit-scrollbar { display: none; }

.city-card {
  position: relative; display: block; flex: 0 0 190px; height: 112px;
  border-radius: 14px; overflow: hidden; text-decoration: none; scroll-snap-align: start;
}
.city-card .ph__cap { top: 8px; bottom: auto; left: auto; right: 8px; }   /* cap top-right on city cards */
.city-card--soon { opacity: .5; pointer-events: none; }
.city-card__label {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 0 52px 9px 12px;   /* right padding clears the arrow badge */
  background: linear-gradient(to top, rgba(12,13,18,.9), rgba(12,13,18,0));
}
.city-card__name { font: 600 18px/1.1 var(--font); letter-spacing: -.01em; color: #FFFFFF; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.city-card__count { margin-top: 2px; font: 400 8px var(--mono); letter-spacing: .04em; color: var(--gold); }
.city-card__count.is-soon { font-size: 16px; color: var(--teal); }   /* owner v613: twice the size, brand teal */
/* Crop anchors for the destination photo, mirroring .hero__photo--* — the card is a much wider,
   thinner box than the hero, so the three photos that already carry a heroFocus override need it
   here too. Written as .ph__img.<mod> (0,2,0) so they beat .ph__img's own object-position no matter
   where they sit in the file. */
.ph__img.city-card__photo--bottom { object-position: center bottom; }
.ph__img.city-card__photo--center { object-position: center center; }
.ph__img.city-card__photo--left-top { object-position: left top; }
/* arrow badge — its own circle, bottom-right, both breakpoints (was inline-after-the-name, desktop-only) */
.city-card__arrow {
  position: absolute; right: 12px; bottom: 12px; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: rgba(10,11,17,.55); border: 1px solid rgba(255,255,255,.32);
  color: #FFFFFF; font: 700 15px var(--font);
}

@media (min-width: 760px) {
  .chooser { max-width: 1080px; margin: 0 auto; padding: 28px 0 48px; align-items: center; }
  .chooser__title, .chooser__sub, .chooser__sub2, .chooser__pick, .chooser__account-q, .chooser__account-btns { width: 100%; }
  /* desktop keeps the original grouped layout, unaffected by the mobile-only reorder above:
     hero (title → sub1 → sub2) → account (question → buttons) → pick-line (own divider) → regions */
  .chooser__title { order: 1; padding-top: 0; font-size: 36px; letter-spacing: -.025em; }
  .chooser__sub { order: 2; }
  .chooser__sub2 { order: 3; margin-top: 0; }
  .chooser__account-q { order: 4; margin-top: 14px; }
  .chooser__account-btns { order: 5; margin-top: 12px; }
  .chooser__divider { display: none; }
  .chooser__pick { order: 6; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-strong); font-size: 17px; }
  .chooser__regions { order: 7; width: 100%; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 22px; }
  .region { display: block; margin-top: 0; }
  /* desktop region-head: centered piped line in its own bordered pill, no dots (unchanged from before the mobile revert) */
  .region__head { display: flex; justify-content: center; border-radius: 14px; padding: 13px 17px; border: 1px solid rgba(35,166,157,.5); }
  .region__head-dt { font: 700 20px/1 var(--font); letter-spacing: -.02em; text-align: center; }
  .region__dots { display: none; }
  .region__rail { flex-direction: column; overflow: visible; margin: 0; padding: 0; gap: 10px; margin-top: 12px; }
  .city-card { flex: none; width: 100%; height: 128px; scroll-snap-align: none; }
  .city-card__arrow { width: 30px; height: 30px; font-size: 16px; }
}

/* ---------- photo placeholders ----------
   Every "photo" in the source is a CSS-gradient placeholder with a
   mono caption. No real images exist in the design files. */
.ph { position: relative; overflow: hidden; background-repeat: no-repeat; background-size: cover; }
/* real uploaded photo, covering the gradient. If it 404s, app.js adds .is-broken
   → it hides and the gradient shows through (no inline onerror). */
/* image policy: cover fills the frame for BOTH portrait & landscape (symmetric centre
   crop). The only way to fill a fixed frame gap-free; trims a little, by design. */
.ph__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; z-index: 0; }
.wf-photo.is-broken { display: none; }
.ph__cap {
  position: absolute; left: 8px; bottom: 8px; z-index: 1;
  font: 400 8.5px var(--mono); color: rgba(255,255,255,.85);
  background: rgba(0,0,0,.42); padding: 2px 6px; border-radius: 4px;
  white-space: nowrap; max-width: calc(100% - 16px); overflow: hidden; text-overflow: ellipsis;
}
.ph__cap--tr { left: auto; right: 8px; bottom: auto; top: 8px; }
.ph__like {
  position: absolute; right: 12px; top: 12px; width: 38px; height: 38px; border-radius: 50%;
  background: rgba(0,0,0,.35); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.35);
  display: flex; align-items: center; justify-content: center;
  font: 400 17px/1 sans-serif; color: #fff;
}
/* gradient recipes (theme-independent — they represent photos) */
.ph-food   { background-image: radial-gradient(140px 90px at 60% 30%, rgba(218,37,29,.25), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#b7a08a,#7d6650); }
.ph-blue   { background-image: radial-gradient(140px 90px at 60% 30%, rgba(245,194,66,.30), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#9aa4b8,#5d6678); }
.ph-tan    { background-image: radial-gradient(140px 90px at 60% 30%, rgba(218,37,29,.20), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#a89a8a,#6b5c4a); }
.ph-green  { background-image: radial-gradient(140px 90px at 60% 30%, rgba(245,194,66,.28), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#a8b89a,#5c6e50); }
.ph-purple { background-image: radial-gradient(140px 90px at 60% 30%, rgba(218,37,29,.18), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#b39ab0,#6e5468); }
.ph-slate  { background-image: radial-gradient(140px 90px at 60% 30%, rgba(218,37,29,.14), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#a0a8b0,#565e66); }
.ph-teal   { background-image: radial-gradient(140px 90px at 60% 30%, rgba(94,120,255,.20), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#9ab0b8,#50646e); }
.ph-indigo { background-image: radial-gradient(140px 90px at 60% 30%, rgba(94,120,255,.20), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#8f96b8,#3a405c); }
.ph-river  { background-image: radial-gradient(140px 90px at 70% 30%, rgba(94,160,255,.22), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#1d2a3d,#0f1622); }
.ph-river-l{ background-image: radial-gradient(140px 90px at 70% 30%, rgba(218,37,29,.15), transparent), repeating-linear-gradient(45deg, rgba(255,255,255,.045) 0 10px, rgba(255,255,255,0) 10px 20px), linear-gradient(160deg,#8f96b8,#3a405c); }

/* ---------- HERO (city landing) ---------- */
.hero { position: relative; }
.hero__media {
  position: relative; height: 300px; border-radius: 0 0 20px 20px; overflow: hidden;
}
/* real hero photo layered over the gradient placeholder (gradient shows if the image is slow/blocked) */
.hero__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center top; z-index: 0; }
.hero__photo--bottom { object-position: center bottom; }   /* anchor the crop to the image's bottom (e.g. Hội An's river-level boats) */
.hero__photo--center { object-position: center center; }   /* centre the crop vertically (e.g. HCMC — the Landmark spire sits too high for center top) */
.hero__photo--left-top { object-position: left top; }      /* pin the crop to the top-LEFT, so it's the bottom-right that gets cut (Đà Nẵng: skyline + beach are top-left, the rest is open sea) */
.hero__grad, .hero .site-header, .hero__cap { z-index: 1; }
.hero__inner { position: absolute; inset: 0; display: flex; flex-direction: column; }
.hero__grad {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 0 14px 14px;
  background: linear-gradient(180deg, rgba(12,13,18,0), rgba(12,13,18,.9) 72%);
  color: #ECEBF2;
}
/* visiting persona: title top-left, city name bottom-right — each sits on its OWN tight mask
   (dark right at the edge, quick falloff) rather than a broad wash, so most of the photo stays
   visible; the mask height is sized to the text, not the whole top/bottom third of the image */
.hero__top {
  position: absolute; left: 0; right: 0; top: 0; z-index: 1; padding: 14px 14px 16px;
  background: linear-gradient(180deg, rgba(8,9,13,.82) 0%, rgba(8,9,13,0) 100%);
  color: #ECEBF2;
}
.hero__top .hero__title { margin-top: 0; }
.hero__title-break {}   /* mobile: renders as a normal line break */
@media (min-width: 760px) { .hero__title-break { display: none; } }   /* desktop: back to one line */
.hero__city-mask {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 16px 14px 14px;
  background: linear-gradient(0deg, rgba(8,9,13,.82) 0%, rgba(8,9,13,0) 100%);
  text-align: right;
}
.hero__city {
  color: var(--gold);
  font: 700 40px var(--font); letter-spacing: .005em;
}
.hero__eyebrow { font: 500 10px var(--mono); letter-spacing: .13em; color: var(--gold); }
.hero__title { margin-top: 6px; font: 700 30px/1.1 var(--font); letter-spacing: -.025em; }
.hero__sub { margin-top: 5px; font: 400 12.5px/1.5 var(--font); color: rgba(236,235,242,.68); }
.hero__row { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.state-chip {
  font: 500 8px var(--mono); letter-spacing: .14em; color: #0C0D12;
  background: var(--gold); border-radius: var(--r-pill); padding: 3px 8px; white-space: nowrap;
}
@media (min-width: 760px) {
  /* on desktop the hero floats as a card that matches the content width, not full-bleed */
  .hero { max-width: 1128px; margin: 16px auto 0; padding: 0 40px; }
  .hero__media { height: 340px; border-radius: 20px; }
  .hero__title { font-size: 40px; white-space: nowrap; }
  .hero__sub { font-size: 14px; }
  .hero__grad { padding: 0 18px 18px; }
  .hero__top { padding: 18px 18px 20px; }
  .hero__city-mask { padding: 20px 18px 18px; }
  .hero__city { font-size: 42px; }
}

/* visiting persona: the tagline that used to live in the hero, now sitting just above the section
   tiles, right-aligned */
.sections__lead { margin: 0; padding-top: 14px; text-align: left; color: #fff; font: 500 13px var(--font); }
@media (min-width: 760px) { .sections__lead { padding-top: 18px; font-size: 14px; } }

/* ---------- section tiles (Eat/Drink/Explore/Stay/Live) ---------- */
.sections { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; padding-top: 8px; }
@media (min-width: 760px) { .sections { grid-template-columns: repeat(4, 1fr); gap: 12px; padding-top: 10px; } }
.section-tile {
  position: relative; height: 90px; border-radius: var(--r-tile); overflow: hidden; display: block;
}
.section-tile__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
@media (min-width: 760px) { .section-tile { height: 120px; border-radius: var(--r-photo); } }
.section-tile__label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;   /* above the is-empty darken overlay */
  display: flex; align-items: flex-end; gap: 7px; padding: 0 12px 10px;
  background: linear-gradient(180deg, rgba(12,13,18,0), rgba(12,13,18,.82) 80%);
  color: #ECEBF2;
}
/* stacked: title over the count chip */
.section-tile__text { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; min-width: 0; }
.section-tile__name { font: 600 17px/1 var(--font); letter-spacing: -.01em; text-shadow: 0 1px 4px rgba(0,0,0,.85); }
/* count in its own tinted-black chip */
.section-tile__count {
  font: 400 8px var(--mono); color: var(--gold); white-space: nowrap;
  background: rgba(0,0,0,.55); padding: 3px 7px; border-radius: 5px;
}
.section-tile__arrow { margin-left: auto; font: 500 14px/1 var(--font); text-shadow: 0 1px 4px rgba(0,0,0,.85); }
/* 0-place tile: read as clearly UNavailable, not just un-hovered — desaturate + dim the photo, deepen
   the overlay, and mute the text to grey (no bright white, no drop-shadow pop) so it never looks clickable */
.section-tile.is-empty { cursor: default; }
.section-tile.is-empty .section-tile__photo { filter: grayscale(1) brightness(.55); }
.section-tile.is-empty::after { content: ''; position: absolute; inset: 0; z-index: 1; background: rgba(8,9,12,.55); }
.section-tile.is-empty .section-tile__label { background: linear-gradient(180deg, rgba(12,13,18,0), rgba(12,13,18,.6) 80%); }
.section-tile.is-empty .section-tile__name { color: #8B8B93; text-shadow: none; }
.section-tile.is-empty .section-tile__count { color: var(--teal); background: rgba(0,0,0,.35); }   /* "Coming Soon" in teal */
@media (min-width: 760px) {
  .section-tile__name { font-size: 19px; }
  .section-tile__count { font-size: 8.5px; }
}

/* ---------- feed (recently posted / useful this week) ---------- */
.feed { padding-top: 20px; }
.feed__head { display: flex; align-items: baseline; gap: 10px; }
.feed__list { margin-top: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }   /* minmax(0,…): let the track shrink so nowrap blurbs ellipsize, not overflow the screen */
@media (min-width: 760px) { .feed__list { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 40px; row-gap: 13px; } }
.feed-row { display: flex; align-items: center; gap: 11px; text-align: left; width: 100%; }
.feed-row__thumb { width: 50px; height: 50px; border-radius: var(--r-thumb); flex: none; }
.feed-row__body { flex: 1; min-width: 0; }
.feed-row__top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.feed-row__name { min-width: 0; font: 600 13px var(--font); letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* THE FEED IS THE SMALLEST THIS GLYPH EVER RENDERS — 13px, against 20px on the place
   detail's icons row and 26px in its title, because .type-ico__svg is 1em of whatever
   name it leads. Only `work_spot` cares: a Lucide mark is three or four strokes with
   room between them and simply gets lighter, while that one is a doodle carrying a head,
   a body, an arm and a laptop in the same 24-unit box. Its interior gaps are a couple of
   units wide — one or two real pixels at 13px — so they fill in and the outline reads as
   a solid blob. Thinning it here is the fix the glyph's own history already found once
   (it shipped at stroke-width 1 for exactly this, then went to 2 to match its neighbours
   at the sizes it is big).
   ⚠ `stroke-width` on the <g> is a PRESENTATION ATTRIBUTE, so this plain rule beats it —
   no !important needed, and none should be added.
   Covers the saved list too, which reuses .feed-row__name. */
.feed-row__name .ico-dense { stroke-width: 1.25; }
/* The SECTION LISTING card has the same problem one size up — its name is 15px against the feed's
   13px, so the laptop doodle's interior gaps are still only a few real pixels and it reads heavy
   beside a plain Lucide neighbour. Thinned less than the feed, because 15px has more room.
   ⚠ Both rules are stroke-width on a PRESENTATION ATTRIBUTE, so a plain selector wins. Never
   reach for !important here. Everywhere bigger (detail icons row 20px, detail title 26px) the
   glyph is left at the set's own weight 2 on purpose. */
.place-card__name .ico-dense { stroke-width: 1.5; }
.feed-row__meta { display: inline-flex; align-items: center; margin-left: auto; font: 500 10px var(--mono); flex: none; white-space: pre; }   /* inline-flex + center so the ₫ meter sits on the same line-centre as the uppercase AGO/CAT text (vertical-align:middle rode low against caps). white-space:pre keeps the " · " separator spaces as flex items */
.feed-row__meta .cat { color: var(--eyebrow); }
.feed-row__meta .ago, .feed-row__meta .pr { color: var(--ink-4); }
.feed-row__desc { margin-top: 2px; font: 400 11px/1.4 var(--font); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* mobile: only the SAVED-places list stacks (title over its category meta). The Recent-Updates FEED now
   inherits the desktop inline row — đồng meter on the SAME line as the name, name truncates to ellipsis
   (owner wanted the feed to render like desktop). Scoped to .saved-list so savedRow is untouched. */
@media (max-width: 759px) {
  .saved-list .feed-row__top { flex-direction: column; align-items: flex-start; gap: 2px; }
  .saved-list .feed-row__meta { order: -1; margin-left: 0; }
  .saved-list .feed-row__meta, .saved-list .feed-row__meta .cat { color: var(--teal); }
  .saved-list .feed-row__name { white-space: normal; }
}
.feed-row__heart { font: 400 15px var(--font); color: var(--primary); flex: none; }

/* ---------- section listing (Eat in Hải Phòng) ---------- */
/* Redesigned head (v2): title left; count-over-byline column to its right; a full-width hairline divider;
   then the ₫ legend as a horizontal "Per Pax" row below it (was a right-side reverse-staircase stack). */
/* sticky section head: it pins right below the (also-sticky) site header so place cards scroll UP
   and vanish beneath the divider line. --header-h is JS-measured (header height varies: safe-area on
   mobile, padding desktop); z below the z-40 header so any 1px overlap is harmlessly covered; opaque
   --bg (matches the page) so passing cards are hidden, not seen through. Only renders on section pages. */
.listing-head { padding-top: 22px; position: sticky; top: var(--header-h, 56px); z-index: 30; background: var(--bg); }
/* ‹ back button, inline-left of the title (byline below stays flush-left). ROUNDED SQUARE, not a
   circle (v615): people were telling the owner a bare circle did not read as "back". The rounded
   square is the shape phones use for this control, so it carries the meaning the circle did not. */
.listing-head__row { display: flex; align-items: center; gap: 12px; }
.listing-head__home {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 11px;
  /* Solid fill, NO outline (v616) — the owner's reference is a filled tile, and an outlined box on
     the card colour was not reading as the same control. --tile-fill resolves in both themes; a
     hardcoded near-black would vanish on the light one. */
  border: none; color: var(--ink);
  /* A domed piece of glass — brightest at the top where the surface turns toward you, falling away
     down the face, with a soft settle along the bottom edge. Deliberately ONE BROAD GRADIENT across
     the whole fill and NOT an inset 1px top edge: that is the standard way to fake glass and it is
     exactly what was built and rejected on the floating nav capsule, where it read as a drawn line
     rather than as light on a curve. A gradient has no edge to misread.
     The source sits off to the TOP RIGHT rather than straight overhead — a centred, symmetric
     falloff reads as a shape with a gradient on it, an off-centre one reads as a lit object. */
  background:
    radial-gradient(120% 110% at 78% -12%, var(--tile-sheen), transparent 58%),
    var(--tile-fill);
  /* the settle sits BOTTOM-LEFT, opposite the light, and the drop shadow leans the same way —
     a centred shadow under an angled highlight is the tell that the light was never real. */
  box-shadow: -1px 2px 3px rgba(0,0,0,.20), inset 3px -5px 10px -8px rgba(0,0,0,.6);
  text-decoration: none;
  transition: color .12s ease, border-color .12s ease, background-color .12s ease;
}
.listing-head__home:hover { color: var(--nav-hover); }   /* border-color would be inert now — the tile has no border */
.listing-head__home-ico { width: 22px; height: 22px; }   /* SVG chevron — geometry centers it, no glyph-metric hack needed */
/* the desktop box grows 36 -> 40, so its radius has to grow with it or the tile renders less round
   there than on a phone. Proportional roundness is what the reference has. */
@media (min-width: 760px) { .listing-head__home { width: 40px; height: 40px; border-radius: 12px; } .listing-head__home-ico { width: 24px; height: 24px; } }
.listing-head__title { font: 700 30px/1.1 var(--font); letter-spacing: -.02em; }
/* suggest-gate title row: back button sits left of the (still page-centered) title. The button's own
   width is mirrored as invisible right-side spacing so the title's centre point doesn't drift toward
   the button. */
.wizard__title-row { justify-content: center; gap: 0; margin-top: 20px; }
.wizard__title-row .listing-head__home { margin-right: 12px; }
.wizard__title-row .wizard__title { flex: none; margin-top: 0; margin-right: 48px; }
@media (min-width: 760px) { .wizard__title-row .wizard__title { margin-right: 52px; } }
/* multi-step wizard's own exit control (add.html): small muted text, bottom-right of the card, below
   the Back/Next footer — deliberately NOT a top corner X, which reads as a reflex "close" tap and got
   hit by accident. Opens a confirm dialog (base .overlay + .sheet--wizcancel below) rather than
   exiting directly. */
.wiz-cancel-row { display: flex; justify-content: flex-end; margin-top: 36px; }
.wiz-cancel-link {
  background: none; border: 0; padding: 4px; cursor: pointer;
  font: 500 12px var(--font); color: var(--ink-4); text-decoration: underline;
}
.wiz-cancel-link:hover { color: var(--primary); }
/* confirm dialog — same .overlay/.sheet pattern as qhelp/name-editor, declared after the base rules
   (source-order rule this file has scars from — see the qhelp comment above). */
.sheet--wizcancel { text-align: center; }
.sheet--wizcancel__title { font: 700 17px var(--font); color: var(--ink); }
.sheet--wizcancel__text { margin-top: 8px; font: 400 14px/1.55 var(--font); color: var(--ink-3); }
.sheet--wizcancel__foot { display: flex; gap: 10px; margin-top: 20px; }
.sheet--wizcancel__foot .btn { flex: 1; }
/* "Coming Soon" placeholder page (soon.html) — back button left of the title, left-aligned (not
   centered like the suggest-gate row above); same margin-onto-the-row fix so align-items:center works */
.soon-title-row { margin-top: 20px; }
.soon-title-row .page__title { margin-top: 0; }

/* Services hub rows (2026-08-12) — Itinerary/Tours as non-tappable "real thing, not available yet"
   cards, one level down from the section-tiles' 0-place treatment (that one's built for a dark photo
   backdrop; soon.html is a data-wash page that renders on both themes, so this is authored with an
   explicit light default + dark override rather than reusing those literals — see the v242/wizard-
   phase-1 scars for what happens when a dim-on-dark color ships unconditionally). */
.soon-service-list { margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
.soon-service {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  border: 1px solid var(--line-strong); border-radius: 14px; padding: 16px 18px;
  background: rgba(140,147,166,.06);
}
.soon-service__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.soon-service__name { font: 600 15px var(--font); color: var(--ink-3); }
.soon-service__sub { font: 400 12.5px/1.4 var(--font); color: var(--ink-4); }
.soon-service__tag {
  flex: 0 0 auto; font: 500 9.5px var(--mono); letter-spacing: .12em; text-transform: uppercase;
  color: var(--teal); background: rgba(35,166,157,.1); border-radius: 999px; padding: 5px 10px;
}
[data-theme="dark"] .soon-service__tag { color: var(--gold-ink); background: rgba(245,194,66,.12); }
@media (min-width: 760px) { .listing-head__title { font-size: 40px; } }
.listing-head__count { margin-left: 12px; font: 500 9.5px var(--mono); letter-spacing: .14em; color: var(--gold-ink); white-space: nowrap; }   /* inline in the h1 → bottom-aligned to the title baseline */
/* byline + Per Pax legend share ONE row on desktop (byline left, legend right); stacked on mobile.
   The full-width divider sits BELOW this whole row. */
.listing-head__legend { margin-top: 10px; padding-bottom: 14px; border-bottom: 1px solid var(--field-line); display: flex; flex-direction: column; gap: 8px; }
/* One item since the price key was deleted (25 Aug). Kept as a row rather than collapsed, so the
   byline still sits on its own line above the hairline exactly where it always did. */
@media (min-width: 760px) { .listing-head__legend { flex-direction: row; align-items: baseline; gap: 20px; } }
.listing-head__byline { font: 400 12.5px/1.4 var(--font); color: var(--ink); }   /* bright text (was muted --ink-2 → unreadable) */
/* đồng price legend — horizontal, full-width, under a divider. "Per Pax" lead + pipe-separated tiers.
   Each tier (incl. its leading |) is nowrap so it wraps at tier boundaries and never strands a pipe. */

.place-list { padding-top: 18px; display: grid; grid-template-columns: 1fr; gap: 18px; }
@media (min-width: 760px) { .place-list { grid-template-columns: 1fr 1fr; column-gap: 40px; row-gap: 26px; } }
.place-card { display: block; text-align: left; width: 100%; }
.place-card__photo { height: 150px; border-radius: var(--r-photo); }
@media (min-width: 760px) { .place-card__photo { height: 190px; } }
/* "Back to …" from a place detail scrolls this card to centre + a brief gold ring so the
   user sees which card they returned to (JS adds .is-return for ~2s). */
.place-card.is-return { border-radius: 14px; animation: cardReturn 2s ease-out; }
@keyframes cardReturn {
  0% { box-shadow: 0 0 0 3px var(--gold); }
  100% { box-shadow: 0 0 0 3px rgba(245,194,66,0); }
}
@media (prefers-reduced-motion: reduce) {
  .place-card.is-return { animation: none; box-shadow: 0 0 0 3px var(--gold); }
}
.place-card__head { margin-top: 9px; display: flex; align-items: baseline; gap: 8px; }
.place-card__name { font: 600 15px var(--font); letter-spacing: -.01em; }
/* Two lines, then an ellipsis. A derived blurb is a whole sentence and Vietnamese runs longer
   than English, so the card has to bound it rather than trust the copy to be short. The feed's
   own .feed-row__desc is already clipped to one line and needs nothing. */
.place-card__desc { margin-top: 3px; font: 400 12px/1.5 var(--font); color: var(--ink-2);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.place-card .chips { margin-top: 7px; }
.listing-more { padding-top: 16px; text-align: center; font: 400 9.5px var(--mono); letter-spacing: .12em; color: var(--ink-4); }

/* ---------- place detail ---------- */
.detail { padding-top: 14px; }
/* A real pill button (replaced the whisper-grey footnote link) — ~40px tap target, reads as
   "go back" at a glance. Lives inside .detail__body now (last item, via .listing-back below) —
   NOT centred below the whole two-column grid, which on desktop stranded it far under the shorter
   text column whenever the photo column ran taller (v259: was flex order:2 on .detail, moved
   after the media-taller-than-text gap complaint). */
.detail__back {
  margin: 26px 0 4px;
  display: inline-flex; align-items: center; gap: 3px; padding: 11px 18px 11px 13px;
  border-radius: var(--r-pill);
  font: 600 13px var(--font); color: var(--ink);
  /* Same domed glass as the .listing-head__home tile, lit from the same top-right, using the same
     two tokens. The NUMBERS differ because the shape does: this pill is wide and its width changes
     with the label, so the tile's tight corner hotspot would leave the left two thirds flat and
     read as a stain. The dome is broader and less cornered here, sweeping across the label instead.
     Border dropped to match the tile — glass with an outline is neither one thing nor the other.
     Lit from the TOP LEFT here, while the square tile is lit from the top right — the owner picked
     each one on its own, on the device. */
  border: none;
  background:
    radial-gradient(170% 130% at 24% -25%, var(--tile-sheen), transparent 62%),
    var(--tile-fill);
  box-shadow: 1px 2px 3px rgba(0,0,0,.18), inset -2px -4px 9px -8px rgba(0,0,0,.5);
  /* /legal renders this pill as a <button> (go-back, not an href) — a button does not get the
     link cursor for free, and every other use of this class is an <a>. */
  cursor: pointer;
  transition: color .15s, background .15s;
}
.detail__back:hover { color: var(--nav-hover); }   /* border-color would be inert now — the pill has no border, same as the tile at v616 */
.detail__back:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.detail__back-ico { font-size: 19px; line-height: 1; margin-top: -2px; }
/* this wrapper centres the inline-flex pill via text-align on a block parent — reused verbatim by
   section-list/suggest-gate/missing-place/account, same pattern everywhere it appears. */
.listing-back { text-align: center; }
/* Name (+ round back button, mobile only) ABOVE the photos; venue-type icons + đồng price meter in
   their own row BELOW the gallery — SAME on both breakpoints (v260: dropped the v251 "desktop
   keeps icons+name+price on one line" behavior — the owner's row-by-row list put icons/price on
   their own line under the title on every breakpoint, reversing that earlier answer). */
/* MOBILE IS ONE ORDERED FLEX COLUMN, NOT A GRID (2026-08-26). The phone now runs the same
   blocks as the laptop but in a different sequence — title, photos, the WRITE-UP, then the
   At a glance stack, then the back pill — and those blocks live at three different depths of
   the markup. Flattening .detail__body and .detail__col2 with display:contents puts every one
   of them in the same formatting context, and `order` then sequences them without the
   template having to emit a different DOM per breakpoint. The laptop switches back to grid. */
.detail__grid { display: flex; flex-direction: column; gap: 0; }
.detail__body { display: contents; }
.detail__grid > .detail__titlerow,
.detail__grid > .detail__media { flex: none; }
.detail__titlerow      { order: 1; }
.detail__media         { order: 2; }
/* the icons, đồng and share ride straight under the photos on a phone, ahead of the write-up */
.detail__iconsrow      { order: 3; }
.detail__sub           { order: 4; }
.detail__copy          { order: 5; }
.detail__colhead--find { order: 7; }
.social-links          { order: 8; }
.detail__facts         { order: 9; }
.detail__colhead--tags { order: 10; }
.detail__meta          { order: 11; }
.detail__colhead--added{ order: 12; }
.added-by              { order: 13; }
.listing-back          { order: 99; }
.detail__titlerow { grid-area: title; display: flex; align-items: center; gap: 10px; min-width: 0; margin-bottom: 14px; }
.detail__titlerow .listing-head__home { flex: none; }
/* .detail__title's 7px top margin was tuned for its old spot right under the (removed) eyebrow;
   zero it here so it doesn't throw off centering against the round back button */
.detail__titlerow .detail__title { margin-top: 0; }
/* ₫ price meter shared styling — moved up here (was orphaned near the review-queue rules ~1214)
   so it lives with the rest of the detail-page rules, base-before-modifiers. */
.detail__price { flex: none; display: inline-flex; align-items: center; }
.detail__price .price-meter__dong { font-size: 19px; }
/* Three cells: venue icons left, đồng in the TRUE centre of the row, share right. It was a
   two-item flex with space-between, where the đồng sat hard right. 1fr auto 1fr is what centres
   the middle cell against the ROW rather than against its two neighbours, whose widths differ.
   Same on both breakpoints, like the rest of this row. */
.detail__iconsrow { grid-area: iconsrow; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; margin-top: 14px; }
.detail__iconsrow .detail__price { justify-self: center; }
/* The installed app has no address bar, so this is the only way a link gets out of it. */
.detail__share { justify-self: end; display: inline-flex; align-items: center; justify-content: center; width: 46px; height: 46px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--gold-ink); cursor: pointer; transition: background .18s ease, transform .12s ease; }
/* Gold at rest, and the hover is the fill alone — the icon does not change colour, because it is
   already the colour it changes to. `--gold-ink` is deep gold in light and bright gold in dark,
   so it reads on either ground. */
.detail__share:hover { background: var(--tile-fill); }
.detail__share:active { transform: scale(.92); }
.detail__share-ico { width: 24px; height: 24px; display: block; }
/* .type-ico__svg is sized in em off its containing font-size — set an explicit size here so they
   don't shrink to the ambient ~13px body text size */
.detail__iconsrow-icons { display: inline-flex; align-items: center; font-size: 24px; }
@media (min-width: 900px) {
  .detail__titlerow { padding-top: 18px; margin-bottom: 0; }
  /* The back tile shows on desktop too (v629). It used to be mobile-only, on the grounds that the
     bottom .detail__back pill was affordance enough up there — the owner brought it back for
     uniformity across breakpoints, so a place page now carries both, same as every other page. */
  /* iconsrow's 28px margin-top (below) and .detail__body's 18px padding-top (above the tags/
     write-up, ~745 area) were both tuned for the mobile gallery-to-icons gap — too much once the
     row sits directly under a title with no photos in between (desktop). Tighten desktop only. */
  .detail__iconsrow { margin-top: 14px; }
  .detail__body { padding-top: 12px; }
}
.detail__hero { position: relative; height: 235px; border-radius: 20px; overflow: hidden; }
/* NOT sticky: a pinned hero (tall portrait photo, or the 300px placeholder) hangs into the flow space
   of its .detail__thumbs sibling, which then paints over it on scroll. Text col is usually shorter than
   the media anyway, so pin-while-reading bought little. (To restore pinning, sticky the whole .detail__media.) */
@media (min-width: 900px) { .detail__hero { height: 300px; } }
/* Cover photo FILLS a fixed LANDSCAPE hero box, cover-cropped, on BOTH mobile (235px) and desktop (300px),
   so every place's detail reads the same regardless of the source photo's shape (owner). Tapping opens the
   full/portrait image in the carousel (contain). */
.detail__hero-img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; }
.detail__thumbs { margin-top: 10px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.detail__thumb { position: relative; height: 56px; border-radius: 10px; overflow: hidden; }
.detail__thumb .lbl { position: absolute; left: 5px; bottom: 4px; font: 400 7.5px var(--mono); color: rgba(255,255,255,.8); }
/* ONE row of four on the phone: three photos and a +N counter. Tiles past the third are in
   the markup (the laptop shows eight) and are hidden here, so the counter falls into slot 4
   by itself. The counter is never hidden by that rule — it is what replaces them. */
.detail__thumb:nth-child(n+4):not(.detail__thumb--more) { display: none; }
.detail__thumb--more { display: grid; place-items: center; background: var(--card-2, rgba(255,255,255,.06));
  cursor: zoom-in; }
.detail__more-n { position: relative; z-index: 1; font: 700 20px var(--mono); color: #fff;
  letter-spacing: .02em; text-shadow: 0 1px 6px rgba(0,0,0,.55); }
.detail__more-n--d { display: none; }   /* the laptop figure, swapped in at 900px */
/* the next photo in line, dimmed, so the tile reads as more pictures rather than a button */
.detail__more-peek { position: absolute; inset: 0; display: block; opacity: .38; }
.detail__more-peek img { width: 100%; height: 100%; object-fit: cover; display: block; }
.detail__more-peek--d { display: none; }
/* photos that open the fullscreen carousel */
.detail__hero-img[data-action], .detail__thumb--live { cursor: zoom-in; }
/* Select tier — a light scrim + centred play button over the hero photo (poster for the promo film) */
.detail__hero--film::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.34), rgba(0,0,0,.14) 55%, rgba(0,0,0,.28)); }
.detail__hero-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 2;
  width: 68px; height: 68px; border-radius: 50%; border: 2px solid rgba(255,255,255,.9); cursor: pointer;
  display: flex; align-items: center; justify-content: center; color: #fff;
  background: rgba(20,20,24,.42); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  box-shadow: 0 4px 18px rgba(0,0,0,.4); transition: transform .12s ease, background .12s ease; }
.detail__hero-play svg { width: 30px; height: 30px; display: block; margin-left: 3px; }   /* optical-centre the triangle */
.detail__hero-play:hover { transform: translate(-50%,-50%) scale(1.06); background: rgba(20,20,24,.58); }
@media (prefers-reduced-motion: reduce) { .detail__hero-play, .detail__hero-play:hover { transition: none; } }

/* ===== Fullscreen photo carousel (native <dialog>, top layer over the tabbar) ================= */
.lightbox { padding: 0; border: 0; max-width: 100vw; max-height: 100dvh; width: 100vw; height: 100dvh;
  background: #0b0b0d; color: #fff; overflow: hidden; }
.lightbox::backdrop { background: rgba(8,8,10,.94); }
.lightbox__track { display: flex; width: 100%; height: 100%;
  overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scroll-behavior: auto;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.lightbox__track::-webkit-scrollbar { display: none; }
.lightbox__slide { flex: 0 0 100%; width: 100%; height: 100%; scroll-snap-align: center; scroll-snap-stop: always;
  display: flex; align-items: center; justify-content: center; }
/* uniform stage: every image is shown whole, letterboxed — mixed portrait/landscape never jumps the frame */
.lightbox__img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; -webkit-user-select: none; user-select: none; }
.lightbox__close, .lightbox__nav { position: fixed; z-index: 2; display: flex; align-items: center; justify-content: center;
  border: 0; color: #fff; cursor: pointer; }
/* close = SOLID brand-red circle (Việt Red) with a white ✕ (owner) */
.lightbox__close { bottom: calc(14px + env(safe-area-inset-bottom)); right: calc(14px + env(safe-area-inset-right));
  width: 42px; height: 42px; border-radius: 50%; font-size: 18px; line-height: 1; background: var(--primary); color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,.4); }
.lightbox__close:hover { filter: brightness(1.08); }
/* nav = "ghost" circle: faint translucent fill + light ring so the caret reads on a near-black photo */
.lightbox__nav { top: 50%; transform: translateY(-50%); width: 46px; height: 46px; border-radius: 50%; font-size: 30px; line-height: 1; padding-bottom: 4px;
  background: rgba(255,255,255,.12); border: 1.5px solid rgba(255,255,255,.45); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.lightbox__nav:hover { background: rgba(255,255,255,.2); }
.lightbox__nav--prev { left: calc(10px + env(safe-area-inset-left)); }
.lightbox__nav--next { right: calc(10px + env(safe-area-inset-right)); }
.lightbox__nav[disabled] { opacity: 0; pointer-events: none; }
.lightbox__counter { position: fixed; z-index: 2; left: 50%; transform: translateX(-50%);
  bottom: calc(16px + env(safe-area-inset-bottom)); padding: 5px 12px; border-radius: 999px;
  background: rgba(20,20,24,.55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); font: 500 12px var(--mono); letter-spacing: .06em; }
/* touch swipe is the primary control on phones → hide the arrow buttons there */
@media (hover: none) and (pointer: coarse) { .lightbox__nav { display: none; } }
@media (prefers-reduced-motion: reduce) { .lightbox__track { scroll-behavior: auto; } }
/* YouTube player modal — reuses the lightbox backdrop + close button; a centred 16:9 stage */
.video-modal { padding: 0; border: 0; max-width: 100vw; max-height: 100dvh; width: 100vw; height: 100dvh;
  background: transparent; overflow: hidden; }
.video-modal::backdrop { background: rgba(8,8,10,.94); }
.video-modal__frame { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
  width: min(92vw, 1100px); aspect-ratio: 16 / 9; max-height: 84dvh; background: #000;
  border-radius: 12px; overflow: hidden; box-shadow: 0 12px 48px rgba(0,0,0,.6); }
.video-modal__iframe { width: 100%; height: 100%; border: 0; display: block; }
/* the YouTube trigger is a <button> in the social row — strip UA chrome so it matches the <a> icons */
.social-link--yt { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
/* both wrappers are flattened so `order` above can sequence their children directly */
.detail__col2 { display: contents; }
/* no tag chip at all → owner's rule (2026-08-01): "if no tags exist, no spacing should be
   required". Was still landing at body-padding(18) + meta:empty+para(15) = 33px even with zero
   tags; tightened to a nominal 10px total instead of literal 0 (fully touching read as cramped) */
.detail__body--no-tags { padding-top: 10px; }
/* the write-up's own gap under the icons row, on a phone */
.detail__copy { margin-top: 14px; }

/* Drop cap, first paragraph, English only — an illuminated capital sitting on a block a shade
   lighter than the page, which is exactly what --card already is in both themes. The float is
   what makes the following lines wrap around it.
   ⚠ THE BOX MUST BE A HAIR UNDER TWO LINES TALL, and the two breakpoints set different type:
   the phone's paragraph is 13px × 1.7 = 22.1px a line, so two lines are 44.2 and the box is 42.
   A box that overruns two lines shoves the THIRD line across as well, which is what it did on
   the laptop before the numbers were worked out rather than guessed. */
.dropcap {
  float: left;
  width: 42px; height: 49px;
  margin: 0 11px 0 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--card);
  border-radius: 5px;
  color: var(--gold-ink);
  font: 700 39px/1 var(--font);
}
/* a numeric cap holds two or three glyphs, so the box grows and the type comes down a step */
.dropcap--wide { width: auto; min-width: 42px; padding: 0 9px; font-size: 33px; letter-spacing: -.01em; }
.detail__eyebrow { font: 500 9.5px var(--mono); letter-spacing: .14em; color: var(--gold-ink); }
.detail__title { margin-top: 7px; font: 700 26px/1.15 var(--font); letter-spacing: -.02em; }
.detail__sub { margin-top: 5px; font: 400 13px/1.5 var(--font); color: var(--ink-2); }
.detail__meta { margin-top: 11px; }                 /* WITH a tag: keep breathing room under the title */
.detail__meta:empty { margin-top: 0; }              /* no tag: collapse so the title→write-up gap can tighten */
.writeup-head { margin-top: 18px; display: flex; align-items: center; gap: 10px; }
.writeup-head .lbl { font: 500 9px var(--mono); letter-spacing: .14em; color: var(--ink-4); }
.detail__para { margin-top: 10px; font: 400 15px/1.7 var(--font); color: var(--ink); text-wrap: pretty; }
/* the wrapper now carries the gap above the first paragraph, so that paragraph must not add
   its own on top — paras 2..n keep the 10px rhythm. */
.detail__copy > .detail__para:first-child { margin-top: 0; }
.detail__actions { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.detail__actions-row { display: flex; gap: 8px; }
.detail__actions-row .btn { flex: 1; }
/* place-page social/link icons (official brand marks) */
.social-links { margin-top: 20px; display: flex; gap: 14px; align-items: center; }
.social-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 11px; color: var(--ink);
  transition: transform .12s ease;
}
.social-link:hover { transform: translateY(-2px); }
.social-link svg { width: 32px; height: 32px; display: block; }
.social-link--off { filter: grayscale(1); opacity: .32; pointer-events: none; cursor: default; }
.added-by { margin-top: 16px; padding-top: 13px; border-top: 1px solid var(--line-soft); display: flex; align-items: center; gap: 10px; }
/* sized against the two-line byline beside it — a 26px disc under a 17px name read as a dot.
   ⚠ NO PADDING IS SET HERE AND NEVER WAS: the gap around the initial is font metrics, a
   capital drawing at roughly 0.7 of its em box. Closing it means raising the TYPE, not
   removing a padding that does not exist. 28px puts the letter at ~20px in a 44px disc. */
.added-by__badge { width: 44px; height: 44px; border-radius: 50%; background: var(--primary); color: var(--on-primary); display: flex; align-items: center; justify-content: center; font: 700 28px/1 var(--font); flex: none; }
/* a pair of initials is roughly twice as wide, so it takes its own step down */
.added-by__badge--pair { font-size: 19px; letter-spacing: -.01em; }
/* A curator with a portrait on file wears it here instead of their initials. The disc keeps its
   44px and never moves — same standing rule as the Team card at .tm-card__ph: the frame is fixed,
   the transform lives on the child img. The crop is the Team card's, copied exactly, and it holds
   at 44px because every value in it is a percentage (object-fit cover + a percentage origin).
   ⚠ Do NOT copy .tm-card__ph-img--raw here: its translates are in px and would not scale down. */
/* 5px Việt Red ring that grows OUTWARD (owner, 2026-08-28). box-sizing is border-box globally,
   which draws a border inward and ate 10px off the photo — the one override here flips this disc
   to content-box, so 50px is the PHOTO and the ring sits outside it at 60px overall. The extra
   10px is real layout, not a box-shadow, so the byline beside it moves over instead of being
   overlapped. The frame is still fixed; the transform lives on the child img. */
.added-by__badge--photo { box-sizing: content-box; width: 50px; height: 50px; overflow: hidden;
  background: var(--primary-soft); border: 5px solid var(--primary); }
.added-by__ph { width: 100%; height: 100%; object-fit: cover; display: block;
  transform-origin: 100% 0; transform: scale(1.1); }
/* two lines: the label, then who. Both a step up in size — this was 11.5px and read as a
   footnote on a phone. The laptop block no longer needs its own copy of the b rule. */
.added-by__text { font: 400 13.5px var(--font); color: var(--ink-2); }
.added-by__text b { display: block; font: 600 17px var(--font); color: var(--ink); }

/* EN / VI language toggle (inline pill) */
.lang-toggle { display: inline-flex; background: var(--primary-soft); border-radius: var(--r-pill); padding: 2px; margin-left: auto; }
[data-theme="light"] .lang-toggle { background: rgba(25,26,34,.06); }
.lang-toggle button { border-radius: var(--r-pill); padding: 4px 12px; font: 500 10px var(--font); color: var(--ink-2); }
.lang-toggle button.is-active { background: var(--primary); color: var(--on-primary); font-weight: 700; }

/* ---------- generic content page (about / auth / forms) ---------- */
.page { padding-top: 26px; padding-bottom: 26px; max-width: 640px; }
.page__eyebrow { font: 500 9px var(--mono); letter-spacing: .16em; color: var(--eyebrow); }
.page__title { margin-top: 10px; font: 700 28px/1.12 var(--font); letter-spacing: -.02em; }
.page__lead { margin-top: 14px; font: 400 13.5px/1.65 var(--font); color: var(--ink-2); }
.page__lead + .page__lead { margin-top: 10px; }
/* darker body text on the light gold-wash pages (About / Contact / Suggest …) for legibility.
   BUG FIXED (2026-07-31): the bare `:root body[data-wash]` selector matched unconditionally —
   `:root` always matches the <html> element regardless of its data-theme value, so this dark-ink
   override was silently winning in DARK theme too, making .page__lead/.mono-label nearly invisible
   (dark text on a dark bg) on signin.html and every other data-wash page in dark mode. Scoped with
   `:not([data-theme="dark"])` so it only applies when light is explicit OR unset (the documented
   "light = default" case), never dark. */
[data-theme="light"] body[data-wash] .page__lead,
html:not([data-theme="dark"]) body[data-wash] .page__lead { color: rgba(25,26,34,.82); }
[data-theme="light"] body[data-wash] .mono-label,
html:not([data-theme="dark"]) body[data-wash] .mono-label { color: rgba(25,26,34,.58); }
.page-photo { margin-top: 18px; height: 160px; border-radius: var(--r-photo); }

/* ---------- legal (/legal — Privacy Policy + Terms of Service) ----------
   The ONLY page whose body content is static HTML in the file rather than rendered by app.js
   (see the comment at the top of legal.html for why). Nothing here is page-specific styling for
   its own sake: it's a long-form reading treatment that the rest of the app has never needed,
   since no other page carries more than a few paragraphs.
   Both documents live on one page under #privacy / #terms anchors, so every heading that can be
   linked to needs scroll-margin-top — .site-header is `position: sticky` and would otherwise sit
   on top of whatever you just jumped to. */
.legal__jump { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 8px; }
.legal__jump a {
  padding: 7px 14px; border-radius: var(--r-pill);
  border: 1px solid var(--line-mid); background: var(--card);
  font: 500 11.5px var(--font); color: var(--ink);
}
.legal__jump a:hover { border-color: var(--primary); color: var(--primary); }

/* The whole head of the page — eyebrow, title, lead, the two document tabs and the rule under
   them — rides along as one sticky block while the document scrolls beneath it. Sticks at every
   width, owner's call. It needs its own opaque background or the prose shows through, and it sits
   below the site header, so the offset is the header's own height variable. */
.legal__sticky {
  /* ⚠ Two things at once, and they are related. The block SWALLOWS .page's 26px top padding
     (negative margin, same padding back) so its own opaque background covers that band — left
     outside the block it painted nothing and the prose scrolled through it. Swallowing it also
     puts the block's top exactly at --header-h on load, which is its pin point, so it is
     born-stuck and never travels. Coupled on purpose: change .page's padding-top and both
     numbers move with it. */
  position: sticky; top: var(--header-h, 56px); z-index: 5;
  margin-top: -26px; padding-top: 26px;
  background: var(--bg);
}
.legal__sticky-rule { margin: 14px 0 0; border: 0; border-top: 1px solid var(--line); }

.legal__doc { margin-top: 30px; padding-top: 26px; border-top: 1px solid var(--line); scroll-margin-top: calc(var(--header-h, 56px) + 16px); }
.legal__doc-title { font: 700 21px/1.15 var(--font); letter-spacing: -.01em; }
.legal__updated { margin-top: 7px; font: 400 9px var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-5); }
.legal__h { margin-top: 26px; font: 600 14px/1.3 var(--font); color: var(--ink); scroll-margin-top: calc(var(--header-h, 56px) + 16px); }
.legal__p { margin-top: 10px; font: 400 13.5px/1.7 var(--font); color: var(--ink-2); }
.legal__list { margin-top: 10px; padding-left: 20px; list-style: disc; }
.legal__list li { margin-top: 7px; font: 400 13.5px/1.7 var(--font); color: var(--ink-2); }
.legal__list li b { color: var(--ink); font-weight: 600; }
/* ⚠ SCOPED TO THE PROSE ELEMENTS ON PURPOSE — do NOT widen this back to `.legal a`.
   That is what it was at v450, and it was wrong: `.legal a` is (0,1,1) while `.detail__back` is
   (0,1,0), so the broad rule silently won and painted the "Back to Wayfinder" pill at the bottom of
   the page red, underlined and a weight lighter. Any shared component dropped inside .legal would
   have been captured the same way. Target the text, not the container. */
.legal__p a, .legal__list li a { color: var(--primary); font-weight: 500; text-decoration: underline; text-underline-offset: 2px; }
/* Active pill — (0,2,1), and it follows `.legal__jump a` / `:hover` in source order, so it wins
   without !important. The explicit `:hover` twin stops the hover rule taking the label back. */
.legal__jump a.is-active,
.legal__jump a.is-active:hover { background: var(--primary); border-color: var(--primary); color: var(--on-primary); }

/* ---- tab mode ----
   `is-tabbed` is added by app.js's setLegalDoc(), NEVER by default. Both documents are static
   markup in legal.html and a no-JS reader (Google's branding check, App Store review) must get
   both, stacked. Making `display:none` a CSS default would delete half the page for exactly the
   audience it was written for. */
.legal.is-tabbed .legal__doc.is-off { display: none; }
/* With one document showing, the hairline that separated the stacked pair is just a stray line
   under the pills. */
.legal.is-tabbed .legal__doc { margin-top: 24px; padding-top: 0; border-top: 0; }
/* Same light/gold-wash legibility fix .page__lead gets above — legal.html carries data-wash, so
   without this the body copy inherits --ink-2 and reads faint on the light gold ground. */
[data-theme="light"] body[data-wash] .legal__p,
[data-theme="light"] body[data-wash] .legal__list li,
html:not([data-theme="dark"]) body[data-wash] .legal__p,
html:not([data-theme="dark"]) body[data-wash] .legal__list li { color: rgba(25,26,34,.82); }
@media (min-width: 760px) {
  .legal__doc-title { font-size: 24px; }
  .legal__h { font-size: 15px; margin-top: 30px; }
  .legal__p, .legal__list li { font-size: 14px; }
}

/* Static pre-render summary in index.html's footer slot (v452). renderChrome() replaces that slot
   before first paint, so in practice this styles a thing nobody sees — it exists for the one case
   where it IS seen, which is JavaScript failing outright. Legible, not decorative. */
.nojs-summary { padding: 26px 0; max-width: 640px; font: 400 13.5px/1.7 var(--font); color: var(--ink-2); }
.nojs-summary p + p { margin-top: 10px; }
.nojs-summary b { color: var(--ink); }
.nojs-summary a { color: var(--primary); font-weight: 500; }
/* ⛔ THE HIDING MUST STAY JS-APPLIED — same trap as /legal's tab switcher. boot.js adds .wf-js in
   the <head>, pre-paint, so a browser running JS never sees this block. With JS off the class never
   lands and the summary stays, which is the entire audience it was written for (Google's branding
   check reads it without running scripts). A bare `.nojs-summary { display:none }` default deletes
   the page's only words for exactly those readers. */
/* ⚠ VISUALLY HIDDEN, NOT `display: none`, AND THAT IS THE WHOLE POINT. This block used to be
   display:none behind this class, which removed it from Chrome's language detection — and since
   the shell ships with no other text, Chrome was classifying an empty page and guessing. Real
   users got a "Translate from German?" bar on an English site. Text that is clipped is still
   rendered, so the detector reads it and screen readers keep it. Never swap this back. */
/* ⚠ 2026-08-27: the 1x1 clipped box above did NOT survive Chrome's language capture. Adding a
   365-character English paragraph inside it changed nothing, so the detector was still reading a
   near-empty page and guessing German. Off-screen with a REAL layout box is the fix: full width,
   natural height, nothing clipped, so the text is laid out and read. Never go back to 1x1. */
html.wf-js .nojs-summary {
  position: absolute; left: -10000px; top: 0;
  width: 640px; height: auto; overflow: visible;
  clip-path: none; white-space: normal;
}

/* ---------- forms ---------- */
.field { margin-top: 14px; }
/* two fields sharing one row (e.g. First/Last name) — each .field keeps its own margin-top,
   which lands as ONE gap above the row since the pair sits side by side, not stacked. */
.field-row { display: flex; gap: 10px; }
.field-row .field { flex: 1; min-width: 0; }
/* show/hide eye toggle sits INSIDE the password input, so the input needs right-padding to
   clear it. Button is absolute-positioned relative to this wrapper, not the input itself,
   so its own layout can't affect the input's width. */
/* margin-top moved from .input onto the wrapper (and zeroed on the input) so it can't collapse
   through .field__pw — keeps the wrapper's own box height == the input's, so top:50% below centers
   the eye button correctly regardless of the input's margin. */
.field__pw { position: relative; margin-top: 6px; }
.field__pw .input { margin-top: 0; padding-right: 44px; }
.field__eye {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%); width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-4); border-radius: 50%;
}
.field__eye:hover { color: var(--ink-2); }
.field__eye svg { width: 18px; height: 18px; }
.field__label { font: 500 10px var(--mono); letter-spacing: .18em; text-transform: uppercase; color: var(--ink-4); display: flex; align-items: baseline; }
.field__label .hint { margin-left: auto; font: 400 11px var(--font); letter-spacing: 0; text-transform: none; color: var(--ink-4); }
/* a text field gets its label-to-control gap from the input's own margin-top (see .input below);
   .chips has no such margin, so a field whose control is a chip row (e.g. step 8's flags) had the
   label sitting almost flush against the pills — scoped to .field so it doesn't touch the review
   card's standalone flag-chip display, which isn't wrapped in a .field. */
.field .chips { margin-top: 8px; }
/* .field__label is shared across every form in the app (Contact/Suggest/Sign-in/wizard/Account) —
   scope teal to the admin edit overlay, the add-wizard, and the signed-in Suggest form only,
   everywhere else keeps the muted --ink-4 default */
.overlay--edit .field__label { color: var(--teal); }
.wizard__body .field__label { color: var(--teal); }
body[data-page="suggest"] .field__label { color: var(--teal); }
body[data-page="contact"] .field__label { color: var(--teal); }
/* Region <optgroup> labels (North/Central/South) in the Suggest page's CITY select — `color`/
   `font-weight` on <optgroup> are among the few things browsers generally honor for native select
   popups (rendering is otherwise OS-controlled, unlike everything else in this app); explicit
   `<option>` color keeps the city names from inheriting the teal. */
body[data-page="suggest"] select[data-field="city"] optgroup { color: var(--teal); font-style: normal; font-weight: 600; }
body[data-page="suggest"] select[data-field="city"] option { color: var(--ink); font-weight: 400; }
.input, .textarea, .select {
  width: 100%; margin-top: 6px;
  background: var(--field-bg); border: 1px solid var(--field-line);
  border-radius: var(--r-input); padding: 13px 15px;
  font: 400 16px var(--font); color: var(--ink);   /* 16px min: stops iOS from zooming on focus */
}
/* Chrome/Safari autofill (incl. Google's "suggest strong password") forces its own light-blue fill
   and dark text — `background`/`color` can't override it, so an autofilled field went pale blue in
   dark mode AND swallowed the eye toggle, which is coloured for a dark field. An INSET box-shadow is
   the one paint autofill leaves alone; it needs an OPAQUE colour (--field-bg is rgba(...,.06) in
   dark, so the blue would show straight through), hence the --field-fill token — the flattened twin
   of --field-bg, so a filled field matches an empty one exactly. The absurd transition delay is the
   standard trick to stop the blue flashing in before the shadow paints; verified .input has no other
   transition to clobber. Applies to EVERY input in the app (signin, suggest, contact, add-wizard,
   admin editor), not just the two on the create form — deliberately consistent. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.textarea:-webkit-autofill,
.select:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 1000px var(--field-fill) inset;
          box-shadow: 0 0 0 1000px var(--field-fill) inset;
  caret-color: var(--ink);
  transition: background-color 9999s ease-in-out 0s;
}
.input::placeholder, .textarea::placeholder { color: var(--ink-5); }
.textarea { resize: vertical; min-height: 90px; }
/* the address is two lines, not a write-up — a 90px box for one line of text reads as a mistake */
.textarea--addr { min-height: 0; height: 62px; line-height: 1.45; }
/* soft character count under a write-up box — guidance, never a stop */
.field__count { margin-top: 5px; text-align: right; font: 500 11px var(--mono); color: var(--ink-3); }
.field__count.is-over { color: var(--gold-ink); }
.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
.select { display: flex; align-items: center; justify-content: space-between; }
.field__note { margin-top: 5px; font: 400 10.5px/1.4 var(--font); color: var(--ink-4); }

.form-card { margin-top: 18px; background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 18px 18px 20px; }
.form-card .field:first-child { margin-top: 0; }

/* google / oauth button + OR divider */
/* "Sign in with Google" — per Google branding guidelines. Light theme: #FFFFFF fill,
   #747775 stroke, #1F1F1F text. Dark theme: #131314 fill, #8E918F stroke, #E3E3E3 text.
   4-colour G logo unaltered. Pill radius (an allowed variant). Font is Google Sans Medium
   per spec, which isn't publicly loadable — closest neutral stack used. */
.gsi-btn {
  margin-top: 22px; display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; padding: 12px; border-radius: var(--r-pill); cursor: pointer;
  background: #FFFFFF; border: 1px solid #747775; color: #1F1F1F;
  font: 500 14px/20px 'Roboto', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
}
[data-theme="dark"] .gsi-btn { background: #131314; border-color: #8E918F; color: #E3E3E3; }
.gsi-btn:hover { box-shadow: 0 1px 2px rgba(60,64,67,.30), 0 1px 3px 1px rgba(60,64,67,.15); }
.gsi-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--primary-ring); }
.gsi-btn__icon { display: flex; flex: none; }
.gsi-btn__icon svg { display: block; }
.or-divider { margin-top: 20px; display: flex; align-items: center; gap: 12px; }
.or-divider::before, .or-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.or-divider span { font: 400 8.5px var(--mono); letter-spacing: .16em; color: var(--ink-5); }

/* auth tab switch */
.auth-tabs, .segmented { display: flex; background: var(--primary-soft); border-radius: var(--r-pill); padding: 3px; }
[data-theme="light"] .auth-tabs, [data-theme="light"] .segmented { background: rgba(25,26,34,.06); }
.auth-tabs a, .segmented button { flex: 1; text-align: center; padding: 9px 0; border-radius: var(--r-pill); font: 500 12.5px var(--font); color: var(--ink-3); }
.auth-tabs a.is-active, .segmented button.is-active { background: var(--primary); color: var(--on-primary); font-weight: 700; }
/* Create-account consent line. Sits directly under the Create button, above NO SPAM, NO SHARING. */
.auth-legal { margin-top: 14px; text-align: center; font: 400 11px/1.55 var(--font); color: var(--ink-3); }
.auth-legal a { color: var(--primary); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.auth-note { margin-top: 14px; text-align: center; font: 400 12px var(--font); color: var(--ink-3); }
.auth-note a { color: var(--primary); font-weight: 600; display: inline-block; margin-top: 4px; }
/* signin.html has NO header (header:false) — nothing else on this page accounts for the iOS status
   bar, so a home-screen/standalone install (edge-to-edge, status bar overlays content) crowds the
   brand lockup right up against it. Every other page gets this for free from .site-header's own
   safe-area padding; this page needs its own. */
.auth { position: relative; max-width: 420px; padding-top: calc(40px + env(safe-area-inset-top, 0px)); }
/* round back button — this page is a full navigation (header:false/footer:false), so unlike every
   other screen there's no site-header/tab-bar to fall back to; without this a signed-out visitor who
   opens Sign In / Create Account and changes their mind is stuck here. Absolutely positioned (not in
   the flex row with the centered brand-lockup) so it doesn't need the title-row centering math the
   suggest-gate back button needed — same .listing-head__home component + go-back action, though. */
/* the back chevron moved into the title row (see authTitle in app.js), so it no longer floats
   beside the lockup and no longer needs absolute positioning. */
/* sits under the email input rather than under the submit button — the reassurance belongs beside
   the thing it reassures about. Left-aligned with the field, not centred like the old footer line. */
.auth-nospam { margin-top: 7px; text-align: left; }

/* .page__title's own margin-top:10px is an unmatched margin on one flex item, which defeats
   align-items:center and reads as "the chevron isn't level with the heading". Hand it to the ROW.
   ⚠ This is the FIFTH copy of this correction — soon-title-row, .member, suggest and contact each
   carry their own. It belongs on .listing-head__row itself; lifting it is a separate change. */
.auth .listing-head__row { margin-top: 10px; }
.auth .listing-head__row .page__title { margin-top: 0; }
.auth .wordmark { display: block; margin-bottom: 26px; }
/* "Vietnam [logo] Wayfinder" lockup — replaces the small lowercase wayfinder* wordmark on the
   sign-in/create-account card. Aboreto matches the first-visit splash's brand wordmark
   treatment; unlike the splash this isn't on a hardcoded navy bg, so it uses --ink (readable in
   both themes) rather than the splash's white/gold pairing. */
.brand-lockup { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 26px; }
.brand-lockup__word { font-family: 'Aboreto', serif; font-size: 16px; letter-spacing: .2em; text-transform: uppercase; color: var(--ink); white-space: nowrap; }
.brand-lockup__icon svg { width: 30px; height: 30px; border-radius: 8px; display: block; flex: none; }
/* letter-spacing lands after the final letter too, so VIETNAM sat .2em further from the badge than
   WAYFINDER did. Match the wider gap on the right rather than trimming the left — the owner's call
   on the flyout's copy, which sets its own smaller value. */
.brand-lockup__word:last-child { margin-left: .2em; }
/* footer reuse of the same lockup (was a bare app-icon <img>, now the full "Vietnam [logo]
   Wayfinder" mark) — zero the signin-tuned bottom margin, and DESKTOP ONLY like the footer
   wordmark always was (mobile keeps the brand in its header, not the footer). Declared AFTER
   the base .brand-lockup rule so this display:none actually wins on mobile. */
.brand-lockup--footer { display: none; margin-bottom: 0; }
/* the nav flyout's head — the same lockup, stepped down to fit a phone sheet */
.brand-lockup--sheet { display: flex; margin-bottom: 0; gap: 8px; }
.brand-lockup--sheet .brand-lockup__word { font-size: 15px; letter-spacing: .19em; }
/* letter-spacing lands after the LAST letter too, so VIETNAM sat .19em further from the badge than
   WAYFINDER did. Cancelling that on the left evened them at the TIGHTER gap; the owner preferred
   the wider one, so the right word is pushed out to match instead. Both gaps are now 8px + .19em. */
.brand-lockup--sheet .brand-lockup__word:last-child { margin-left: .19em; }
.brand-lockup--sheet .brand-lockup__icon svg { width: 25px; height: 25px; border-radius: 7px; }
.nav-sheet__head--brand { justify-content: center; }
@media (min-width: 760px) { .brand-lockup--footer { display: inline-flex; } }
/* EMAIL/PASSWORD labels + OR divider in brand gold; the "Forgot?" hint in teal — scoped to the
   auth page only (field__label/or-divider are shared base classes used across every other form). */
.auth .or-divider span { color: var(--gold); }
.auth .field__label { color: var(--gold); }
.auth .field__label .hint { color: var(--teal); }
.auth .auth-note { color: var(--gold); }
/* post-signup "Check your email" screen (v392). Declared AFTER the .auth/.page__lead base rules so
   these win on source order — the house scar is modifiers declared before their base losing silently.
   .page__lead's --ink-2 is a 60%-opacity muted grey: right for a form hint under a field, unreadable
   as the main message on a screen that IS just body copy, so it goes to full --ink here. The round
   back chevron stays absolutely positioned at the left, so centring the text doesn't disturb it. */
.auth--sent { text-align: center; }
.auth--sent .page__lead { color: var(--ink); }
/* This screen is almost entirely whitespace + four short lines, so it needs a much looser rhythm
   than a form does — the shared .page__title/.page__lead margins are tuned for pages with a lot
   more below them. Owner-set on device (v394). */
.auth--sent .brand-lockup { margin-bottom: 40px; }
/* gold title + gold address — the theme-aware pair (--gold-ink is a legibility-adjusted brown in
   light mode; flat --gold would wash out on the light background). Address gets its own line so a
   long one can't wedge itself mid-sentence, and word-break so it can't overflow the 420px card. */
.auth--sent .page__title { margin-top: 0; color: var(--gold-ink); }
[data-theme="dark"] .auth--sent .page__title { color: var(--gold); }
.auth--sent__email { color: var(--gold-ink); font-weight: 600; font-size: 20px; word-break: break-word; }
[data-theme="dark"] .auth--sent__email { color: var(--gold); }
.auth--sent .page__lead { margin-top: 30px; }
.auth--sent .page__lead + .page__lead { margin-top: 26px; }
/* the spam line is a separate thought from the "here's your link" block — extra air sets it apart */
.auth--sent .auth--sent__spam { margin-top: 38px; }
.auth--sent .btn--primary { margin-top: 38px; }
/* "Forgot?" became a real <button> on 2026-08-13 (was a decorative <span> that did nothing).
   The global `button { border:0; background:none }` reset already strips the chrome, so this only
   has to undo the font/padding a button brings and restore the inherited .hint look. Declared after
   `.field__label .hint` and `.auth .field__label .hint` so the colour still comes from those. */
/* live "do these two match" readout on the reset screen — see updatePwMatch() for why it exists
   (a browser-generated password can't be revealed by the eye toggle, so the eye can't answer this). */
.pw-match { margin-top: 8px; min-height: 16px; font: 500 12px var(--font); }
.pw-match.is-ok { color: var(--teal); }
.pw-match.is-bad { color: var(--primary); }
.hint--btn { padding: 0; font: inherit; color: inherit; cursor: pointer; }
.hint--btn:hover { text-decoration: underline; }
.form-meta { margin-top: 12px; text-align: center; font: 400 8.5px var(--mono); letter-spacing: .12em; color: var(--ink-5); }
.form-meta--teal { color: var(--teal); }

/* ---------- wizard (add a place) ---------- */
.wizard { max-width: 560px; margin: 0 auto; padding-top: 24px; padding-bottom: 24px; }
/* add.html runs header:false (no .site-header to absorb the iOS status bar in standalone/
   home-screen mode) — suggest.html's signed-out gate reuses .wizard but keeps header:true,
   so the extra inset is scoped here, not on the bare class (would double-pad suggest). */
body[data-page="add"] .wizard { padding-top: calc(24px + env(safe-area-inset-top, 0px)); }
.wizard__eyebrow { font: 500 9px var(--mono); letter-spacing: .16em; color: var(--eyebrow); }
.wizard__eyebrow--teal { color: var(--teal); }
.progress { margin-top: 10px; display: flex; gap: 4px; }
.progress span { flex: 1; height: 3px; border-radius: var(--r-pill); background: var(--primary-soft); }
.progress span.on { background: var(--primary); }
.wizard__title { margin-top: 20px; font: 700 26px/1.1 var(--font); letter-spacing: -.02em; }
@media (min-width: 760px) { .wizard__title { font-size: 28px; } }
.wizard__sub { margin-top: 6px; font: 400 13px/1.5 var(--font); color: var(--ink-2); }
.wizard__sub--gold { color: var(--gold); margin-top: 16px; }
.wizard__sub--perks-head { text-align: center; margin-left: 0; font-size: 16px; }
.wizard__sub--bold { font-weight: 700; }
.wizard__sub--emoji { font-size: 28px; }
.wizard__or { text-align: center; font: 500 11px var(--mono); letter-spacing: .08em; color: var(--teal); text-transform: uppercase; }
/* .wizard__perks itself carries NO indent — the title line and the list both measure their
   margin-left as a % of THIS shared, unshifted box, so the two numbers are directly comparable
   and scale together on any mobile width (fixes list nested-in-title compounding). */
.wizard__perks { display: block; text-align: left; }
.wizard__list { margin: 8px 0 0; padding-left: 20px; text-align: left; list-style: disc; font: 400 13px/1.5 var(--font); color: var(--gold); }
@media (min-width: 760px) { .wizard__perks > .wizard__list { margin-left: 32.5%; } }
@media (max-width: 759px) { .wizard__perks > .wizard__list { margin-left: 20%; } }
.wizard__list li { margin-top: 4px; }
.wizard__list .wizard__list { margin-top: 6px; padding-left: 20px; list-style: circle; }
.wizard__body { margin-top: 18px; }
.wizard__foot { margin-top: 26px; display: flex; gap: 10px; }
.wizard__foot .btn--primary { flex: 1; }
.wizard__foot--stack { flex-direction: column; align-items: stretch; gap: 20px; }
.wizard__foot--stack .btn { flex: none; width: 100%; }
.wizard__foot--stack .btn--ghost { color: var(--ink); border-color: var(--ink-3); background: rgba(35, 166, 157, .25); transition: background-color .15s, border-color .15s; }
.wizard__foot--stack .btn--ghost:hover { background: rgba(35, 166, 157, .4); border-color: var(--teal); }
/* The step's gate, said in colour. Grey until WIZ_GATES is satisfied, then the red button.
   ⚠ The full transition is restated here because a `transition` on a more specific selector
   REPLACES .btn's, it doesn't add to it — dropping transform/filter would kill the press-down
   and the hover on every wizard Next. */
.wizard__foot .btn--primary {
  transition: transform .04s ease, filter .15s ease,
              background-color .18s ease, color .18s ease, box-shadow .18s ease;
}
/* Still tappable — the toast is what says WHY. box-shadow, not border: a border would add 2px
   and make the button change size the moment it unlocked. */
.wizard__foot .btn--primary.is-locked {
  background: var(--line); color: var(--ink-3);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.wizard__foot .btn--primary.is-locked:hover { filter: none; }
.wizard__sub--cta { margin-top: 32px; }

/* selectable option rows */
.opt-list { display: flex; flex-direction: column; gap: 9px; }
.opt {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-card); padding: 13px 15px;
}
.opt.is-selected { border: 1.5px solid var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
/* navIcon() supplies its own rounded card + colour tile + glyph — just centers it, no swatch bg of its own */
.opt__icon { width: 46px; height: 46px; flex: none; display: flex; align-items: center; justify-content: center; }
.opt__icon .nav-ico { width: 44px; height: 44px; }
.opt__body { flex: 1; min-width: 0; }
.opt__name { display: block; font: 600 15px var(--font); letter-spacing: -.01em; }
.opt__desc { display: block; font: 400 11.5px var(--font); color: var(--ink-3); }
.radio { width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--line-strong); box-sizing: border-box; flex: none; }
.opt.is-selected .radio { border: 5px solid var(--primary); }
.opt.is-selected .opt__name { color: var(--gold-ink); }
.opt.is-selected .opt__desc { color: var(--ink); }
[data-theme="dark"] .opt.is-selected .opt__desc { color: #fff; }
/* Stay/Live sub-toggle (2026-08-09) — appears below the merged "Stay / Live" option once picked,
   required before Next proceeds (wiz-guarded-next). Negative margin pulls it up against the .opt
   button above so it reads as one attached unit despite .opt-list's flex `gap` applying uniformly
   between all direct children (can't zero the gap for just this one pair). */
.opt__sub { margin-top: -8px; border: 1.5px solid var(--primary); border-top: none; border-radius: 0 0 13px 13px; padding: 14px 15px 15px; background: var(--card); }
.opt__sub-q { font: 600 12.5px var(--font); color: var(--ink); margin-bottom: 8px; }
/* two plain chips, not a joined segmented toggle — see the comment at the call site in app.js */
.stay-live-row { display: flex; gap: 8px; }
.stay-live-chip { flex: 1; border: 1.5px solid var(--line-mid); border-radius: 12px; padding: 12px 10px; font: 700 12px var(--font); text-align: center; color: var(--ink-2); background: none; }
.stay-live-chip.is-selected { border-color: var(--primary); background: var(--primary-ring); color: var(--gold-ink); }

/* city grid */
/* region-grouped city step (v00.01.373) — 4-per-row so a region's cities read as one dense block
   instead of the old plain 2/3-col grid with no grouping. */
.city-region { margin-top: 18px; }
.city-region:first-child { margin-top: 0; }
.city-region__label { font: 700 12px var(--mono); letter-spacing: .14em; color: var(--teal); margin-bottom: 8px; }
.city-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
@media (min-width: 520px) { .city-grid { grid-template-columns: repeat(4, 1fr); } }
.city-chip {
  border: 1.5px solid var(--line-mid); border-radius: 12px; padding: 9px 4px;
  font: 700 11.5px var(--font); text-align: center; color: var(--ink);
}
.city-chip.is-selected { border-color: var(--primary); background: var(--primary-ring); color: var(--gold-ink); }
.city-typed { margin-top: 12px; border: 1px dashed var(--line-strong); border-radius: 13px; padding: 12px 14px; font: 400 13px var(--font); color: var(--ink-5); }

/* upload dropzone */
.dropzone {
  margin-top: 18px; height: 190px; border: 1.5px dashed var(--line-strong); border-radius: 18px;
  background-image: repeating-linear-gradient(45deg, rgba(127,127,127,.06) 0 10px, rgba(127,127,127,0) 10px 20px);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; width: 100%;
}
.dropzone__ring { width: 38px; height: 38px; border-radius: 50%; border: 1.5px solid var(--ink-3); display: flex; align-items: center; justify-content: center; font: 400 16px var(--font); color: var(--ink-2); }
.dropzone__t { font: 600 14px var(--font); }
.dropzone__hint { font: 400 8.5px var(--mono); letter-spacing: .14em; color: var(--ink-4); }
.dropzone--disabled { opacity: .65; cursor: default; }
/* wizard photo picker (a <label> wrapping a hidden file input) */
.dropzone--pick { cursor: pointer; position: relative; overflow: hidden; }
.dropzone--pick.is-busy { opacity: .6; pointer-events: none; }
.dropzone__input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dropzone__preview { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; }
.dropzone__status { margin-top: 8px; font: 400 8.5px var(--mono); letter-spacing: .14em; color: var(--teal); text-align: center; }
[data-theme="dark"] .dropzone__status { color: var(--gold-ink); }
.gallery { margin-top: 8px; display: flex; gap: 8px; }
.gallery__item { width: 72px; height: 72px; border-radius: 12px; overflow: hidden; }
.gallery__add { width: 72px; height: 72px; border-radius: 12px; border: 1.5px dashed var(--line-strong); display: flex; align-items: center; justify-content: center; font: 400 20px var(--font); color: var(--ink-4); }

/* review card (step 9), rebuilt from the Claude Design "WF - Wizard - Review" frame 2a (2026-08-09) —
   full-width photo, name leads (matches pickName()'s language-aware order, not hardcoded English),
   eyebrow in text flow, single "FIX SOMETHING" pill rail replacing the old per-row inline Edit links. */
.review-card2 { margin-top: 16px; border-radius: 18px; border: 1px solid var(--line); background: var(--card); overflow: hidden; }
.review-card2__photo { position: relative; height: 190px; }
.review-card2__body { padding: 18px 22px 20px; }
.review-card2__namerow { display: flex; align-items: baseline; gap: 10px; }
/* the section tile leads the name — baseline alignment is for the text, so the tile centres
   against it instead and sits closer than the row's own gap */
.review-card2__ico { align-self: center; display: flex; flex: none; margin-right: -3px; }
.review-card2__name { font: 700 21px var(--font); letter-spacing: -.01em; color: var(--ink); }
.review-card2__second { margin-top: 3px; font: 400 14px var(--font); color: var(--ink-3); }
.review-card2__eyebrow { margin-top: 9px; font: 500 10px var(--mono); letter-spacing: .16em; color: var(--teal); }
.review-card2__writeup { margin-top: 14px; border-top: 1px solid var(--line-soft); padding-top: 13px; font: 400 14px/1.55 var(--font); color: var(--ink-2); }
.review-row { margin-top: 12px; border-top: 1px solid var(--line-soft); padding-top: 11px; display: flex; align-items: baseline; gap: 8px; }
.review-row .lbl { font: 500 8.5px var(--mono); letter-spacing: .14em; color: var(--ink-4); }
.review-row .val { flex: 1; font: 400 12px/1.5 var(--font); color: var(--ink); }
/* ---- step 9 audit: one row per wizard step, each with its own ✎ jump ----
   Filled panels in the editor's language, no rules between rows — the gap is the separation.
   A row is label | value | fix, and the fix is always in the same column so the eye can run
   straight down it. */
.audit { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }
.audit__row {
  display: grid; grid-template-columns: 78px minmax(0, 1fr) 34px;
  align-items: start; gap: 12px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-input); padding: 12px 12px 12px 14px;
}
[data-theme="dark"] .audit__row { background: rgba(255,255,255,.03); }
.audit__lbl { font: 600 10px var(--mono); letter-spacing: .14em; text-transform: uppercase;
  color: var(--teal); padding-top: 3px; }
.audit__val { min-width: 0; font: 400 14px/1.5 var(--font); color: var(--ink); word-break: break-word; }
.audit__val--miss { color: var(--ink-4); font-style: italic; }
/* the language tag in front of a name or a write-up — the old card showed one and dropped the other */
.audit__sub { display: inline-block; min-width: 22px; margin-right: 7px;
  font: 600 9.5px var(--mono); letter-spacing: .1em; color: var(--ink-4); }
.audit__body + .audit__body { margin-top: 8px; }
.audit__ico { display: inline-flex; vertical-align: -6px; margin-right: 7px; }
.audit__ico .nav-ico { width: 20px; height: 20px; }
.audit__thumbs { display: flex; flex-wrap: wrap; gap: 6px; }
.audit__thumb { position: relative; width: 46px; height: 46px; border-radius: 8px; overflow: hidden;
  background: rgba(127,127,127,.16); flex: none; }
.audit__thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.audit__thumb.is-cover { outline: 2px solid var(--gold); outline-offset: -2px; }
.audit__note { margin-top: 7px; font: 400 11.5px var(--font); color: var(--ink-4); }
.audit__link { display: flex; align-items: center; gap: 8px; min-width: 0; }
.audit__link + .audit__link { margin-top: 7px; }
.audit__link-ico { flex: none; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; color: var(--ink-2); }
.audit__link-ico svg { width: 20px; height: 20px; display: block; }
.audit__link-val { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 400 13px var(--font); color: var(--ink-2); }
.audit__meter { display: inline-flex; margin-right: 9px; vertical-align: -3px; }
/* the ✎ is a real target, not a glyph floating in text — same column on every row */
.audit__fix {
  width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-strong); color: var(--ink-2); font-size: 14px; line-height: 1;
  transition: background-color .15s, border-color .15s, color .15s;
}
.audit__fix:hover { background: rgba(35,166,157,.12); border-color: var(--teal); color: var(--teal); }
/* redesigned from a plain muted footnote into a prominent card (single call site: step 9's role
   hint) — light/dark-safe: --field-bg is literally white in light theme, so white hint text would be
   invisible there, same class of bug as the phase-1 selected-state colors. */
.wizard__hint { margin-top: 20px; font: 500 13px var(--font); color: var(--ink); text-align: center; padding: 14px; background: var(--field-bg); border-radius: 12px; }
[data-theme="dark"] .wizard__hint { color: #fff; }
.wizard__hint b { color: var(--gold-ink); font-weight: 700; }

/* ---------- member area ---------- */
.member { padding-top: 24px; padding-bottom: 26px; max-width: 960px; }
/* "Your corner" (account) page — narrow column, ~half of the 960 base (owner's call).
   Scoped to data-page="account" so /review (also .member) keeps its 960px. No @media:
   a ~390px phone is already < 480, so this cap engages on DESKTOP ONLY — mobile unchanged. */
body[data-page="account"] .member { max-width: 480px; }
.member__hi { margin-top: 8px; font: 700 26px/1.1 var(--font); letter-spacing: -.02em; }
/* headings that carry the go-back chevron (2026-08-08; the review queue joined them): the button
   sits beside .member__hi in a .listing-head__row — an unmatched top margin on one flex child vs.
   its sibling breaks align-items:center (same lesson as the suggest-gate title row), so the margin
   moves off the h2 and onto the row instead. Scoped to .member so the remaining bare .member__hi
   headings (signed-out, admin-only) keep their own standalone spacing untouched. */
.member .listing-head__row { margin-top: 8px; }
.member .listing-head__row .member__hi { margin-top: 0; }
.member__acct { margin-top: 6px; font: 400 11.5px var(--font); color: var(--ink-2); }
.name-edit { margin-top: 14px; background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 16px 18px; }
.name-edit__hint { margin-top: 6px; font: 400 11px/1.5 var(--font); color: var(--ink-2); }
.name-edit__row { margin-top: 13px; display: flex; gap: 10px; }
.name-edit__row .btn { flex: 1; }
.member__tabs { margin-top: 16px; }
.saved-list { margin-top: 16px; display: flex; flex-direction: column; gap: 12px; }
/* author-CSS always beats the browser's own [hidden]{display:none} default, regardless of
   specificity — the unconditional display:flex above was silently keeping the "Added" tab's
   list visible underneath "Saved" the whole time (2026-08-07 fix). Declared after the base rule. */
.saved-list[hidden] { display: none; }
.member-nudge { margin-top: 20px; background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 16px 18px; }
.member-nudge__t { font: 600 14px var(--font); letter-spacing: -.01em; }
.member-nudge__p { margin-top: 4px; font: 400 11.5px/1.5 var(--font); color: var(--ink-2); }
.member-nudge .btn { margin-top: 13px; }

/* ---------- Desktop Account Hub (2026-08-08, stage 1) ----------
   Only ever rendered at desktop widths — app.js's RENDER.account gates the whole markup on
   isDesktopWidth(), so mobile never sees any of this and .member above is untouched. The @media
   wrap here is defensive/documentary (matches the "min-width:760px" convention used elsewhere in
   this file), not the actual gate — the JS branch is. */
body[data-page="account"] .acct-hub { max-width: 920px; padding-top: 28px; padding-bottom: 60px; display: flex; flex-direction: column; gap: 20px; }
.hub-head { margin-bottom: 2px; }
/* version readout, desktop hub only — the mobile nav sheet carries its own copy in .nav-sheet__foot
   (same mono/muted treatment). margin-left:auto pushes it to the right edge of the title row. */
.hub-head__ver { margin-left: auto; align-self: center; font: 400 10px var(--mono); letter-spacing: .1em; color: var(--ink-4); white-space: nowrap; }
.acct-hub__row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: stretch; }
.acct-hub__row .acct-card { display: flex; flex-direction: column; }

.acct-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-card); padding: 26px; }
.acct-card--admin { background: linear-gradient(180deg, rgba(245,194,66,.07), transparent); border-color: rgba(245,194,66,.32); }

.acct-eyebrow-row { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
.acct-eyebrow { font: 700 10.5px var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--gold-ink); }
[data-theme="dark"] .acct-eyebrow { color: var(--gold); }
.acct-eyebrow-line { flex: 1; height: 1px; background: var(--line); }
/* "You're a/an [Role]" (2026-08-08) — only the role noun itself is teal; "You're a[n]" stays the
   surrounding .acct-eyebrow gold. */
.acct-eyebrow--role { color: var(--teal); }

.acct-identity { display: flex; align-items: center; gap: 16px; }
.acct-avatar { width: 46px; height: 46px; border-radius: 50%; flex: none; background: linear-gradient(135deg, var(--primary), #8f1712); color: #fff; display: flex; align-items: center; justify-content: center; font: 700 18px var(--font); }
.acct-identity__name { font: 800 24px/1.15 var(--font); letter-spacing: -.02em; }

.acct-fields { display: flex; flex-direction: column; gap: 14px; margin-top: 20px; flex: 1; justify-content: space-between; }
/* Name | Display side by side (2026-08-08) — Name has the one Edit button (opens the pop-out
   dialog below); Display is read-only, no button of its own, just reflects whatever was saved. */
.acct-fields-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.acct-field { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: rgba(127,127,127,.06); border: 1px solid var(--line); border-radius: var(--r-input); }
.acct-field__text { min-width: 0; }
.acct-field__label { font: 700 9px var(--mono); letter-spacing: .13em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 3px; }
[data-theme="dark"] .acct-field__label { color: var(--gold); }
.acct-field__value { font: 600 14.5px var(--font); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-field__value--muted { color: var(--ink-4); font-weight: 500; }
.acct-field__action { flex: none; font: 700 11.5px var(--font); color: var(--teal); background: none; border: 0; cursor: pointer; padding: 6px 4px; }
.acct-field__action[disabled] { color: var(--ink-4); cursor: default; }
/* white-space + flex:none keep this on ONE line — "VIA GOOGLE" was wrapping to two inside the
   email row, because a flex item shrinks below its content before its neighbour does. The email
   beside it already truncates, so it is the half that should give way. */
.acct-tag { font: 700 8.5px var(--mono); letter-spacing: .1em; color: var(--ink-4); background: rgba(127,127,127,.1); border: 1px solid var(--line); padding: 3px 8px; border-radius: var(--r-pill); white-space: nowrap; flex: none; }
.acct-tag--inline { margin-left: 6px; vertical-align: 1px; }

/* Name-edit pop-out dialog (2026-08-08) — reuses the .overlay/.sheet component the admin place
   editor and settings sheet already use, not a bespoke modal. */
.sheet--name { margin-top: 78px; width: min(440px, calc(100% - 28px)); }
/* Scoped color overrides (2026-08-08) — declared after .field__label/.edit__close's own base
   rules so they win on source order, not just specificity (house lesson, hit repeatedly). */
.sheet--name .edit__head .field__label { color: var(--gold-ink); }
[data-theme="dark"] .sheet--name .edit__head .field__label { color: var(--gold); }
.sheet--name .edit__close { background: var(--primary); color: var(--on-primary); }
/* Change-password pop-out (v431) — same dialog shell as the name editor, deliberately narrower
   since it's three stacked single-column fields, not a 2-col name row plus a format list.
   Body styling comes entirely from the existing .field/.field__pw/.pw-match components the
   sign-in and reset screens already use; only the shell + head colors are declared here. */
.sheet--pw { margin-top: 78px; width: min(400px, calc(100% - 28px)); }
.sheet--pw .edit__head .field__label { color: var(--gold-ink); }
[data-theme="dark"] .sheet--pw .edit__head .field__label { color: var(--gold); }
.sheet--pw .edit__close { background: var(--primary); color: var(--on-primary); }
/* .field already brings its own 14px top margin; only the first one needs extra air under the head. */
.sheet--pw .field:first-of-type { margin-top: 16px; }

.name-edit__row2 { display: flex; gap: 10px; margin-top: 14px; }
.name-edit__col { flex: 1 1 0; min-width: 0; }
.name-edit__col .field__label { color: var(--teal); }
.name-edit__col .input { width: 100%; margin-top: 4px; }
.name-edit__hint { margin-top: 8px; font: 400 11.5px/1.5 var(--font); color: var(--ink-2); }
.format-label { margin-top: 16px; margin-bottom: 8px; font: 700 9px var(--mono); letter-spacing: .13em; text-transform: uppercase; color: var(--gold-ink); }
[data-theme="dark"] .format-label { color: var(--gold); }
.name-edit__row { margin-top: 16px; display: flex; gap: 10px; }
.name-edit__row .btn { flex: 1; }
/* real "Added by ___" byline, exactly as it renders on a place page — live preview inside the
   dialog so picking a format shows what it actually looks like, not just raw text (owner's ask).
   Centered, half-width. */
.byline-preview { display: flex; align-items: center; gap: 10px; width: fit-content; max-width: 100%; margin: 0 0 12px auto; padding: 10px 12px; background: rgba(35,166,157,.08); border: 1.5px dashed var(--teal); border-radius: var(--r-input); }
.byline-preview__avatar { width: 26px; height: 26px; border-radius: 50%; flex: none; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font: 700 11px var(--font); }
.byline-preview__text { font: 400 13.5px var(--font); color: var(--ink-3); }
.byline-preview__text b { color: var(--ink); font-weight: 700; }

/* Display Name format picker (migration 0020/0021, 2026-08-08) — reuses the .radio circle atom
   from the add-wizard's option-list component, in a lighter row (no icon, two-line body). */
.acct-format-list { display: flex; flex-direction: column; gap: 8px; }
.acct-radio-opt { display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-input); background: rgba(127,127,127,.04); text-align: left; cursor: pointer; }
.acct-radio-opt.is-selected { border-color: var(--primary); background: var(--primary-ring); }
.acct-radio-opt.is-selected .radio { border: 5px solid var(--primary); }
.acct-radio-opt__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.acct-radio-opt__label { font: 700 14px var(--font); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-radio-opt__desc { font: 400 11.5px var(--font); color: var(--ink-3); }

.acct-prefs { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 20px; }
.acct-prefs__full { grid-column: 1 / -1; }
.acct-prefs__hr { grid-column: 1 / -1; border: 0; border-top: 1px solid var(--line); margin: 2px 0; }
/* new component — gold labels here (matches the Account Hub design source), distinct from the
   settings popup's teal .ctl__label (.sheet--nav/.sheet--account) — not a drift, a deliberate,
   separately-scoped convention for this new surface. */
.acct-hub .ctl__label { color: var(--gold-ink); text-transform: uppercase; }
[data-theme="dark"] .acct-hub .ctl__label { color: var(--gold); }
.acct-hub .ctl--block { margin-top: 0; }
.acct-hub .city-dd { width: 100%; margin-top: 8px; }
.acct-hub .city-dd__trigger { width: 100%; justify-content: space-between; }
.ctl__hint { margin-top: 8px; font: 400 12px/1.5 var(--font); color: var(--ink-3); }

.acct-admin-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
.acct-btn--gold { background: var(--gold); color: #171A26; }
.acct-btn--gold:hover { filter: brightness(1.05); }
.acct-btn--line { background: none; border: 1px solid var(--teal); color: var(--teal); }
.acct-btn--line:hover { background: rgba(35,166,157,.1); }

.acct-hub__foot { display: flex; flex-direction: column; gap: 12px; }
.acct-hub__signout { width: 100%; }
/* Live since v581. `cursor: not-allowed` came from the disabled stub this replaced — a real
   control that still reads as forbidden is worse than no control. Left muted on purpose though:
   it must be findable without being invited. */
.acct-danger-link { align-self: flex-end; background: none; border: 0; padding: 0; font: 500 11.5px var(--font); text-decoration: underline; color: var(--ink-4); cursor: pointer; }
.acct-danger-link:hover { color: var(--primary); }

/* Your Places — 4-category compact grid (stage 2, 2026-08-08), desktop Account Hub only. Mobile
   (below) uses .acct-group/.acct-row instead — a single stacked column suits a narrow screen better
   than 4 side-by-side columns. */
.acct-hub .saved-list { margin-top: 16px; display: block; }
.acct-hub .saved-list[hidden] { display: none; }
.cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; }
@media (max-width: 900px) { .cols { grid-template-columns: 1fr 1fr; row-gap: 18px; } }
.col { padding: 0 14px; border-left: 1px solid var(--line-soft); }
.col:first-child { padding-left: 0; border-left: 0; }
.col:last-child { padding-right: 0; }
@media (max-width: 900px) { .col:nth-child(odd) { padding-left: 0; border-left: 0; } .col:nth-child(even) { padding-right: 0; } }
.col__h { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.col__h .nav-ico { width: 24px; height: 24px; flex: none; }
.col__h-text { font: 700 15px var(--font); color: var(--ink); letter-spacing: -.01em; }
.col__row { display: flex; align-items: flex-start; justify-content: space-between; gap: 6px; padding: 8px 0; border-top: 1px solid var(--line-soft); min-height: 44px; box-sizing: border-box; }
.col__row:first-of-type { border-top: 0; }
.col__name { font: 600 12.5px/1.3 var(--font); color: var(--ink); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.col__heart { font: 400 14px var(--font); color: var(--ink-4); flex: none; margin-top: 1px; }
.col__heart.is-on { color: var(--primary); }
.col__empty { font-size: 11.5px; color: var(--ink-4); font-style: italic; padding: 8px 0; min-height: 44px; box-sizing: border-box; display: flex; align-items: center; }

/* Notification toggle pills (stage 3, migration 0019) */
.notif-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 700px) { .notif-row { grid-template-columns: 1fr; } }
.toggle-pill { display: flex; align-items: center; justify-content: space-between; gap: 10px; background: rgba(127,127,127,.06); border: 1px solid var(--line); border-radius: var(--r-input); padding: 14px 16px; }
.toggle-pill__label { font: 700 14px var(--font); display: flex; align-items: center; }
.switch { width: 40px; height: 22px; border-radius: 999px; background: rgba(127,127,127,.25); position: relative; flex: none; cursor: pointer; transition: background .15s; }
.switch.is-on { background: var(--primary); }
.switch__knob { width: 16px; height: 16px; border-radius: 50%; background: #fff; position: absolute; top: 3px; left: 3px; transition: left .15s; }
.switch.is-on .switch__knob { left: 21px; }

/* "?" help button — used on the notification toggles. Opens a real centered overlay (below) rather
   than a hover/focus-anchored tooltip (2026-08-09) — the old version opened off-center and
   overlapped the toggle-pills below it on mobile, with no visible way to close it. */
.qhelp__q {
  width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid var(--gold-ink); color: var(--gold-ink);
  font: 700 10px var(--font); display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; background: none; padding: 0; margin-left: 6px; vertical-align: -2px;
}
[data-theme="dark"] .qhelp__q { border-color: var(--gold); color: var(--gold); }

/* Mobile Account Hub (2026-08-08, "Wayfinder - Profile & Settings Mobile" from Claude Design) —
   reuses every .acct-* card class above; these are the mobile-only additions. The @media threshold
   here matches isDesktopWidth()'s own matchMedia('(min-width:760px)') JS gate, so none of this can
   ever be visible on the desktop hub, which never renders these classes to begin with. */
/* sticky under the site header (same pattern as .listing-head) — the title row (with its ‹ back
   button) AND the jump chips stay reachable while scrolling through the hub. First shipped as
   .acct-chips alone being sticky; the owner then pointed out the title/back-button row should stay
   too — with a fixed site header + a normal (non-sticky) hamburger fallback removed on this page,
   the back chevron here was the only way out, and it needs to stay visible the whole time you
   scroll, not just once you scroll back up. Both live inside one sticky wrapper rather than two
   separately-offset sticky elements, so they can't drift apart if either one's height changes. */
/* padding-top baked in HERE, not left on .acct-hub's own padding-top (28px, still used by desktop's
   first child .acct-hub__row) — a sticky element ignores its normal-flow ancestor's padding once it
   reaches its stuck position, so leaving the breathing room on .acct-hub only held on initial paint
   (before any scroll) and collapsed the instant the row became truly stuck under the header, a
   visible ~28px jump on the very first chip tap (owner caught live: "should not move at all").
   Baking the same 28px into the sticky element's own box means it's identical whether flowing
   normally or pinned — nothing to collapse. */
.acct-hub__sticky { position: sticky; top: var(--header-h, 56px); z-index: 30; background: var(--bg); padding-top: 28px; padding-bottom: 4px; }
.acct-hub__sticky .hub-head { margin-bottom: 0; }
@media (max-width: 759px) {
  /* neutralize .acct-hub's own padding-top on mobile only — desktop's first child (.acct-hub__row)
     isn't sticky and still needs it; mobile's is baked into .acct-hub__sticky above instead. */
  body[data-page="account"] .acct-hub { padding-top: 0; }
}
.acct-chips {
  display: flex; gap: 8px; overflow-x: auto; padding: 10px 0 2px; margin: 0;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.acct-chips::-webkit-scrollbar { display: none; }
.acct-chip {
  flex: none; font: 700 10.5px var(--mono); letter-spacing: .08em; text-transform: uppercase;
  padding: 9px 14px; border-radius: var(--r-pill); border: 1px solid var(--line-strong);
  background: rgba(127,127,127,.05); color: var(--ink-3); cursor: pointer;
}
.acct-chip.is-active { border-color: var(--gold); background: rgba(245,194,66,.14); color: var(--gold-ink); }
[data-theme="dark"] .acct-chip.is-active { color: var(--gold); }
/* a tapped chip is a <button> with no focus styling of its own, so it was picking up the browser's
   UA-default red/blue focus ring on top of (and clashing with) the .is-active gold border. Replace
   with an intentional gold focus-visible ring — keyboard nav still gets a visible indicator, mouse/
   touch taps don't show the browser default. */
.acct-chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
.acct-chip:focus:not(:focus-visible) { outline: none; }
/* jump-target offset is computed in JS at click time (acct-jump handler) against the sticky title+
   chips block's actual rendered height — a static scroll-margin-top here only ever accounted for
   the site header, leaving the target's top edge parked under the taller sticky block once the
   title row joined the chips there. Removed rather than left alongside the JS fix to avoid two
   offset mechanisms silently fighting over the same scroll position. */

.acct-group + .acct-group { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line-soft); }
.acct-group__empty { font-size: 12.5px; color: var(--ink-4); font-style: italic; padding: 4px 0 2px; }
.acct-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 0; min-height: 44px; box-sizing: border-box; border-top: 1px solid var(--line-soft); }
.acct-row:first-of-type { border-top: 0; }
.acct-row__name { font: 600 14px/1.3 var(--font); color: var(--ink); }
.acct-row__heart { font: 400 16px var(--font); color: var(--ink-4); flex: none; }
.acct-row__heart.is-on { color: var(--primary); }

@media (max-width: 759px) {
  .acct-admin-actions { grid-template-columns: 1fr; }
}

/* contact extras */
.contact-alt { margin-top: 14px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding: 0 4px; }
.contact-alt .lbl { font: 500 8.5px var(--mono); letter-spacing: .14em; color: var(--ink-4); }
.contact-alt .h { font: 500 12.5px var(--font); }
.suggest-cta { margin-top: 16px; display: flex; align-items: center; gap: 12px; background: var(--card); border: 1px solid var(--line); border-radius: var(--r-card); padding: 14px 16px; width: 100%; text-align: left; }
.suggest-cta__t { display: block; font: 600 14px var(--font); letter-spacing: -.01em; }
.suggest-cta__p { display: block; margin-top: 2px; font: 400 11.5px var(--font); color: var(--ink-3); }
.suggest-cta__arrow { margin-left: auto; font: 500 14px var(--font); color: var(--primary); }

/* ---------- settings sheet (overlay under profile) ---------- */
.overlay {
  position: fixed; inset: 0; z-index: 60; display: none;
  background: rgba(12,13,18,.55); backdrop-filter: blur(2px);
  align-items: flex-start; justify-content: center;
}
.overlay.is-open { display: flex; }
.sheet {
  margin-top: 78px; width: min(420px, calc(100% - 28px));
  background: var(--sheet); backdrop-filter: blur(14px);
  border: 1px solid var(--sheet-line); border-radius: var(--r-sheet);
  padding: 18px 18px 20px; box-shadow: var(--shadow-pop);
  animation: sheet-in .18s ease;
}
@keyframes sheet-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }

/* qhelp overlay — reuses the app's standard .overlay/.sheet pattern (backdrop, click-outside-to-
   close). Declared AFTER the base .overlay/.sheet rules (2026-08-09 fix) — an earlier placement
   above them lost the cascade on source order (equal-specificity single-class rules, whichever
   comes LAST in the file wins), so overrides here were silently no-op'd — the exact recurring bug
   class this file has scars from elsewhere. Card is TOP-anchored (align-items:flex-start +
   margin-top:66vh, not a margin on a centered item — a margin on an align-items:center child
   participates in the centering math and lands somewhere unintended) so it sits in the lower third
   of the screen and grows downward (owner's explicit spec, 2026-08-09), not screen-centered.
   overflow-y:auto on the overlay + padding-bottom so a long hint that would run the card off the
   bottom (34vh of room left below the 66vh top) stays reachable by scrolling instead of clipping
   the ✕. */
.overlay--qhelp { align-items: flex-start; overflow-y: auto; padding-bottom: 24px; }
.sheet--qhelp { position: relative; width: min(320px, calc(100% - 40px)); margin-top: 66vh; padding: 24px 22px 20px; text-align: left; }
.sheet--qhelp__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.sheet--qhelp__title { font: 700 15px var(--font); color: var(--gold-ink); }
[data-theme="dark"] .sheet--qhelp__title { color: var(--gold); }
.sheet--qhelp__text { font: 500 14px/1.6 var(--font); color: var(--ink); }
.sheet--qhelp__close {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  border: 1.5px solid var(--line-strong); background: var(--card); color: var(--ink-2); cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 13px;
}
.sheet--qhelp__close:hover { border-color: var(--primary); color: var(--primary); }
.sheet__label { font: 500 8.5px var(--mono); letter-spacing: .18em; color: var(--ink-4); margin-top: 16px; }
.sheet__label:first-child { margin-top: 0; }
.seg { margin-top: 8px; display: flex; background: var(--primary-soft); border-radius: var(--r-pill); padding: 3px; }
[data-theme="light"] .seg { background: rgba(25,26,34,.06); }
.seg button { flex: 1; display: flex; align-items: center; justify-content: center; gap: 7px; text-align: center; padding: 8px 0; border-radius: var(--r-pill); font: 500 12px var(--font); color: var(--ink-2); }
.seg button.is-active { background: var(--primary); color: var(--on-primary); font-weight: 700; }
.seg .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
.seg .dot--ring { background: none; border: 2px solid currentColor; box-sizing: border-box; }
.sheet__cities { margin-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
.sheet__cities .city-chip { padding: 9px 12px; font-size: 12px; border-radius: 11px; }
.sheet__persona { margin-top: 16px; border-top: 1px solid var(--sheet-line); padding-top: 14px; display: flex; align-items: center; gap: 11px; }
.sheet__persona .lbl { font: 500 13px var(--font); color: var(--ink-3); }
.sheet__persona .lbl.is-active { color: var(--ink); font-weight: 600; }
.toggle { width: 46px; height: 26px; border-radius: var(--r-pill); background: var(--primary-soft); position: relative; flex: none; }
[data-theme="light"] .toggle { background: rgba(25,26,34,.12); }
.toggle__knob { position: absolute; left: 3px; top: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 12px var(--primary-ring); transition: left .18s ease; }
.toggle.is-on .toggle__knob { left: 23px; }

/* compact settings controls — 3-up row: language / theme / you */
.sheet__controls { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-top: 14px; }   /* was missing entirely — LANGUAGE/THEME sat flush against the header divider */
.ctl__label { font: 500 8px var(--mono); letter-spacing: .14em; color: var(--ink-4); margin-bottom: 6px; text-align: center; }
.seg--mini { padding: 3px; }
.seg--mini button { padding: 8px 0; font: 600 12px var(--font); }
.seg--icon button { padding: 7px 0; }
.seg--icon svg { width: 16px; height: 16px; display: block; margin: 0 auto; }
/* ---------- custom city dropdown (settings sheet) ----------
   Native <select> strands the chevron at the widest-option width; this trigger sizes to
   its content so the chevron hugs the value, and opens a styled, theme-aware panel. */
.city-dd { position: relative; align-self: flex-start; margin-top: 8px; }
.city-dd__trigger {
  display: inline-flex; align-items: center; gap: 10px;
  background: var(--field-bg); border: 1px solid var(--field-line);
  border-radius: var(--r-input); padding: 12px 15px;
  font: 500 16px var(--font); color: var(--ink); cursor: pointer;
}
.city-dd__trigger:hover { border-color: var(--line-strong); }
.city-dd__trigger:focus-visible { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
.city-dd__chev { width: 18px; height: 18px; color: var(--ink-3); flex: none; }
.sheet--nav .city-dd__value, .sheet--account .city-dd__value, .acct-hub .city-dd__value { color: var(--gold-ink); }   /* nav/settings-sheet + account-hub closed trigger shows the chosen city in gold, matching .city-dd__opt.is-current in the open panel — scoped off the header pin dropdown, which keeps its own dark-pill/white-text styling */
[data-theme="dark"] .sheet--nav .city-dd__value, [data-theme="dark"] .sheet--account .city-dd__value, [data-theme="dark"] .acct-hub .city-dd__value { color: var(--gold); }
/* "Pick Your City" (no city stored yet) — gold means "the city you've chosen," so this one link
   stays teal instead, both themes (--teal has no dark override). Declared after the gold rule so
   the extra class wins the cascade regardless of specificity math. Widened to .acct-hub (2026-08-08)
   — a signed-in, city-less first-timer lands on the hub with no other city-picker UI reachable (the
   nav-sheet has none, and signed-in users now skip the settings popup's own "Pick Your City" link
   entirely), so this dropdown is their only way out; it must not lie and show the Hải Phòng default
   as an already-made choice. */
.sheet--nav .city-dd__value--pick, .sheet--account .city-dd__value--pick, .acct-hub .city-dd__value--pick { color: var(--teal); }
.city-dd__panel {
  position: absolute; left: 0; top: calc(100% + 6px); z-index: 40; min-width: 100%;
  background: #FFFFFF; border: 1px solid var(--field-line); border-radius: var(--r-input);
  box-shadow: 0 10px 28px rgba(0,0,0,.35); padding: 6px; max-height: 264px; overflow-y: auto;
}
[data-theme="dark"] .city-dd__panel { background: #1C1F2A; }   /* opaque — a menu must not let content bleed through */
/* region group label — same brand treatment as the landing chooser's .region__label + the dropdown's
   own Coming-Soon labels: teal in light, gold in dark (NOT the faint --ink-4 grey it was). */
.city-dd__region { padding: 10px 11px 7px 2px; margin-top: 12px; margin-bottom: 4px; border-bottom: 1px solid var(--field-line); font: 500 10px var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--gold-ink); white-space: nowrap; }   /* owner: GOLD (gold-ink light, gold dark); left-flush (2px) so the title sits LEFT of the 11px-indented city list; margin-top = larger gap from the previous region; rule BELOW the header; nowrap widens the panel */
[data-theme="dark"] .city-dd__region { color: var(--gold); }
.city-dd__region:first-child { margin-top: 0; padding-top: 4px; }   /* no gap at the panel top */
.city-dd__opt {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  width: 100%; text-align: left; background: none; border: 0;
  padding: 9px 11px; border-radius: 9px; font: 400 15px var(--font); color: var(--ink);
  white-space: nowrap; cursor: pointer;
}
.city-dd__opt:hover { background: var(--primary-soft); }
/* selected city = GOLD (not red — red read as "bad"). gold-ink on the light panel for legibility, gold on dark. */
.city-dd__opt.is-current { color: var(--gold-ink); font-weight: 600; }
[data-theme="dark"] .city-dd__opt.is-current { color: var(--gold); }
.city-dd__opt--soon { color: var(--ink-4); cursor: default; }
.city-dd__opt--soon:hover { background: none; }
.city-dd__soon { font: 500 8.5px var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--teal); }   /* owner: teal in BOTH modes */
.logout-btn { display: inline-flex; width: auto; gap: 7px; padding: 8px 14px; font-size: 12px; margin-top: 14px; }
.logout-btn svg { width: 15px; height: 15px; }
.sheet__ver { margin-top: 14px; text-align: center; font: 500 8.5px var(--mono); letter-spacing: .12em; color: var(--ink-5); }

/* admin single-pane place editor */
/* DESTINATION picker — the wizard's region-grouped chip grid, not a <select>. A 13-option native
   menu inside a modal opens past the bottom of the window (shipped that way in v488, reported the
   same day). Chips have no popup to overflow and behave identically on both breakpoints.
   Columns auto-fill so the wider desktop sheet gets more per row instead of three fat ones. */
/* DESTINATION dropdown. Full width so the field keeps the one-line symmetry of the two name
   inputs above it; the region titles are large enough to actually divide the list, and the names
   are indented well clear of the panel edge so they read as sitting under their region. */
.city-dd--edit { width: 100%; margin-top: 8px; }
.city-dd--edit .city-dd__trigger { width: 100%; justify-content: space-between; }
.city-dd--edit .city-dd__value { color: var(--gold-ink); font-weight: 600; }
[data-theme="dark"] .city-dd--edit .city-dd__value { color: var(--gold); }
.city-dd--edit .city-dd__region { padding: 10px 10px 8px 12px; margin-bottom: 5px;
  font: 700 14px var(--mono); letter-spacing: .14em; }
.city-dd--edit .city-dd__opt { padding-left: 26px; }
.overlay--edit { align-items: flex-start; overflow-y: auto; padding: 22px 0 44px; }
.sheet--edit { margin-top: 22px; width: min(560px, calc(100% - 28px)); }
.edit__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
/* reuses .detail__title (the big bold place-name style) for "Edit" — zero its 7px top margin
   (tuned for its detail-page spot under the removed eyebrow) so it doesn't throw off centering
   against the close button in this flex row */
.edit__title { margin-top: 0; }
.edit__close { width: 30px; height: 30px; border-radius: 50%; background: var(--primary-soft); color: var(--ink-2); font: 400 14px/1 var(--font); flex: none; }
.edit__foot { display: flex; align-items: center; gap: 10px; margin-top: 22px; }
.btn--danger { border: 1px solid var(--primary); color: var(--primary); font-weight: 600; font-size: 13px; padding: 12px 18px; background: none; }

/* ---------- review queue (admin / curator) ---------- */
.rq-group { margin-top: 20px; }
.btn--sm { padding: 7px 13px; font-size: 12px; }
/* Edit moved onto the byline row, right-justified (byline stays left) */
.added-by .edit-pencil { margin-left: auto; }
.edit-pencil { display: inline-flex; align-items: center; gap: 5px; flex: none; cursor: pointer; background: none;
  font: 600 11px var(--font); color: var(--teal); border: 1px solid var(--line-strong); border-radius: var(--r-pill); padding: 6px 12px; }
[data-theme="dark"] .edit-pencil { color: var(--gold-ink); }
/* editor "More photos" gallery */
.edit-gallery { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.edit-gallery__item { position: relative; width: 66px; height: 66px; border-radius: 10px; overflow: hidden; background: rgba(127,127,127,.14); flex: none; }
.edit-gallery__item img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.edit-gallery__rm { position: absolute; top: 3px; right: 3px; width: 20px; height: 20px; border-radius: 50%; background: rgba(0,0,0,.6); color: #fff; font: 400 11px/1 var(--font); display: flex; align-items: center; justify-content: center; cursor: pointer; }
.edit-gallery__add { width: 66px; height: 66px; border-radius: 10px; border: 1.5px dashed var(--line-strong); display: flex; align-items: center; justify-content: center; cursor: pointer; flex: none; font: 600 11px var(--font); color: var(--ink-3); position: relative; overflow: hidden; }
/* unified editor photo pool — one grid, ★ crowns the main, ✕ removes; add tile = multi-picker */
.field__hint { font: 400 8.5px var(--mono); letter-spacing: .1em; color: var(--ink-3); margin-left: 6px; }
.edit-photos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.edit-photos__item { position: relative; width: 74px; height: 74px; border-radius: 10px; overflow: hidden; background: rgba(127,127,127,.14); flex: none; }
.edit-photos__item img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.edit-photos__item.is-main { outline: 2px solid var(--gold); outline-offset: -2px; }
.edit-photos__badge { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--gold); color: #1F1F1F; font: 700 11px/1 var(--font); display: flex; align-items: center; justify-content: center; }
.edit-photos__rm { position: absolute; top: 3px; right: 3px; width: 20px; height: 20px; border-radius: 50%; background: rgba(0,0,0,.6); color: #fff; font: 400 11px/1 var(--font); display: flex; align-items: center; justify-content: center; cursor: pointer; }
/* ‹ › reorder bar along the tile bottom — front tile = main, so moving to slot 1 crowns it */
.edit-photos__move { position: absolute; left: 0; right: 0; bottom: 0; display: flex; }
.edit-photos__mv { flex: 1; height: 22px; border: 0; background: rgba(0,0,0,.55); color: #fff; font: 400 15px/1 var(--font); display: flex; align-items: center; justify-content: center; cursor: pointer; }
.edit-photos__mv:first-child { border-right: 1px solid rgba(255,255,255,.25); }
.edit-photos__mv:hover:not([disabled]) { background: var(--gold); color: #1F1F1F; }
.edit-photos__mv[disabled] { opacity: .3; cursor: default; }
.edit-photos__add { width: 74px; height: 74px; border-radius: 10px; border: 1.5px dashed var(--line-strong); display: flex; align-items: center; justify-content: center; cursor: pointer; flex: none; font: 600 11px var(--font); color: var(--ink-3); position: relative; overflow: hidden; }
.edit-photos__add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
/* ---------- nav sheet + settings sheet — "2a Hybrid" redesign ----------
   (Claude Design, Wayfinder - Menu Tweak.dc.html; DesignSync'd 2026-08-05, advisor-reviewed).
   Both sheets share a head row (brand/status eyebrows), a bottom version+close row, and (settings
   only) the 2-up language/theme + full-width toggle rows. Colours map the doc's dark-mode hex onto
   existing tokens — NOT pasted verbatim — so light mode reads correctly: #DA251D→--primary,
   #F5C242→--gold(-ink), #23A69D→--teal, #3E7DC4/#1F56AB→--explore (already light/dark aware). */
.sheet--nav, .sheet--account { width: min(330px, calc(100% - 28px)); }
.nav-sheet__head, .sheet--account .nav-sheet__head {
  display: flex; align-items: baseline; gap: 10px;
  padding-bottom: 14px; border-bottom: 1px solid var(--sheet-line);
}
.nav-sheet__eyebrow { font: 600 10px var(--mono); letter-spacing: .2em; color: var(--gold-ink); }
[data-theme="dark"] .nav-sheet__eyebrow { color: var(--gold); }
.nav-sheet__eyebrow--city { margin-left: auto; font-weight: 500; color: var(--ink-4); }
/* solid-gold promoted CTA — "Preferences ⚙ Sign-In". The gear sits between the two words rather
   than replacing them: a bare gear was tried and it left a signed-out visitor with no visible way
   in, since this bar is the only route to Sign In. */
.nav-sheet__cta {
  /* GRID, not flex: centred flex items sit either side of the group's midpoint, so the gear landed
     off-centre whenever the two words were different widths ("Preferences" vs "Sign-In"). Equal
     1fr columns put the gear on the button's true centre and the words the same distance from it. */
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; column-gap: 10px;
  width: 100%; margin-top: 14px; padding: 12px 0;
  border: 2px solid var(--gold); border-radius: var(--r-pill); background: var(--gold);
  font: 600 14px var(--font); color: #171A26; cursor: pointer;
}
/* each word centred in its own half, not hugging the gear — so the three parts read as evenly
   spaced across the whole bar */
.nav-sheet__cta > span { text-align: center; }
/* 27px = 18 +50%. The bar's height comes from its own padding and the 14px label, both of which
   are taller than the gear's row box would need — but a 27px item WOULD grow it, so the gear is
   pulled out of the height calculation with a fixed 18px row box and allowed to overflow it. */
.nav-sheet__cta svg { width: 27px; height: 27px; display: block; margin: -4.5px 0; }
/* who is holding the phone — sits between the head and the tiles */
.nav-who { display: flex; align-items: center; gap: 11px; margin-top: 14px; }
.nav-who__av {
  flex: none; width: 46px; height: 46px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--gold); color: #171A26; font: 700 18px var(--font); letter-spacing: .02em;
}
.nav-who__name { min-width: 0; flex: 1; font: 700 17px var(--font); letter-spacing: -.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-who__tier { flex: none; font: 600 10px var(--mono); letter-spacing: .18em; color: var(--gold-ink); }

/* 2x2 section tiles — colour wash bg, icon square + name/count column */
.nav-sheet__tiles { margin-top: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.nav-sheet__tile {
  display: flex; align-items: center; gap: 9px; padding: 12px 10px;
  border-radius: 16px; text-decoration: none; color: var(--ink);
}
.nav-sheet__tile .nav-ico { width: 44px; height: 44px; flex: none; }
.nav-sheet__tile-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; align-items: center; text-align: center; }
.nav-sheet__tile-name { font: 700 19px var(--font); letter-spacing: -.01em; }
.nav-sheet__tile-count { font: 400 9px var(--mono); letter-spacing: .04em; color: var(--ink-3); }
.nav-sheet__tile--eat     { background: rgba(218,37,29,.13); }
.nav-sheet__tile--drink   { background: rgba(245,194,66,.15); }
.nav-sheet__tile--stay,
.nav-sheet__tile--live    { background: rgba(35,166,157,.14); }
.nav-sheet__tile--explore { background: rgba(31,86,171,.13); }
[data-theme="dark"] .nav-sheet__tile--explore { background: rgba(62,125,196,.15); }

/* secondary link grid — About/Contact/Services/[Review queue]/Suggest Place, 2 cols, all links the
   same size (divider above is the preceding .sheet__div, not baked in here) */
.nav-sheet__more { margin-top: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.nav-sheet__more-link, .nav-sheet__more-link:visited {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  border: 1px solid var(--line-strong); border-radius: 12px; padding: 11px 0;
  text-align: center; font: 600 14px var(--font); color: var(--ink); text-decoration: none;
}
/* Services row (2026-08-12): a name + chevron hint that it opens onto more, plus a subtitle naming
   what's inside (Itinerary/Tours) — so the sheet itself signals there's something under the hood
   before you tap it, without a second interaction state (see the soon-service-list note in app.js
   for why this isn't an in-sheet expand/accordion). Declared after the base rule above (source-order
   safe) though nothing here overrides it — .nav-sheet__more-link-* are new, scoped classes. */
.nav-sheet__more-link-name { display: flex; align-items: center; gap: 3px; }
.nav-sheet__more-link-chev { width: 13px; height: 13px; flex: 0 0 auto; opacity: .6; }
.nav-sheet__more-link-sub { font: 400 10.5px var(--mono); letter-spacing: .02em; color: var(--ink-4); text-transform: none; }
/* Review Queue: full width under the 2-up rows, taller for its second line, and FILLED teal.
   Outlined, its note read as text floating under an empty tile rather than part of the button. */
.nav-sheet__more-link--wide, .nav-sheet__more-link--wide:visited {
  grid-column: 1 / -1; padding: 13px 0;
  background: var(--teal); border-color: var(--teal); color: #171A26;
}
.nav-sheet__more-link--wide .nav-sheet__more-link-name { color: #171A26; }
.nav-sheet__more-link--wide:hover { filter: brightness(1.06); }
/* the grid is 2-up; a divider has to be told to cross both columns */
.nav-sheet__more-rule { grid-column: 1 / -1; margin: 6px 0 2px; }

/* the promoted link — "Suggest a Place", or "Add a Place" for anyone who can reach the wizard.
   Teal outline and teal text: it is the one thing in this menu asking something of the reader,
   and teal keeps it distinct from the red used for destructive and primary actions elsewhere. */
/* ⚠ :visited MUST be in this selector. The base rule is `.nav-sheet__more-link,
   .nav-sheet__more-link:visited`, and once suggest.html/add.html has been opened once, that
   :visited half (0,2,0) outweighs a plain `.nav-sheet__more-link--cta` (0,1,0) and puts the white
   border back. Same reason the --wide rule below carries it. */
.nav-sheet__more-link--cta, .nav-sheet__more-link--cta:visited {
  border-color: var(--teal); color: var(--teal);
}
.nav-sheet__more-link--cta .nav-sheet__more-link-name { color: var(--teal); }
.nav-sheet__more-link--cta:hover { background: rgba(35, 166, 157, .12); }

.nav-sheet__foot { position: relative; display: flex; align-items: center; justify-content: center; margin-top: 14px; min-height: 42px; font: 400 10px var(--mono); letter-spacing: .1em; color: var(--ink-4); }
/* filled red disc, bold white ✕ — was an outlined ring with a red glyph */
.nav-sheet__close {
  position: absolute; right: 0; top: 50%; translate: 0 -50%;
  width: 42px; height: 42px; border-radius: 50%; border: 0;
  background: var(--primary); color: #fff; font: 700 18px/1 var(--font);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: filter .15s;
}
.nav-sheet__close:hover { filter: brightness(1.08); }

/* settings sheet specifics */
.sheet__controls--2up { grid-template-columns: 1fr 1fr; }
.ctl--block { margin-top: 14px; }
.ctl__label--left { text-align: left; }
.sheet--nav .ctl__label { color: var(--teal); }   /* nav-sheet "YOUR CITY" label — brand teal, not the muted grey used elsewhere */
.sheet--account .ctl__label { color: var(--teal); }   /* settings sheet LANGUAGE/THEME/YOU ARE/YOUR CITY labels — brand teal */
.ctl--block .seg { margin-top: 8px; }   /* was 6 — matches the base .seg label→control gap used by LANGUAGE/THEME */
.ctl--block .city-dd { margin-top: 8px; width: 100%; }   /* was 6, same reason */
.ctl--block .city-dd__trigger { width: 100%; justify-content: space-between; }
.sheet__div { margin-top: 14px; border-top: 1px solid var(--sheet-line); }
.sheet__greet { margin-top: 14px; text-align: center; }
.sheet__greet-hi { font: 700 20px var(--font); letter-spacing: -.01em; }
.sheet__greet-sub { margin-top: 2px; font: 400 13px var(--font); color: var(--ink-3); }
.sheet__signin { margin-top: 14px; }   /* was 2 — matches the 14px rhythm used elsewhere in the sheet, was reading cramped against the subtext above */
.sheet__or { margin: 14px 0; text-align: center; font: 500 9px var(--mono); letter-spacing: .2em; color: var(--teal); }   /* was 10px 0, same rhythm fix */
.btn--teal { background: var(--teal); color: #fff; }
.btn--teal:hover { filter: brightness(1.05); }

/* ---------- mobile bottom tab bar (in-city category switch) ---------- */
.tabbar { display: none; }
@media (max-width: 759px) {
  .tabbar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
    display: flex; align-items: stretch;
    background: var(--header-flat); border-top: 1px solid var(--header-flat-line);
    /* CONSTANT inset (no env()) so the bar height NEVER changes as the iOS home-screen web view's
       toolbar shows/hides — 22px clears the home-indicator gesture strip on notched phones */
    padding: 0 16px 22px;   /* side padding keeps the far-left/right icons off the screen edge + pulls the row in a bit */
    box-shadow: 0 -3px 14px rgba(0,0,0,.14);
  }
  .tabbar__item {
    flex: 1 1 0; min-width: 0; min-height: 44px; box-sizing: border-box;
    display: flex; align-items: center; justify-content: center;
    padding: 9px 2px; text-decoration: none; color: var(--ink-2);
  }
  .tabbar__item .nav-ico { width: 32.2px; height: 32.2px; opacity: .5; transition: opacity .12s ease; }   /* 28 +15% */
  .tabbar__item.is-active .nav-ico { opacity: 1; }
  .tabbar__item.is-active { color: var(--ink); }
  .tabbar__item.is-active .nav-ico { opacity: 1; }
  /* The label is in the markup for EVERY variant so a screen reader is told the same thing
     whichever is on — CSS decides who SEES it. Hidden on the shipped bar. */
  .tabbar__label { display: none; line-height: 1; white-space: nowrap; }

  /* ============================================================================
     THE FLOATING BOTTOM NAV  (Preferences → BOTTOM NAV → Float)
     A CAPSULE, not a band: inset from all three edges, content runs underneath
     and around it, NO top border (a gold hairline traces the whole shape), and
     the glass is real. The brand colour TILES are unchanged — only the container.
     Off / Fixed / Float is a per-device choice; `fixed` is the plain bar above.
     ⚠ Height must stay near 72px including the bottom inset, because
     `body.has-tabbar .app { padding-bottom: 84px }` is a CONSTANT.
     ============================================================================ */
  .tabbar--float {
    /* ⚠ PERCENTAGE, not a pixel calc(). A fixed 20px gutter is 5.1% of a 390pt phone and 4.6%
       of a Pro Max, so the bar creeps toward the edges as the screen grows. */
    width: 86%; max-width: 520px;
    left: 0; right: 0; bottom: 20px;
    margin: 0 auto;
    /* ⚠ The vertical padding is what makes the selector look INSET. Without it the pill fills
       the capsule's full height and reads as a block, not a floating selection. */
    padding: 6px 7px;
    position: fixed;   /* restated: the selector is absolutely positioned against this box */
    /* ⚠ 1px, NOT 0.5px. A sub-pixel border cannot be antialiased around an arc — the corners
       went speckled while the flats stayed crisp. A LIGHTER edge is a lighter COLOUR on a
       whole pixel, which is what the alpha below is for. */
    border: 1px solid rgba(245,194,66,.62);
    border-radius: 999px;
    background: rgba(255,255,255,.45);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
            backdrop-filter: saturate(180%) blur(20px);
    box-shadow: 0 8px 30px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.10);
    transition: transform .26s cubic-bezier(.4,0,.2,1), opacity .26s ease,
                border-color .26s ease;
    transform-origin: 50% 100%;
    will-change: transform;
  }
  /* ⚠ NOT a prefers-color-scheme media query — the theme is a [data-theme] stamp written by
     boot.js, so a dark OS with the light theme chosen would get the wrong one. */
  [data-theme="dark"] .tabbar--float {
    background: rgba(20,22,32,.45);
    border-color: rgba(245,194,66,.5);
    box-shadow: 0 8px 30px rgba(0,0,0,.44), 0 2px 8px rgba(0,0,0,.3);
  }
  .tabbar--float .tabbar__item {
    flex: 1 1 0; min-width: 0; padding: 9px 4px; border-radius: 999px;
    position: relative; z-index: 1;
    transition: background-color .18s ease;
  }
  /* ⚠ The lit tile grows with TRANSFORM, never width/height. Changing the box would change the
     item's width, and the sliding selector measures item widths — every tap would then size the
     selector off the wrong number, because the tapped tile is still the small one when measured. */
  .tabbar--float .tabbar__item .nav-ico {
    width: 35px; height: 35px; opacity: .5;
    transition: transform .3s cubic-bezier(.32,.72,0,1), opacity .18s ease;
  }
  .tabbar--float .tabbar__item.is-active .nav-ico { opacity: 1; transform: scale(1.286); }
  /* The tap is acknowledged the instant it lands — the tile takes the press before the selector
     has finished travelling to it, so nothing feels unheard during the slide. */
  .tabbar__item.is-pressing .nav-ico { opacity: 1; transform: scale(1.286); }
  .tabbar__item.is-pressing { pointer-events: none; }

  /* THE SELECTOR — one element that slides, inset off the capsule's top and bottom edges.
     ⚠ It must NOT be a background on .is-active: a background can only appear and disappear,
     and this has to travel. */
  .tabbar__pill {
    position: absolute; top: 6px; bottom: 6px; left: 0; width: 0;
    /* ⚠ border-box, because app.js sets an explicit pixel width. */
    box-sizing: border-box; border: 0;
    /* A rounded RECTANGLE, not a stadium — 999px on a short wide box is a fat circle-ended blob. */
    border-radius: 14px; background: rgba(0,0,0,.075);
    pointer-events: none; z-index: 0;
    transition: transform .42s cubic-bezier(.32,.72,0,1), width .42s cubic-bezier(.32,.72,0,1);
  }
  [data-theme="dark"] .tabbar__pill { background: rgba(255,255,255,.14); }
  /* ⚠ The FIRST placement must not animate — a selector that starts at x=0 would visibly slide
     in from the left edge on every page load. app.js adds .is-placed after the first frame. */
  .tabbar__pill:not(.is-placed) { transition: none; }

  /* NOT IN USE — shrinks and fades, stays put, stays tappable. Reached two ways: scrolling down,
     or sitting still (see tabWake). Asleep, the gold edge drops back to a plain hairline, so the
     colour only ever means "this bar is live". */
  .tabbar--float.is-idle { transform: scale(.72); opacity: .45; border-color: rgba(0,0,0,.16); }
  [data-theme="dark"] .tabbar--float.is-idle { border-color: rgba(255,255,255,.2); }
  
  body.has-tabbar .site-footer { display: none; }
  body.has-tabbar .app { padding-bottom: 84px; }   /* constant — matches the constant-height bar so content never shifts */
}
.nav-sheet__link:last-child { border-bottom: 0; }
.nav-sheet__link:hover { color: var(--primary); }

/* ---------- "New Version Available" card (v434, redesigned v440) ----------
   Rides the shared `.overlay` (dim + blur + z-60), so it clears the mobile tab bar for free and
   needs no bottom-offset maths of its own — that was the first version's whole problem.
   The lockup is the splash's stacked treatment rather than the sign-in page's horizontal one:
   badge, then VIETNAM over a hairline over WAYFINDER. Colours differ from the splash by necessity
   — that sits on hardcoded navy and can use white/gold, this sits on --sheet in both themes, so
   VIETNAM takes --ink and WAYFINDER --gold-ink (the gold that stays legible on a light ground).
   ⚠ The negative right margins are NOT a mistake and are copied from .splash-word: letter-spacing
   appends a trailing space after the final letter, so without them each word reads shifted left of
   the rule it sits on. Each margin must equal its own tracking. */
.overlay--center { align-items: center; }
/* Slow, pure fade — no movement (v441). The shared `.sheet-in` (opacity + an 8px slide over .18s)
   is right for a sheet the user summoned with a tap, where a fast entrance confirms the tap landed.
   This card arrives unbidden on resume, and a fast slide read as a jerk. The dim leads by .1s so the
   ground is established before the card resolves on it, and nothing translates. */
@keyframes wf-fade-in { from { opacity: 0; } to { opacity: 1; } }
.overlay--fade { animation: wf-fade-in .125s ease both; }         /* dim:  0 → .125s */
.update-card {
  width: min(300px, calc(100% - 48px));
  background: var(--sheet); backdrop-filter: blur(14px);
  border: 1px solid var(--sheet-line); border-radius: var(--r-sheet);
  padding: 20px 18px; text-align: center; cursor: pointer;
  box-shadow: var(--shadow-pop);
  animation: wf-fade-in .125s ease .125s both;                    /* card: .125 → .25s */
}
.update-card__lock { display: flex; align-items: center; justify-content: center; gap: 13px; }
.update-card__icon svg { width: 40px; height: 40px; border-radius: 5px; display: block; flex: none; }
.update-card__stack { display: flex; flex-direction: column; align-items: center; }
.update-card__word { font-family: 'Aboreto', serif; text-transform: uppercase; white-space: nowrap; line-height: 1.15; }
.update-card__word--top { font-size: 20px; letter-spacing: .32em; margin-right: -.32em; color: var(--ink); }
.update-card__word--bot { font-size: 12px; letter-spacing: .55em; margin-right: -.55em; color: var(--gold-ink); }
.update-card__hr { align-self: stretch; height: 1px; margin: 4px 0; background: var(--line-strong); }
.update-card__title { display: block; margin-top: 17px; font: 700 17px/1.25 var(--font); color: var(--ink); }
/* a styled span, not a nested <button> — the whole card is the button (see showUpdateBar) */
.update-card__cta {
  display: block; width: min(200px, 100%); margin: 17px auto 0;
  background: var(--primary); color: var(--on-primary); border-radius: var(--r-pill);
  font: 700 14px var(--font); padding: 12px 20px;
}
@media (prefers-reduced-motion: reduce) { .overlay--fade, .update-card { animation: none; } }

/* ---------- toast ---------- */
.toast {
  position: fixed; left: 50%; bottom: 26px; translate: -50% 20px;
  background: var(--ink); color: var(--bg);
  padding: 12px 20px; border-radius: var(--r-pill); font: 600 13px var(--font);
  box-shadow: var(--shadow-pop); z-index: 80; opacity: 0; pointer-events: none;
  transition: opacity .2s ease, translate .2s ease;
}
.toast.is-show { opacity: 1; translate: -50% 0; pointer-events: auto; }
/* A failure must not look like a confirmation. The default pill is the ink/paper inversion,
   which reads as "done" — Việt Red says the opposite before the words are read. */
.toast--err { background: var(--primary); color: var(--on-primary); }
/* Worked, and you can't see the result from here — teal, white text. Earned only where the
   outcome lands somewhere you aren't looking. */
.toast--ok { background: var(--teal); color: #fff; }
/* centered/larger variant — currently used only by the save-place sign-in gate, see toast(msg, big) in app.js */
.toast--big {
  top: 50%; bottom: auto; left: 50%; translate: -50% calc(-50% + 10px);
  max-width: min(320px, calc(100% - 48px));
  padding: 20px 26px; border-radius: 18px;
  font: 600 15px var(--font); line-height: 1.4; text-align: center;
}
.toast--big.is-show { translate: -50% -50%; }
/* optional real link inside a toast (2026-08-07, ♡ sign-in gate) — see toast(msg, big, linkHref, linkLabel) */
.toast__link { display: block; margin-top: 10px; color: var(--gold); font-weight: 700; text-decoration: underline; }

/* ---------- misc ---------- */
.empty { padding: 60px 0; text-align: center; color: var(--ink-3); font: 400 14px var(--font); }
.back-link { display: inline-flex; align-items: center; gap: 6px; margin-top: 18px; font: 500 12px var(--font); color: var(--ink-3); }
.back-link:hover { color: var(--ink); }

/* ---------- utilities ---------- */
.mono-label { font: 500 8.5px var(--mono); letter-spacing: .18em; text-transform: uppercase; color: var(--ink-4); }
.u-mt { margin-top: 18px; }
.u-mt-sm { margin-top: 12px; }
.u-center { text-align: center; }
.u-lg { font-size: 17px; }

/* ---------- landing hero header + caption overrides ---------- */
.hero .site-header { position: absolute; top: 0; left: 0; right: 0; padding: 14px 0 0; z-index: 5; }
.hero__cap { top: 72px; right: 12px; left: auto; bottom: auto; }

/* landing feed: all 6 rows on mobile (stacked) and desktop (2 cols) */

/* place-detail grid item */
.detail__media { grid-area: media; min-width: 0; }

/* wizard confirmation (done) */
.wizard--center { text-align: center; }
.wizard--center .wizard__foot { justify-content: center; }

/* Center the narrow single-column pages within the wide wrapper (so on
   desktop they read as a centered card, not a mobile view stuck left). */
.auth, .page, .wizard, .member, .acct-hub { margin-left: auto; margin-right: auto; }

/* ============================================================
   Splash — intro animation (index.html only; v287, REPLACED the Four Tiles
   pop+flip sequence — see wayfinder-splash-screen memory). Gold badge fades
   in → teal V + red V fade in together → red V slides onto the teal V →
   VIETNAM fades in (synced with the V's appearance, not the slide) →
   WAYFINDER fades in. ~2.8s of motion + a .6s hold ≈ 3.4s total.
   PLAYS: once ever, for a first-time visitor in a regular browser tab
   (WF_State.has('city') false) — OR once per app SESSION on an installed
   home-screen app (v314, session-scoped fix v315; "standalone" launch,
   detected in boot.js), city saved or not — i.e. on the first screen after
   a genuine cold launch (icon tapped after fully closed), not on every
   screen change while it's open (this is a multi-page site — every nav is
   a real reload, so "standalone" alone can't tell a fresh launch from
   switching cities). See boot.js's isStandalone + sessionStorage logic.
   CSS is the FLOOR (fail-safe): the outer .splash-screen keyframe holds
   the final frame and self-dismisses at a fixed ~5.4s ceiling with zero
   JS — if app.js never runs (script error, CDN down) the splash still
   can't get stuck open. JS is the ACCELERATOR: app.js primeSplash() adds
   .is-done EARLY, once whatever's actually rendering underneath (chooser
   OR that city's landing — see splashContentReady() in app.js) is ready
   (floored at ~3.4s so a fast connection never cuts the animation short)
   — see the wf-has-city gate below, set pre-paint by boot.js so it never
   flashes for a returning browser-tab visitor.
   ============================================================ */
.splash-screen {
  position: fixed; inset: 0; z-index: 9999;
  background: #171A26; /* matches theme-color/manifest — brand navy, not the light/dark toggle */
  display: flex; align-items: center; justify-content: center;
  animation: wf-splash-out 5.4s ease both;
}
/* JS-driven early dismiss — see primeSplash() in app.js. A CSS transition here would silently
   no-op: a running animation's effect value wins the cascade for the properties it touches, so
   a `transition: opacity` fights the still-running wf-splash-out and commonly just snaps to 0
   with no fade. Swapping to a second, shorter animation avoids that outright. */
.splash-screen.is-done { animation: wf-splash-dismiss .4s ease both; }
html.wf-has-city .splash-screen { display: none; }

/* logo, then VIETNAM/WAYFINDER stacked together below it (owner's preferred layout — reverted
   from a brief "VIETNAM above / WAYFINDER below" experiment that didn't stick) — a plain flex
   column, no absolute-positioning trick needed */
.splash-stage { display: flex; flex-direction: column; align-items: center; gap: 18px; }

.splash-logo { display: flex; align-items: center; justify-content: center; }
/* shadow fades in on its own timer (matches the badge's .5s/1s) via an animated box-shadow —
   NOT a filter: drop-shadow on .splash-badge (tried v289, reverted here): drop-shadow forces the
   browser to re-rasterize + re-blur the SVG's actual pixel shape every frame its opacity changes,
   which is much heavier than a plain compositor-level box-shadow — with 4 other animations
   (badge/teal/red fades, the red slide, both words) all overlapping in the same ~1s window, that
   extra cost was likely what the owner saw as the red V's slide stuttering. */
.splash-logo svg { width: 141px; height: 141px; border-radius: 22px; box-shadow: 0 8px 28px rgba(0,0,0,0); animation: wf-splash-shadow-in .75s ease .25s both; }

.splash-badge { opacity: 0; animation: wf-splash-fade .75s ease .25s both; }
.splash-teal { opacity: 0; animation: wf-splash-fade .75s ease .75s both; }
/* Retimed choreography (2026-08-07, owner spec):
     .25s       badge starts appearing
     .75s       teal V + red V (the W glyphs) start fading in together
     1.0s       badge fully visible
     1.25s      red V slide starts
     1.5s       teal V, red V fully visible (appeared, not yet slid)
     2.0s       red V slide finishes — V is in final position; WAYFINDER starts fading in
     2.05s      VIETNAM starts fading in — the .05s offset after WAYFINDER is intentional
                (perceptually syncs the gold WAYFINDER with the white VIETNAM text; keep it)
     3.0s       WAYFINDER fully visible AND VIETNAM fully visible (VIETNAM's .95s duration vs.
                WAYFINDER's 1s makes up for its .05s later start — both land at the same instant)
   red V + its shadow move as ONE unit (shared <g>) so the shadow never lags behind during the slide. */
.splash-red {
  opacity: 0; transform: translate(87.986px, -0.25px);
  animation: wf-splash-fade .75s ease .75s both, wf-splash-red-slide .75s cubic-bezier(.65,0,.35,1) 1.25s forwards;
}

.splash-wordmark { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 4px; max-width: 88vw; }
.splash-vietnam-wrap { display: inline-flex; flex-direction: column; align-items: stretch; }
.splash-hr { height: 1px; margin-bottom: 3px; background: #FFFFFF; opacity: 0; animation: wf-splash-fade .75s ease 2.25s both; }
.splash-word { font-family: 'Aboreto', serif; white-space: nowrap; opacity: 0; animation: wf-splash-word-in 1s cubic-bezier(.2,.8,.2,1) both; }
.splash-word--top { margin-right: -.32em; font-size: clamp(17px,5.5vw,27px); letter-spacing: .32em; animation-delay: 2.25s; animation-duration: .75s; color: #FFFFFF; }
.splash-word--bottom { margin-right: -.55em; font-size: clamp(12px,3.6vw,18px); letter-spacing: .55em; animation-delay: 2.25s; animation-duration: .75s; color: var(--gold); }

@keyframes wf-splash-fade { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes wf-splash-shadow-in { 0% { box-shadow: 0 8px 28px rgba(0,0,0,0); } 100% { box-shadow: 0 8px 28px rgba(0,0,0,.35); } }
@keyframes wf-splash-red-slide { 0% { transform: translate(87.986px, -0.25px); } 100% { transform: translate(0, 0); } }
@keyframes wf-splash-word-in { 0% { opacity: 0; } 100% { opacity: 1; } }
/* fail-safe ceiling: holds the finished frame, then self-dismisses even with zero JS involvement */
@keyframes wf-splash-out { 0% { opacity: 1; } 92.6% { opacity: 1; } 100% { opacity: 0; visibility: hidden; pointer-events: none; } }
/* JS early-dismiss variant of the above — same end state, no hold phase (dismiss() is only called once ready) */
@keyframes wf-splash-dismiss { 0% { opacity: 1; } 100% { opacity: 0; visibility: hidden; pointer-events: none; } }

@media (prefers-reduced-motion: reduce) {
  .splash-screen { display: none; }
}

/* ---- Suggest form: "chat about this place" consent toggle + confirmation screen ----
   Appended at the end of the file deliberately. .switch/.toggle-pill are defined ~line 1470 for
   the account hub's notification pills; anything here that shares a property with them has to be
   declared AFTER to win on source order. This file has produced that exact bug four separate
   times (see the modifier-before-base lessons in CLAUDE.md) — do not move this block up. */
/* Boxed, matching .toggle-pill in the account hub. First pass left the switch bare on the page with
   justify-content:space-between, which on the 640px desktop column flung it ~600px from the label it
   belongs to, with dead space between — fine on a phone, terrible on a laptop. The border makes the
   label, the switch and the explanation read as ONE control at any width. The note's max-width came
   off for the same reason: capped at 46ch it stopped well short of the box edge and looked ragged. */
/* ---- consent box, rebuilt 2026-08-16 into THREE ROWS ----
   1 the question · 2 the explanation · 3 a hairline, then what will actually happen.
   ⚠ A GRID, and that is load-bearing. There is exactly ONE switch in the DOM; the breakpoints move
   it by changing its `grid-row`, which is the only way to put a single control in two different
   places without rendering it twice. Two elements with role="switch" would be a screen-reader lie
   and their states would drift the moment anything touched only one of them.
   The text spans the full width in both layouts, so it stays centred in the BOX rather than in
   whatever space is left beside the control. */
/* ⚠ The rule above this box is a real <hr class="form-rule"> in the markup, NOT a border-top here.
   This element already carries `border: 1px solid` for the card outline, and the shorthand wins over
   any border-top written before it — the first attempt at v464 silently did nothing for that reason. */
.suggest-chat {
  margin-top: 18px; background: rgba(127,127,127,.06); border: 1px solid var(--line);
  border-radius: var(--r-input); padding: 14px 16px;
  display: grid; grid-template-columns: 1fr auto; row-gap: 10px; column-gap: 12px; align-items: center;
}
/* ⚠ EVERY row is placed EXPLICITLY, and that is not tidiness — it is the fix for a live bug at
   v459. Grid places explicitly-positioned items BEFORE auto-placed ones, so a switch pinned to
   row 2 claimed that cell first and the full-width note, still auto-placed, could no longer fit
   there and was pushed to row 3. The switch ended up stranded on a line of its own. With all four
   rows explicit, the note and the switch legitimately SHARE row 2 and overlap as intended. */
.suggest-chat__row  { grid-row: 1; grid-column: 1 / -1; display: flex; justify-content: center; }
.suggest-chat__note { grid-row: 2; grid-column: 1 / -1; }
.suggest-chat__rule { grid-row: 3; grid-column: 1 / -1; margin: 0; border: 0; border-top: 1px solid var(--line); }
/* MOBILE the address takes column 1 only, so the switch beside it has real space and the centred
   text cannot run underneath it at 320px. DESKTOP it spans the full width again, because there the
   switch has moved up to row 2. */
.suggest-chat__addr { grid-row: 4; grid-column: 1; }
.suggest-chat__label { font: 700 14px/1.35 var(--font); color: var(--ink); text-align: center; }
/* Brand gold, theme-aware pair — --gold flat would wash out on the light theme (--gold-ink is the
   legibility-adjusted variant). Same treatment as the Suggest gate card's copy. */
.suggest-chat__note { font: 400 12.5px/1.55 var(--font); color: var(--gold-ink); text-align: center; }
[data-theme="dark"] .suggest-chat__note { color: var(--gold); }
/* Row 3: teal sentence, gold WILL, gold address. */
.suggest-chat__addr { margin: 0; text-align: center; font: 400 12.5px/1.55 var(--font); color: var(--teal); }
.suggest-chat__addr b { font-weight: 700; color: var(--gold-ink); }
.suggest-chat__addr .em { color: var(--gold-ink); }
/* OFF: the sentence goes grey and WILL NOT turns TEAL — the inverse of the on state, so the two
   read as a pair rather than as two unrelated treatments. The operative words stay the loudest
   thing in the box: the whole point of this state is that the user is certain we won't contact them. */
.suggest-chat.is-off .suggest-chat__addr { color: var(--ink-3); }
.suggest-chat.is-off .suggest-chat__addr b { color: var(--teal); }
/* The switch's grid placement. Rows are: 1 question · 2 note · 3 rule · 4 outcome.
   DESKTOP → row 2, hard right, beside the explanation.
   MOBILE  → row 4, beside the outcome line (owner's call; there is no room on row 2 at 320px). */
.suggest-chat .switch { grid-column: 2; grid-row: 4; justify-self: end; }
/* The switch now sits on the OUTCOME row (4) at both widths — it belongs beside the sentence that
   states what it does, not beside the explanation above the rule. Nothing breakpoint-specific left
   here, so the desktop override is gone: the base rule already puts it on row 4, column 2, and the
   address stays in column 1 so the two share the row without overlapping. */
/* Breakpoint-only line breaks. A <br> is permanent, so each one is display:none at the other
   width rather than hard-coded into the string. */
.br-lap { display: none; }
.br-ph  { display: inline; }
@media (min-width: 760px) { .br-lap { display: inline; } .br-ph { display: none; } }

/* The address gets its own line and the brand gold so it reads as the thing being confirmed,
   not as part of the sentence around it. word-break guards a long address inside the narrow
   centred card — same clamp reasoning as the signup "check your email" screen. */
.suggest-thanks__email { margin-top: 4px; font: 700 18px var(--font); color: var(--gold-ink); word-break: break-word; }
[data-theme="dark"] .suggest-thanks__email { color: var(--gold); }

/* The round back chevron shares a flex row with .page__title, which carries margin-top:10px of its
   own. An unmatched top margin on one flex item defeats align-items:center and reads as "the
   chevron isn't level with the heading" — the same trap logged on this page's signed-out gate card
   in 2026-07-27. Move the margin onto the ROW so both items sit on one baseline. Scoped to the
   Suggest page: .listing-head__row is shared with the listing pages and the account hub. */
body[data-page="suggest"] .listing-head__row { margin-top: 10px; }
body[data-page="suggest"] .listing-head__row .page__title { margin-top: 0; }

/* "WE READ EVERY ONE" in brand teal. Scoped to the Suggest page — .form-meta is shared with the
   contact form and others, which keep the muted treatment. Teal is theme-flat here, same as the
   field labels on this page. */
/* Matched to the field labels and the LINKS heading — 12px / .12em, up from .form-meta's shared
   8.5px / .12em. Same reasoning as those: at 8.5px, wide-set mono reads as texture. */
body[data-page="suggest"] .form-meta { color: var(--teal); font-size: 12px; font-weight: 700; }

/* ---- Suggest page pass, 2026-08-16 ----
   Scoped throughout: .page__lead renders on 24 screens, .field__label and .field__note on every
   form in the app. Only .suggest-chat* and the two new classes below are Suggest-only. */
body[data-page="suggest"] .page__lead { color: var(--gold-ink); }
/* MOBILE: the lead centres and breaks into three lines (the br-ph spans in RENDER.suggest).
   ⚠ This centring was in the preview artifact and I failed to port it at v459 — the page shipped
   left-aligned on phones for nine builds. */
@media (max-width: 759px) { body[data-page="suggest"] .page__lead { text-align: center; } }
/* Bold, 12px, and the tracking pulled back from .18em to .12em. Matched to the LINKS heading.
   At 10px with .18em tracking these were technically legible and practically not — wide-tracked
   small mono reads as texture rather than words. The size did less work here than the spacing.
   The shared atom stays 8.5px everywhere else in the app. */
body[data-page="suggest"] .field__label { font-weight: 700; font-size: 12px; letter-spacing: .12em; }
/* centred under the dropdown it belongs to, both breakpoints */
body[data-page="suggest"] .field__note { color: var(--gold-ink); text-align: center; }
/* "IMPORTANT" / "NICE TO HAVE" beside a label — gold against the teal label, same mono size and
   tracking so it reads as part of the row rather than a chip bolted on. */
.field__flag { color: var(--gold-ink); }
/* separator between a label and its flag — the app's own "·", same as "2 DAYS · EN + VI".
   ::before rather than markup so the flag stays a single clean span. */
.field__flag::before,
.form-section__sub::before { content: "·"; margin: 0 8px; color: var(--ink-4); }
/* hairline above the one long answer, separating it from the two one-liners */
.field--ruled { margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); }
/* section heading with the rule UNDER it — the rule belongs to the group it opens */
.form-section {
  margin-top: 26px; padding-bottom: 10px; border-bottom: 1px solid var(--line);
  font: 700 12px var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--teal);
}
/* NB no margin-left: the ::before dot supplies the gap on both sides. A margin here plus a literal
   space in the markup was what made "Links · Nice to Have" sit wider than "WHY IT'S GOOD · Important". */
.form-section__sub { color: var(--gold-ink); }
/* standalone hairline between groups, for where a border on the next element would be clobbered
   by that element's own border shorthand (see .suggest-chat) */
.form-rule { margin: 26px 0 0; border: 0; border-top: 1px solid var(--line); }
/* double the breathing room between the consent box and Send the tip — .u-mt's 18px read as if the
   button belonged to the box rather than closing the form */
body[data-page="suggest"] .suggest-chat + .btn { margin-top: 36px; }
.form-section + .link-grid { margin-top: 16px; }
.link-grid .field { margin-top: 0; }
.link-grid { display: flex; flex-direction: column; gap: 14px; }
@media (min-width: 760px) {
  .link-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 16px; row-gap: 14px; }
}

/* "WE REPLY WITHIN 2 DAYS · EN + VI" in gold (2026-08-16). Scoped the same way and for the same
   reason as the teal rule above — .form-meta is shared with the Suggest page and the create-account
   screen, so a bare override would repaint all three. --gold-ink is theme-aware (#9A6B00 on light,
   bright #F5C242 on dark), which matters here because Contact carries data-wash and the light
   ground is the pale gold wash. */
body[data-page="contact"] .form-meta { color: var(--gold-ink); }

/* ---- Contact page, rest of the 2026-08-16 pass ----
   Everything here is scoped to the page. .page__lead appears on 24 screens and .field__label on
   every form in the app, so an unscoped override would repaint the whole product. */
body[data-page="contact"] .page__lead { color: var(--gold-ink); }
/* Field labels teal — identical to what the Suggest page already does one rule up the file. */
body[data-page="contact"] .field__label { color: var(--teal); }
/* The round back chevron shares a flex row with .page__title, which carries its own margin-top:10px;
   that has to move to the row or it throws the vertical centring against the circle. Exactly the fix
   the Suggest page needed for the same component — see body[data-page="suggest"] .listing-head__row. */
body[data-page="contact"] .listing-head__row { margin-top: 10px; }
body[data-page="contact"] .listing-head__row .page__title { margin-top: 0; }
/* Legal's head is a hand-copy of the same component and had drifted: without these two the
   title's own 10px pushed it down INSIDE the flex row instead of pushing the row away from the
   eyebrow, so the gap read short and the title sat low against the chevron. */
body[data-page="legal"] .listing-head__row { margin-top: 10px; }
body[data-page="legal"] .listing-head__row .page__title { margin-top: 0; }
/* NEEDS picked out in gold inside the CTA card. */
.suggest-cta__t .hi { color: var(--gold-ink); font-weight: 700; }
/* The CTA pill sits where the bare red arrow used to. */
.suggest-cta__cta { margin-left: auto; flex: none; }
/* MOBILE: the lead breaks into its three sentences, centred, and the CTA card stacks with a
   full-width pill. The .ln spans are inline by default (desktop) and only become blocks here —
   see the note in RENDER.contact for why they are spans and not <br>. */
@media (max-width: 759px) {
  body[data-page="contact"] .page__lead { text-align: center; }
  body[data-page="contact"] .page__lead .ln { display: block; }
  body[data-page="contact"] .suggest-cta { flex-direction: column; align-items: stretch; gap: 12px; }
  body[data-page="contact"] .suggest-cta__t { text-align: center; }
  body[data-page="contact"] .suggest-cta__cta { margin-left: 0; width: 100%; }
}

/* ============================================================
   /review inbox — tabs + suggestion/message rows (2026-08-13)
   Appended at the end, per the house rule: a modifier or override declared BEFORE the
   base rule it overrides loses on source order at equal specificity. This file has been
   bitten by that four separate times.
   ============================================================ */

/* --- tab bar: Places · Suggestions · Messages ------------------------------ */
.inbox-tabs { display: flex; gap: 8px; margin-top: 18px; flex-wrap: wrap; }
.inbox-tab { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  padding: 9px 14px; border-radius: var(--r-pill); border: 1px solid var(--line-strong);
  background: none; color: var(--ink-2); font: 500 12px var(--mono); letter-spacing: .06em;
  text-transform: uppercase; }
.inbox-tab:hover { color: var(--ink); }
.inbox-tab.is-active { color: var(--gold-ink); border-color: currentColor; }
[data-theme="dark"] .inbox-tab.is-active { color: var(--gold); }
/* keyboard users keep a visible ring; a tap doesn't get the UA default on top of .is-active */
.inbox-tab:focus:not(:focus-visible) { outline: none; }
.inbox-tab:focus-visible { outline: 2px solid var(--gold-ink); outline-offset: 2px; }
/* the count is what still needs doing, so it carries the emphasis */
.inbox-tab__n { font-size: 11px; padding: 1px 6px; border-radius: var(--r-pill);
  background: var(--primary); color: var(--on-primary); }
/* a zero still renders (so "nothing waiting" can't be mistaken for "still loading")
   but drops the alarm colour — otherwise every tab shouts equally. */
.inbox-tab__n--zero { background: none; color: var(--ink-2); padding: 1px 2px; }

/* a queue that failed to load must not read as a reassuring "nothing here" */
.inbox-err { color: var(--primary); }

/* ------------------------------------------------------------------
   Placeholder-flash fix (2026-08-14) — see the long note above photoImg() in ui.js.
   A card that HAS a photo drops the saturated gradient for a near-invisible neutral, so the
   moment the image paints over it reads as nothing happening rather than a colour change.
   Cards with no photo, and the .is-broken fallback, keep their gradient untouched — that's
   what the gradients are actually for.
   Specificity: .ph.ph--photo (0,2,0) beats .ph-blue/.ph-tan/etc (0,1,0), and it's declared
   after them, so it wins on both counts.
   ------------------------------------------------------------------ */
.ph.ph--photo { background-image: none; background-color: rgba(25,26,34,.10); }
[data-theme="dark"] .ph.ph--photo { background-color: rgba(255,255,255,.06); }

/* ------------------------------------------------------------------
   "Tap for sound" pill on the video modal (2026-08-14).
   The film autoplays MUTED because that is the only autoplay browsers permit — see the note above
   videoModal() in ui.js. This is where the sound comes back. It sits INSIDE .video-modal__frame
   (which is already positioned), bottom-centre so it never collides with the ✕ at the corner, and
   app.js removes it the moment it is tapped.
   ------------------------------------------------------------------ */
.video-modal__sound {
  /* 60, not 16: YouTube's own control strip (play/seek/volume) is ~48px tall and is visible
     during the opening seconds of an autoplay, so a pill at the very bottom would land on top
     of the scrubber. */
  position: absolute; left: 50%; bottom: 60px; transform: translateX(-50%); z-index: 2;
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 10px 16px; border-radius: var(--r-pill); border: 0;
  background: rgba(23,26,38,.82); color: #fff;
  font: 600 13px var(--font); letter-spacing: .01em;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.video-modal__sound:hover { background: rgba(23,26,38,.94); }
.video-modal__sound svg { width: 17px; height: 17px; flex: none; }

/* ============================================================================
   /review — curator & admin queue.  Port of Claude Design "WF - Review.dc.html",
   STACKED CARDS layout only (the owner's working layout; Split view and Focus
   mode were explored there and dropped).

   ⚠ EVERYTHING IS SCOPED TO body[data-page="review"] — deliberately, and not as
   tidiness. The unscoped-selector lesson is written into this file twice already:
   `.page__lead` renders on 24 screens and `.field__label` on every form, so a
   page-specific rule with a generic name repaints screens nobody was looking at.

   ⚠ THE MOCK IS DRAWN IN OUR DARK THEME. Every hex in it is a token — #F5C242 is
   --gold, #23A69D is --teal, #DA251D is --primary, #171A26 is --card. None of them
   are hard-coded below. Two consequences worth knowing before editing:
     · Gold TEXT must be --gold-ink (#9A6B00) in light. Raw --gold on white is
       unreadable, which is why .rv-lbl and friends use the ink variant.
     · The mock's washes and hairlines are WHITE-alpha (rgba(255,255,255,.04)).
       Those are invisible on a light background, so they're page-scoped custom
       properties below, ink-alpha in light and white-alpha in dark.
   ============================================================================ */

body[data-page="review"] {
  --rv-wash: rgba(25,26,34,.04);
  --rv-wash-2: rgba(25,26,34,.06);
  --rv-hair: var(--line);
}
[data-theme="dark"] body[data-page="review"] {
  --rv-wash: rgba(255,255,255,.04);
  --rv-wash-2: rgba(255,255,255,.06);
  --rv-hair: rgba(255,255,255,.1);
}

/* The queue is a work surface, not a reading column — it needs the width the
   three-column preview grid was drawn for. .member caps at 480px for the account
   hub, which would squeeze the Places card into a single stack on a laptop. */
body[data-page="review"] .member--review { max-width: 1040px; }

/* ---- tabs: mono pills, red count badge ---- */
body[data-page="review"] .inbox-tabs { gap: 8px; margin: 18px 0 22px; }
body[data-page="review"] .inbox-tab {
  font: 700 10.5px var(--mono); letter-spacing: .08em; white-space: nowrap;
  border-radius: var(--r-pill); padding: 8px 14px; gap: 5px;
  border: 1px solid var(--line-mid); background: none; color: var(--ink-2);
}
body[data-page="review"] .inbox-tab.is-active { color: var(--gold-ink); border-color: var(--gold-ink); }
body[data-page="review"] .inbox-tab__n {
  background: var(--primary); color: var(--on-primary);
  font: 700 10px var(--mono); padding: 1px 7px; border-radius: var(--r-pill);
}
body[data-page="review"] .inbox-tab__n--zero { background: none; color: var(--ink-3); padding: 1px 2px; }

/* ---- group headings: teal mono, at twice the size they shipped at ----
   Tried in white Space Grotesk (v547) and put back the same day: this rule governs the
   bands on ALL FOUR tabs, and the page's own voice on every one of them was a bigger
   change than the size problem being solved. So the face, the colour and the tracking
   are the originals — only the size moved, because they were too small to find. 20px was
   tried first and shouted; 14px is the landing point. */
body[data-page="review"] .rq-group { margin-top: 26px; }
body[data-page="review"] .rq-group .sheet__label {
  font: 700 14px var(--mono); letter-spacing: .12em; text-transform: uppercase;
  color: var(--teal); margin: 0 0 10px;
}

/* ---- the card ---- */
body[data-page="review"] .rv-card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-card); padding: 16px 22px; margin-bottom: 10px;
}
body[data-page="review"] .rv-card.is-open { border-color: var(--gold); padding: 20px 22px; }
/* A message card carries .is-open permanently — it has no collapsed state, because there
   is nothing to hide behind a header. So it must NOT take the gold "you opened this" edge:
   gold here would mark every message as expanded-by-you and stop meaning anything. */
body[data-page="review"] .rv-card--msg.is-open { border-color: var(--line); padding: 18px 22px; }

/* The header is a real <button> so it is reachable by keyboard and announces its
   expanded state. That means undoing the UA button look completely — without this
   every card header renders as a grey, centred, 13px system button. */
body[data-page="review"] .rv-head {
  display: flex; align-items: center; gap: 12px; width: 100%;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
body[data-page="review"] .rv-head__main {
  flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 12px; overflow: hidden;
}
body[data-page="review"] .rv-name { font: 700 15.5px var(--font); color: var(--ink); flex: none; }
body[data-page="review"] .rv-sep { color: var(--ink-5); flex: none; }
body[data-page="review"] .rv-date,
body[data-page="review"] .rv-city {
  font: 700 11px var(--mono); letter-spacing: .08em; color: var(--gold-ink); flex: none;
}
body[data-page="review"] .rv-date--end { margin-left: auto; }
body[data-page="review"] .rv-nlinks {
  font: 700 11px var(--mono); letter-spacing: .08em; color: var(--teal); flex: none;
}
body[data-page="review"] .rv-snip {
  font: 400 13px var(--font); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body[data-page="review"] .rv-chev { width: 16px; height: 16px; flex: none; color: var(--ink-4); }
body[data-page="review"] .rv-card.is-open .rv-chev { color: var(--gold-ink); transform: rotate(180deg); }

body[data-page="review"] .rv-body { margin-top: 14px; }

/* ---- chips + small labels ---- */
body[data-page="review"] .rv-lbl {
  font: 700 11.5px var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--gold-ink); margin-bottom: 6px;
}
body[data-page="review"] .rv-lbl--rule {
  margin: 14px 0 6px; padding-top: 14px; border-top: 1px solid var(--rv-hair);
}
body[data-page="review"] .rv-chip,
body[data-page="review"] .rv-flag,
body[data-page="review"] .rv-citychip,
body[data-page="review"] .rv-flagchip,
body[data-page="review"] .rv-via,
body[data-page="review"] .rv-cat {
  font: 700 9px var(--mono); letter-spacing: .1em; border-radius: var(--r-pill);
  padding: 3px 9px; flex: none; white-space: nowrap;
}
body[data-page="review"] .rv-chip--new { color: var(--teal); border: 1px solid var(--teal); }
body[data-page="review"] .rv-chip--gold { color: var(--gold-ink); border: 1px solid var(--gold); }
body[data-page="review"] .rv-chip--teal { color: var(--teal); border: 1px solid var(--teal); }
body[data-page="review"] .rv-chip--red { color: var(--primary); border: 1px solid var(--primary); }
body[data-page="review"] .rv-chip--mute { color: var(--ink-3); border: 1px solid var(--line-mid); }
body[data-page="review"] .rv-flag { color: var(--ink-3); border: 1px solid var(--line-mid); }
body[data-page="review"] .rv-flag--off { color: var(--primary); border-color: var(--primary); }
body[data-page="review"] .rv-citychip { color: var(--gold-ink); border: 1px solid var(--gold); }
body[data-page="review"] .rv-flagchip { color: var(--teal); border: 1px solid var(--teal); }
body[data-page="review"] .rv-via { color: var(--gold-ink); border: 1px solid var(--gold); }
/* Red, because it means "not finishable yet" — this row still needs the Wizard. */
body[data-page="review"] .rv-via--sugg { color: var(--primary); border-color: var(--primary); }
/* Category colours are the brand's own four. Stay and Live share the teal seat. */
body[data-page="review"] .rv-cat { color: var(--ink-2); border: 1px solid var(--line-mid); }
body[data-page="review"] .rv-cat--eat { background: var(--primary); color: #fff; border-color: transparent; }
body[data-page="review"] .rv-cat--drink { background: var(--gold); color: #171A26; border-color: transparent; }
body[data-page="review"] .rv-cat--explore { background: var(--explore); color: #fff; border-color: transparent; }
body[data-page="review"] .rv-cat--stay,
body[data-page="review"] .rv-cat--live { background: var(--teal); color: #0C0D12; border-color: transparent; }

/* ---- the member's words: quoted, gold-barred ---- */
body[data-page="review"] .rv-quote {
  font: 400 15px/1.55 var(--font); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere;
  background: var(--rv-wash); border-left: 3px solid var(--gold);
  border-radius: 0 10px 10px 0; padding: 12px 16px;
}
body[data-page="review"] .rv-quote--soft { font-size: 14px; line-height: 1.6; color: var(--ink-2); }
body[data-page="review"] .rv-na { font: 700 10px var(--mono); letter-spacing: .1em; color: var(--primary); }
body[data-page="review"] .rv-none { font: italic 400 12.5px var(--font); color: var(--ink-4); }

/* ---- links ---- */
body[data-page="review"] .rv-links { margin-top: 14px; }
body[data-page="review"] .rv-links__set { display: flex; flex-wrap: wrap; gap: 4px 14px; }
body[data-page="review"] .rv-link { font: 400 12.5px var(--font); color: var(--ink); min-width: 0; }
body[data-page="review"] .rv-link a { color: var(--teal); }
/* An unvalidated value that isn't a URL prints as text, never as a link (ui.js does
   the protocol test). It reads muted so it can't be mistaken for something clickable. */
body[data-page="review"] .rv-link__raw { color: var(--ink-3); }

/* ---- who sent it ---- */
/* Label on its own line, name and address beneath it — the mock ran them inline, but the
   address is long enough that the pair reads as one run-on string. Stays right-aligned,
   which is where the design put the whole block. */
/* Left-aligned on BOTH breakpoints — it lines up with LINKS and the tip above it, and a
   single right-aligned block in an otherwise left-aligned card reads as stray rather than
   as a deliberate column.
   Opening hairline only: it used to close with one too, which put two rules a few pixels
   apart — its own bottom edge and .rv-actions' top edge. The actions rule is the one that
   stays, because the Places card needs it as well and this block isn't on that card. */
body[data-page="review"] .rv-by {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  border-top: 1px solid var(--rv-hair);
  padding: 12px 0 0; margin: 14px 0 0;
}
body[data-page="review"] .rv-by .rv-lbl { margin: 0; }
body[data-page="review"] .rv-by__hd { display: flex; align-items: center; gap: 8px; }
body[data-page="review"] .rv-by__val {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font: 400 13px var(--font); color: var(--ink-2); overflow-wrap: anywhere;
}
body[data-page="review"] .rv-by__val b { color: var(--ink); }
body[data-page="review"] .rv-mail { color: var(--teal); }
body[data-page="review"] .rv-anon { font-style: italic; }

/* ---- Places: the three-column preview ----
   minmax(0,1fr) is load-bearing on all three columns: a long URL in SOCIALS would
   otherwise set the column's minimum width and blow the whole grid out sideways. */
body[data-page="review"] .rv-grid {
  display: grid; grid-template-columns: minmax(0,1fr) 1px minmax(0,1fr) 1px minmax(0,1fr);
  gap: 20px; margin-top: 16px; align-items: stretch;
  border-top: 1px solid var(--rv-hair); padding-top: 16px;
}
body[data-page="review"] .rv-vr { background: var(--rv-hair); width: 1px; align-self: stretch; }
body[data-page="review"] .rv-col { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
body[data-page="review"] .rv-col__sec { border-top: 1px solid var(--rv-hair); padding-top: 12px; }
body[data-page="review"] .rv-nm { font: 800 17px var(--font); letter-spacing: -.01em; color: var(--ink); overflow-wrap: anywhere; }
body[data-page="review"] .rv-nm + .rv-nm { margin-top: 4px; }
body[data-page="review"] .rv-nm__k { font: 700 11px var(--mono); letter-spacing: .08em; color: var(--teal); }
body[data-page="review"] .rv-tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
body[data-page="review"] .rv-tags--flags { margin-top: 8px; }
/* The shared priceMeter renders TEXT ₫ glyphs (four slots, faded past the tier), not an
   svg — so it is sized through its own __dong span. 12px matches the mock and keeps the
   meter in scale beside 9px chips; the default 14px reads oversized in that row. */
body[data-page="review"] .rv-price { display: inline-flex; align-items: center; }
body[data-page="review"] .rv-price .price-meter__dong { font-size: 12px; }
/* Label left, tier hard right — the tier is the answer to the label, so it belongs on that
   line rather than trailing the name below it. */
body[data-page="review"] .rv-subhd {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
body[data-page="review"] .rv-subhd .rv-lbl { margin-bottom: 0; }
body[data-page="review"] .rv-sub {
  display: flex; align-items: baseline; justify-content: flex-start; gap: 6px; flex-wrap: wrap;
  margin-top: 6px; font: 400 13px var(--font); color: var(--ink); overflow-wrap: anywhere;
}
body[data-page="review"] .rv-sub__name { font-weight: 700; }
body[data-page="review"] .rv-sub__sep { color: var(--ink-5); }
/* Tier chip. MEMBER teal, every tier above it gold — so "a stranger's first tip" and
   "someone who does this" are told apart before you read the word. */
body[data-page="review"] .rv-role {
  font: 700 9px var(--mono); letter-spacing: .1em; border-radius: var(--r-pill);
  padding: 2px 8px; flex: none; white-space: nowrap;
  color: var(--gold-ink); border: 1px solid var(--gold);
}
body[data-page="review"] .rv-role--member { color: var(--teal); border-color: var(--teal); }

/* ---- photos ---- */
/* ⚠ position:relative is LOAD-BEARING, not decoration. photoImg() returns an
   `.ph__img` — `position:absolute; inset:0` — because everywhere else in the app it sits
   inside a `.ph` box, which is positioned. Without a containing block here the image
   escapes the card entirely and covers the whole document. Exactly the failure the
   /suggest switch knob had, from the same cause: an absolutely-positioned child whose
   parent forgot to be a parent. */
body[data-page="review"] .rv-photo {
  position: relative;
  height: 104px; border-radius: 12px; overflow: hidden; cursor: zoom-in;
  background: var(--rv-wash); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
body[data-page="review"] .rv-photo img { width: 100%; height: 100%; object-fit: cover; }
body[data-page="review"] .rv-photo--none {
  cursor: default; border: 1px dashed var(--primary);
  font: 700 9.5px var(--mono); letter-spacing: .12em; color: var(--primary);
}
body[data-page="review"] .rv-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 5px; }
body[data-page="review"] .rv-tile {
  position: relative; aspect-ratio: 1; border-radius: 6px; overflow: hidden; cursor: zoom-in;
  background: var(--rv-wash); border: 1px solid var(--line);
}
body[data-page="review"] .rv-tile img { width: 100%; height: 100%; object-fit: cover; }
body[data-page="review"] .rv-tile__more {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(12,13,18,.6); color: #fff; font: 700 10px var(--mono);
}
body[data-page="review"] .rv-photo__n { font: 400 9px var(--mono); letter-spacing: .08em; color: var(--teal); margin-top: 5px; }

/* ---- socials ---- */
body[data-page="review"] .rv-social { min-width: 0; }
body[data-page="review"] .rv-social__k { font: 700 12.5px var(--font); color: var(--ink); }
body[data-page="review"] .rv-social__v {
  font: 400 12.5px var(--font); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body[data-page="review"] .rv-social__v a { color: var(--teal); }

/* ---- actions: three slots, each with its own caption ---- */
body[data-page="review"] .rv-actions {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: start;
  margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rv-hair);
}
/* Each slot names its OWN column. Without that, a card with only two actions (a
   suggestion-born place has no Approve) auto-places them into columns 1 and 2 and Pass
   lands in the middle. Grid fills explicitly-positioned items first — the same rule that
   stranded the /suggest consent switch at v459. */
body[data-page="review"] .rv-act--start { grid-column: 1; justify-self: start; text-align: center; }
body[data-page="review"] .rv-act--mid { grid-column: 2; justify-self: center; text-align: center; }
body[data-page="review"] .rv-act--end { grid-column: 3; justify-self: end; text-align: center; }
body[data-page="review"] .rv-btn {
  border-radius: var(--r-pill); padding: 11px 20px; border: 0; cursor: pointer;
  font: 700 13.5px var(--font);
}
body[data-page="review"] .rv-btn--gold { background: var(--gold); color: #171A26; }
body[data-page="review"] .rv-btn--teal { background: var(--teal); color: #0C0D12; }
body[data-page="review"] .rv-btn--red { background: var(--primary); color: var(--on-primary); }
body[data-page="review"] .rv-btn--ghost {
  background: none; border: 1px solid var(--line-strong); color: var(--ink-2);
  font-size: 12.5px; padding: 10px 18px;
}
body[data-page="review"] .rv-btn[disabled] {
  background: var(--rv-wash-2); color: var(--ink-5); cursor: not-allowed;
}

/* ---- replies already sent ---- */
body[data-page="review"] .rv-thread { margin-top: 14px; }
body[data-page="review"] .rv-bubble {
  max-width: 78%; border-radius: 12px; padding: 10px 14px; margin-bottom: 8px;
  background: rgba(35,166,157,.1); border: 1px solid rgba(35,166,157,.35);
}
body[data-page="review"] .rv-bubble__hd {
  font: 700 9px var(--mono); letter-spacing: .1em; color: var(--ink-3); margin-bottom: 4px;
}
body[data-page="review"] .rv-bubble__tx {
  font: 400 13.5px/1.5 var(--font); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere;
}

/* ---- the reply composer ---- */
body[data-page="review"] .rv-compose {
  background: var(--rv-wash); border: 1px solid var(--teal);
  border-radius: var(--r-input); padding: 14px 16px; margin: 14px 0;
}
body[data-page="review"] .rv-compose__hd {
  font: 400 9.5px var(--mono); letter-spacing: .08em; color: var(--ink-3);
  margin-bottom: 8px; overflow-wrap: anywhere;
}
body[data-page="review"] .rv-compose__ta {
  width: 100%; box-sizing: border-box; resize: vertical;
  background: var(--field-bg); border: 1px solid var(--field-line);
  border-radius: 12px; padding: 12px 14px;
  font: 400 14px/1.5 var(--font); color: var(--ink);
}
body[data-page="review"] .rv-compose__row { display: flex; gap: 10px; margin-top: 10px; }
body[data-page="review"] .rv-compose__note {
  font: 400 11.5px/1.5 var(--font); color: var(--ink-3); margin-top: 8px; text-align: center;
}

/* ---- messages ---- */
body[data-page="review"] .rv-msg__top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* `messages.body` allows 4000 characters. Without a bound, one long message pushes every
   other row off screen and the queue stops being scannable. It scrolls inside its own box
   rather than being truncated — nothing is hidden, the row just can't run away. (Carried
   over from .inbox-row__body, which this replaced.) */
body[data-page="review"] .rv-msg__body {
  font: 400 14.5px/1.55 var(--font); color: var(--ink);
  margin-top: 10px; white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 200px; overflow-y: auto;
}
/* Reply sits dead centre with HANDLED pinned right — a 1fr/auto/1fr track, not
   space-between, so the button stays centred on the CARD rather than being pushed around
   by whatever is beside it. Both children are placed EXPLICITLY: grid fills
   explicitly-positioned items before auto-placed ones, and leaving either to auto-place is
   how the /suggest consent box shipped with its switch stranded on its own row at v459. */
body[data-page="review"] .rv-msg__foot {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; margin-top: 14px;
}
body[data-page="review"] .rv-msg__reply { grid-column: 2; justify-self: center; padding: 10px 20px; font-size: 12.5px; }
body[data-page="review"] .rv-msg__foot .rv-switch { grid-column: 3; justify-self: end; }
/* ONE element carrying role="switch" — never a visible twin per breakpoint. Two of them
   is a lie to a screen reader and their states drift the moment anything touches one. */
body[data-page="review"] .rv-switch {
  display: inline-flex; align-items: center; gap: 8px; margin-left: auto;
  background: none; border: 0; padding: 0; cursor: pointer;
}
body[data-page="review"] .rv-switch__lbl { font: 700 9px var(--mono); letter-spacing: .1em; color: var(--ink-3); }
body[data-page="review"] .rv-switch__track {
  position: relative; width: 34px; height: 20px; border-radius: var(--r-pill);
  background: var(--rv-wash-2); border: 1px solid var(--line-mid); flex: none;
}
body[data-page="review"] .rv-switch__knob {
  position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--ink-4);
  transition: transform .22s ease, background .22s ease;
}

/* ---- marking a message handled ----
   The status change is instant; the card leaving is not. Without this the row simply
   vanishes from one group and appears in another, and a tap that produces no visible event
   reads as a tap that didn't land — so people press again. The sequence is: the switch
   flips and the card turns teal (you did that), THEN it slides away (it's moving).
   The hold in the middle of the keyframes is what separates the two beats; without it the
   card is halfway gone before the colour has registered. app.js waits out the same
   duration before committing — see rvHandOff(). */
body[data-page="review"] .rv-card.is-leaving {
  border-color: var(--teal);
  animation: rv-handoff .85s ease forwards;
}
body[data-page="review"] .rv-card--msg.is-leaving .rv-switch__track {
  background: rgba(35,166,157,.35); border-color: var(--teal);
}
body[data-page="review"] .rv-card--msg.is-leaving .rv-switch__knob {
  transform: translateX(14px); background: var(--teal);
}
/* It travels DOWN, not sideways — the HANDLED group is below, so down is the direction the
   card is actually going. Sideways said "gone" where this needs to say "moved there". */
/* Where it landed. A brief gold ring on the row that just arrived (or came back), so nothing
   has to be hunted for after it moves. */
body[data-page="review"] .is-landed {
  animation: rv-landed 2s ease forwards;
  border-color: var(--gold);
}
@keyframes rv-landed {
  0%   { box-shadow: 0 0 0 2px var(--gold); }
  70%  { box-shadow: 0 0 0 2px var(--gold); }
  100% { box-shadow: 0 0 0 2px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  body[data-page="review"] .is-landed { animation: none; box-shadow: 0 0 0 2px var(--gold); }
}

@keyframes rv-handoff {
  0%   { opacity: 1; transform: none; }
  45%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(30px); }
}
/* Someone who has asked for less motion still needs to see that something happened — they
   get the colour change, and none of the travel. */
@media (prefers-reduced-motion: reduce) {
  body[data-page="review"] .rv-card.is-leaving { animation: none; }
  body[data-page="review"] .rv-switch__knob { transition: none; }
}

/* ---- decided / handled rows ---- */
body[data-page="review"] .rv-done {
  border: 1px dashed var(--line-mid); border-radius: 12px;
  padding: 12px 18px; margin-bottom: 8px;
}
body[data-page="review"] .rv-done.is-open { border-style: solid; }
body[data-page="review"] .rv-done__row { display: flex; align-items: center; gap: 12px; }
/* The peek chevron. Sized like the Reopen button beside it so the pair reads as one
   control group rather than a button and a stray glyph. */
body[data-page="review"] .rv-peek {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 26px; flex: none; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  background: none; color: var(--ink-2); padding: 0;
}
body[data-page="review"] .rv-peek:hover { color: var(--gold-ink); border-color: var(--gold-ink); }
body[data-page="review"] .rv-peek .rv-chev { width: 14px; height: 14px; }
body[data-page="review"] .rv-peek.is-open .rv-chev { transform: rotate(180deg); color: var(--gold-ink); }
body[data-page="review"] .rv-done__full {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--rv-hair);
  font: 400 14px/1.55 var(--font); color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* SHRINKABLE, not flex:none. With a fixed basis a long name simply pushed the chip and the
   buttons past the right edge of the card — "Highlands Coffee — Bờ Hồ" was enough to shove
   Undo and Delete off a 390px screen. It truncates instead; min-width:0 is what actually
   permits that, since a flex item won't shrink below its content without it. */
body[data-page="review"] .rv-done__name {
  font: 600 14px var(--font); color: var(--ink-2); flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body[data-page="review"] .rv-done__snip {
  font: 400 12.5px var(--font); color: var(--ink-4); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 11px, same as the dates and mono labels on the cards above — 9.5px was small enough to
   read as a footnote rather than a field. */
body[data-page="review"] .rv-done__date {
  font: 700 11px var(--mono); letter-spacing: .08em; color: var(--gold-ink); flex: none;
}
/* Controls hard right, always. This used to depend on the snippet being present — the chip
   took the auto margin only when it wasn't — which meant the phone, where the snippet is
   hidden but still in the markup, matched the selector anyway and left everything huddled
   at the left. An explicit end group doesn't care what's in front of it. */
body[data-page="review"] .rv-done__end {
  margin-left: auto; display: flex; align-items: center; gap: 8px; flex: none;
}
body[data-page="review"] .rv-undo {
  border-radius: var(--r-pill); padding: 6px 13px; font: 700 11px var(--font);
  border: 1px solid var(--line-strong); background: none; color: var(--ink-2);
  cursor: pointer; flex: none;
}
body[data-page="review"] .rv-undo:hover { color: var(--gold-ink); border-color: var(--gold-ink); }
body[data-page="review"] .rv-undo--danger { color: var(--primary); border-color: var(--primary); }

/* ---- photo viewer, review-sized ----
   The shared lightbox fills the screen, which is right on a place page: the reader came to
   look at the photo. Here they came to CHECK it, against the name and the write-up they
   were just reading — so it opens as a card over a dimmed queue instead of replacing it.
   Everything else about the component is untouched: same markup, same swipe, same keys.
   The controls are re-anchored to the card because the shared rules pin them to the
   VIEWPORT with position:fixed, which would leave the close button floating in the corner
   of the screen with the photo somewhere in the middle. */
body[data-page="review"] .lightbox {
  width: min(78vw, 860px); height: min(72vh, 620px);
  max-width: none; max-height: none;
  border-radius: var(--r-photo); box-shadow: 0 30px 80px rgba(0,0,0,.6);
}
body[data-page="review"] .lightbox::backdrop { background: rgba(8,8,10,.78); }
body[data-page="review"] .lightbox__close,
body[data-page="review"] .lightbox__nav,
body[data-page="review"] .lightbox__counter { position: absolute; }

/* ---- confirm: centred card on desktop, bottom sheet on a phone ----
   .sheet carries margin-top:78px, which on a vertically-centred item participates in
   the centring maths and lands the card low — the same trap documented on the qhelp
   sheet. It is zeroed here rather than worked around. */
.overlay--rvc { align-items: center; }
.sheet--confirm { margin-top: 0; width: min(380px, calc(100% - 28px)); text-align: center; }
.rv-confirm__title { font: 700 16px var(--font); color: var(--ink); margin-bottom: 6px; }
/* account deletion only. Must follow .rv-confirm__title — equal specificity, source order decides. */
.rv-confirm__title--gold { color: var(--gold-ink); }
[data-theme="dark"] .rv-confirm__title--gold { color: var(--gold); }
/* Account deletion reads at FULL strength — no muted grey anywhere in it. Scoped by ID because
   every class here is shared with the editor's Delete Place and /review's confirms, where the
   muted body is correct. The gold spans inside survive: .rv-confirm__word matches those elements
   directly, and a direct match always beats an inherited colour whatever the parent's weight. */
#del-acct-overlay .rv-confirm__body,
#del-acct-overlay .rv-confirm__ask,
#del-acct-overlay .rv-btn--ghost { color: var(--ink); }
.rv-confirm__body { font: 400 13px/1.5 var(--font); color: var(--ink-2); margin-bottom: 16px; }
/* One line each, at EVERY width. These were inline on a phone at first, with a generated
   trailing space meant to separate them — which doesn't work: whitespace in `content` is
   collapsed exactly like whitespace in markup, so the fragments ran together as
   "see.It goes". Blocks need no separator at all, which is the better answer anyway; the
   breaks are chosen in both layouts rather than left to the width of the box. */
.rv-confirm__ln { display: block; }
/* the account dialog's stack is two sentences with air between them, not a list of three lines —
   the warning has to sit apart from the description or it reads as another bullet. Must come
   AFTER .rv-confirm__ln: same specificity, so source order is what decides it. */
.rv-confirm__ln--gap { margin-top: 14px; }
/* the warning lifts out of .rv-confirm__body's muted --ink-2 to full --ink; the gold word
   inside it is .rv-confirm__word, shared with the TYPE DELETE line below. */
.rv-confirm__ln--warn { color: var(--ink); }
/* the typed-word guard on the account-deletion dialog (v581). Centred and letter-spaced so the
   word being asked for and the word being typed sit in the same visual register. */
.rv-confirm__ask { font: 700 15px var(--font); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 8px; }
/* gold on the word being asked for, so the instruction and the thing to type are the same colour.
   Two tokens, same split the rest of the app uses: --gold is only legible on the dark ground. */
.rv-confirm__word { color: var(--gold-ink); }
[data-theme="dark"] .rv-confirm__word { color: var(--gold); }
.rv-confirm__input { display: block; width: 160px; margin: 0 auto 16px; text-align: center; letter-spacing: .18em; font-weight: 700; text-transform: uppercase; }
.rv-confirm__row { display: flex; gap: 10px; justify-content: center; }
.rv-confirm__row .rv-btn { border-radius: var(--r-pill); padding: 10px 18px; border: 0; cursor: pointer; font: 700 13px var(--font); }
.rv-confirm__row .rv-btn--gold { background: var(--gold); color: #171A26; }
.rv-confirm__row .rv-btn--red { background: var(--primary); color: var(--on-primary); }
.rv-confirm__row .rv-btn--ghost { background: none; border: 1px solid var(--line-strong); color: var(--ink-2); }

/* ============================ MOBILE ============================
   760px, the site's own breakpoint — not the mock's 390px phone frame, which is a
   drawing device rather than a media query. Cards still expand IN PLACE: there is no
   drill-in screen (the mock draws one, the handoff note overrides it). */
@media (max-width: 760px) {
  body[data-page="review"] .rv-lead { display: none; }
  body[data-page="review"] .inbox-tabs { gap: 6px; flex-wrap: nowrap; }
  /* Equal-width pills filling the row — the handoff's one explicit tab decision.
     The type drops to 9.5px/.05em here, below the mock's 10.5px/.08em, because the mock
     never had to fit the real words: SUGGESTIONS plus its count badge measures wider than
     a third of a 390px screen at the drawn size, and a nowrap row that overflows makes the
     PAGE scroll sideways. min-width:0 is what lets flex:1 shrink a pill below its own
     content — without it the row overflows no matter what the flex basis says. */
  body[data-page="review"] .inbox-tab {
    flex: 1; min-width: 0; justify-content: center; padding: 10px 6px; min-height: 40px;
    font-size: 9.5px; letter-spacing: .05em;
  }
  body[data-page="review"] .rv-card { padding: 16px; }
  body[data-page="review"] .rv-card.is-open { padding: 16px; }
  /* The head goes two-line: name above a mono meta line, and the collapsed snippet is
     dropped — there is no room for it beside a name on a phone. */
  /* SUGGESTION head, two rows: date + name, then city · links with the status chip pushed
     right and the chevron last. It was three rows, with the chip stranded up beside the
     name and a line of its own for the city.
     Same `display: contents` move as the Places head — dissolve the inner wrapper so every
     part becomes a grid cell in its own right, and place each one explicitly. */
  body[data-page="review"] .rv-head:not(.rv-head--place) {
    display: grid; grid-template-columns: auto auto 1fr auto;
    align-items: center; row-gap: 7px; column-gap: 8px;
  }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-head__main { display: contents; }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-date { grid-row: 1; grid-column: 1; }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-name {
    grid-row: 1; grid-column: 2 / 4; justify-self: start; width: auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* Status chip on the first row, chevron under it on the second — one control stacked over
     the other down the right edge, rather than the pair sharing a line. */
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-chip { grid-row: 1; grid-column: 4; justify-self: end; }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-city { grid-row: 2; grid-column: 1; }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-nlinks { grid-row: 2; grid-column: 2; }
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-chev { grid-row: 2; grid-column: 4; justify-self: end; }
  body[data-page="review"] .rv-snip, body[data-page="review"] .rv-sep { display: none; }
  body[data-page="review"] .rv-name { font-size: 15px; }
  body[data-page="review"] .rv-date, body[data-page="review"] .rv-city { font-size: 9.5px; }
  /* City matches LINKS on the suggestion head — they sit on the same line, and two mono
     labels a size apart read as a mistake. Scoped off the Places head, which keeps its
     smaller city because it shares its line with the kind chip instead. */
  body[data-page="review"] .rv-head:not(.rv-head--place) .rv-city { font-size: 11px; }
  /* Scoped to the CARD HEAD. The head wraps its date under the name on a phone, but the
     message card's date must keep the auto margin that pins it right, the way it does on
     the laptop — an unscoped reset here silently flattened both. */
  body[data-page="review"] .rv-head .rv-date--end { margin-left: 0; }
  body[data-page="review"] .rv-msg__top { flex-wrap: wrap; align-items: baseline; }
  body[data-page="review"] .rv-msg__top .rv-date--end { margin-left: auto; margin-top: 0; }

  /* PLACES card head on a phone: the origin tag moves to the very top right, on its own
     line, with the name under it and the kind + city under that.
     `display: contents` on the inner wrapper is what makes this possible — it dissolves
     that span so its children become grid items of the head itself, which is the only way
     to place them individually without changing the markup order the laptop depends on.
     Every item names its own cell, because grid places explicit items before auto ones and
     a single auto-placed leftover would land in whatever cell it liked. */
  body[data-page="review"] .rv-head--place {
    display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto;
    align-items: center; row-gap: 11px; column-gap: 8px;
  }
  body[data-page="review"] .rv-head--place .rv-head__main { display: contents; }
  /* Row 1 is metadata only: where it came from on the left, when it arrived on the right. */
  body[data-page="review"] .rv-head--place .rv-via { grid-row: 1; grid-column: 1; justify-self: start; }
  body[data-page="review"] .rv-head--place .rv-date--end { grid-row: 1; grid-column: 2 / -1; justify-self: end; margin: 0; }
  /* Row 2 is the place: name, kind, city, and the control to open it. The name takes the
     only flexible column and truncates rather than pushing the chips off the card — a
     long name would otherwise overflow a 390px screen and scroll the page sideways. */
  body[data-page="review"] .rv-head--place .rv-name {
    grid-row: 2; grid-column: 1; width: auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-page="review"] .rv-head--place .rv-cat { grid-row: 2; grid-column: 2; }
  body[data-page="review"] .rv-head--place .rv-city { grid-row: 2; grid-column: 3; margin: 0; }
  body[data-page="review"] .rv-head--place .rv-chev { grid-row: 2; grid-column: 4; }
  /* Expanded, row 2 has nothing left in it, so the chevron joins row 1 rather than sitting
     alone on a line of its own — and the date shortens to leave it the last column. */
  body[data-page="review"] .rv-card.is-open .rv-head--place .rv-date--end { grid-column: 2 / 4; }
  body[data-page="review"] .rv-card.is-open .rv-head--place .rv-chev { grid-row: 1; grid-column: 4; }

  body[data-page="review"] .rv-grid { grid-template-columns: 1fr; gap: 12px; }
  body[data-page="review"] .rv-vr { display: none; }
  body[data-page="review"] .rv-sub { text-align: left; }
  body[data-page="review"] .rv-photo { height: 120px; }
  body[data-page="review"] .rv-links__set { flex-direction: column; gap: 4px; }
  /* More air between the blocks of an expanded card. A phone shows one card at a time and
     these sections stack vertically, so the laptop's 14px — which reads fine beside a
     three-column grid — leaves them looking pushed together here. */
  body[data-page="review"] .rv-body > * + * { margin-top: 20px; }
  body[data-page="review"] .rv-links,
  body[data-page="review"] .rv-thread,
  body[data-page="review"] .rv-compose { margin-top: 20px; }
  body[data-page="review"] .rv-by { margin-top: 20px; padding-top: 16px; }
  body[data-page="review"] .rv-actions { margin-top: 20px; padding-top: 20px; }

  /* Actions stack full-width, ≥48px, each keeping its caption — the caption IS the
     tooltip here, because a touch device never hovers. An HR separates Pass from the
     other two so the destructive one is never a mis-tap away from them. */
  body[data-page="review"] .rv-actions { display: flex; flex-direction: column; gap: 14px; }
  body[data-page="review"] .rv-act { width: 100%; }
  body[data-page="review"] .rv-act--start,
  body[data-page="review"] .rv-act--mid,
  body[data-page="review"] .rv-act--end { justify-self: stretch; }
  body[data-page="review"] .rv-act--end { border-top: 1px solid var(--rv-hair); padding-top: 14px; }
  body[data-page="review"] .rv-btn { width: 100%; min-height: 48px; padding: 14px; font-size: 15px; }
  body[data-page="review"] .rv-compose__row { flex-direction: column; }
  body[data-page="review"] .rv-compose__row .rv-btn { min-height: 44px; }
  /* FROM and TO get a line each on a phone — together they're two long addresses, and
     wrapping mid-address reads as one run-on string. The separating dot goes with them. */
  body[data-page="review"] .rv-compose__from,
  body[data-page="review"] .rv-compose__to { display: block; }
  body[data-page="review"] .rv-compose__dot { display: none; }
  /* Back to a stacked flex column on a phone — the centred three-track row is a laptop
     shape. Both grid placements are neutralised with it, or they'd fight the stack. */
  /* Reply and HANDLED share one row on a phone. Reply is the only button on this card that
     is NOT full-width: a message has two possible responses rather than one, and stacking
     them makes the smaller one look like an afterthought while costing a whole row. It
     keeps a 44px target — smaller in width, not in reach. */
  body[data-page="review"] .rv-msg__foot {
    display: flex; flex-direction: row; align-items: center; gap: 12px;
  }
  body[data-page="review"] .rv-msg__reply {
    grid-column: auto; justify-self: auto;
    width: auto; min-height: 44px; padding: 11px 22px; font-size: 13.5px;
  }
  body[data-page="review"] .rv-msg__foot .rv-switch { grid-column: auto; justify-self: auto; margin-left: auto; }
  body[data-page="review"] .rv-switch { justify-content: center; min-height: 44px; }
  /* The controls take their own line on a phone rather than competing with the name for a
     390px row. They stay right-aligned there, so the row still reads left-to-right as
     "what it is, then what you can do about it". */
  body[data-page="review"] .rv-done__row { flex-wrap: wrap; row-gap: 10px; }
  body[data-page="review"] .rv-done__snip { display: none; }

  /* HANDLED SUGGESTION, two rows on a phone:
       date · name .......... Reopen
       PASSED ............... ⌄
     The two controls have to land on different rows, which flex-wrap cannot do — they're
     siblings inside one group. `display: contents` on that group dissolves it so each
     button becomes a grid cell of the row itself.
     Scoped to rows that HAVE a chevron and DON'T lead with the chip: decided places carry
     two buttons (Undo and Delete) that would collide in one cell, and handled messages lead
     with their status chip, so neither wants this arrangement. */
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-done__row {
    display: grid; grid-template-columns: auto 1fr auto;
    align-items: center; row-gap: 10px; column-gap: 10px;
  }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-done__end { display: contents; }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-done__date { grid-row: 1; grid-column: 1; }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-done__name { grid-row: 1; grid-column: 2; }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-undo { grid-row: 1; grid-column: 3; justify-self: end; }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-chip { grid-row: 2; grid-column: 1 / 3; justify-self: start; }
  body[data-page="review"] .rv-done--peek:not(.rv-done--chipfirst) .rv-peek { grid-row: 2; grid-column: 3; justify-self: end; }

  /* DECIDED PLACE, two rows on a phone:
       date · name
       LIVE ................. View ↗
     The outcome sits under the name it belongs to rather than trailing it, and the controls
     stay grouped on the right — Passed rows carry two of them (Undo, Delete) and they must
     not be split across cells the way the suggestion row's pair deliberately is. */
  body[data-page="review"] .rv-done--place .rv-done__row {
    display: grid; grid-template-columns: auto 1fr;
    align-items: center; row-gap: 10px; column-gap: 10px;
  }
  body[data-page="review"] .rv-done--place .rv-done__date { grid-row: 1; grid-column: 1; }
  body[data-page="review"] .rv-done--place .rv-done__name { grid-row: 1; grid-column: 2; }
  body[data-page="review"] .rv-done--place .rv-chip { grid-row: 2; grid-column: 1; justify-self: start; }
  body[data-page="review"] .rv-done--place .rv-done__end { grid-row: 2; grid-column: 2; justify-self: end; }

  /* Still a card on a phone, just a roomier one — the point is that the queue stays
     visible behind it, so you can see you're checking a photo rather than having left. */
  body[data-page="review"] .lightbox { width: 92vw; height: 58vh; }

  /* CENTRED CARD ON A PHONE TOO — not the bottom sheet the mock drew. A sheet welded to
     the bottom edge reads as a menu you can flick away; this is a question with
     consequences (publishing, or sending mail to a real person under our name), and it
     should sit in the middle of the screen and be answered. Inset from both edges for the
     same reason — full-bleed reads as a new screen rather than something on top of the one
     you were just on. Buttons stay full-width inside it. */
  .overlay--rvc { align-items: center; }
  .sheet--confirm {
    width: min(340px, calc(100% - 44px)); max-width: none;
    border-radius: var(--r-sheet); padding: 22px 20px 24px;
  }
  .rv-confirm__row { flex-direction: column; }
  .rv-confirm__row .rv-btn { width: 100%; min-height: 48px; padding: 14px; font-size: 15px; }
}

/* ============================================================================
   PEOPLE tab — /review, admin only. Find someone, see who they are, change
   what they can do.

   NO ADMIN CHIP, DELIBERATELY (owner, 2026-08-20). The tier control offers
   Member / Advisor / Curator and nothing else: if a session is ever hijacked,
   the screen that could mint more admins is not one that exists. Admin is
   granted in the Supabase table editor, by hand, by a person. ⚠ The database
   still ALLOWS it — profiles_admin_all has no clause about which role you may
   write — so this is a guard against accident and against a stolen session
   driving the UI, not a wall. Making it a wall is a `with check` clause on
   that policy, which is a migration and a separate decision.
   ============================================================================ */

/* ---- the tabs: folder tabs on a rail, ONE set of rules for both widths ----
   Was: outlined pills with a small red circle on each. Now the PILL is the signal —
   red means this queue has something waiting, teal means it is clear — the count leads
   the word, and the shape is a folder tab: square at the bottom, rounded on top, sitting
   on the list it belongs to.

   The rail is the same code at every width. On a laptop four tabs fit and nothing
   scrolls, so the snapping never fires; on a phone they run off the edge and it does.
   No second layout, no breakpoint deciding between two behaviours.

   ⚠ Each rule names .inbox-tab plus a modifier so it out-weighs the originals above,
   and this block sits later in the file. */
body[data-page="review"] .inbox-tabs {
  gap: 4px; margin: 18px 0 22px; flex-wrap: nowrap;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
body[data-page="review"] .inbox-tabs::-webkit-scrollbar { display: none; }

body[data-page="review"] .inbox-tab--todo,
body[data-page="review"] .inbox-tab--clear {
  flex: none; scroll-snap-align: center;
  border-radius: 14px 14px 0 0; padding: 11px 12px 15px; min-height: 44px;
  font: 700 11px var(--mono); letter-spacing: .07em;
  /* THE BOTTOM DISSOLVES INTO THE PAGE. A mask, not a background gradient, so the
     outlined tab's border fades with its fill instead of staying as a hard hairline
     across the bottom of a shape that has already faded out. Starts at 70% because the
     label sits vertically centred and anything earlier eats the bottom of the type. */
  -webkit-mask-image: linear-gradient(180deg, #000 70%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 70%, transparent 100%);
  /* UNSELECTED IS FADED, SELECTED IS FULL STRENGTH. That one difference is the whole
     "you are here" signal now — no cap, no ring, no swapped fill. */
  opacity: .42;
}
body[data-page="review"] .inbox-tab--todo.is-active,
body[data-page="review"] .inbox-tab--clear.is-active { opacity: 1; }

/* Work waiting: filled red. Nothing waiting: teal, outlined — a queue with nothing in
   it should not carry as much weight on the page as one that does. */
body[data-page="review"] .inbox-tab--todo {
  background: var(--primary); color: var(--on-primary); border: 0;
}
body[data-page="review"] .inbox-tab--clear {
  background: none; color: var(--teal); border: 1px solid var(--teal);
}
/* ⚠ .inbox-tab.is-active sets BOTH a gold-ink border AND gold-ink text, and it
   out-weighs the two rules above (one more class). Clear both here, or the open tab —
   the one tab you most need to read — turns gold and outlined. Colour has to be
   restated, not just the border; leaving it out is a silent half-fix. */
body[data-page="review"] .inbox-tab--todo.is-active { border: 0; color: var(--on-primary); }
body[data-page="review"] .inbox-tab--clear.is-active { border-color: var(--teal); color: var(--teal); }

/* The count leads the word and loses its own circle — it had become a red badge sitting
   inside a red tab. Always bold, whatever the number. Fixed-width digits so 9 → 10
   doesn't nudge the word along. */
body[data-page="review"] .inbox-tab--todo .inbox-tab__n,
body[data-page="review"] .inbox-tab--clear .inbox-tab__n {
  background: none; padding: 0; color: inherit;
  font-weight: 800; font-variant-numeric: tabular-nums;
}

@media (max-width: 760px) {
  /* The rail runs to both screen edges, so a tab half off the edge reads as "there is
     more this way" rather than as a tab that got cut. The padding puts the content back
     where the column is, so the first and last tabs still line up with everything else.
     ⚠ The equal-width rule above (flex:1; min-width:0) has to be undone here or every
     tab shrinks to a quarter of the screen and nothing ever overflows to scroll. */
  body[data-page="review"] .inbox-tabs {
    margin-inline: -20px; padding-inline: 20px; scroll-padding-inline: 50%;
  }
  body[data-page="review"] .inbox-tab--todo,
  body[data-page="review"] .inbox-tab--clear {
    flex: none; min-width: auto; font-size: 11px; letter-spacing: .06em;
    padding: 12px 13px 16px; min-height: 46px;
  }
}

/* ---- find someone ---- */
body[data-page="review"] .rv-find {
  display: flex; align-items: center; gap: 10px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 10px 18px;
}
body[data-page="review"] .rv-find__ico { width: 16px; height: 16px; flex: none; color: var(--ink-4); }
body[data-page="review"] .rv-find__inp {
  flex: 1; min-width: 0; background: none; border: 0; padding: 0;
  font: 400 14px var(--font); color: var(--ink);
}
body[data-page="review"] .rv-find__inp:focus { outline: none; }
body[data-page="review"] .rv-find__inp::placeholder { color: var(--ink-4); }
body[data-page="review"] .rv-find__n {
  font: 700 10px var(--mono); letter-spacing: .1em; color: var(--ink-3); flex: none;
}

/* ---- the row: everything, on one line ----
   The whole point of this tab is reading a list, so every fact a person has lives on
   the collapsed line and opening a card is only for the control. The three meta cells
   are FIXED WIDTHS, not auto — auto would size each card to its own contents and the
   columns would stagger down the page, which is the difference between a table you can
   scan and nine unrelated sentences. */
body[data-page="review"] .rv-person__mail {
  font: 400 13px var(--font); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body[data-page="review"] .rv-person__meta {
  flex: none; display: grid; grid-template-columns: 106px 74px 132px 186px;
  align-items: baseline; gap: 14px;
}
/* The public byline. Reads as a NAME, not as data — same face and size as the address
   beside it, so the three identity fields look like one another and not like the mono
   columns that follow them. */
body[data-page="review"] .rv-person__disp { font: 400 13px var(--font); color: var(--ink-2); }
body[data-page="review"] .rv-person__meta > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A blank home destination and a person who has contributed nothing both keep their
   column — an absent cell would shift everything after it out of line. */
body[data-page="review"] .rv-person__none { color: var(--ink-4); }
/* The tier pill sits where the other three tabs put their status chip, and it IS the
   status: what this person can do is the one thing you came here to see. */
/* THE PILL RANKS THE THREE TIERS, it doesn't just name them: Member is a teal outline,
   Advisor a gold outline, Curator solid gold. Only the top one is filled, so the people
   who can publish and edit stand out of a long list without being read.
   Member and Advisor need no rules — the base .rv-role is already a gold outline and
   .rv-role--member is already a teal one. Only the fill is new.
   ⚠ Scoped to the person row. The SAME .rv-role renders beside a submitter's name on the
   Suggestions cards, where it is a footnote to somebody else's tip; a solid pill there
   would shout on every card in the queue.
   Fixed width so the right edge of every row lines up and the meta columns beside them
   can't be nudged apart by a longer word. */
body[data-page="review"] .rv-head--person .rv-role {
  flex: none; width: 76px; text-align: center;
}
body[data-page="review"] .rv-head--person .rv-role--curator {
  background: var(--gold); color: #171A26; border-color: transparent;
}

/* ---- opened: the tier control ---- */
body[data-page="review"] .rv-tier__set { display: flex; flex-wrap: wrap; gap: 8px; }
body[data-page="review"] .rv-tierchip {
  font: 700 12.5px var(--font); border-radius: var(--r-pill);
  padding: 9px 20px; min-height: 38px; cursor: pointer;
  border: 1px solid var(--line-mid); background: none; color: var(--ink-2);
}
/* RINGED, not filled — the same language the ✎ editor's SECTION control uses,
   so "this is the one that's set" reads the same in both places. */
body[data-page="review"] .rv-tierchip.is-on {
  color: var(--gold-ink); border-color: var(--gold);
  box-shadow: inset 0 0 0 1px var(--gold);
}
/* THE CHIP MATCHES THE PILL IT PRODUCES. Member teal outline, Advisor gold outline,
   Curator solid gold — the same three-step ranking the row's tier pill uses, so picking
   a tier here looks like the badge you are about to give someone. Advisor needs no rule:
   the gold ring above already is it. */
body[data-page="review"] .rv-tierchip--member.is-on {
  color: var(--teal); border-color: var(--teal); box-shadow: inset 0 0 0 1px var(--teal);
}
body[data-page="review"] .rv-tierchip--curator.is-on {
  background: var(--gold); color: #171A26; border-color: transparent; box-shadow: none;
}
/* HELP TEXT IS FULL-STRENGTH INK, NOT MUTED. A line that explains what a control does
   is the one thing on the card a reader hasn't already worked out, so it can't be the
   quietest thing on it. Same correction the section head's byline needed. */
body[data-page="review"] .rv-tier__cap {
  margin: 10px 0 0; font: 400 12.5px/1.5 var(--font); color: var(--ink);
  /* RIGHT on the laptop, where the chips sit left and the card is wide enough that a
     left-aligned sentence under them reads as a second row of the control rather than as
     a note about it. Put back to left on a phone below — a right-aligned paragraph on a
     narrow column has a ragged left edge, which is the harder one to read down. */
  text-align: right;
}
/* The tab's own lead sentence, for the same reason. Scoped to /review, and desktop-only
   in practice — the phone hides .rv-lead. */
body[data-page="review"] .rv-lead { color: var(--ink); }

@media (max-width: 759px) {
  /* ⚠ SPECIFICITY: the suggestion head's phone rules are written
     `.rv-head:not(.rv-head--place)`, which MATCHES a person head and outweighs
     a plain `.rv-head--person`. Repeating the :not() here levels the weight,
     and this block sits later in the file, so it wins. Do not "tidy" the
     :not() away — the person head silently reverts to the suggestion grid. */
  body[data-page="review"] .rv-head--person:not(.rv-head--place) {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center; row-gap: 6px; column-gap: 8px;
  }
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-head__main { display: contents; }
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-role {
    grid-row: 1; grid-column: 1; width: auto;
  }
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-name {
    grid-row: 1; grid-column: 2; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-chev { grid-row: 1; grid-column: 3; }
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-person__mail {
    grid-row: 2; grid-column: 1 / -1;
  }
  /* Fixed columns are a laptop idea. At 390px the four values wrap as a plain run,
     because a 74px column of its own would strand each one on a line. */
  body[data-page="review"] .rv-head--person:not(.rv-head--place) .rv-person__meta {
    grid-row: 3; grid-column: 1 / -1;
    display: flex; flex-wrap: wrap; gap: 4px 12px;
  }
  body[data-page="review"] .rv-tier__cap { text-align: left; }
  /* Three chips share the row evenly rather than wrapping the third onto a
     line of its own, and each clears the 44px touch target. */
  body[data-page="review"] .rv-tier__set { gap: 6px; }
  body[data-page="review"] .rv-tierchip { flex: 1; min-width: 0; padding: 10px 4px; min-height: 44px; }
}

/* ════════════════════════════════════════════════════════════════════════════════
   PLACE EDITOR — six blocks, two columns, one Links row (v481)
   Scoped to .sheet--edit throughout, NOT to a page: this sheet opens from a place
   detail AND from /review, so a body[data-page] scope would kill half of it.
   NB the wide layout is a MEDIA query, not a container query — the sheet would have
   had to be its own container while also being what the query resizes.
   ════════════════════════════════════════════════════════════════════════════════ */
.edit__who { font: 400 11.5px var(--font); color: var(--ink-3); margin-top: 3px; }
/* ONE line: "Edit - Cơm Tấm Ba Ghiền · HCMC". The font size is set by fitEditHead() in app.js,
   which steps down from 30px until the line stops overflowing — a short name gets the big
   treatment, a long one shrinks rather than wrapping. The halves are sized in `em` so they follow
   whatever the fitter lands on. */
/* no rule under the head: the blocks below are filled panels now, and each one already draws
   its own edge — a divider between the title and the first panel just adds a second line saying
   the same thing */
.sheet--edit .edit__head {
  align-items: center; margin-bottom: 0;
  padding-bottom: 10px; border-bottom: 0;
}
.sheet--edit .edit__title { display: flex; flex-wrap: nowrap; align-items: baseline;
  flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; line-height: 1.2; }
.sheet--edit .edit__dash { color: var(--ink-4); font-weight: 400; white-space: pre; }
.sheet--edit .edit__who { font: 500 .88em/1.2 var(--font); color: var(--gold-ink);
  white-space: nowrap; margin-top: 0; }
[data-theme="dark"] .sheet--edit .edit__who { color: var(--gold); }
/* a filled red disc, not a grey glyph — closing is the one control up here, and 28px is a real
   target rather than a 14px character */
.sheet--edit .edit__close { align-self: center; width: 28px; height: 28px;
  background: var(--primary); color: #FFFFFF; font: 500 15px/1 var(--font); }
/* one label gap for every control in this sheet. SCOPED — the global .input rule is
   every other form in the app and has not been signed off for this. */
/* Photos and the film are two different jobs — side by side once the sheet has the room, stacked
   when it doesn't, with the divider turning from a vertical rule into a short centred one. */
.ed-pf { display: grid; grid-template-columns: minmax(0, 1fr); gap: 15px; }
.ed-pf__photos, .ed-pf__film { min-width: 0; }
.ed-pf__film { position: relative; padding-top: 15px; }
.ed-pf__film::before { content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 84px; height: 1px; background: var(--line-mid); }
.ed-pf .ed-blk__head { margin-bottom: 9px; }
.ed-pf .edit-photos { margin-top: 0; }
/* the photo hint is instructions, not a status stamp — 8.5px mono at .14em tracking is a label,
   and this is a sentence someone has to read. SCOPED: the wizard's own dropzone status is a
   two-word confirmation and keeps the small treatment. */
.sheet--edit .dropzone__status { margin-top: 10px; font: 400 13.5px/1.45 var(--font);
  letter-spacing: 0; text-align: center; }
.ed-pf__film { display: flow-root; }   /* contains the floated toggle below */
/* The film switch. ⚠ The GLOBAL .toggle-pill is the Suggest consent row: full-width, because it
   sits in a stack of rows that have to line up. Here there is nothing to line up with, and
   stretched across the column it read as a container waiting to be filled. Shrink-wrapped, and
   floated right so its edge meets the YouTube row's above it. */
.sheet--edit .toggle-pill { display: inline-flex; width: auto; align-items: center; gap: 14px;
  float: right; clear: both; border-radius: var(--r-pill);
  padding: 9px 12px 9px 16px; margin-top: 10px; cursor: pointer; }
.sheet--edit .toggle-pill:hover { border-color: var(--line-strong); }
.sheet--edit .toggle-pill__label { font: 700 13.5px var(--font); color: var(--ink); white-space: nowrap; }
/* The cover leads at three tiles square — it becomes the card and the hero, and at 74px it looked
   like one of the eight that don't. The rest sit three across, so both halves land on the same
   grid. On a phone the pair is wider than the screen and wraps into a stack; centred, because
   left-aligned it sat to one side of its own panel with dead space beside it. */
.edit-photos { justify-content: center; }
.edit-photos__lead .edit-photos__item { width: 236px; height: 236px; }
.edit-photos__lead .edit-photos__badge { width: 26px; height: 26px; font-size: 14px; }
.edit-photos__rest { display: grid; grid-template-columns: repeat(3, 74px); gap: 8px; }
.sheet--edit .input, .sheet--edit .textarea { margin-top: 8px; }
.sheet--edit .textarea--short { min-height: 58px; }

/* ── the blocks: full-width 2px rules, headings you scroll past rather than click ── */
/* FILLED PANELS, no rules. At 980px four blocks are visible at once, and a horizontal rule can
   say nothing about the VERTICAL gap between two columns — a line separates in one direction, a
   region separates in two. It also removes a judgement call that never held: with rules, the
   first block in each column had to suppress its own, and the two columns had to agree. */
.ed-blk { padding: 15px 16px 17px; margin-top: 14px; border-top: 0;
  border-radius: var(--r-card); background: rgba(127,127,127,.05); }
[data-theme="dark"] .ed-blk { background: rgba(255,255,255,.055); }
/* A filled panel and a filled input are both surfaces, so the fields would go flat against the
   block. Every input, textarea and link row lifts off it. */
.ed-blk .input, .ed-blk .textarea, .ed-blk .lk-row { background: var(--card); }
[data-theme="dark"] .ed-blk .input,
[data-theme="dark"] .ed-blk .textarea,
[data-theme="dark"] .ed-blk .lk-row { background: rgba(0,0,0,.22); }
.ed-blk__head { display: flex; align-items: baseline; gap: 9px; }
.ed-blk__name { margin: 0; font: 700 15px var(--font); letter-spacing: -.01em; color: var(--gold-ink); }
/* The hint reads as part of the heading — "Photos | First = Cover | 6 of 9" — so it sits directly
   beside the name with its own leading pipe, not flung to the far right where the eye has to
   travel to collect the second half of one sentence. */
.ed-blk__hint { font: 400 11px var(--font); color: var(--ink-4); }
.ed-blk__hint::before { content: '|'; margin-right: 9px; color: var(--line-strong); }
.ed-blk__head + .field, .ed-blk__head + .lk { margin-top: 10px; }
/* the first block sits under the head's own rule, so it needs none of its own */
.ed-col:first-child > .ed-blk:first-child { margin-top: 0; }

/* between FIELDS: a short centred hairline. Different length and weight from the block
   rules above, so the two can never read as the same kind of break. */
.ed-blk .field + .field, .ed-blk .field + .lk, .ed-blk .lk + .field {
  position: relative; margin-top: 15px; padding-top: 15px;
}
/* The short centred hairlines between fields are GONE — a filled panel already separates its
   region, and a rule inside one is a second divider inside something already divided. The spacing
   they created stays exactly as it was: only the line is removed, not the gap it sat in. */

.ed-cols { display: grid; grid-template-columns: minmax(0, 1fr); }
.ed-col { min-width: 0; }

/* ── section chips wear the nav tile (U.navIcon, the same asset the header draws) ── */
.sheet--edit .chip { display: inline-flex; align-items: center; gap: 5px; }
.sheet--edit .chip__mark { width: 15px; height: 15px; display: none; }   /* shown ≥760px */
.sheet--edit .chip__mark .nav-ico { width: 15px; height: 15px; filter: none; }
/* A SELECTED section is RINGED in its own brand colour — no fill. The tile inside the chip
   already carries that colour, so the outline extends it round the pill; a fill would fight the
   tile it sits next to. Label goes bold.
   ⚠ The label is white on DARK only. In light the sheet is white, so white text would vanish —
   there it takes the section's own colour instead. Same idea, only one that survives both grounds. */
.sheet--edit .chip--toggle.is-on { font-weight: 700; background: none; box-shadow: none; }
[data-theme="dark"] .sheet--edit .chip--toggle.is-on { color: #FFFFFF; }
.sheet--edit .chip--toggle.is-on[data-value="eat"]     { border-color: #DA251D; color: #DA251D; }
.sheet--edit .chip--toggle.is-on[data-value="drink"]   { border-color: #F5C242; color: var(--gold-ink); }
.sheet--edit .chip--toggle.is-on[data-value="stay"],
.sheet--edit .chip--toggle.is-on[data-value="live"]    { border-color: #23A69D; color: #23A69D; }
.sheet--edit .chip--toggle.is-on[data-value="explore"] { border-color: var(--explore); color: var(--explore); }
[data-theme="dark"] .sheet--edit .chip--toggle.is-on[data-value="eat"],
[data-theme="dark"] .sheet--edit .chip--toggle.is-on[data-value="drink"],
[data-theme="dark"] .sheet--edit .chip--toggle.is-on[data-value="stay"],
[data-theme="dark"] .sheet--edit .chip--toggle.is-on[data-value="live"],
[data-theme="dark"] .sheet--edit .chip--toggle.is-on[data-value="explore"] { color: #FFFFFF; }
.sheet--edit .chips--spread { justify-content: space-between; }
.sheet--edit .chips--center { justify-content: center; }

/* ── Links: what a place HAS is a row, what it doesn't is a chip on the rail ────── */
.lk { margin-top: 14px; }
.lk__rows { margin-top: 8px; display: flex; flex-direction: column; gap: 7px; }
.lk-row {
  display: grid; grid-template-columns: 34px minmax(0, 1fr) 30px;
  align-items: center; gap: 9px;
  background: var(--field-bg); border: 1px solid var(--field-line);
  border-radius: var(--r-input); padding: 6px 7px 6px 9px;
}
.lk-row:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
/* dashed = nothing in it yet. An empty row ASKS for the link; that's the whole point of it. */
.lk-row--empty { border-style: dashed; }
/* the video row: same anatomy, no ✕ — a film isn't a link you attach and detach */
.lk-row--solo { grid-template-columns: 34px minmax(0, 1fr); margin-top: 8px; }
.lk-row__mark { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; color: var(--ink); }
.lk-row__mark svg { width: 26px; height: 26px; display: block; }
.lk-row__input {
  width: 100%; border: 0; background: none; padding: 9px 0; margin: 0;
  font: 400 16px var(--font); color: var(--ink); min-width: 0;
}
.lk-row__input:focus { outline: none; }
.lk-row__drop {
  width: 28px; height: 28px; border: 0; background: none; cursor: pointer;
  color: var(--ink-5); font-size: 15px; line-height: 1; border-radius: 50%;
}
.lk-row__drop:hover { color: var(--primary); }
.lk__add { margin-top: 10px; }
/* brand teal — it heads the rail beneath it, it isn't a caption on the rows above */
.lk__add-label { font: 600 10.5px var(--font); color: var(--teal); display: block;
  margin-bottom: 7px; letter-spacing: .04em; }
.lk__rail { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.lk-chip {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line-strong); border-radius: var(--r-pill);
  background: none; cursor: pointer; padding: 4px 11px 4px 6px;
  font: 400 11.5px var(--font); color: var(--ink-2);
}
.lk-chip:hover { border-color: var(--teal); color: var(--ink); }
.lk-chip__mark { width: 19px; height: 19px; display: flex; }
.lk-chip__mark svg { width: 19px; height: 19px; display: block; }
.lk__rail--done { font: 400 11.5px var(--font); color: var(--ink-4); }

/* ── the sheet uses the width when it has it ───────────────────────────────────── */
@media (min-width: 760px) {
  .sheet--edit { width: min(980px, calc(100% - 40px)); }
  .ed-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 30px; }
  /* the second column starts level with the first, so it needs no rule above it either */
    .ed-col + .ed-col > .ed-blk:first-child { margin-top: 0; }
  /* The SECTION row is five short words with room to spare, so the chips take it — bigger tile,
     bigger label. `nowrap` is the guard: the ask was explicitly one row, and a wrapped fifth chip
     is worse than a smaller one. Scoped to .chips--spread so the 28-chip TAGS grid below is
     untouched — that one has no room to give. */
  .sheet--edit .chip__mark { display: inline-flex; width: 19px; height: 19px; }
  .sheet--edit .chip__mark .nav-ico { width: 19px; height: 19px; }
  /* ⚠ `flex-wrap: nowrap` on the ROW is the guarantee, not `white-space: nowrap` on the chip —
     that only stops a label breaking mid-word; the row still wrapped Live onto a line of its own.
     And `space-between` has no gap to shrink: the space between chips IS the leftover, so the only
     way to close it is to make the chips narrower. Sized to fit five in the column with room. */
  .sheet--edit .chips--spread { flex-wrap: nowrap; }
  .sheet--edit .chips--spread .chip { padding: 4px 9px; font-size: 12.5px; gap: 6px; white-space: nowrap; }
  /* Delete far left, Cancel centre, Save far right — the width is the safety. Clustered at the
     right end, Delete sat one pill from Save. The phone can't spread them, so it stacks and
     fences Delete behind a rule instead; that's why the two breakpoints order this differently. */
  .sheet--edit .edit__foot { justify-content: space-between; }
  .sheet--edit .edit__foot .btn--danger { order: 1; }
  .sheet--edit .edit__foot .btn--ghost { order: 2; }
  .sheet--edit .edit__foot .btn--primary { order: 3; }
  /* Photos beside Film. The pool is a fixed width by construction, so the film takes the rest,
     and the divider between them turns vertical — it separates two columns now, not two stacked
     things. */
  /* stretch, not start: the divider is drawn on the film column, so `start` collapsed that
     column to the height of its own content and the rule stopped level with the toggle rather
     than running the depth of the photo pool beside it */
  .ed-pf { grid-template-columns: max-content minmax(300px, 1fr); column-gap: 26px; align-items: stretch; }
  .ed-pf__film { padding-top: 0; padding-left: 26px; }
  .ed-pf__film::before { top: 4px; bottom: 4px; left: 0; height: auto; width: 1px; transform: none; }
  /* Six flags left to wrap broke 5 + 1, and the stranded chip read as more important than the
     five above it. Three per row — and `justify-items` is load-bearing: a grid item defaults to
     `stretch`, which widened each chip to its COLUMN while its own text stayed put. */
  .sheet--edit .chips--center { display: grid; grid-template-columns: repeat(3, max-content);
    gap: 7px 10px; justify-content: center; justify-items: center; }
  /* the tier scale has room here, so it goes back to its normal size */
  .seg--tier button { padding: 8px 10px; font-size: 12px; letter-spacing: 0; }
}

/* The Live/Pending/Flagged segment is a SETTING; Save is the act of committing it — at 22px they
   read as one group of controls. */
.sheet--edit .edit__foot { margin-top: 30px; }
/* DOM order is Save · Cancel · rule · Delete, which IS the phone's stacking order — so the phone
   needs no `order` rules at all. The laptop row reverses it (see the ≥760px block above) because
   at that width the three can be spread apart, which is a better fence than a rule.
   ⚠ It is a real <hr>: .btn--danger carries a `border` shorthand that beats any border-top
   longhand written before it. Same trap as .suggest-chat. */
.edit__foot-rule { display: none; }
@media (max-width: 759px) {
  /* Three pills side by side overflow a 390px sheet, and the whole editor could be dragged
     sideways. Stacked, Save and Cancel are the same decision made two ways and get air rather
     than a rule; only Delete is fenced. */
  .sheet--edit .edit__foot { flex-direction: column; align-items: stretch; gap: 18px; }
  .sheet--edit .edit__foot .btn { width: 100%; }
  .sheet--edit .edit__foot-rule {
    display: block; width: 100%; height: 0;
    margin: 0; border: 0; border-top: 1px solid var(--line);
  }
  /* a wider moat above and below, so a thumb aiming for Cancel has somewhere harmless to land */
  .sheet--edit .edit__foot-rule--pre-delete { margin: 20px 0 16px; border-top-color: var(--line-strong); }
  /* the photo hint breaks into two lines on a phone. Spans, not a <br>: a <br> is permanent and
     there'd be no way to un-break it on the wide sheet. Same device as the Contact page's lead. */
  .sheet--edit .dropzone__status .ln { display: block; }
  body[data-page="add"] .wizard__sub .ln { display: block; }
  body[data-page="signin"] .page__lead { text-align: center; }
  body[data-page="signin"] .page__lead .ln { display: block; }
  /* The 40px above the lockup was tuned for a page with no header, but on a phone it sits on top of
     the safe-area inset and reads as a large empty band. Halved. The inset itself STAYS — in the
     installed app it is the only thing keeping the lockup clear of the Dynamic Island; in Chrome
     and Safari the browser chrome occupies that space and the inset reports 0. */
  .auth { padding-top: calc(20px + env(safe-area-inset-top, 0px)); }
  /* ⚠ SCOPED. Unscoped, this beat .brand-lockup--sheet's margin-bottom:0 — same weight, later in
     the file — and gave the nav and account sheets 20px of dead space under their lockups. */
  body[data-page="signin"] .brand-lockup { margin-bottom: 20px; }
}

/* ════════════════════════════════════════════════════════════════════════════════
   ABOUT — ported from the "WF - About - Design" canvas, layout 1b (Split Sidebar)
   with link Option B. Replaces .people/.person/.made-in/.about-legal, all deleted.
   ════════════════════════════════════════════════════════════════════════════════ */
.ab { max-width: 840px; margin: 0 auto; padding: 26px 0 44px; }
.ab__eyebrow { font: 500 10px var(--mono); letter-spacing: .2em; color: var(--gold-ink); }
/* the design sets this on one line at both sizes; clamp lets it shrink on a narrow phone
   rather than pushing the page into a sideways scroll */
.ab__title {
  margin: 14px 0 0; font: 700 clamp(20px, 6.1vw, 26px)/1.15 var(--font);
  letter-spacing: -.02em; white-space: nowrap;
}
.ab__title i { font-style: italic; color: var(--gold-ink); }
.ab__lead { margin: 18px 0 0; font: 400 15px/1.7 var(--font); color: var(--ink); }
.ab__para { margin: 18px 0 0; font: 400 15px/1.7 var(--font); color: var(--ink-2); }
/* about.html carries data-wash, and the documented light-theme legibility override targets
   .page__lead / .mono-label BY NAME — these classes would not inherit it and would go faint on
   the gold wash. The :not([data-theme="dark"]) guard is load-bearing; see the note on .page__lead. */
[data-theme="light"] body[data-wash] .ab__lead,
html:not([data-theme="dark"]) body[data-wash] .ab__lead { color: rgba(25,26,34,.92); }
[data-theme="light"] body[data-wash] .ab__para,
html:not([data-theme="dark"]) body[data-wash] .ab__para { color: rgba(25,26,34,.82); }

.ab__split { margin-top: 20px; display: flex; flex-direction: column; gap: 18px; }
.ab__split-body { display: flex; flex-direction: column; }
.ab__split-body .ab__para:first-child { margin-top: 0; }
.ab__photo { position: relative; height: 260px; border-radius: var(--r-photo); overflow: hidden; }
.ab__photo-badge {
  position: absolute; right: 12px; bottom: 10px; border-radius: 6px; padding: 4px 10px;
  background: rgba(10,11,17,.6); color: var(--teal);
  font: 500 9px var(--mono); letter-spacing: .14em;
}

/* byline — on a phone it sits in its own card, per the design */
.ab__by {
  margin-top: 22px; display: flex; align-items: center; gap: 12px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 12px 14px;
}
.ab__avatars { display: flex; align-items: flex-end; flex: none; }
.ab__avatar {
  width: 40px; height: 40px; border-radius: 50%; flex: none;
  border: 2px solid var(--card); display: flex; align-items: center; justify-content: center;
  font: 600 13px var(--font); color: var(--ink);
}
.ab__avatar--a { background: linear-gradient(140deg, #3a4358, #232838); }
.ab__avatar--b { background: linear-gradient(140deg, #4a3d33, #232838); margin-left: -10px; }
/* smaller, and sitting lower than the other two — little Trí is little. Deliberate, not a slip. */
.ab__avatar--c { width: 30px; height: 30px; font-size: 12px; margin-left: -8px;
  background: linear-gradient(140deg, #3d4a35, #232838); }
.ab__avatar--a, .ab__avatar--b, .ab__avatar--c { color: rgba(236,235,242,.9); }
.ab__by-name { display: block; font: 600 14px var(--font); }
.ab__by-role { display: block; margin-top: 2px; font: 400 12px var(--font); color: var(--ink-3); }

/* links — 2×2 on a phone, four across on a laptop */
.ab__links {
  margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--line);
  display: grid; grid-template-columns: 1fr 1fr; gap: 24px 16px; justify-items: center;
}
.ab__links-col { display: flex; flex-direction: column; gap: 8px; align-items: center; text-align: center; }
.ab__links-head { font: 700 12px var(--mono); letter-spacing: .16em; color: var(--teal); }
/* nowrap because the hover bolds: the grid columns are fixed fractions so nothing shifts
   sideways, but without this a bolded "Terms of Service" could wrap and jump the row's height */
.ab__links-a { font: 400 13px var(--font); color: var(--ink-2); white-space: nowrap; }
.ab__links-a:hover { color: var(--gold-ink); font-weight: 700; }

@media (min-width: 760px) {
  .ab { padding: 40px 0 56px; }
  .ab__title { font-size: 44px; letter-spacing: -.03em; }
  .ab__lead, .ab__para { font-size: 16px; }
  /* the split: photo rail left, story right — the whole point of layout 1b */
  .ab__split { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 22px; align-items: stretch; }
  .ab__photo { height: auto; min-height: 280px; }
  .ab__split-body { gap: 22px; }
  .ab__split-body .ab__para { margin-top: 0; }
  /* desktop drops the card and right-aligns the byline against the story column */
  .ab__by { background: none; border: 0; padding: 0; align-self: flex-end;
    margin-left: auto; width: max-content; }
  .ab__avatar { width: 44px; height: 44px; font-size: 14px; border-color: var(--bg); }
  .ab__avatar--b { margin-left: -12px; }
  .ab__avatar--c { width: 33px; height: 33px; font-size: 13px; }
  .ab__by-name { font-size: 15px; }
  .ab__by-role { font-size: 13px; }
  .ab__links { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0 24px; }
}


/* ============================================================================
   PLACE DETAIL — LAPTOP (>=900px) THREE-COLUMN REWORK
   Mobile is deliberately untouched: everything below either lives inside the
   900px query or hides a laptop-only element that mobile never showed.
   The column heads, the facts block and the title-row share button are all
   emitted on both breakpoints and hidden here, because they sit in different
   grid parents and CSS cannot move an element between two of them.
   ============================================================================ */
/* ===== At a glance — shared by both breakpoints (the phone runs the same blocks in a
   different order, see .detail__grid above). Only sizes and the column layout differ, and
   those overrides stay in the 900px block. ===== */
/* every gold label on the detail page is one size — the fact labels and the column
   heads were 12px and 10.5px, which read as two systems on one screen */
.detail__colhead,
.detail__fact-lbl {
  display: block; font: 500 12px var(--mono); letter-spacing: .14em;
  text-transform: uppercase; color: var(--gold-ink);
}

/* hairline between the stacked sections */
.detail__colhead--find,
.detail__colhead--tags,
.detail__colhead--added,
.detail__facts { margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--line-soft); }
.detail__facts { display: grid; gap: 16px; }
.detail__fact-val { display: block; margin-top: 5px; font: 400 15px/1.45 var(--font); color: var(--ink); }
/* the address arrives pre-split, so line two continues the line above rather than
   starting a new field */
.detail__fact-val + .detail__fact-val { margin-top: 0; }

/* The week, Monday to Sunday. An OPEN day wears the same domed glass as the back tile and
   the Back-to pill — same two tokens, same off-centre light, same settle — so the three
   read as one family. A CLOSED day is the flat fill with no light on it at all, which is
   the whole point of the treatment: the lit ones look like something you can use.
   An UNKNOWN day is fainter still, because "we never recorded it" must not read as "shut". */
/* ⚠ THE GAP CLAMPS BECAUSE SEVEN CIRCLES IS ALREADY MOST OF A NARROW PHONE. At 36px a side
   they are 252px before any gap at all, and a 320px screen has about 288px of content width —
   so a fixed 12px gap would overflow there while looking right on everything larger. */
.detail__week { display: flex; justify-content: center; gap: clamp(5px, 2.8vw, 12px); margin-top: 10px; }
.detail__day {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 50%;
  font: 600 15px var(--mono); letter-spacing: 0;
}
.detail__day--open {
  color: var(--ink);
  background:
    radial-gradient(120% 110% at 78% -12%, var(--tile-sheen), transparent 58%),
    var(--tile-fill);
  box-shadow: -1px 2px 3px rgba(0,0,0,.20), inset 3px -5px 10px -8px rgba(0,0,0,.6);
}
.detail__day--closed { background: var(--tile-fill); color: var(--ink-4); }
.detail__day--unknown { background: none; color: var(--ink-4); opacity: .45; }
/* today is the day the times above belong to, so it says so */
.detail__day.is-today { color: var(--gold-ink); }
/* the ADDED BY head now carries the hairline, so the byline itself no longer draws one */
.added-by { border-top: 0; padding-top: 0; }

/* The place page's tag chips. Scoped to .detail__meta on purpose — the bare .chip is also the
   wizard's flag chips, the editor's and /review's, and those are dense forms where 10px muted
   is right. Here they are content a reader is meant to take in, so they carry full ink at a
   readable size on both breakpoints. */
.detail__meta .chip {
  font: 500 12px var(--font);
  color: var(--ink);
  padding: 6px 14px;
}
/* the 320px column has more room per chip than a phone's full-width row does */
@media (min-width: 900px) { .detail__meta .chip { font-size: 13.5px; padding: 7px 16px; } }


@media (min-width: 900px) {
  /* .detail__body becomes display:contents so ITS children are cells of the page
     grid — that is what lets tags, socials and the byline sit in three different
     columns while the mobile DOM order stays exactly as it was. */
  /* TWO columns. Added by moved out of its own third column and down the left one,
     under Tags, which is what closed the long empty gap on the right.
     ⚠ THE `1fr` FILLER ROW IS LOAD-BEARING. The write-up spans every row of the left
     column, and a grid distributes a spanning item's extra height EQUALLY across the
     auto rows it covers — which is what pushed At a glance, Street, Find them and Tags
     hundreds of pixels apart. Sizing those rows `min-content` and giving the copy one
     `1fr` row to grow into sends all the slack to that row instead. */
  .detail__grid {
    display: grid;
    grid-template-columns: 320px minmax(0, 1fr);
    grid-template-areas:
      "title    title"
      "media    media"
      ".        sub"
      "iconsrow copy"
      "fhead    copy"
      "socials  copy"
      "facts    copy"
      "thead    copy"
      "tags     copy"
      "ahead    copy"
      "addedby  copy"
      ".        copy";
    grid-template-rows: auto auto repeat(9, min-content) 1fr;
    column-gap: 56px;
    align-items: start;
  }
  .detail__body { display: contents; }

  .detail__sub      { grid-area: sub; align-self: end; }
  .detail__col2     { grid-area: copy; display: block; }
  .detail__meta     { grid-area: tags; }
  .social-links     { grid-area: socials; }
  .added-by         { grid-area: addedby; }
  .detail__facts    { grid-area: facts; }
  .detail__iconsrow { grid-area: iconsrow; }
  .detail__colhead--find   { grid-area: fhead; }
  .detail__colhead--tags   { grid-area: thead; }
  .detail__colhead--added  { grid-area: ahead; }

  /* laptop takes the smaller of the two, the column heads' old size */
  .detail__colhead,
  .detail__fact-lbl { font-size: 10.5px; letter-spacing: .2em; }

  /* --- media: full-width hero, thumbs along the bottom ---
     The tiles start BOTTOM LEFT and fill to the right. Only when the bottom row is
     full do they climb the far right side of the hero, and while that is not needed
     the hero takes the whole width. At today's 9-photo cap (1 cover + 8) the bottom
     row always holds every tile, so the side stack cannot appear and the hero is
     always full width — the reference drew it because it was mocked with 13.
     Four rows of 96 + three 10px gaps is the design's 414px hero, which is why the
     hero keeps an explicit row span now that nothing else occupies rows 1-4. */
  .detail__media {
    margin-top: 24px;
    display: grid;
    grid-template-columns: repeat(9, minmax(0, 1fr));
    grid-auto-rows: 96px;
    gap: 10px;
  }
  /* a full-width rule under the photos. With the At a glance head gone there was nothing
     dividing the pictures from the two columns, and the head's own margin had been the
     only thing holding them apart. The media grid spans both columns, so its own bottom
     border is the full-width line. */
  .detail__media { padding-bottom: 26px; border-bottom: 1px solid var(--line-soft); }
  .detail__hero { grid-column: 1 / -1; grid-row: 1 / 5; height: auto; }
  .detail__thumbs { display: contents; }
  /* a definite row with no column: auto-placement walks them across row 5, left to
     right. Both template branches emit .detail__thumb, so the gradient placeholders
     land the same way as real photos. */
  .detail__thumb { height: 96px; border-radius: 12px; grid-row: 5; }
  /* eight photos and a counter fill row 5's nine columns exactly, so nothing wraps here
     either. A place with eight or fewer gallery photos needs no counter at this width. */
  .detail__thumb:nth-child(n+4):not(.detail__thumb--more) { display: block; }
  .detail__thumb--more.is-mobile-only { display: none; }
  .detail__more-n--m { display: none; }
  .detail__more-n--d { display: block; }
  .detail__more-n { font-size: 26px; }
  .detail__more-peek--m { display: none; }
  .detail__more-peek--d { display: block; }

  /* --- title row: share moves up beside the name --- */
  .detail__title { font-size: 34px; }

  /* --- column 1 --- */

  /* three columns, as on mobile: icons far left, đồng centred, share far right. Only the
     spacing changes here — the 1fr auto 1fr grid and justify-self are what place them. */
  .detail__iconsrow { margin-top: 26px; }
  /* the phone reads the week a size up; the 320px column cannot spare it */
  .detail__week { gap: 9px; }
  .detail__day { width: 32px; height: 32px; font-size: 13px; }
  .detail__iconsrow-icons { font-size: 29px; color: var(--gold-ink); }


  .social-links { margin-top: 16px; flex-wrap: wrap; gap: 16px; }   /* the design drew six; some places carry nine */
  .social-links .social-link svg { width: 34px; height: 34px; }
  .detail__meta { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 10px; }
  .detail__meta:empty { margin-top: 0; }

  /* --- column 2 --- */
  .detail__copy { margin-top: 18px; max-width: 760px; }
  .detail__col2 > .detail__copy { margin-top: 26px; }
  /* `text-wrap: pretty` asks the browser to avoid leaving a single word alone on the last
     line. It is a hint, not a guarantee, and a browser without it simply wraps as before. */
  .detail__para { font: 400 19px/1.8 var(--font); }
  /* the laptop write-up is 16.5px, so its cap steps up to match */
  .dropcap { width: 56px; height: 66px; margin-right: 14px; font-size: 62px; }
  .dropcap--wide { width: auto; min-width: 56px; padding: 0 12px; font-size: 52px; }
  .detail__copy > .detail__para + .detail__para { margin-top: 18px; }
  /* one line of the write-up below the last paragraph, wherever that lands */
  .listing-back { margin-top: 30px; }

  /* --- column 3 --- */
  .added-by { margin-top: 16px; padding-top: 0; border-top: 0; align-items: center; }
  /* auto on BOTH sides, not just the left: a single auto margin pins the button to the right
     wall, a matched pair splits the leftover space evenly and centres it between the end of
     the name and that wall, which is what was asked for. flex:none keeps it at its own width. */
  .added-by .edit-pencil { flex: none; margin: 0 auto; }
}

/* ============================================================================
   THE TEAM — /about/team. Ported from Claude Design "WF - The Team v2", frame 2D
   Portraits, both its desktop and mobile frames. The canvas is dark-only and uses raw
   hexes; everything here runs on the site's tokens so light mode works too. Heading is
   Aboreto, the site's display face, in place of the canvas's Poiret One.
   ============================================================================ */
.tm { padding: 26px 0 44px; display: flex; flex-direction: column; gap: 28px; }

.tm-head { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 12px; }
.tm-head__eyebrow { font: 500 9.5px var(--mono); letter-spacing: .22em; color: var(--gold-ink); }
.tm-head__title { font: 400 30px/1.15 'Aboreto', var(--font); letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink); margin: 0; }
.tm-head__bar { width: 44px; height: 3px; background: var(--primary); }

/* founders — stacked on the phone, photo beside the copy on the laptop */
.tm-founders { display: flex; flex-direction: column; gap: 14px; }
.tm-founders__photo { position: relative; height: 210px; border-radius: 16px;
  background: repeating-linear-gradient(45deg, rgba(127,127,140,.10) 0 10px, rgba(127,127,140,0) 10px 20px), var(--card); }
.tm-founders__cap { position: absolute; left: 12px; bottom: 10px; border-radius: 6px; padding: 3px 8px;
  font: 400 9px var(--mono); color: var(--ink-2); background: var(--card-2, rgba(127,127,140,.16)); }
.tm-founders__copy { text-align: center; display: flex; flex-direction: column; gap: 8px; }
.tm-founders__eyebrow { font: 500 9px var(--mono); letter-spacing: .18em; color: var(--gold-ink); }
.tm-founders__names { font: 700 19px var(--font); color: var(--ink); }
.tm-founders__blurb { margin: 0; font: 400 13px/1.7 var(--font); color: var(--ink-2); }
.tm-founders__stamp { font: 500 9px var(--mono); letter-spacing: .16em; color: var(--teal); }

/* section rules */
.tm-group { display: flex; flex-direction: column; gap: 22px; }
.tm-rule { display: flex; align-items: center; gap: 12px; }
.tm-rule__line { flex: 1; height: 1px; background: var(--line-soft); }
.tm-rule__label { font: 500 9.5px var(--mono); letter-spacing: .18em; }
.tm-rule--gold .tm-rule__label { color: var(--gold-ink); }
.tm-rule--teal .tm-rule__label { color: var(--teal); }

/* the flip cards. Two up on the phone, per the 2d Mobile frame. */
.tm-grid { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; }
.tm-grid--advisors { gap: 10px; }
.tm-card { perspective: 800px; padding: 0; border: 0; background: none; cursor: pointer;
  height: 225px; width: 100%; text-align: center; }
.tm-card--advisor { height: 118px; }
.tm-card__inner { position: relative; display: block; width: 100%; height: 100%;
  transform-style: preserve-3d; transition: transform .5s; }
.tm-card.is-flipped .tm-card__inner { transform: rotateY(180deg); }
/* half speed on the way out, but only for a card closed BY opening another one — a card you
   tap closed yourself still runs at .5s, because you asked for that one directly. */
.tm-card.is-closing .tm-card__inner { transition-duration: 1s; }
.tm-card__face { position: absolute; inset: 0; -webkit-backface-visibility: hidden; backface-visibility: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; padding: 10px; border-radius: 16px; border: 1px solid var(--line-soft); background: var(--card); }
.tm-card--advisor .tm-card__face { border-radius: 14px; }
.tm-card__face--back { transform: rotateY(180deg); background: var(--card-2, var(--card)); }
.tm-card--curator .tm-card__face--back { border-color: rgba(245,194,66,.35); }
.tm-card--advisor .tm-card__face--back { border-color: rgba(35,166,157,.4); }
/* placeholder portrait — a Lucide glyph in the circle a photo will fill */
.tm-card__ph { width: 96px; height: 96px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--card-2, rgba(127,127,140,.16)); border: 4px solid var(--bg); color: var(--ink-2);
  box-shadow: 0 8px 20px rgba(0,0,0,.28); }
.tm-card__ph svg { width: 34px; height: 34px; }
/* ⚠ THE CIRCLE NEVER MOVES. The photo is a child inside it and the transform lives on the photo,
   never on .tm-card__ph — scaling the circle scales its border and its layout box drifts off the
   card's centre line, which is exactly the bug this replaced. Anchored top-right so zooming grows
   the picture down and to the left inside a frame that stays put. */
.tm-card__ph { overflow: hidden; margin-bottom: 14px; }   /* air under the circle only — the
   face's 5px gap still keeps the three text lines tight together */
.tm-card__ph-img { width: 100%; height: 100%; object-fit: cover; display: block;
  transform-origin: 100% 0; transform: scale(1.1); }
/* the comparison card: the photo exactly as delivered, no zoom, no anchor */
/* ⚠ PER BREAKPOINT. The circle is 96px on the phone and 130px on the laptop, so one scale and
   one offset cannot serve both — these were judged on device, separately. */
.tm-card__ph-img--raw { transform-origin: 0 100%; transform: scale(1.5) translate(-13.3px, 18px); }
.tm-card--advisor .tm-card__ph { width: 50px; height: 50px; border-width: 3px; }
.tm-card--advisor .tm-card__ph svg { width: 24px; height: 24px; }
.tm-card__name { font: 700 15px var(--font); color: var(--ink); }
.tm-card--advisor .tm-card__name { font-size: 12.5px; }
.tm-card__city { font: 500 11px var(--mono); letter-spacing: .14em; color: var(--gold-ink); }
.tm-card--advisor .tm-card__city { font-size: 10px; letter-spacing: .12em; color: var(--teal); }
.tm-card__places { font: 400 11px var(--mono); color: var(--teal); }
.tm-card--advisor .tm-card__places { font-size: 10px; }
.tm-card__story { font: 400 11px/1.5 var(--font); color: var(--ink-2); }
.tm-card--advisor .tm-card__story { font-size: 10px; }
@media (prefers-reduced-motion: reduce) { .tm-card__inner { transition: none; } }

@media (min-width: 900px) {
  .tm { padding: 48px 0 64px; gap: 46px; }
  .tm-head { gap: 16px; }
  .tm-head__title { font-size: 46px; letter-spacing: .06em; }
  .tm-head__bar { width: 56px; }
  .tm-founders { display: grid; grid-template-columns: 1fr 420px; gap: 44px; align-items: center; }
  .tm-founders__photo { height: 320px; }
  .tm-founders__copy { text-align: left; gap: 12px; }
  .tm-founders__eyebrow { font-size: 10px; }
  .tm-founders__names { font-size: 26px; }
  .tm-founders__blurb { font-size: 15.5px; line-height: 1.75; }
  .tm-founders__stamp { font-size: 10px; }
  .tm-group { gap: 26px; }
  .tm-rule { gap: 16px; }
  .tm-rule__label { font-size: 11px; letter-spacing: .2em; }
  /* the canvas draws 5 curators and 6 advisors per row; with 4 and 2 the tracks are fixed
     so a short roster does not stretch each card across the page */
  .tm-grid--curators { grid-template-columns: repeat(5, 1fr); gap: 14px; }
  .tm-grid--advisors { grid-template-columns: repeat(6, 1fr); gap: 10px; }
  .tm-card { height: 250px; }
  .tm-card--advisor { height: 130px; }
  .tm-card__ph { width: 130px; height: 130px; }
  .tm-card__ph-img--raw { transform: scale(1.7) translate(-19px, 21px); }   /* 10px left, 10px down on screen */
  .tm-card__ph svg { width: 46px; height: 46px; }
  .tm-card--advisor .tm-card__ph { width: 56px; height: 56px; }
  .tm-card__name { font-size: 17px; }
  .tm-card--advisor .tm-card__name { font-size: 13px; }
  .tm-card__city { font-size: 12.5px; letter-spacing: .16em; }
  .tm-card__places { font-size: 12.5px; }
  .tm-card__story { font-size: 10.5px; }
}
