/* scope everything to .sm-home so it can't leak into wp-admin bar or plugins */

:root{
  /* These alias the shared design tokens in assets/css/tokens.css, which loads
     first in <head>. Change a colour there and the whole homepage follows. The
     local names are kept so the rest of this template's CSS is untouched. */
  --paper:var(--bg-primary,#fbfaf7);
  --paper2:var(--bg-sand,#f2efe8);
  --ink:var(--text-main,#0a0806);
  --ink-soft:var(--text-muted,#6a6050);
  --line:var(--border-color,#e5e0d6);
  --green:var(--color-brand,#12814A);
  /* THE LOGO'S OWN GREEN. Deliberately NOT --green: the wordmark accent used
     to resolve to #189554 while the brandmark's rect is hardcoded #12814A, so
     the two halves of one lockup were 1.37x apart in luminance. #12814A also
     matches the printed business card, which cannot be changed after ordering.
     Both halves read this one value. */
  --logo-green:#12814A;
  --sf:var(--color-accent,#e86000);
  --saffron:var(--color-accent,#e86000);
  --green-deep:var(--color-brand-deep,#12814A);
  --green-wash:var(--color-brand-tint,#eaf5ee);
  --white:var(--bg-card,#fff);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
/* ==========================================================================
   [hidden] MUST WIN
   ==========================================================================
   ⚠ THE `hidden` ATTRIBUTE ONLY WORKS THROUGH THE BROWSER'S OWN
   `[hidden]{display:none}`, WHICH ANY AUTHOR `display` RULE BEATS. So every
   element given a display rule silently stops responding to hidden — the JS
   sets the attribute, nothing happens, and the failure is invisible in code
   review because both halves look correct.

   This codebase had already hit it at least TEN times and patched it one
   selector at a time (.nav-burger[hidden], .sm-intro[hidden], .foot-share[hidden]
   and so on). Two known cases were still live: the "no businesses match" line on
   /businesses, which sat permanently visible showing whatever text was last
   written into it; and the Pinned view tab, which home.js hides with
   `pt.hidden = livePinned === 0` while .sm-vt-btn sets display:inline-flex — so
   a tab that opens an empty view could not hide itself, which is precisely the
   dead end the comment beside that line exists to prevent.

   One rule, once, ends the whole class. !important is warranted here because it
   has to outrank arbitrary author rules by design — that is the entire job.
   ========================================================================== */
[hidden] { display: none !important; }

html{scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
.sm-home{background:var(--paper);color:var(--ink);font-family:'Inter Tight',sans-serif;font-weight:400;line-height:1.5;overflow-x:hidden;}
body{margin:0;background:var(--paper);}
a{color:inherit;text-decoration:none;}
::selection{background:var(--green);color:var(--on-brand, #fff);}

/* ---- type scale ---- */
.display{font-family:'Fraunces',serif;font-weight:500;letter-spacing:-.02em;line-height:.98;}
.tight{font-family:'Inter Tight',sans-serif;}
/* MUTED, NOT GREEN. "Popular right now" and "Popular areas" are labels for
   the content beneath them. In green they read as links and competed with
   every other green on the page; the letter-spacing and caps already do the
   work of marking them as section headings. */
.eyebrow{font-family:'Inter Tight',sans-serif;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft,#6a6050);}

/* ---- nav ---- */
.nav{position:sticky;top:0;z-index:100;background:var(--header-bg, rgba(251,251,249,.85));backdrop-filter:blur(12px);border-bottom:1px solid var(--line);}
.nav-inner{max-width:1200px;margin:0 auto;padding:16px 32px;display:flex;align-items:center;gap:32px;}
.logo{display:flex;align-items:center;gap:9px;}
/* ⚠ THIS RULE WAS SILENTLY WINNING. `.logo svg` is (0,1,1) and `.logo-mark`
   is (0,1,0), so the mark rendered at 26px while the rule below declared 30px
   and the comment beneath it reasoned about a '30px mark' that never existed.
   Size now lives in one place: .logo-mark. */
.logo svg{width:auto;display:block;}
.logo{display:inline-flex;align-items:center;gap:10px;}
.logo-mark{width:32px;height:32px;border-radius:8px;flex:0 0 auto;}
/* 33px (+15%, 2026-08-07), mark 32px. The old note here reasoned about the
   wordmark against "its own 30px mark" — but the mark was rendering at 26px,
   overridden by the .logo svg rule above, so that comparison was never the one
   on screen. Both numbers are now real and both copies of this header agree.
   Kept in step with .site-logo__word in style.css, which styles the SAME
   header on every other page. */
.logo-word{font-family:'Inter Tight',sans-serif;font-size:33px;font-weight:700;letter-spacing:-.028em;color:var(--ink);}
.logo-word-accent{color:var(--logo-green);}
/* The mark's fill is an inline SVG attribute, which CSS outranks — so this is
   what keeps the square and the wordmark on the same green in both modes. */
.logo-mark rect{fill:var(--logo-green);}
:root[data-theme="dark"]{ --logo-green:#2f9d63; }
/* Nudged down ~3px so the links sit just below the wordmark's centreline —
   at equal centring a 29px brand and 14px links read as competing for the
   same line. Small offset, but it makes the logo clearly lead. */
/* padding-top drops the links below the wordmark's optical centre — baseline
   alignment made them read as the same level in the hierarchy as the brand.
   Scaled with the wordmark: 3px was tuned when it was 22px, it's now 29px. */
/* align-items:center is what stops the plain text links sitting HIGHER than
   the button's label. Both navs are display:flex with no align-items, so
   they defaulted to stretch: each <a> grew to the row's full height and
   its text sat at the top of that stretched box, while the button — being
   padded — centres its own label. Fine while every item was plain text,
   visibly off the moment one became a button.
   padding-top is left alone deliberately: it positions the whole nav
   group against the logo, and changing it would move the group rather
   than fix the mismatch inside it. */
.nav-links{display:flex;align-items:center;gap:28px;margin-left:0;align-self:center;}
/* Location dropdown — pushed to the far right of the nav */
/* Mobile-only live badge that fills the empty band left of the city picker
   when the nav wraps. Hidden on desktop, where the hero heading does this job
   in place and the nav has no spare room. */
/* ⚠ VISIBLE AT EVERY WIDTH NOW. It used to be display:none by default and
   revealed only inside the mobile media query, because it lived in the header
   where desktop had its own hero. It now sits on the board's sort row, which
   every width has — hiding it on desktop would leave that row lopsided and the
   status nowhere at all. */
.nav-live{display:inline-flex;align-items:center;gap:10px;
  font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:15px;
  letter-spacing:-.015em;color:var(--ink);white-space:nowrap;min-width:0;
  /* ⚠ auto margin, NOT justify-content — it beats the flex-end the existing
     media rule sets on this row, so the status stays left and the tabs stay
     right without either rule having to know about the other. */
  margin-right:auto;}
.nav-live .hero-live{width:18px;height:18px;margin:0;top:0;flex:0 0 auto;}
.nav-live-txt{overflow:hidden;text-overflow:ellipsis;}
/* When there is no board there is no sort row, so the bare wrapper just holds
   the status and keeps the same spacing below it. */
.sm-viewtoggle--bare{margin:0 0 14px;}
/* ⚠ .nav-search's RULES ARE GONE WITH ITS MARKUP. The link moved to the filter
   row; leaving these behind would be dead CSS that the next person has to work
   out is unused — and worse, the margin-left:auto here was load-bearing for the
   header's right-alignment, so it had to be re-homed rather than just deleted.
   .nav-loc now carries it (see the standalone block below). */
/* ⚠ margin-left:auto MOVED HERE FROM .nav-search. That rule was what pushed the
   right-hand group to the edge; with the search link gone to the filter row,
   the picker is now the last item on the row and has to carry it, or the header
   collapses left and the logo and the city sit jammed together. */
.nav-loc{margin-left:auto;position:relative;}

/* ── BROWSER HEADER: LOGO + BURGER, PICKER ON ITS OWN ROW BELOW ────────────
   ⚠ His call. The picker moves out of the top row and sits under the divider,
   which is where the phone browser already put it — this makes every browser
   width agree instead of two arrangements needing separate reasoning.
   ⚠ IT STAYS INSIDE .nav IN THE MARKUP, and wraps to a second row instead of
   being moved. Moving it out would put it in a different parent, and the
   installed app still wants it up in the header beside the logo — one element
   cannot be in two parents, so a markup move would force a second copy, which
   is the duplication trap this page has paid for repeatedly.
   ⚠ THE DIVIDER MOVES WITH IT rather than a second line being drawn: .nav's own
   bottom border comes off and the picker row carries a top border instead, so
   there is still exactly ONE line and the picker now sits below it. Adding a
   border without removing the other would band the picker between two rules.
   The standalone block re-overrides all of this — see the app rules. */
/* ⚠ THE HEADER'S OWN BOTTOM PADDING WAS SIZED FOR A ONE-ROW HEADER. With the
   picker wrapped below the divider there is now a second row inside .nav, so
   16px underneath it lands on top of the picker's own spacing and the board's
   top margin — three separate gaps stacked between the city name and Category.
   Halved here only; the app keeps the original, because there the header is
   still a single row. */
.nav-inner{flex-wrap:wrap;row-gap:0;padding-bottom:8px;}
.nav{border-bottom:0;}
.nav-loc{order:5;width:100%;margin-left:0;margin-top:0;padding-top:6px;
  border-top:1px solid var(--line);display:flex;justify-content:flex-end;}
/* ⚠ FLUSH AT EVERY BROWSER WIDTH, not just wide — his call once he saw it on a
   phone. It was scoped to min-width:601px, which is why the pill survived here.
   Same declaration as the installed-app rule in style.css, still, rather than a
   third version of "flush".
   ⚠ padding:2px 0 is what removes the pill's own 8px/14px, and with the row's
   border-top that also collapses most of the gap that had built up: header
   padding, then the row's margin, then the pill's padding, three spacings
   stacked to say one thing. */
.nav-loc-btn{background:none;border:0;padding:2px 0;box-shadow:none;}
.nav-loc-btn:hover{border-color:transparent;color:var(--green, #12814A);}

/* ── ONCE THE PICKER HAS MOVED INTO THE FILTER ROW ─────────────────────────
   home.js relocates it and sets .sm-loc-inrow on <html>. Everything the picker
   needed while it was a wrapped row inside the header is undone here, and the
   header goes back to one row carrying its own divider.
   ⚠ THE CLASS IS ON <html>, NOT <body> — it applies on the same paint as the
   move rather than a frame later.
   ⚠ EVERY BROWSER DECLARATION IS UNDONE, not just the visible ones: a leftover
   width:100% or border-top would draw a stray rule through the filter row. */
.sm-loc-inrow .nav{border-bottom:1px solid var(--line);}
.sm-loc-inrow .nav-inner{padding-bottom:16px;}
/* ⚠ LAST AND PUSHED RIGHT — his call. It led the row on the first pass, which
   put the least-changed control first and pushed the refresh icon onto a second
   line. margin-left:auto rather than justify-content, so it does not fight
   whatever the row already sets for its other children.
   The pill goes with it (see the rule below): with three controls plus refresh
   on a 390px row, the capsule's 14px of side padding was the difference between
   one line and two. */
.sm-int-dropdowns .nav-loc{order:99;width:auto;margin:0 0 0 auto;padding:0;
  border-top:0;display:inline-flex;align-items:center;}
/* ⚠ THE FLUSH TREATMENT LIVES HERE, on the row's own selector, and that is not
   tidiness — it is the only version that can win. The base .nav-loc-btn pill
   rule is declared LATER in this file, so a single-class flush rule loses to it
   at equal specificity (which is exactly what shipped: the picker moved onto the
   row and arrived still wearing its pill). Two classes beats one, wherever
   either sits.
   ⚠ AND THE PICKER IS NO LONGER INSIDE .nav once home.js moves it, so anything
   scoped to the header stops applying to it the moment the move happens.
   It is a filter now, so it reads like its neighbours — Category and Area are
   14px on this row. */
.sm-int-dropdowns .nav-loc-btn{background:none;border:0;padding:2px 0;
  box-shadow:none;font-size:14px;}
.sm-int-dropdowns .nav-loc-btn:hover{border-color:transparent;color:var(--green, #12814A);}
.nav-loc-btn:hover .nav-loc-caret{color:var(--green, #12814A);}

.nav-loc-btn{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border:1px solid var(--rule);border-radius:999px;background:var(--bg-card, #fff);font-family:'Inter Tight',sans-serif;font-size:14px;font-weight:600;color:var(--ink);transition:border-color .18s;}
.nav-loc-btn::-webkit-details-marker{display:none;}
.nav-loc-btn:hover{border-color:var(--ink-soft);}
/* ⚠ NO PILL ON THE WIDE LAYOUT — his call. The border and filled background
   were doing a job that only exists on a phone: on a cramped header the picker
   has to read as tappable, so it is drawn as a control. On a wide header there
   is space, the caret already says it opens, and a bordered white capsule
   floating on the right of an otherwise open row reads as a second button
   competing with the nav.
   ⚠ THE PADDING STAYS. Stripping it would shrink the tap target and shift the
   text against the header edge; only the border and fill go, so nothing moves.
   ⚠ Hover keeps a visible change — a control that looks like plain text still
   has to answer "is this clickable" when you point at it. */
.nav-loc-pin{font-size:12px;}
.nav-loc-caret{font-size:11px;color:var(--ink-soft);transition:transform .2s;}
.nav-loc[open] .nav-loc-caret{transform:rotate(180deg);}
.nav-loc-menu{position:absolute;top:calc(100% + 8px);right:0;min-width:220px;background:var(--bg-card, #fff);border:1px solid var(--rule);border-radius:14px;box-shadow:0 16px 40px -18px rgba(10,8,6,.4);padding:6px;z-index:600;}
.nav-loc-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border-radius:9px;font-size:14px;}
.nav-loc-item.is-active{font-weight:600;color:var(--ink);background:var(--color-brand-tint, #eaf5ee);}
.nav-loc-tick{color:var(--green);font-weight:700;}
.nav-loc-item.is-soon{color:var(--ink-soft, #6a6050);cursor:not-allowed;}
.nav-loc-soon{font-family:'Inter Tight',sans-serif;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft, #6a6050);border:1px solid var(--rule);border-radius:999px;padding:2px 8px;}
@media(max-width:600px){
  .nav-loc-btn{padding:8px 12px;}
  .nav-loc-city{font-size:14px;}  /* keep the city NAME visible, it's essential context */
}
/* ⚠ THE VIEW BUTTONS NEED A BASE RESET, NOT ONLY A DRAWER ONE. Saved, Pinned
   and Hidden are <button> elements sitting among <a> peers. Every rule that
   made them look like links lived inside a drawer media block, so on a WIDE
   viewport — where the nav is an inline row and no drawer block applies — they
   fell through to the platform's native button chrome and rendered as grey
   boxes next to the text links. Declared here at base level so every layout
   inherits it and no future breakpoint can lose them again. */
/* ⚠ THE WRAPPER IS INVISIBLE TO LAYOUT BY DEFAULT. display:contents makes the
   three buttons behave exactly as they did before it existed, so the phone
   drawer is untouched; the wide layout overrides it into a real box below. */
.nav-views{display:contents;}

/* ── WIDE LAYOUT: page links inline, view buttons in the burger ────────────
   His call: "keep the hamburger even on desktop, saved/pinned at least should
   be there". Only the personal SETS move — About, How we work, Contact and
   Get listed stay on the row, because hiding primary navigation behind a menu
   on a screen with room for it trades discoverability for tidiness.
   ⚠ THIS IS ALSO THE CONTEXT-AWARENESS FIX. Nothing here hides Pinned when it
   is empty; syncDrawerViews() in nav-menu.js does, and it runs on drawer open.
   Inline, these were never in a drawer, so it never ran for them. */
@media (min-width: 601px) and (hover: hover) and (pointer: fine){
  .nav-views{display:none;position:absolute;top:100%;right:0;z-index:60;
    flex-direction:column;align-items:stretch;gap:0;min-width:150px;
    margin-top:8px;padding:4px;
    background:var(--bg-card,#fff);border:1px solid var(--border-warm,#ece7dd);
    border-radius:12px;box-shadow:0 10px 28px rgba(0,0,0,.18);}
  .nav-links.is-open .nav-views{display:flex;}
  .nav-views .nav-links-view{padding:6px 10px;border-radius:7px;text-align:left;}
  .nav-views .nav-links-view:hover{background:var(--bg-sand2,#f2efe8);}
  /* The panel hangs off the nav, so the nav is what it is positioned against. */
  .nav-links{position:relative;}
  /* ⚠ THE BURGER SITS BEFORE THE NAV IN THE MARKUP, which is right for the
     phone (where the nav is a drawer and the burger is the only control on the
     row) and wrong here, where it would land between "Get listed" and "About".
     Reordered visually rather than moved in the DOM: the markup order is what
     the phone layout and the tab bar's More button both depend on, and tab
     order following the visual row matters less than not disturbing either. */
  /* ⚠ EVERY CHILD GETS AN ORDER, NOT JUST THE ONES BEING MOVED. Flex `order`
     defaults to 0, so naming three children put the two UNNAMED ones — the
     logo and the location picker — ahead of all of them. The picker sits last
     in the DOM and jumped to the front beside the logo. Partial ordering is
     never a partial change; it reorders everything you did not mention. */
  .nav-inner > .logo{order:0;}
  .nav-inner > .nav-cta{order:1;}
  .nav-inner > .nav-links{order:2;}
  .nav-inner > .nav-burger{order:3;margin-left:2px;}
  .nav-inner > .nav-loc{order:4;}
}

.nav-links a,
.nav-links .nav-links-view{font-size:14px;font-weight:500;color:var(--ink-soft);transition:color .18s;
  background:none;border:0;padding:0;margin:0;font-family:inherit;line-height:inherit;
  cursor:pointer;white-space:nowrap;}
.nav-links a:hover,
.nav-links .nav-links-view:hover{color:var(--ink);}
/* The one nav item we actually want tapped. Compact on purpose: three
   items already fill this row tightly on a phone, so the padding is
   kept tight and trimmed further at the mobile breakpoint below. */
/* BLACK, and the CTA is a sibling of the nav now — so the selector cannot be
   scoped inside it. margin-left:auto pushes the CTA and the burger right as a
   pair, leaving the logo alone on the left. */
.nav-inner a.nav-cta{background:none;border:0;padding:0;border-radius:0;margin-left:auto;color:var(--ink, #0a0806);font-weight:700;transition:color .18s;}
/* ---- Mobile nav drawer ---- */
.nav-burger{display:inline-flex;flex-direction:column;justify-content:center;gap:4px;
  width:34px;height:34px;padding:0 7px;box-sizing:border-box;
  background:none;border:0;cursor:pointer;flex:0 0 auto;}
.nav-burger[hidden]{display:none;}

/* ⚠ THE DRAWER'S OWN "Get listed" IS HIDDEN UNLESS THE DRAWER EXISTS. There are
   deliberately TWO copies of this link: the styled button outside the nav, and
   this one inside the list so it survives when the list folds into a drawer.
   .nav-cta{display:none} hides the OUTER copy on touch layouts — but nothing
   ever hid this INNER copy above that breakpoint, so on a desktop viewport
   "Get listed" appeared twice in one row, once bold and once as a plain link.
   Default-hidden and revealed only inside an open drawer, rather than hidden
   in a desktop media query: that way a new breakpoint cannot resurrect it. */
.nav-links-cta{display:none;}


/* ==========================================================================
   BOTTOM TAB BAR — installed app only
   ==========================================================================
   ⚠ HIDDEN BY DEFAULT AND REVEALED BY A MEDIA QUERY. display-mode:standalone
   matches only when the site is running from the home screen, so browser
   visitors never see it and nothing has to detect anything.
   ⚠ env(safe-area-inset-bottom) IS NOT OPTIONAL — without it the bar sits under
   the home indicator on every notched iPhone and the labels are unreadable.
   ========================================================================== */
.sm-tabbar{display:none;}

@media (display-mode: standalone){
  /* ⚠ THE APP KEEPS THE OLD HEADER — his explicit choice: the picker moves
     below the divider on every BROWSER width, and the installed app is left
     alone. The app has no burger (the More tab is its menu), so the top row is
     just the logo, and the picker sitting beside it is what fills that row.
     Undoing all four browser declarations, not some of them: leaving the wrap
     or the missing border behind would give the app a header that is neither
     arrangement. */
  .nav{border-bottom:1px solid var(--line);}
  .nav-inner{flex-wrap:nowrap;padding-bottom:16px;}
  .nav-loc{order:0;width:auto;margin-left:auto;margin-top:0;padding-top:0;
    border-top:0;display:block;}
  /* ⚠ AND THE PILL COMES BACK. Flush is now the BROWSER default, so the app has
     to restore the control treatment as well as the position — in the app's
     header the picker sits beside the logo with nothing else on the row, which
     is the cramped case the pill was drawn for. */
  .nav-loc-btn{background:var(--bg-card, #fff);border:1px solid var(--rule);
    padding:8px 12px;border-radius:999px;}

  /* ⚠⚠ THIS ONE LINE IS WHY THE MENU OPENED AT THE TOP. .nav carries
     backdrop-filter:blur(12px), and ANY filter or backdrop-filter makes an
     element the containing block for position:fixed DESCENDANTS. So the
     drawer's `bottom: 58px` was measured from the bottom of the header, not
     the viewport — putting it above the header instead of above the bar.
     Removing the blur in the app restores the viewport as the reference. It
     also drops a known iOS compositing cost that was already on the suspect
     list for the corrupted-photo bug. */
  /* ⚠⚠ BOTH HEADERS, NOT JUST .nav. page-home.php uses .nav; every OTHER page
     uses .site-header — and it carries the same backdrop-filter:blur(12px).
     Any filter makes an element the containing block for position:fixed
     DESCENDANTS, so on those pages the drawer measured its `bottom` from the
     header and shot off the TOP of the screen, above the status bar. Fixing
     only .nav fixed only the homepage. */
  .nav, .site-header{backdrop-filter:none;-webkit-backdrop-filter:none;}

  .sm-tabbar{
    display:flex;position:fixed;left:0;right:0;bottom:0;z-index:900;
    padding-bottom:env(safe-area-inset-bottom);
    background:var(--paper,#fbfaf7);border-top:1px solid var(--line,#ece7dd);
  }
  /* ⚠ COMPACT — his call. The first pass took 58px of a 667pt screen for four
     labels. Icon 21->19, label 10.5->9.5, padding halved: ~46px, which is still
     above the 44px touch minimum because the whole cell is the target. */
  .sm-tab{
    flex:1 1 0;display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:5px 2px 4px;background:none;border:0;font:inherit;
    color:var(--ink-soft,#6a6050);text-decoration:none;cursor:pointer;
  }
  .sm-tab-i svg{width:19px;height:19px;}
  .sm-tab-l{font-size:9.5px;letter-spacing:.01em;line-height:1.1;}
  .sm-tab.is-on{color:var(--green,#12814A);}
  .sm-tab.is-on .sm-tab-i svg{stroke-width:2.4;}

  /* ⚠ A PRESSED STATE, because the tap-feedback pass suppressed
     -webkit-tap-highlight-color on every <a> and <button> and never gave .sm-tab
     a replacement. So the tabs lost their ONLY feedback: the grey flash was
     gone and nothing took its place, while the tap itself is a real page
     navigation costing 300-425ms of server time now caching is off. For that
     whole interval the tab looked untouched. */
  .sm-tab:active{transform:scale(.94);opacity:.7;transition:transform .1s ease,opacity .1s ease;}

  /* ⚠ AND THE TAPPED TAB GOES GREEN IMMEDIATELY, before the new page arrives —
     see the JS. A pressed state that ends the moment the finger lifts still
     leaves a third of a second of nothing, which is the part that reads as
     unresponsive rather than merely slow. */
  .sm-tab.is-going{color:var(--green,#12814A);}
  .sm-tab.is-going .sm-tab-i svg{stroke-width:2.4;}

  @media (prefers-reduced-motion: reduce){
    .sm-tab:active{transform:none;}
  }

  /* ⚠ THE BURGER AND THE CTA MOVE INTO THE BAR — they do not appear twice.
     Two navigations offering the same links is how a site starts feeling
     bigger than it is, and it was the whole reason for doing this. */
  .nav-burger{display:none !important;}
  .nav-cta{display:none;}

  /* ⚠ THE PAGE MUST CLEAR THE BAR. A fixed bar covers the last ~60px of every
     page, which on this site is the footer's theme toggle and, on a deal page,
     the sticky action row. Padding on the body is the only place that reaches
     all of them. */
  body{padding-bottom:calc(46px + env(safe-area-inset-bottom));}
  /* ⚠ THE FOOTER NEEDS ITS OWN CLEARANCE TOO. Body padding adds space after
     the footer's background box, so the last row inside it — the Light/Auto/
     Dark control — still sat behind the bar and could not be tapped. */
  /* ⚠ BOTH FOOTER CLASSES. page-home.php's footer is .foot; every other page
     uses .site-footer. Targeting only .site-footer left the homepage's own
     Light/Auto/Dark control clipped behind the bar — the same one-name-fixes-
     one-page fault as .nav vs .site-header above. */
  .site-footer, .foot{padding-bottom:calc(58px + env(safe-area-inset-bottom));}

  /* Anything else already pinned to the bottom has to sit above it, or it
     lands underneath and looks broken. */
  .sm-undo{bottom:calc(58px + env(safe-area-inset-bottom));}
  .sm-peek{padding-bottom:calc(46px + env(safe-area-inset-bottom));}

  /* ⚠ THE DRAWER OPENS FROM THE BOTTOM, NOT THE TOP. Tapping "More" at the
     foot of the screen and having a menu appear at the very top is the single
     most disorienting thing about a bottom bar — the panel must come from the
     control that summoned it. Anchored just above the bar, full width, so it
     reads as the tab expanding rather than as the old header menu. */
  .nav-links.is-open{
    position:fixed;left:0;right:0;top:auto;
    bottom:calc(46px + env(safe-area-inset-bottom));
    width:100%;max-width:none;margin:0;border-radius:14px 14px 0 0;
    box-shadow:0 -10px 30px rgba(0,0,0,.28);
  }
  /* ⚠ LINE-HEIGHT IS WHAT CONTROLS ROW HEIGHT HERE, not padding — two earlier
     passes trimmed padding and barely moved it, because the body's 1.6 on
     14px type is 22px before any padding at all. 5px + 1.35 on 13.5px gives
     ~28px a row, so seven rows come to ~200px rather than ~320px. */
  .nav-links.is-open a,
  .nav-links.is-open .nav-links-view{
    display:block;width:100%;text-align:left;
    padding:5px 18px;font-size:13.5px;line-height:1.35;
    background:none;border:0;font-family:inherit;color:inherit;cursor:pointer;
  }
  /* Get listed reads as the action it is, and sits apart from the pages. */
  .nav-links.is-open .nav-links-cta{
    display:block;   /* re-shown here — see the default-hidden rule above */
    color:var(--green,#12814A);font-weight:600;
    border-top:1px solid var(--line,#ece7dd);margin-top:4px;padding-top:8px;
  }
  .nav-links-n{opacity:.6;font-weight:400;}
  /* ⚠ THE DRAWER MUST NOT OUTGROW THE SCREEN. With About, How we work,
     Contact, Get listed, Saved, Pinned and Hidden it is seven rows tall, and on
     a short phone that reached past the header — which is how it ended up over
     the status bar once the containing block was wrong. Capping it and letting
     it scroll means it can never escape upward again, whatever gets added to
     the menu later. */
  .nav-links.is-open{overflow-y:auto;-webkit-overflow-scrolling:touch;}
  .nav-links.is-open{max-height:52vh;padding:5px 0 6px;}

  /* ⚠ HEADER ORDER, app only. Row one becomes logo + location; the live status
     and search drop to row two, which is where the status sat before the bar
     existed. .nav-inner already wraps, so ordering the children is enough — no
     markup moves and the browser layout is untouched.
     ⚠ THE WRAPPER IS .nav-inner AND THE BRAND IS .logo — Claude first wrote
     .site-header__inner and .nav-logo, which are the FOOTER's and header.php's
     names, not this template's. Neither exists here, so the ordering would
     have applied to nothing and quietly done nothing at all. Checked against
     the markup rather than assumed. */
  .nav-inner{flex-wrap:wrap;}
  .logo{order:1;}
  .nav-loc{order:2;margin-left:auto;}
  /* ⚠ FLUSH, NOT A PILL — his call. Beside the wordmark on the top row a
     bordered capsule reads as a button competing with the brand; the label and
     caret alone read as a place name you can change, which is what it is. */
  .nav-loc-btn{background:none;border:0;padding:2px 0;box-shadow:none;}

  /* The board's own tabs reduce to the two SORTS; the three SETS live in the
     drawer. Hidden rather than removed — home.js still drives them, and the
     drawer buttons forward their clicks here. */
  .sm-vt-btn[data-view="mine"],
  .sm-vt-btn[data-view="pinned"],
  .sm-restore{display:none !important;}
}
.nav-burger span{display:block;height:2px;border-radius:2px;background:var(--ink, #0a0806);
  transition:transform .18s, opacity .18s;}
.nav-burger.is-open span:nth-child(1){transform:translateY(6px) rotate(45deg);}
.nav-burger.is-open span:nth-child(2){opacity:0;}
.nav-burger.is-open span:nth-child(3){transform:translateY(-6px) rotate(-45deg);}

/* ⚠ NOT WIDTH ALONE — a phone in landscape is wider than 600px, which un-folded
   the menu. The touch test holds in either orientation. THIS CONDITION IS
   DUPLICATED IN assets/js/nav-menu.js — change both. */
@media (max-width: 600px), (hover: none) and (pointer: coarse) {
  .nav-inner{position:relative;}
  /* ⚠ THE CTA MOVES INTO THE DRAWER ON TOUCH LAYOUTS. It is rendered inside
     .nav-links as well, so showing both would put "Get listed" on screen twice
     — once beside the burger and once inside it. Desktop keeps the header
     button, because there is no drawer there to hold it. */
  .nav-cta{display:none;}
  /* ⚠ THE BURGER GOES TO THE RIGHT EDGE. With the CTA hidden it sat immediately
     after the wordmark with the whole row empty to its right, which reads as
     unfinished — and it should mirror where the location picker sits on the row
     below. margin-left:auto is safe alongside .nav-loc's own auto margin
     because the row WRAPS: they land on separate lines. */
  .nav-burger{margin-left:auto;}
  .nav-links{display:none;position:absolute;top:100%;right:12px;z-index:60;
    flex-direction:column;align-items:flex-start;gap:0;
    min-width:158px;padding:4px;margin:0;
    background:var(--bg-card, #fff);border:1px solid var(--border-warm, #ece7dd);
    border-radius:12px;box-shadow:0 10px 28px rgba(0,0,0,.18);}
  .nav-links.is-open{display:flex;}
  /* ⚠ THE VIEW BUTTONS BELONG HERE TOO. Saved, Pinned and Hidden are <button>
     elements, and the matching reset lives in the display-mode:standalone
     block — so in a BROWSER they got no reset at all and rendered with the
     platform's native button chrome: grey rounded boxes, sitting outside the
     panel's own padding. The drawer has two style paths, installed and
     browser, and these were only ever wired into one of them. Same
     two-copies trap as the headers themselves. */
  .nav-links a,
  .nav-links .nav-links-view{display:block;width:100%;padding:6px 10px;border-radius:7px;
    font-size:14px;line-height:1.45;white-space:nowrap;
    text-align:left;background:none;border:0;font-family:inherit;
    color:inherit;cursor:pointer;}
  .nav-links a:hover,
  .nav-links .nav-links-view:hover{background:var(--bg-sand2, #f2efe8);}
}

.nav-links a.nav-cta:hover{color:var(--color-brand-deep, #12814A);}
.nav-right{margin-left:auto;display:flex;align-items:center;gap:18px;}
.sm-filter-refresh{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:0;background:transparent;color:var(--ink-soft);cursor:pointer;transition:color .15s;flex:none;}
/* Reload itself is instant enough that tapping this often looked like it
   did nothing — most of the time the deal list genuinely hasn't changed
   since the last load, so an instant reload and a broken button looked
   identical. This holds the spin for a beat BEFORE reloading (see the
   click handler below), rather than spinning during the reload itself,
   since the reload's own network time is unpredictable and can't be
   relied on to last long enough to actually be seen. */
/* Three earlier attempts all adjusted THIS animation's own speed, and none
   of them changed anything — including a 10x slowdown, which should have
   been unmistakable if speed were the real variable. That's what gave it
   away: the actual conflict was never here. A separate, pre-existing
   :active rule (removed below) rotated the icon BACKWARD to -180deg while
   held down, with its own .3s transition — and that reverses back toward
   0deg at exactly the moment release fires and THIS animation starts
   rotating it forward. Two different mechanisms fighting over the same
   rotation on the same element, not a perceptual effect at all. With that
   rule gone, back to two full clockwise turns over the full 2s — the
   fuller motion originally asked for, since the illusion it was traded
   away for was never really about rotation speed. */
.sm-filter-refresh.is-spinning svg{animation:sm-refresh-spin 2s linear 1;}
@keyframes sm-refresh-spin{
  0%{ transform:rotate(0deg); color:var(--color-brand-deep, #12814A); }
  100%{ transform:rotate(720deg); color:var(--ink-soft, #6a6050); }
}
.sm-filter-refresh:hover{color:var(--green);}
.nav-signin{font-size:14px;font-weight:500;color:var(--ink-soft);transition:color .18s;}
.nav-signin:hover{color:var(--ink);}
.btn{font-family:'Inter Tight',sans-serif;font-size:14px;font-weight:600;padding:10px 18px;border-radius:999px;border:1px solid var(--btn-solid);background:var(--btn-solid);color:var(--on-btn-solid);cursor:pointer;transition:all .18s;display:inline-flex;align-items:center;gap:7px;}
.btn:hover{background:transparent;color:var(--ink);}
.btn-green{background:var(--green);border-color:var(--green);color:var(--on-brand, #fff);}
.btn-green:hover{background:var(--green-deep);border-color:var(--green-deep);color:var(--on-brand, #fff);}
.btn-ghost{background:transparent;color:var(--ink);}
.btn-ghost:hover{background:var(--btn-solid);color:var(--on-btn-solid);}

/* ---- hero ---- */
.hero{max-width:1200px;margin:0 auto;padding:72px 32px 32px;}
.hero-grid{display:block;max-width:760px;}
.hero h1{font-size:clamp(48px,7vw,92px);margin:20px 0 0;}
.hero h1 .live{color:var(--green);font-style:italic;position:relative;}
.hero-cta{display:flex;gap:14px;margin-top:36px;flex-wrap:wrap;}
.hero-meta{display:flex;gap:30px;margin-top:44px;padding-top:28px;border-top:1px solid var(--line);}
.hero-meta .n{font-family:'Inter Tight',sans-serif;font-size:26px;font-weight:700;color:var(--ink);letter-spacing:-.01em;}
.hero-meta .l{font-size:13px;color:var(--ink-soft);margin-top:2px;}

/* ---- hero deal card (the signature) ---- */
.deal-live{background:var(--white);border:1px solid var(--line);border-radius:20px;overflow:hidden;box-shadow:0 1px 0 rgba(20,21,15,.02),0 24px 48px -28px rgba(20,21,15,.22);}
.dl-top{padding:16px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);}
.dl-shop{display:flex;align-items:center;gap:10px;}
.dl-avatar{width:32px;height:32px;border-radius:8px;background:var(--green-wash);color:var(--green);display:flex;align-items:center;justify-content:center;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:13px;}
.dl-shopname{font-size:13px;font-weight:600;}
.dl-shoploc{font-size:12px;color:var(--ink-soft);}
.dl-timer{font-family:'Inter Tight',sans-serif;font-size:13px;font-weight:600;color:var(--sf);display:flex;align-items:center;gap:6px;}
.dl-dot{width:7px;height:7px;border-radius:50%;background:var(--sf);animation:pulse 1.6s infinite;}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.8)}}
.dl-body{padding:22px 20px;}
.dl-cat{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-soft);}
.dl-name{font-family:'Inter Tight',system-ui,sans-serif;font-size:24px;font-weight:700;line-height:1.2;margin-top:8px;letter-spacing:-.015em;}
.dl-price-row{display:flex;align-items:baseline;gap:12px;margin-top:18px;}
.dl-now{font-family:'Inter Tight',sans-serif;font-size:42px;font-weight:700;color:var(--sf);letter-spacing:-.02em;line-height:1;}
.dl-was{font-size:16px;color:var(--ink-soft);text-decoration:line-through;}
.dl-off{margin-left:auto;font-family:'Inter Tight',sans-serif;font-size:13px;font-weight:700;color:var(--sf);background:rgba(232,96,0,.1);padding:5px 10px;border-radius:999px;}
.dl-foot{display:flex;align-items:center;justify-content:space-between;margin-top:20px;padding-top:16px;border-top:1px solid var(--line);}
.dl-left{font-size:13px;color:var(--ink-soft);}
.dl-left b{color:var(--ink);font-weight:600;}
.dl-claim{font-family:'Inter Tight',sans-serif;font-size:13px;font-weight:600;color:var(--ink);background:transparent;border:1.5px dashed var(--line);padding:8px 14px;border-radius:8px;}
.dl-claim strong{color:var(--green);letter-spacing:.04em;}
.dl-claim.note{border:none;color:var(--ink-soft);font-weight:500;padding-left:0;}
.dl-note{text-align:center;font-size:12px;color:var(--ink-soft);padding:11px;background:var(--paper2);}

/* ---- strip ---- */
.strip{border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:var(--white);}
.strip-inner{max-width:1200px;margin:0 auto;padding:18px 32px;display:flex;align-items:center;gap:14px;font-size:14px;color:var(--ink-soft);flex-wrap:wrap;}
.strip b{color:var(--ink);font-weight:600;}
.strip-sep{width:4px;height:4px;border-radius:50%;background:var(--line);}

/* ---- section ---- */
.section{max-width:1200px;margin:0 auto;padding:80px 32px;}
/* two-column home: deals (scrolls) + sticky rail (spotlight, new shops, ads) */
.home-layout{max-width:1200px;margin:0 auto;padding:32px 32px 72px;display:grid;grid-template-columns:1fr 320px;gap:40px;align-items:start;}
/* ⚠ THE HERO <h1> IS VISUALLY HIDDEN AT EVERY WIDTH — his call, and it settles
   a duplication that had two plausible fixes. "On right now" was appearing
   twice on desktop: once as this heading above the filters, once as the status
   badge beside the sort tabs. Removing the badge was tried first; the right
   answer is the other one, because it makes desktop match the order the phone
   already uses — filters first, then status and tabs together above the cards.
   One layout at all widths, and the badge sits next to the thing it describes.
   ⚠ CLIPPED, NOT display:none. The <h1> is still the page's heading for screen
   readers and for search engines, which read it as what this page is about. It
   has to stay in the document; it just takes no space.
   ⚠ THIS RULE USED TO LIVE INSIDE @media(max-width:600px). Moving it to the
   base is the whole change — anything that re-scopes it to a breakpoint brings
   the duplicate straight back. */
.home-hero{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;}

/* --- First-visit explainer ------------------------------------------------
   Shown once per device, then never again. Replaces the old collapsible under
   the heading: that made every visitor manage a disclosure widget forever for
   a message that only matters on the first visit. */
.sm-intro{display:flex;align-items:flex-start;gap:10px;
  background:var(--color-brand-tint, #eaf5ee);border:1px solid var(--rule);
  border-radius:10px;padding:10px 12px;margin:0 0 14px;
  animation:introDrop .32s cubic-bezier(.2,.8,.3,1) both;}
[data-theme="dark"] .sm-intro{background:var(--color-brand-tint, #1c3a2a);}
.sm-intro[hidden]{display:none;}
.sm-intro-txt{margin:0;flex:1;min-width:0;font-size:13.5px;line-height:1.45;
  color:var(--ink);text-wrap:pretty;}
.sm-intro-x{flex:0 0 auto;width:28px;height:28px;display:grid;place-items:center;
  border:0;border-radius:50%;background:transparent;color:var(--ink-soft);
  cursor:pointer;transition:background .15s,color .15s;}
.sm-intro-x:hover{background:rgba(0,0,0,.06);color:var(--ink);}
[data-theme="dark"] .sm-intro-x:hover{background:rgba(255,255,255,.08);}
.sm-intro.is-going{animation:introLift .25s ease forwards;}
@keyframes introDrop{from{opacity:0;transform:translateY(-8px);}to{opacity:1;transform:none;}}
@keyframes introLift{to{opacity:0;transform:translateY(-6px);}}
@media(prefers-reduced-motion:reduce){
  .sm-intro,.sm-intro.is-going{animation:none;}
}

/* --- Live indicator -------------------------------------------------------
   The dot is HONEST: it only pulses when deals are actually on the board.
   Empty board -> muted, static dot and the heading says so. A pulsing "live"
   badge over nothing would train people to distrust it, and this board is
   legitimately empty at times by design. */
/* At 3x the old size a flat blob would be too heavy, so the dot is built as a
   RADAR PING: a solid core with a ring expanding out of it. The ring is a
   pseudo-element scaling from 1 -> 2.6, which is far more visible than the old
   box-shadow approach (that spread only 9px and was easy to miss, and would be
   clipped by any ancestor with overflow:hidden). */
.hero-live{display:inline-block;position:relative;width:27px;height:27px;flex:0 0 auto;
  margin-right:10px;vertical-align:middle;top:-2px;}
/* Core — 14px rather than 11px. The 27px box is mostly headroom for the ping,
   so the visible dot was reading much smaller than the box suggested. */
.hero-live::after{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:14px;height:14px;border-radius:50%;background:var(--ink-soft);opacity:.6;}
.hero-live.is-live::after{background:var(--green, #12814A);opacity:1;}
/* Dark mode: --ink-soft at low opacity nearly disappears against a dark panel,
   which is what made the muted state look tiny. Lift it. */
[data-theme="dark"] .hero-live::after{background:#8a8378;opacity:.85;}
[data-theme="dark"] .hero-live.is-live::after{background:#3fb87a;opacity:1;}
/* Expanding ring — only when actually live. */
.hero-live::before{content:'';position:absolute;left:50%;top:50%;
  width:14px;height:14px;border-radius:50%;opacity:0;
  transform:translate(-50%,-50%) scale(1);background:var(--green, #12814A);}
.hero-live.is-live::before{animation:heroPing 2s cubic-bezier(0,0,.2,1) infinite;}
[data-theme="dark"] .hero-live.is-live::before{background:#3fb87a;}
/* scale 2.6 -> 1.95: with a 14px core (was 11px) the old scale pushed the ring
   past the 27px box and clipped it. 14 x 1.95 = ~27px, so it now expands right
   to the edge and no further. */
@keyframes heroPing{
  0%   {opacity:.55;transform:translate(-50%,-50%) scale(1);}
  75%  {opacity:0;   transform:translate(-50%,-50%) scale(1.95);}
  100% {opacity:0;   transform:translate(-50%,-50%) scale(1.95);}
}
@media(prefers-reduced-motion:reduce){.hero-live.is-live::before{animation:none;}}

/* ── Launch strip ─────────────────────────────────────────
   Quiet by design: it earns patience, it doesn't sell anything. */
.home-launch{display:flex;align-items:flex-start;gap:8px;margin:0 0 18px;padding:9px 12px;
  background:var(--green-wash);border-radius:10px;
  font-family:'Inter Tight',system-ui,sans-serif;font-size:13px;line-height:1.45;color:var(--ink-soft);}
.home-launch-dot{width:6px;height:6px;border-radius:50%;background:var(--color-brand-deep, #12814A);flex:0 0 auto;margin-top:6px;}
.home-launch-txt{flex:1;min-width:0;}
/* Inline so it sits at the end of the sentence rather than starting a new
   block — the strip is three lines already and a fourth would push the first
   deal card further down the screen, which is the thing actually doing the
   selling. */
.home-launch-go{color:var(--color-brand-deep, #12814A);font-weight:700;
  text-decoration:none;white-space:nowrap;}
.home-launch-go:hover{text-decoration:underline;}
[data-theme="dark"] .home-launch-go{color:#5fd398;}
/* Quiet, and sized as a real tap target — it sits beside body text, not in a
   toolbar, so it must not read as a button competing with the message. */
.home-launch-x{flex:0 0 auto;width:26px;height:26px;margin:-3px -4px 0 0;display:grid;
  place-items:center;border:0;border-radius:50%;background:transparent;
  color:var(--ink-soft);cursor:pointer;transition:background .15s,color .15s;}
.home-launch-x:hover{background:rgba(0,0,0,.06);color:var(--ink);}
[data-theme="dark"] .home-launch-x:hover{background:rgba(255,255,255,.08);}

/* ── Collapsible explainer ────────────────────────────────
   The heading IS the control, so it sits flush with the rule above and carries
   no button chrome. grid-template-rows 1fr→0fr animates to the content's real
   height, which max-height guessing can't do without clipping or lag. */
/* The heading is now three short words, so it needs far less size than the
   old sentence did. Smaller = filters and the first cards clear the fold. */
/* Sans here, not the .display serif. "On right now" is a live status readout,
   not a piece of editorial voice, and it sits beside a pulsing indicator, so
   the clean sans reads as instrumentation rather than a magazine headline.
   Fraunces stays on the section headings elsewhere. */
/* ⚠ SIZED FOR THE COLUMN, NOT THE VIEWPORT. It was clamp(26px,3.4vw,38px), and
   3.4vw is a share of the WINDOW — but this heading sits in a board column
   capped at ~776px. On a 1334px screen the window says 45px (clamped to 38)
   while the column it has to sit above never grows past 776, so the heading
   kept getting bigger relative to nothing.
   ⚠ THIS IS THE SAME MISTAKE AS THE SORT-ROW ONE documented in the process
   notes — a viewport measurement standing in for a fixed-width column. Worth
   noticing that it has now caused two separate faults on this page. */
.home-hero .display{font-family:'Inter Tight',system-ui,sans-serif;font-weight:800;
  font-size:clamp(24px,2.4vw,30px);line-height:1.12;letter-spacing:-.03em;}
.home-today-stat{display:inline-flex;align-items:center;gap:6px;margin-top:10px;font-family:'Inter Tight',sans-serif;font-size:12px;font-weight:600;color:var(--green);background:var(--green-wash);padding:4px 10px;border-radius:999px;}
.home-today-dot{width:6px;height:6px;border-radius:50%;background:var(--green);}
/*
 * ⚠⚠ min-width:0 IS LOAD-BEARING. .home-layout is a GRID and this is a grid
 * item, and grid and flex items default to min-width:auto — they refuse to
 * shrink below their content's MIN-CONTENT width. For a white-space:nowrap
 * title that is the entire untruncated string, so one long deal title in
 * "Recently ended" (71 characters, from Balti Towers) widened this column past
 * the viewport and dragged the empty state and the launch notice off the right
 * edge with it. Found 2026-08-31.
 *
 * ⚠ THE ELLIPSIS ON .sm-later-title WAS ALREADY CORRECT and never got a chance
 * to apply: the column grew to fit rather than the text being clipped. That is
 * what made it look like a content bug rather than a layout one, and it is why
 * only one shop's title ever triggered it.
 *
 * Same trap as the console Shops list, where names wrapped mid-phrase until
 * min-width:0 was added. Any new grid or flex item holding text needs this.
 */
/*
 * ⚠⚠ min-width:0 IS THE WHOLE FIX FOR THE OVERFLOWING BOARD, 2026-08-31.
 * MEASURED, not guessed: at a 487px viewport .home-main computed to 556px with
 * min-width:AUTO, while every child reported min-width:0px. A grid item defaults
 * to min-width:auto, which means it refuses to shrink below its own min-content
 * — so the track grew to fit the widest thing inside and pushed the launch
 * notice, the filters and the empty state off the right edge. The page itself
 * did not scroll, because an ancestor clips, which is why it read as text
 * mysteriously cut off rather than as a layout that was too wide.
 *
 * ⚠ IT IS NEEDED AT EVERY WIDTH, not just where the grid has two columns. The
 * single-column layout under 900px is still a grid, so the default still bites.
 */
.home-layout .home-main{max-width:none;margin:0;padding:0;min-width:0;}
/* The other grid item, and a flex container itself — same default, same fix,
   applied now rather than the next time a long shop name lands in a rail. */
/* Deploy marker: computed min-width on .home-main should read 0px, not auto. */
.home-rail{position:sticky;top:24px;display:flex;flex-direction:column;gap:20px;min-width:0;min-width:0;}
.rail-section{background:none;border:none;border-radius:0;padding:0;}
.rail-section .sec-head{display:block;margin-bottom:16px;}
.rail-section .sec-head h2{font-size:20px;max-width:none;}
.rail-section .sec-head p{font-size:13px;max-width:none;margin-top:4px;}
/* Editor's pick — curated, replaces the old ad slot */
.rail-panel{border:1px solid var(--line);border-radius:14px;background:var(--bg-card, #fff);overflow:hidden;}
.rail-row{padding:18px 18px;}
.rail-row,.newshop{position:relative;overflow:hidden;}
/* BENTO image header — matches the deal cards. The shop cover is a real photo
   panel at the top of the card rather than a faint wallpaper behind the text.
   Text never sits over the image, so contrast is constant whatever the shop
   uploaded. Full-bleed: the row's own 18px padding is cancelled with negative
   margins so the photo meets the card edges. */
/* 16/9 -> 4/3: the photo IS the pitch on this sheet, and it was the smallest
   thing on a screen with room to spare. Full-bleed margins unchanged. */
.rail-pick-img{position:relative;margin:-18px -18px 14px;aspect-ratio:4/3;overflow:hidden;
  background:var(--bg-sand2, #f2efe8);}
.rail-pick-img img{width:100%;height:100%;object-fit:cover;display:block;}
/* The tag floats on the photo, mirroring the SastaMeter pill on deal cards.
   NO backdrop-filter here or on .card-meter. Both used blur(6px) behind a
   background that was already 92% opaque — so the blur was doing almost
   nothing visually while forcing the browser to composite the photo behind it
   on the GPU. Several of those stacked over large decoded images is a known
   iOS Safari fault that renders exactly as the posterised green blocks he
   photographed, and it survives a reload because it's a compositing fault
   rather than a content one. Backgrounds are now fully opaque. */
.rail-pick-img .rail-pick-tag{position:absolute;top:10px;left:10px;z-index:2;
  background:#fff;color:var(--color-brand-deep, #12814A);
  padding:5px 10px;border-radius:999px;
  box-shadow:0 1px 6px rgba(0,0,0,.18);letter-spacing:.1em;}
[data-theme="dark"] .rail-pick-img .rail-pick-tag{background:#14110d;color:#5fd398;}
/* With a photo header the top row only carries "Visit shop" — pull it right. */
.rail-pick-top--under{margin-top:-2px;}
.rail-pick-top,.rail-pick-body,.newshop-dots{position:relative;z-index:1;}
.rail-row + .rail-row{border-top:1px solid var(--line);}
.rail-pick-tag{display:inline-block;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--green);}
.rail-pick-body{display:block;}
/* Same placeholder logic as .card-avatar--init: this green only shows when
   the shop has no image, so it was a loud fallback rather than a signal. */
  .rail-pick-avatar{width:46px;height:46px;border-radius:50%;overflow:hidden;background:var(--bg-sand2, #ece6db);display:flex;align-items:center;justify-content:center;margin-bottom:12px;}
.rail-pick-avatar img{width:100%;height:100%;object-fit:cover;}
/* ⚠ THIS WAS WHITE-ON-CREAM IN LIGHT MODE. --on-brand is always #fff, correct
   when this circle was brand green, wrong the moment it became --bg-sand2 —
   which flips, so the initials were fine on dark and invisible on light. Third
   instance of pairing an inverting token with a literal. */
.rail-pick-avatar span{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:15px;color:var(--text-muted, #6a6050);}
.rail-pick-name{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:16px;color:var(--ink);}
/* --light IS NOT DEFINED ANYWHERE in the theme, so this fell back to inherited
   colour rather than a muted one. Same class of fault as --surface-sunken. */
.rail-pick-area{font-size:12px;color:var(--ink-soft, #6a6050);margin-top:2px;}
.rail-pick-note{font-size:13px;color:var(--ink-soft);line-height:1.6;margin-top:10px;}
.rail-pick-top{display:flex;align-items:center;gap:10px;margin-bottom:12px;}
.rail-pick-go{margin-left:auto;flex:0 0 auto;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:12px;color:var(--green);white-space:nowrap;}
.rail-pick-tip{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;border:1px solid var(--green);background:var(--bg-card, #fff);color:var(--green);font-size:10px;font-weight:700;cursor:help;position:relative;vertical-align:middle;margin-left:6px;}
.rail-pick-tip .rail-pick-tiptext{visibility:hidden;opacity:0;position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);width:190px;background:var(--surface-invert);color:var(--on-surface-invert);font-family:'Inter Tight',sans-serif;font-size:12px;font-weight:400;line-height:1.5;padding:9px 11px;border-radius:8px;z-index:20;transition:opacity .15s;}
.rail-pick-tip:hover .rail-pick-tiptext,.rail-pick-tip:focus .rail-pick-tiptext{visibility:visible;opacity:1;}
.rail-pick-badges{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px;}
/* Rail status row: what's true right now. */
.rail-pick-status{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-top:10px;}
.rail-stat{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--ink-soft);line-height:1.4;}
.rail-stat--deals b{font-family:'Inter Tight',sans-serif;font-weight:700;color:var(--green);font-size:13px;}
.rail-open-dot{width:6px;height:6px;border-radius:50%;background:var(--ink-soft, #6a6050);flex:0 0 auto;}
.rail-open.is-open .rail-open-dot{background:var(--color-brand-deep, #12814A);}
.rail-open.is-closed{color:var(--ink-soft, #6a6050);}
.rail-pick-fhrs{margin-top:8px;}
.rail-pick-tip svg{display:block;}
.rail-pick-badge{font-size:11px;font-weight:600;color:var(--ink-soft);background:var(--bg-card, #fff);border:1px solid var(--line);border-radius:999px;padding:3px 9px;line-height:1.4;}
.rail-pick-badge.is-parent{color:var(--green);background:var(--bg-card, #fff);border-color:var(--green);}
.rail-pick:hover .rail-pick-go{color:var(--green);}

/* New on sastamal — single-card rotator */
.newshops{position:relative;}
.newshop{display:none;}
.newshop.is-active{display:block;animation:sm-fade .35s ease;}
@keyframes sm-fade{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
.newshop-dots{display:flex;gap:6px;justify-content:center;margin-top:14px;}
.newshop-dot{width:6px;height:6px;padding:0;border:0;border-radius:50%;background:var(--rule);cursor:pointer;transition:background .2s,transform .2s;}
.newshop-dot.on{background:var(--green);transform:scale(1.3);}
.newshop-dot:hover{background:var(--ink-soft);}
@media(prefers-reduced-motion:reduce){.newshop.is-active{animation:none;}}
/* rail versions of spotlight + new shops (stacked, compact) */
.home-rail .spot-card{flex-direction:column;padding:0;border:none;gap:12px;}
.home-rail .spot-avatar{width:56px;height:56px;}
.home-rail .spot-name{font-size:18px;}
.home-rail .newshops{grid-template-columns:1fr 1fr;gap:10px;}
@media(max-width:900px){
  /* Extra right padding gives the fixed .rail-tabs their own lane, so the
     vertical promo tabs no longer cut across card edges while scrolling.
     The tabs are position:fixed against the viewport edge, so without this the
     grid runs underneath them. */
  /* Right padding clears the fixed .rail-tabs so they don't cut across cards.
     Sized to the tab protrusion (22px) plus a little breathing room, retune
     this if the tab width changes. */
  /* padding-top 20 -> 10: the hero is visually hidden on mobile (the live
     headline moved into the nav), so the 20px was tuned for a hero that
     no longer occupies space. It left a visibly bigger gap below the
     header's border than above it. */
  .home-layout{grid-template-columns:1fr;padding:10px 26px 56px 20px;gap:24px;}
  /* --- Featured shops: mobile BOTTOM SHEET ---------------------------------
     Slides up from the bottom edge, full-width, thumb-friendly. On >=901px the
     rail reverts to a normal right-hand column and none of this applies. */
  /* Sheet elevation. Light mode separates the sheet from the page with a black
     drop shadow, which is invisible on a near-black page, so dark mode has to
     do it with a lighter surface AND a visible edge. That's why --sheet-edge
     exists: measured, a deeper scrim moved separation 1.03 -> 1.07 (nothing),
     because the sheet was --paper, i.e. the same colour as the page behind it. */
  .home-rail{position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;max-height:75vh;z-index:60;
    background:var(--surface-raised, #fbfaf7);border:0;border-top:1px solid var(--sheet-edge, var(--line));
    border-radius:20px 20px 0 0;box-shadow:var(--sheet-shadow, 0 -8px 32px rgba(0,0,0,.20));
    padding:0 16px calc(16px + env(safe-area-inset-bottom,0px));
    overflow-y:auto;-webkit-overflow-scrolling:touch;
    transform:translateY(100%);transition:transform .35s cubic-bezier(0.16,1,0.3,1);}
  .home-rail.is-open{transform:translateY(0);}
  /* Tablet (e.g. iPad portrait ~768px): a full-bleed phone sheet looks wrong on
     a wide screen, so cap the width, centre it, and round all top corners. */
  @media(min-width:601px){
    .home-rail{left:50%;right:auto;width:min(560px,92vw);margin:0;
      transform:translate(-50%,100%);border-radius:20px 20px 0 0;
      border-left:1px solid var(--line);border-right:1px solid var(--line);
      max-height:70vh;padding-left:22px;padding-right:22px;}
    .home-rail.is-open{transform:translate(-50%,0);}
  }
  /* ⚠ THE SHEET WAS THE ONE BOTTOM-ANCHORED THING THAT NEVER GOT TAB-BAR
     CLEARANCE. body, .foot/.site-footer and .sm-peek all already add it; this
     did not, so in the installed app the last ~46px of the featured panel sat
     under the bar. And it could not be scrolled to: the bar is z-index 900
     against the sheet's 60, so it covers rather than pushes, and padding is the
     only thing that makes that content reachable at all.
     ⚠ IT MUST LIVE HERE, AFTER the sheet's own padding rule — same specificity,
     so a copy placed up in the standalone block near the tab bar CSS would be
     overridden by the rule above and look like it had done nothing.
     62px = the bar's 46px plus the 16px breathing room the sheet already had. */
  @media (display-mode: standalone){
    /* ⚠ padding-bottom ALONE DID NOT WORK, and the reason is worth keeping.
       .home-rail is display:flex AND overflow-y:auto. WebKit does not include a
       scroll container's bottom padding in its scrollable area once the content
       overflows — the declaration is honoured on paper and buys no reachable
       space at all, which is exactly what "I still can't see the whole of
       Editor's Pick" looked like after the first fix shipped.
       So the space is taken by a REAL FLEX ITEM instead. ::after is a genuine
       child of the flex container, it participates in layout, and it scrolls
       like anything else, so the clearance exists whatever the engine thinks of
       the padding. The padding stays for engines that do honour it, and the two
       do not stack because the padding is inert precisely where the spacer is
       needed.
       ⚠ flex-shrink:0 IS NOT OPTIONAL — a flex item in a column container with
       content above it will otherwise be squashed to nothing, which is the
       silent version of this same bug. */
    .home-rail{padding-bottom:calc(62px + env(safe-area-inset-bottom,0px));}
    .home-rail::after{content:'';display:block;flex:0 0 auto;
      height:calc(62px + env(safe-area-inset-bottom,0px));}
  }
  .sheet-grab{position:sticky;top:0;display:flex;justify-content:center;padding:10px 0 6px;background:var(--surface-raised, #fbfaf7);cursor:pointer;z-index:2;}
  .sheet-grab span{display:block;width:40px;height:4px;border-radius:999px;background:var(--grab-handle, #ccc);}
  .sheet-head{position:sticky;top:20px;display:flex;align-items:center;justify-content:space-between;gap:12px;
    padding:2px 0 12px;margin-bottom:4px;background:var(--surface-raised, #fbfaf7);border-bottom:1px solid var(--sheet-rule, var(--line));z-index:2;}
  .sheet-title{font-family:'Inter Tight',system-ui,sans-serif;font-size:13px;font-weight:700;letter-spacing:.10em;
    text-transform:uppercase;color:var(--green);margin:0;}
  .rail-close{width:32px;height:32px;flex:0 0 auto;border-radius:50%;border:1px solid var(--line);background:var(--bg-card, #fff);
    color:var(--ink);font-size:14px;line-height:1;cursor:pointer;}
  .home-rail .rail-panel{border:0;border-radius:0;background:transparent;}
  .home-rail .rail-row{padding:0 0 8px;}
  /* The mobile sheet zeroes the row's horizontal padding, so the full-bleed
     negative margins used on desktop would push the photo outside the sheet.
     Reset them here and let the image sit flush within the row instead. */
  .home-rail .rail-pick-img{margin:0 0 12px;border-radius:12px;}
  .home-rail .rail-row + .rail-row{border-top:0;}
  /* The sheet header already names the section, so the card's own tag would
     repeat it, hide it here (keep the "Visit shop" action). */
  .home-rail.is-open .rail-pick-tag{display:none;}
  .home-rail.is-open .rail-pick-top{justify-content:flex-end;margin-bottom:4px;}
  /* The sheet has far more room than the 320px desktop rail — let the shop
     details breathe. Scoped to .is-open so the desktop column is untouched. */
  .home-rail.is-open .rail-pick-avatar{width:88px;height:88px;}
  .home-rail.is-open .rail-pick-avatar span{font-size:28px;}
  .home-rail.is-open .rail-pick-name{font-size:26px;line-height:1.15;margin-top:14px;}
  .home-rail.is-open .rail-pick-area{font-size:16px;margin-top:4px;}
  .home-rail.is-open .rail-pick-badges{gap:8px;margin-top:12px;}
  .home-rail.is-open .rail-pick-badge{font-size:14px;padding:7px 14px;}
  /* Pull-tabs: fixed to the right edge, nudged BELOW centre so they sit in the
     gutter between cards rather than masking a card's image title or price row
     (both of which cluster in the upper half of a card). */
  .rail-tabs{position:fixed;top:56%;right:0;transform:translateY(-50%);z-index:59;display:flex;flex-direction:column;gap:6px;max-height:100vh;}

  /* --- Butter-yellow family -------------------------------------------------
     Charcoal on butter yellow gives a very high contrast ratio, so the labels
     stay crisp at 11px. The three shop tabs are DIFFERENT TONES of the same
     family, distinguishable at a glance without three loud slabs competing.
     Protrusion is deliberately small (~22px): a pull-tab you reach for, not an
     overlay that fights the feed. Tap target stays comfortably past 44px via
     min-height, so the small width costs nothing ergonomically. */
  .rail-tab{
    background:#FDE047;                 /* Editor's pick, the strongest tone */
    color:#18181B;
    border:0;
    border-radius:12px 0 0 12px;        /* rounded on the INNER edge only */
    padding:14px 0;
    /* WIDTH: border-box, so this IS the protrusion — with content-box the
       padding was added on top. And the flex child (vertical text) has its own
       intrinsic width, so it must be allowed to shrink or it forces the button
       open regardless of what's set here. Both were making the tab ~3x wider
       than intended. */
    width:20px;
    min-width:20px;
    max-width:20px;
    min-height:104px;                   /* >> 44px tap target */
    box-sizing:border-box;
    overflow:hidden;
    cursor:pointer;
    display:flex;align-items:center;justify-content:center;
    box-shadow:-2px 4px 12px rgba(0,0,0,.4);
    transition:transform .18s cubic-bezier(.2,.8,.3,1), box-shadow .18s ease, filter .18s ease;
  }
  .rail-tab:nth-child(2){background:#FEF08A;}   /* Spotlight, mid */
  .rail-tab:nth-child(3){background:#FEF9C3;}   /* New shops, lightest */

  /* Reach for it: the tab slides out a touch and lifts. */
  .rail-tab:active{transform:translateX(-3px);box-shadow:-4px 6px 16px rgba(0,0,0,.45);}
  @media(hover:hover){
    .rail-tab:hover{transform:translateX(-3px);filter:brightness(1.03);}
  }
  .rail-tab:focus-visible{outline:3px solid #18181B;outline-offset:-3px;}

  /* Dark mode: a bright yellow slab is the loudest thing on a near-black page,
     which is the opposite of a discreet pull-tab. Same hues, dialled down. */
  [data-theme="dark"] .rail-tab{background:#D9C246;box-shadow:-2px 4px 14px rgba(0,0,0,.6);}
  [data-theme="dark"] .rail-tab:nth-child(2){background:#C9B84F;}
  [data-theme="dark"] .rail-tab:nth-child(3){background:#B8AC5C;}

  /* Broadcast + Announce are URGENCY signals, not shop tabs — they keep the
     saffron/green semantics (and the pulse) on purpose. Turning these yellow
     would strip the one visual difference that makes them mean something. */
  .rail-tab--broadcast{background:var(--color-accent, #e86000) !important;color:#fff !important;animation:sm-brd-pulse 2s ease-in-out infinite;}
  .rail-tab--announce{background:var(--color-brand-deep, #12814A) !important;color:#fff !important;}
  @keyframes sm-brd-pulse{0%,100%{box-shadow:-2px 4px 12px rgba(0,0,0,.4);}50%{box-shadow:-2px 4px 20px rgba(232,96,0,.7);}}
  @media(prefers-reduced-motion:reduce){
    .rail-tab{transition:none;}
    .rail-tab--broadcast{animation:none;}
  }
  /* NOTE: writing-mode alone rotates the text; adding transform:rotate() on top
     pushes the glyph box outside the button and clips it off-screen. */
  .rail-tab__txt{writing-mode:vertical-rl;text-orientation:mixed;font-family:'Inter Tight',system-ui,sans-serif;font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;line-height:1;
    /* Let the rotated label shrink inside the fixed-width tab rather than
       forcing it wider. */
    flex:0 1 auto;min-width:0;}
  .rail-tabs.is-hidden{display:none;}
  /* ⚠ THE FIXED TAB IS HIDDEN OUTRIGHT ON AN EMPTY BOARD — his call. It is NOT
     that the tab showed on first view: the sheet AUTO-OPENS on an empty board
     (see .rail-autoopen) and open() already adds .is-hidden. The tab came back
     the moment the sheet was CLOSED, because close() restores it
     unconditionally — so it ended up floating over the empty state, which is
     the one screen with no cards for it to sit between.
     ⚠ A CLASS, NOT REMOVING THE MARKUP. The sheet module starts with
     `if(!panel || !tabs) return;`, so deleting the element would take the
     auto-open, the scrim and the swipe-to-dismiss with it.
     ⚠ AND NOT THE [hidden] ATTRIBUTE — .rail-tabs sets display:flex, which
     outranks the browser's own [hidden] rule. Same fault this file has hit
     repeatedly. */
  .rail-tabs--board-empty{display:none;}
  /* The way back INTO the featured shops once that sheet is closed. Without it
     the tab hide strands the only content an empty board has: the sheet opens
     itself, and closing it would leave no route back short of a reload — the
     kind of dead end this whole empty-board screen exists to remove.
     ⚠ IT STARTS AS AN INLINE display:none AND IS REVEALED BY JS, because
     whether the sheet exists at all is decided in the sheet module (no tabs =
     no sheet = the link must not appear). Inline styles are how railRecount()
     already shows and hides rail entries, so this follows the file's own idiom
     rather than adding a second mechanism. */
  .sm-rail-open{margin:22px 0 0;font-size:13px;color:var(--ink-soft);text-align:center;}
  .sm-rail-open button{background:none;border:0;padding:0;font:inherit;color:var(--green);
    font-weight:600;text-decoration:underline;text-underline-offset:2px;cursor:pointer;}
  .sm-rail-open button:hover{color:var(--ink);}
  .rail-scrim{position:fixed;inset:0;background-color:var(--scrim, rgba(0,0,0,.5));opacity:0;z-index:58;border:0;
    transition:opacity .35s cubic-bezier(0.16,1,0.3,1);}
  .rail-scrim.is-open{opacity:1;}
  .rail-scrim[hidden]{display:none;}
  .home-rail[data-show] .rail-row{display:none;}
  .home-rail[data-show="pick"] .rail-row[data-rail="pick"],
  .home-rail[data-show="spot"] .rail-row[data-rail="spot"],
  .home-rail[data-show="broadcast"] .rail-row[data-rail="broadcast"],
  .home-rail[data-show="announce"] .rail-row[data-rail="announce"],
  .home-rail[data-show="new"] .rail-row[data-rail="new"]{display:block;}
}
@media(min-width:901px){
  /* .sm-rail-open joins this list: above the breakpoint the featured shops are
     a normal column already on screen, so a link offering to open them is an
     offer to do something that has already happened. */
  .rail-tabs,.rail-scrim,.rail-close,.sheet-grab,.sheet-head,.sm-rail-open{display:none;}
  .home-rail[data-show] .rail-row{display:block;}

  /* Sort toggle sits on its OWN line, right-aligned, at every width from
     here up (901px+). It used to try sharing the filter row via a
     negative-margin pull, on the theory that most screens had room — gated
     by a check at 1080px viewport width, reset to its own line only below
     that.

     That check measured the wrong thing. The board column doesn't track
     viewport past about 900px the way 1080 assumed: .home-layout caps at
     max-width:1200px (grid-template-columns:1fr 320px; gap:40px), so the
     column is capped at roughly 776px on ANY screen 1200px or wider, and
     only grows a little (about 656px to 776px) between 901 and 1200. That's
     why the shared row broke twice, in opposite directions: once on an
     iPad, narrower than the pull-up assumed (901-1080px); and again on a
     laptop comfortably past 1080px, which never even reached the reset —
     the column there is the SAME ~776px cap, just as tight.

     Rather than chase a third breakpoint for a hack that keeps finding new
     widths to break at, this drops the shared-row trick outright. One rule,
     no viewport math, no divider needed since the two groups are visibly on
     separate lines instead of pretending to share one. */
  .sm-interests:not([hidden]) + .sm-viewtoggle{
    margin:0 0 18px;
    width:100%;
    justify-content:flex-end;
  }
}
@media(max-width:900px){
  .home-rail .newshops{grid-template-columns:repeat(4,1fr);}
}
@media(max-width:520px){
  .home-rail .newshops{grid-template-columns:repeat(2,1fr);}
}
.sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px;}
.sec-head--meta{align-items:center;margin-bottom:20px;padding-bottom:12px;border-bottom:1px solid var(--line);}
.sec-head--meta p{font-size:13px;}
/* ⚠ SPACING BETWEEN SECTIONS, and 56px was written for a full board where the
   gap separates two dense grids. Between an empty state and one short list it
   is just a hole. 30px still reads as a break. */
.sec-head--spaced{margin-top:30px;}
/* First section under the filter used to get this same treatment via a now-
   removed header div (.sec-head--first, 8px hug-up) — the 56px --spaced
   rhythm is for separating LABELLED sections from each other, but the first
   one only ever had the filter above it, which already provides the break.
   Ending Soon has no header anymore (see below), so that small hug-up now
   lives directly on its grid instead. */
.deals--first{margin-top:8px;}
.sec-head h2{font-size:clamp(30px,4vw,44px);max-width:16ch;}
.sec-head p{font-size:15px;color:var(--ink-soft);max-width:38ch;}

/* how it works — a real 3-step sequence, numbering earns its place */
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;background:var(--line);border:1px solid var(--line);border-radius:18px;overflow:hidden;}
.step{background:var(--white);padding:32px 28px;}
.step-n{font-family:'Inter Tight',sans-serif;font-size:14px;font-weight:700;color:var(--green);}
.step h3{font-family:'Fraunces',serif;font-size:22px;font-weight:500;margin-top:14px;letter-spacing:-.01em;}
.step p{font-size:14px;color:var(--ink-soft);margin-top:9px;line-height:1.55;}

/* deals grid */
/* align-items:start stops a short card (no price, no badges) being stretched to
   match a tall neighbour, which was leaving big empty gaps mid-card. Each card
   is now only as tall as its own content. */
.deals{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;align-items:start;}

/* ============================================================================
   DEAL CARD, "bento split"
   Image and content occupy SEPARATE planes. Nothing floats over the photo but
   the Sastameter, so every word sits on a solid surface at constant contrast.
   The previous card laid text over a washed photo, which meant legibility
   changed with whatever the shop happened to upload.

   Two shapes, one vocabulary:
     .card            → bento (photo header on top), when a deal has imagery
     .card--compact   → horizontal row (small tile), when it has none
   The compact tile falls back to the shop logo, then initials, so a card is
   never a blank rectangle.
   ========================================================================= */
.card{display:flex;flex-direction:column;background:var(--bg-card, #fff);border:none;border-radius:16px;
  overflow:hidden;box-shadow:0 6px 18px -6px rgba(10,8,6,.18),0 2px 4px -2px rgba(10,8,6,.10);
  transition:transform .18s,box-shadow .18s;text-decoration:none;color:var(--ink);}
@media(hover:hover){.card:hover{transform:translateY(-3px);box-shadow:0 16px 34px -12px rgba(10,8,6,.24),0 4px 8px -3px rgba(10,8,6,.12);}}

/* --- Image header --- */
/* 16:9 keeps the header shallow so more cards fit a screen. */
.card-img{position:relative;aspect-ratio:16/9;background:var(--bg-sand2, #f2efe8);overflow:hidden;flex:0 0 auto;}
.card-img img{width:100%;height:100%;object-fit:cover;display:block;}

/* Sastameter — the ONLY thing that floats on the photo. */
.card-meter{position:absolute;top:10px;left:10px;z-index:2;display:inline-flex;align-items:center;gap:5px;
  height:24px;padding:0 9px;border-radius:999px;font-family:'Inter Tight',sans-serif;font-weight:700;
  font-size:11.5px;line-height:1;background:#fff;color:var(--color-brand-deep, #12814A);
  /* ⚠ A HAIRLINE RING, NOT JUST A DROP SHADOW. This pill is the one thing that
     floats on the photo, so its legibility depends on an image nobody controls.
     The colours inside it are fine either way — the failure is the pill's own
     EDGE disappearing when the photo behind it happens to match: white on a
     pale sky in light mode, and far worse in dark mode, where a #14110d pill
     over a dark region of a photo has no boundary at all and the badge reads as
     floating text. A soft shadow cannot fix that; it needs a hard 1px line.
     ⚠ The ring is a rgba() literal on BOTH sides on purpose. It is drawn over
     an unknown photo, not over a token-coloured surface, so there is no
     background token for it to pair with — this is the one case the
     "never pair an inverting token with a literal" rule does not cover. */
  box-shadow:0 0 0 1px rgba(0,0,0,.10), 0 1px 6px rgba(0,0,0,.18);}
[data-theme="dark"] .card-meter{background:#14110d;color:#5fd398;
  box-shadow:0 0 0 1px rgba(255,255,255,.26), 0 1px 6px rgba(0,0,0,.45);}
.card-meter b{font-weight:800;font-size:13px;}
.card-meter i{font-style:normal;font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;opacity:.75;}

/* --- Content --- */
/* Tight but not cramped: 12px sides, 7px between rows. The card carries five
   short lines, so gap discipline matters more than padding here. */
.card-body{padding:11px 12px 0;display:flex;flex-direction:column;gap:7px;flex:1;min-width:0;}

/* Line 1 — merchant + place, one quiet line. */
.card-merch{display:flex;align-items:center;gap:8px;min-width:0;}
.card-avatar{flex:0 0 auto;width:26px;height:26px;border-radius:50%;overflow:hidden;display:grid;
  place-items:center;background:var(--paper2, #f2efe8);}
.card-avatar img{width:100%;height:100%;object-fit:cover;}
/* WARM NEUTRAL. This is the fallback when a shop has no logo — a placeholder
   should not shout as loudly as a button. It also appeared on every card
   without imagery, which is what made the board read as mostly green. */
.card-avatar--init{background:var(--bg-sand2, #ece6db);color:var(--text-muted, #6a6050);
  font-family:'Inter Tight',sans-serif;font-weight:800;font-size:11px;letter-spacing:.02em;}
.card-merch-txt{font-family:'Inter Tight',sans-serif;font-size:12.5px;color:var(--ink-soft);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1;}
.card-merch-txt b{font-weight:600;}
.card-merch-dot{opacity:.5;margin:0 3px;}

/* Live "busy" chip — the shop's own current state (Quiet / Busy / Slammed).
   Sits INLINE at the end of the merchant row: it's shop status, so it belongs
   beside shop identity, and it no longer costs a row of its own. These classes
   are produced by sm_busy_card_chip() in busy.php. */
.card-busy{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto;
  font-family:'Inter Tight',sans-serif;font-size:10.5px;font-weight:700;line-height:1;
  color:var(--color-brand-deep, #12814A);background:var(--color-brand-tint, #eaf5ee);
  padding:3px 7px;border-radius:999px;white-space:nowrap;}
.card-busy-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:0 0 auto;}
.card-busy--slammed{color:var(--color-accent-deep, #c85000);background:var(--bg-tint, #fdf0e6);}
.card-busy--slammed .card-busy-dot{animation:cardBusyPulse 1.8s ease-in-out infinite;}
@keyframes cardBusyPulse{0%,100%{opacity:1;}50%{opacity:.4;}}
@media(prefers-reduced-motion:reduce){.card-busy--slammed .card-busy-dot{animation:none;}}

/* Line 2 — the deal. Sans, not serif: Fraunces is a display face and loses its
   character at card size, where it just read as "a different font". */
.card-name{font-family:'Inter Tight',sans-serif;font-size:15.5px;font-weight:700;line-height:1.25;
  letter-spacing:-.012em;color:var(--ink);display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden;}

/* Line 3 — offer badges. No "ends at" badge here any more: the footer countdown
   already answers "do I have time?", and two time indicators was noise. */
/* Line 3 — badges LEFT, price RIGHT, sharing one row. Both are short, so
   giving each a full row was wasting vertical space. Wraps on narrow cards. */
.card-offer{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;}
/* Remaining stock. Amber rather than saffron: saffron is the brand's urgency
   colour and is already carrying the offer badges, so a second saffron pill
   next to them would read as another offer instead of a warning. */
.sm-left{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:7px;
  background:var(--alert-warn-bg, #fff3ea);color:#8a4b12;
  font-family:'Inter Tight',system-ui,sans-serif;font-size:11.5px;font-weight:700;line-height:1.3;}
.sm-left-dot{width:5px;height:5px;border-radius:50%;background:#d97706;flex:0 0 auto;}
/* 3 or fewer: the same pill, louder. Emphasis rather than presence, now that
   the badge shows at every level — a high number and a low one have to read
   differently or the line stops meaning anything. */
.sm-left--urgent{background:#ffe3cc;color:#7a3d08;}
.sm-left--urgent .sm-left-dot{background:#b45309;}
.card-badges{display:flex;flex-wrap:wrap;align-items:center;gap:6px;min-width:0;}
.card-badge{display:inline-flex;align-items:center;gap:4px;font-family:'Inter Tight',sans-serif;
  font-size:11px;font-weight:700;line-height:1;padding:4px 8px;border-radius:999px;
  color:var(--color-accent-ink, #a04600);background:var(--bg-tint, #fdf0e6);}
/* Percentage off is a TYPE of offer, not urgency — so it must not use saffron,
   which is reserved for prices, timers and ending-soon. */
.card-badge--pct{color:var(--color-brand-deep, #12814A);background:var(--color-brand-tint, #eaf5ee);}
/* Operational warning (closing early / sold out). Deliberately the loudest
   badge, it changes whether the deal is worth acting on at all. Rare by
   design: the notice field is for warnings, not announcements. */
.card-badge--warn{color:#fff;background:var(--color-accent-deep, #c85000);}
/* Still used by the PEEK card (quick-look popover), which shows an absolute
   "ends 11pm" alongside its own details. The grid card no longer uses it -
   there the footer countdown is the single time indicator. */
.card-ends{display:inline-flex;align-items:center;font-family:'Inter Tight',sans-serif;font-size:11px;
  font-weight:700;color:var(--ink-soft);background:var(--bg-sand5, #f0ece3);border-radius:999px;
  padding:3px 9px;text-transform:lowercase;}

/* Line 4 — price. The number is the decision, so it wins the hierarchy. */
.card-price{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
/* INK. The price is already the largest thing in the row; green added no
   emphasis and stole it from the -33%, which is the number that actually
   sells the deal. Saffron on .card-off is now the only colour in this row. */
.card-now{font-family:'Inter Tight',sans-serif;font-size:19px;font-weight:800;letter-spacing:-.02em;
  color:var(--ink, #0a0806);}
.card-was{font-size:13px;color:var(--ink-soft);text-decoration:line-through;opacity:.8;}
.card-off{font-family:'Inter Tight',sans-serif;font-size:11px;font-weight:800;color:var(--saffron);}

/* --- Footer: one countdown (left) + like (right) --- */
.card-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;
  padding:9px 12px;border-top:1px solid var(--line);}
.card-foot-spacer{flex:1;}
.card-timer{display:inline-flex;align-items:center;gap:6px;font-family:'Inter Tight',system-ui,sans-serif;
  font-weight:700;font-size:12px;color:var(--color-accent-deep, #c85000);}
.card-timer-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:0 0 auto;}
.card-timer-win{color:var(--ink-soft);font-weight:500;}
.card-timer-win::before{content:'·';margin-right:5px;opacity:.5;}

/* ============================================================================
   COMPACT, no deal photo, no shop cover. Rather than show an empty header we
   switch shape: small tile (logo or initials) beside the content.
   ========================================================================= */
.card--compact{display:grid;grid-template-columns:88px 1fr;grid-template-rows:auto auto;
  align-items:stretch;}
.card--compact .card-tile{grid-column:1;grid-row:1 / span 2;display:grid;place-items:center;
  background:var(--bg-sand2, #f2efe8);overflow:hidden;}
.card--compact .card-tile img{width:100%;height:100%;object-fit:cover;}
.card--compact .card-tile-init{font-family:'Inter Tight',sans-serif;font-size:22px;font-weight:800;
  letter-spacing:.02em;color:var(--color-brand-deep, #12814A);opacity:.55;}
.card--compact .card-body{grid-column:2;grid-row:1;padding:11px 12px 0;gap:6px;}
.card--compact .card-foot{grid-column:2;grid-row:2;padding:8px 12px 10px;border-top:0;margin-top:2px;}
.card--compact .card-name{font-size:15px;}
.card--compact .card-now{font-size:17px;}
/* Compact keeps the meter inline (nothing to float over). */
.card--compact .card-meter{position:static;background:var(--color-brand-tint, #eaf5ee);
  color:var(--color-brand-deep, #12814A);box-shadow:none;backdrop-filter:none;align-self:flex-start;}
[data-theme="dark"] .card--compact .card-meter{background:var(--color-brand-tint, #1c3a2a);color:#5fd398;}

@media(max-width:560px){
  .card--compact{grid-template-columns:72px 1fr;}
}
.card-window{font-family:'Inter Tight',system-ui,sans-serif;font-weight:600;font-size:12px;color:var(--ink-soft);}
.card-timer-dot{width:7px;height:7px;border-radius:50%;background:var(--saffron);animation:sm-pulse 1.4s ease-in-out infinite;}
.card-timer.ending{color:#c8102e;}
.card-timer.ending .card-timer-dot{background:#c8102e;}
@keyframes sm-pulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:.35;transform:scale(.8);}}
@media(prefers-reduced-motion:reduce){.card-timer-dot{animation:none;}}
.card.sm-expiring{transition:opacity .6s ease,transform .6s ease;opacity:0;transform:scale(.97);}

/* Reaction — minimal micro-interaction, clean border */
.card-foot-actions{display:inline-flex;align-items:center;gap:8px;flex:0 0 auto;}
/* Share — same pill footprint as the like button beside it, but icon-only
   (no count to show). .is-copied is the desktop fallback's confirmation:
   navigator.share opens a real OS sheet that speaks for itself, whereas a
   silent clipboard copy needs to say something happened. */
.card-share{display:inline-flex;align-items:center;justify-content:center;padding:7px 10px;border-radius:999px;border:1px solid var(--line);background:var(--bg-card, #fff);color:var(--ink-soft);cursor:pointer;transition:border-color .15s,color .15s;}
.card-share:hover{border-color:var(--color-brand-deep, #12814A);color:var(--color-brand-deep, #12814A);}
/* 'Copied' confirmation. The peek's share button is bare now, so painting a
   green disc behind it would make one button briefly grow a circle none of
   its neighbours have. Colour alone carries it there; the card's share
   button still has its own border, so it keeps the filled treatment. */
.card-share.is-copied{background:var(--color-brand-deep, #12814A);border-color:var(--color-brand-deep, #12814A);color:#fff;}
/* NO PILL. The bordered chip made this read as a button competing with Share
   next to it; a bare heart reads as an affordance on the card itself. Padding
   is kept as tap area only — 44px of target without a visible box. */
.card-react{display:inline-flex;align-items:center;gap:5px;padding:6px 4px;border-radius:8px;border:0;background:none;color:var(--color-brand-deep, #12814A);font-family:'Inter Tight',system-ui,sans-serif;font-weight:600;font-size:12px;cursor:pointer;transition:color .15s;}
/* The greyscale filter existed to mute an emoji. The icon is now a stroked
   heart that takes its colour from the button, so the filter would only dull
   the brand green. */
.card-react-emoji{font-size:13px;line-height:1;display:inline-flex;transition:none;}
.card-react:hover{color:var(--color-brand, #189554);}
/* Solid fill, not a pale tint — the tint read as faint rather than
   "selected", the same fix already applied to the peek's own Like button
   (.sm-peek-likes.reacted). Icon is stroke="currentColor" so it recolours to
   white on its own; cursor stays pointer since liked is still tappable
   (tap again to take the like back). */
/* SAVED = the heart FILLS. The outline/fill pair is the whole state signal
   now that there is no pill to invert, so the fill has to come from CSS: the
   SVG carries fill="none" as an attribute and CSS outranks it. */
.card-react.reacted{color:var(--color-brand-deep, #12814A);cursor:pointer;}
.card-react.reacted .card-react-emoji svg{fill:currentColor;}
.card-react[disabled]{opacity:.6;cursor:default;}
.card-react.reacted .card-react-emoji{filter:none;}
.card-react-count{font-size:11px;color:var(--ink-soft);font-weight:700;}
.card-react.reacted .card-react-emoji{filter:none;}
/* Was var(--color-accent-deep) (orange), tuned for the old pale-tint
   background. .card-react-count sets its own explicit color, so it doesn't
   inherit the button's white text just because the button turned solid
   green — needs its own override, white to match the icon. */
/* Was white, for the filled pill that no longer exists — on the card's own
   background that would have been invisible. */
.card-react.reacted .card-react-count{color:var(--color-brand-deep, #12814A);}
@media(max-width:420px){.card-react-text{display:none;}}

/* sell band */
/* For Birmingham shops band — markup lives in template-parts/shops-band.php */
.shops-band{background:var(--surface-invert);color:var(--on-surface-invert);}
.shops-band__inner{max-width:1200px;margin:0 auto;padding:56px 32px;display:grid;grid-template-columns:1fr 1fr;gap:44px;align-items:center;}
.shops-band__eyebrow{display:block;font-family:'Inter Tight',sans-serif;font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:#6fcf97;}
.shops-band__title{font-family:'Fraunces',serif;font-weight:600;font-size:clamp(26px,3.4vw,38px);line-height:1.08;letter-spacing:-.02em;margin-top:14px;color:var(--on-surface-invert);}
.shops-band__text{font-size:15px;color:var(--text-faint3, #b8b0a2);margin-top:14px;line-height:1.55;max-width:42ch;}
.shops-band__cta{margin-top:22px;display:flex;gap:12px;flex-wrap:wrap;}
.shops-band__btn{display:inline-block;padding:13px 24px;border-radius:999px;border:1.5px solid var(--on-surface-invert);background:var(--on-surface-invert);color:var(--surface-invert);font-family:'Inter Tight',sans-serif;font-weight:700;font-size:14px;transition:all .18s;}
.shops-band__btn:hover{background:transparent;color:var(--on-surface-invert);}
.shops-band__btn--alt{background:var(--green);border-color:var(--green);color:var(--on-brand, #fff);}
.shops-band__btn--alt:hover{background:transparent;border-color:var(--green);color:var(--green);}
.shops-band__list{display:flex;flex-direction:column;gap:0;background:none;border-radius:0;overflow:visible;}
.shops-band__item{background:none;border:0;border-radius:0;padding:14px 0;display:flex;gap:14px;align-items:flex-start;border-top:1px solid rgba(255,255,255,.08);}
.shops-band__item:first-child{border-top:0;padding-top:0;}
.shops-band__item:last-child{padding-bottom:0;}
.shops-band__k{font-family:'Inter Tight',sans-serif;font-weight:700;color:#6fcf97;font-size:12px;letter-spacing:.12em;text-transform:uppercase;flex-shrink:0;min-width:58px;padding-top:2px;}
.shops-band__t{font-size:14px;color:var(--on-invert-muted, #d8d0c2);line-height:1.55;}
.shops-band__t b{color:var(--on-surface-invert);font-weight:600;}
@media(max-width:900px){.shops-band__inner{grid-template-columns:1fr;gap:28px;padding:40px 24px;}}

/* footer */
.foot{border-top:1px solid var(--line);}
/* ⚠ MIRRORS .site-footer__grid IN style.css AND MUST BE KEPT IN STEP WITH IT.
   This footer cannot share those rules: the homepage DEQUEUES style.css and is
   a self-contained document with only tokens.css and fonts.css behind it, which
   is the whole reason a second footer exists here at all. It is not drift to be
   tidied away — replacing it with get_footer() renders this markup with no CSS.
   So the two are kept merely LOOKING identical, by hand, in both files.
   Grid rather than the old flex/space-between: flex could not place the social
   row opposite the sastamal column, and wrapped columns sat at uneven widths. */
.foot-inner{max-width:1200px;margin:0 auto;padding:48px 32px;display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:48px;}
@media(max-width:768px){
  /* ⚠ MIRRORS .site-footer / .site-footer__grid in style.css. Desktop padding
     was being kept at every width — 48px top, 32px gutter — while the rest of
     the site drops to a 20px gutter here. That was the excess above the logo. */
  .foot-inner{grid-template-columns:1fr 1fr;gap:24px;padding:28px 20px;}
  .site-footer__stats{padding:6px 20px 0;}
  .foot-bar{padding:20px 20px 24px;}
  /* Brand spans, so the tagline keeps a full measure and the link columns sit
     two-up beneath it. No single-column collapse at any width — three columns
     of two-to-four links stacked make one long thin list. */
  .foot-brand{grid-column:1 / -1;}
}
/* FOOD HYGIENE BADGE.
   ⚠ THE HOMEPAGE HAD NO RULE FOR THIS AT ALL, so the badge already on the rail
   cards has been rendering as unstyled inline text — and the peek sheet would
   have too. It could not simply be shared: .sm-fhrs is declared inline inside
   single-sm_deal.php and single-sm_shop.php, and this page DEQUEUES style.css,
   so there is nowhere the three could read one definition from.
   ⚠ COPIED VERBATIM from single-sm_deal.php and must be kept in step with it —
   the same hand-maintained duplication as the footer above. */
.sm-fhrs{display:inline-flex;align-items:center;gap:4px;margin-top:6px;font-family:'Inter Tight',system-ui,sans-serif;font-size:12px;font-weight:700;color:var(--color-brand-deep, #12814A);background:var(--color-brand-tint, #eaf5ee);border:1px solid rgba(24,149,84,.25);border-radius:999px;padding:3px 10px;}
.sm-fhrs-star{color:var(--color-brand-deep, #12814A);}
.sm-fhrs-lbl{font-weight:600;opacity:.8;}
/* WHY THIS SHOP, in the peek: scrolls rather than truncates.
   ⚠ max-height in em, not px, so it holds ~6 lines whatever the user's text
   size — a px cap clips a large-type reader mid-sentence. -webkit-overflow-
   scrolling keeps the inner scroll from fighting the sheet's own on iOS. */
.sm-peek-note{margin-top:10px;font-size:14px;line-height:1.55;color:var(--ink-soft,#6a6050);max-height:9.5em;overflow-y:auto;-webkit-overflow-scrolling:touch;}
/* ⚠ THE FADE IS CONDITIONAL, and that matters more than it sounds. Applied
   unconditionally it dims the bottom of EVERY note, so a one-line entry looks
   half-faded and broken — CSS cannot tell whether a box overflows, so home.js
   adds .is-clipped only when it actually does. */
.sm-peek-note.is-clipped{-webkit-mask-image:linear-gradient(to bottom,#000 85%,transparent);mask-image:linear-gradient(to bottom,#000 85%,transparent);}
/* On the RAIL card there is no room to scroll, so clamp to three lines. The
   peek is one tap away and shows all of it. */
.sm-rail-note{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}
/* In the peek the badge sits under the shop name, so it starts a new line. */
.sm-peek-fhrs{display:block;margin-top:4px;}
.foot-social{align-self:start;}
.foot-social .sm-social{margin-top:0;}
.foot-col h4{font-family:'Inter Tight',sans-serif;font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px;}
.foot-col a{display:block;font-size:14px;color:var(--ink-soft);margin-bottom:9px;transition:color .18s;}
/* Share button — styled as a footer LINK, not a button. It sits in a list of
   links and should read as one; the only reason it's a <button> is that it
   triggers the native share sheet rather than navigating. */
.foot-share{display:block;font:inherit;font-size:14px;text-align:left;padding:0;border:0;
  background:none;color:var(--ink-soft);margin-bottom:9px;cursor:pointer;transition:color .18s;}
.foot-share:hover{color:var(--ink);}
.foot-share[hidden]{display:none;}
.foot-col a:hover{color:var(--green);}
.foot-brand{max-width:280px;}
.foot-brand p{font-size:13px;color:var(--ink-soft);margin-top:12px;line-height:1.6;}
/* Centred + regular weight — mirrors style.css, see the note there. */
.site-footer__stats{max-width:1200px;margin:0 auto;padding:6px 32px 0;display:flex;flex-wrap:wrap;justify-content:center;gap:8px 18px;}
.foot-stat-live{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:400;color:var(--ink);text-decoration:none;}
a.foot-stat-live:hover{color:var(--green);}
.foot-stat-dot{width:7px;height:7px;border-radius:50%;background:var(--green);flex:0 0 auto;}
.foot-stat{display:inline-block;margin-top:6px;font-size:12px;color:var(--ink-soft);text-decoration:none;}
a.foot-stat:hover{color:var(--green);}
.foot-bar{max-width:1200px;margin:0 auto;border-top:1px solid var(--line);padding:20px 32px 28px;display:flex;align-items:center;justify-content:space-between;gap:14px;}
.foot-copy{font-size:12px;color:var(--ink-soft);letter-spacing:.02em;}
.foot-utility{display:flex;align-items:center;gap:10px;}
.foot-utility a{font-size:12px;color:var(--ink-soft);transition:color .18s;}
.foot-utility a:hover{color:var(--green);}
.foot-dot{font-size:12px;color:var(--line);}
@media(max-width:640px){
  .foot-bar{flex-direction:column;justify-content:center;text-align:center;gap:12px;}
  .foot-utility{justify-content:center;}
}

@media(max-width:900px){
  .hero-grid{grid-template-columns:1fr;gap:40px;}
  .steps,.deals{grid-template-columns:1fr;}
  .sec-head{flex-direction:column;align-items:flex-start;}
}
@media(max-width:600px){
  /* Compact mobile nav: keep About/Contact visible (don't drop them), just
     tighten spacing so logo + links + city picker all fit. */
  /* Tighter above the logo: 14px top left a visible gap between the browser
     chrome and the wordmark. Bottom padding kept at 14px — the nav wraps to
     two rows here, so the space below the second row still has to breathe. */
  /* gap 14 -> 8: measured, logo (~145pt) + the three nav items (~195pt) +
     padding + gaps lands almost exactly on a 390pt viewport, so the row
     sits ON the wrap boundary rather than over it — which is why earlier
     5-10pt trims changed nothing visible. Padding is left at 18px: it's
     what keeps the logo aligned with the content below the header. */
    .nav-inner{gap:8px;padding:10px 20px 14px;flex-wrap:wrap;position:relative;}
  /* Step the wordmark back a touch on phones: at 26px it can push the nav
     links onto a second row unnecessarily. Still larger than it was. */
  /* MOBILE STEPS BACK TO 28px, not the full 33. Measured on a 375pt phone
     (the common iPhone width, and the one his screenshots come from): the
     whole header row — lockup + Get listed + About + Contact — comes to
     376.5px at 30px, which wraps. 28px brings it to 368.6px and keeps the
     row intact while still growing from the old 26px. */
  .logo-word{font-size:28px;}
  .nav-links{padding-top:2px;}
  /* Gap tightened from 16px: the CTA button is physically wider than the
     text link it replaced, and on a phone that was enough to push the whole
     nav off the logo's row onto its own line (so the header went from two
     rows to three). The row has to fit logo + button + About + Contact
     inside ~354pt, which it only does if both the gap and the button's own
     padding give a little. */
  .nav-links{gap:9px;margin-left:0;padding-top:5px;}
  .nav-links a{font-size:13.5px;white-space:nowrap;}
    /* ⚠ .nav-inner, NOT .nav-links. The CTA moved OUT of the nav so it survives
     the drawer folding, and this override kept targeting it inside — so it
     silently stopped applying and the homepage rendered the CTA at desktop
     size while every other page used style.css's 13.5px. Moving markup out
     of a parent breaks every descendant selector aimed at it. */
  .nav-inner a.nav-cta{font-size:13.5px;}
  /* The nav wraps here: row 1 is logo + links, row 2 is this badge (left) and
     the city picker (right, via margin-left:auto). That band was empty. */
  /* The TEXT lines up with the "sastamal" wordmark on the row above. The dot
     would otherwise push it right, so the badge is shifted LEFT by exactly the
     dot's footprint (27px box + 8px gap = 35px), the text lands at the same x
     as the logo and the dot hangs in the gutter beside it.
     align-items:center handles the vertical: dot and text share a centreline. */
  /* Mirror the LOGO's geometry so the two rows line up on a grid:
       row 1:  [26px mark][10px gap][ "sastamal" ]
       row 2:  [26px dot ][10px gap][ "On right now" ]
     The dot is sized and centred inside a 26px box, the same footprint as the
     logo mark, so the dot centres under the mark AND the text starts at the
     same x as the wordmark. No negative margins (the previous -35px shifted the
     whole badge off the left edge on narrow screens and clipped the text). */
  /* ⚠ THE MOBILE OVERRIDE IS GONE. It sized the badge for the HEADER (17px type,
     26px dot, matched to the logo mark's footprint); on the sort row beside
     13px tabs that reads as a heading rather than a status. The base rule above
     now covers every width. */
  /* Heading and explainer both move OUT of the visible hero on mobile — the
     badge above carries the heading, and the sub is dropped so the filters and
     the first cards clear the fold. Desktop keeps both.
     NOTE: the hero is visually hidden, NOT display:none, the <h1> must stay in
     the document for screen readers and for search engines, which read it as
     the page's heading. Clipping it keeps it available to both while taking up
     no space. */
  /* (the visually-hidden rule moved to the base — see the note there) */
  /* Mobile keeps the sub smaller and softer — at this width, full-ink body text
     under the heading reads as a wall. Desktop gets the stronger treatment. */
  /* Mobile: tighten hard so Category/Area and the first card row clear the
     fold. The heading is short enough now to afford it. */
  .home-hero{margin-bottom:12px;}
  .home-hero .display{font-size:24px;}
  .hero-live{width:8px;height:8px;margin-right:7px;}
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;}}


/* filters — one compact row of dropdowns, sits inline above the deals */
.sm-filterbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 18px;}
/* NOTE THE DOUBLE CLASS on the padding rule below. An area row carries BOTH
   .sm-cat-child and .sm-area-row, and the two set padding. Equal specificity,
   so source order decides — and .sm-cat-child is ~150 lines further down, so
   it wins. A plain .sm-area-row{padding:2px 0} here silently does nothing,
   which is exactly what happened on the first attempt: the bell shrank, the
   rows only went 44 -> 38px, and the padding change was never in play at all.
   .sm-cat-child.sm-area-row beats both regardless of where it sits. */
.sm-area-row{display:flex;align-items:center;gap:6px;}
.sm-cat-child.sm-area-row{padding:2px 0;}
.sm-area-row-lbl{display:flex;align-items:center;gap:9px;flex:1;cursor:pointer;min-width:0;}
  /* Popular roads — a distinct section inside the Area panel, so it reads as a
     different way to slice the city rather than more areas. */
  /* "Use my location" — same compact row height as the roads toggle, so the two
     read as a pair of controls above the list rather than two different things. */
  .sm-useloc{display:flex;align-items:center;gap:6px;width:100%;
    padding:7px 12px;background:none;border:0;cursor:pointer;font:inherit;
    font-size:14px;font-weight:600;color:var(--brand,#12814A);text-align:left;}
  .sm-useloc:hover{background:var(--bg-sand2,#f2efe8);border-radius:8px;}
  .sm-useloc.is-busy{opacity:.55;pointer-events:none;}
  .sm-useloc svg{flex:0 0 auto;}

  /* Sits ABOVE the areas, so the rule goes underneath it. */
  .sm-road-sec{border-bottom:1px solid var(--rule,#ece7dd);margin-bottom:4px;padding-bottom:2px;}
  /* ⚠ COMPACT. This is one row above a long list, not a heading — 9px of padding
     and an uppercase block made it read as a section title rather than a
     control, and pushed the areas further down a panel that already overflows. */
  .sm-road-toggle{display:flex;align-items:center;gap:6px;width:100%;
    padding:7px 12px;background:none;border:0;cursor:pointer;font:inherit;
    font-size:14px;font-weight:600;letter-spacing:0;text-transform:none;
    color:var(--ink,#0a0806);text-align:left;}
  /* ⚠ CHEVRON FIRST, NOT PUSHED TO THE FAR EDGE. Against the right wall of a
     wide panel it read as decoration; beside the label it reads as the thing
     that opens. Same reason it points RIGHT when shut — a down chevron on a
     closed row looks like a dropdown that is already showing its contents. */
  .sm-road-chev{transition:transform .18s;flex:0 0 auto;color:var(--mid,#6a6050);}
  .sm-road-toggle[aria-expanded="true"] .sm-road-chev{transform:rotate(90deg);}
  .sm-road-toggle[aria-expanded="true"]{color:var(--brand,#12814A);}
  .sm-road-toggle .sm-road-n{margin-left:auto;font-weight:400;font-size:12.5px;color:var(--mid,#6a6050);}
  /* ⚠ A LINE UNDER THE PINNED AREA. City Centre is a different kind of place —
     somewhere people travel to, not a neighbourhood — so it needs separating
     from the alphabetical run, or being first just looks like a sort error. */
  .sm-area-row.is-pinned{border-bottom:1px solid var(--rule,#ece7dd);
    margin-bottom:4px;padding-bottom:4px;}

/* 26px, down from 32. The bell — not the padding — is what sets this row's
   height: the row is max(bell, 19px tick) + padding, so at 32px every area sat
   ~42px tall against ~29px for a category child, and the two lists read as
   different components when they're the same menu.
   Held at 24 rather than taken to 19 to match exactly, because unlike a
   category row this one has TWO targets side by side (bell, then checkbox) and
   shrinking the smaller one right down next to the other is how you get people
   subscribing to alerts they meant to filter by. The icon itself stays 17px —
   only the button's padding shrinks, so the bell doesn't get harder to see. */
.sm-area-bell{flex:none;order:-1;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:7px;border:0;background:transparent;color:var(--ink-soft,#8a8172);opacity:.4;cursor:pointer;transition:opacity .15s,color .15s;}
.sm-area-bell:hover{opacity:.75;color:var(--green,#189554);}
.sm-area-bell.following{opacity:1;color:var(--green,#189554);}
.sm-area-bell.following svg{fill:currentColor;}
/* Sits just under the filter row now the card is gone, so the two read as one
   control group rather than two floating rows. */
.sm-viewtoggle{display:flex;gap:2px;margin:0 0 10px;flex-wrap:wrap;}
/* ── REFRESH, ONCE home.js HAS MOVED IT BESIDE THE STATUS ──────────────────
   ⚠ THE AUTO MARGIN CHANGES HANDS. .nav-live carries margin-right:auto to push
   the sort tabs right; with refresh sitting after the badge, that auto would
   throw refresh to the far end — the exact placement this move exists to leave.
   The badge gets a fixed gap and refresh takes the auto instead, so status and
   refresh stay together on the left and the tabs stay right.
   ⚠ THREE CLASSES, deliberately: .nav-live's own auto is declared with a note
   saying it beats the row's flex-end, so anything overriding it has to outrank
   it rather than rely on order. */
.sm-refresh-inrow .sm-viewtoggle .nav-live{margin-right:8px;}
.sm-refresh-inrow .sm-viewtoggle .sm-filter-refresh{margin-right:auto;}
/* It sat in a filter row before and inherited that row's spacing; here it needs
   its own alignment or it rides high against the badge's larger text. */
.sm-refresh-inrow .sm-viewtoggle .sm-filter-refresh{align-self:center;}
/* Fewer than two tabs is not a choice, so the buttons go — but the row stays,
   because it carries the live status readout. See the note in home.js. */
.sm-viewtoggle.sm-vt-tabsoff .sm-vt-btn{display:none;}
/* ⚠ DO NOT TRY THE SHARED ROW AGAIN — THREE ATTEMPTS, THREE REVERTS.
   Two used a negative-margin pull-up gated on viewport width (see the long
   note in the @media block above). The third, 2026-08-18, used inline-flex and
   no measurement at all, on the theory that the browser would wrap them when
   they did not fit. It does — and that is the point: ON A PHONE THEY NEVER FIT.
   Category, Area, search and refresh come to roughly 340px of a 390px screen,
   leaving about 50px for a pair of tabs that need 150. So the row wrapped
   exactly as before, and the only visible change was a divider left hanging on
   the wrapped line.
   The arithmetic does not work at phone width, and phone width is the product.
   They stay on separate lines. */

/* ⚠ LOWER PROFILE THAN THE FILTER ROW ABOVE. These pick a VIEW of what the
   filters already chose, so they are subordinate to it — and at equal weight
   the two rows competed and neither read as the primary control. */
/* ⚠ GREY AND LIGHTER THAN THE FILTER ROW. These pick a VIEW of what the
   filters already chose, so they are subordinate — in ink at 600 they read
   as the primary control and the two rows competed. Grey at 500 keeps them
   findable without taking the eye before the deals do. The SELECTED one
   still goes to solid ink below, which is what carries the state. */
.sm-vt-btn{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:500;font-family:inherit;padding:3px 9px;border-radius:999px;border:0;background:transparent;color:var(--mid, #6a6050);cursor:pointer;transition:background .12s,color .12s;}
.sm-vt-btn:hover{background:var(--green-wash, #eaf5ee);}
/* NOT GREEN — "Latest" being selected says where you are, not something to do,
   and a green pill competed with the one real call to action in the header.
   ⚠ BUT THE FIRST ATTEMPT PAIRED AN INVERTING TOKEN WITH A HARDCODED WHITE:
   --ink resolves to near-BLACK in light mode and near-WHITE in dark, while the
   text stayed #fff — so in dark mode the label was white on a near-white pill
   and the selected tab was the LEAST readable thing in the row.
   --surface-invert / --on-surface-invert are the pair designed for this: both
   flip together, so the contrast holds in either mode. The /businesses filter
   chips already use them. RULE: never pair a var() that inverts with a literal
   colour that doesn't. */
/* ⚠ GREY, NOT SOLID INK. The selected pill was the loudest thing on the page
   above the deals — and since Latest is the DEFAULT view, that pill is on
   almost every time anyone arrives. A permanent black blob announcing the
   state nobody chose is the opposite of a low-profile row.
   A soft wash still reads as selected next to plain grey text, without
   competing with the first card. */
.sm-vt-btn.is-on{background:var(--bg-sand2, #eeeae2);color:var(--ink, #0a0806);font-weight:600;}
/* Selected stays full-strength: it is the only thing here carrying state. */
.sm-vt-btn.is-on:hover{background:var(--bg-sand2, #eeeae2);}
.sm-vt-count{opacity:.7;font-weight:500;}
.card.sm-vt-hidden{display:none !important;}
.sm-dd-item:hover{background:var(--paper2);}
.sm-dd-item.on{background:var(--green-wash);color:var(--green-deep);font-weight:600;}
.sm-filter-clear{font-size:12px;font-weight:600;color:var(--ink-soft);text-decoration:underline;padding:4px 6px;}
.sm-filter-clear:hover{color:var(--green);}
@media(max-width:560px){.sm-dd-menu{min-width:150px;}}
/* still used by the "popular areas" row further down the page */
.sm-filters{display:flex;flex-direction:column;gap:12px;margin-bottom:24px;}
/* Near-me chip. Deliberately quiet — it's an offer, not a call to action, and
   it competes with nothing on the page. Hidden by default and revealed by JS
   only when geolocation exists and the visitor hasn't answered before. */
.sm-nearme{display:flex;align-items:center;gap:10px;margin:0 0 14px;}
.sm-nearme-go{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:999px;
  border:1px solid var(--green,#189554);background:transparent;color:var(--green,#189554);
  font-family:'Inter Tight',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;}
.sm-nearme-go:hover{background:var(--green,#189554);color:#fff;}
.sm-nearme-no{border:0;background:none;color:var(--mid,#6a6050);font-size:13px;cursor:pointer;
  font-family:'Inter Tight',sans-serif;padding:8px 2px;}
.sm-nearme-said{margin:0 0 14px;font-size:13.5px;color:var(--mid,#6a6050);}
.sm-nearme-said b{color:var(--ink,#0a0806);}
/* ⚠ THE SELECTOR COVERS BOTH button AND a. It styled only `button`, because
   every action in this message used to be one ("Show all areas"). The
   cross-region switch is a LINK — it navigates — so it inherited the message's
   own grey body colour and rendered as plain text that nobody would tap. An
   action that does not look like one is not an action. */
.sm-nearme-said button,.sm-nearme-said a{border:0;background:none;color:var(--green,#189554);font-weight:600;
  font-size:13.5px;font-family:'Inter Tight',sans-serif;text-decoration:underline;cursor:pointer;padding:0 0 0 4px;}
/* A tap target on a phone, not a word in a sentence. */
.sm-nearme-said a{display:inline-block;padding:4px 0 4px 4px;}
.sm-filter-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
.sm-chip{display:inline-block;padding:7px 14px;border-radius:999px;border:1px solid var(--line);background:var(--white);font-size:13px;font-weight:500;color:var(--ink-soft);transition:all .15s;}
.sm-chip:hover{border-color:var(--green);color:var(--green);}
.sm-chip.on{background:var(--green);border-color:var(--green);color:var(--on-brand, #fff);}
/* hero: discreet inline alerts link instead of a heavy button */
/* empty state */
/* Card arrivals: same box, warmer, because this is a first impression rather
   than a return visit. */
.sm-empty--card{background:var(--tint, #fdf0e6);border-color:rgba(200,80,0,.22);border-style:solid;}
.sm-empty-fine{font-size:13px;color:var(--ink-soft);margin-top:10px;}
/* COMING UP LATER / ENDED TODAY — plain rows, deliberately NOT cards.
   ⚠ THEY MUST NOT READ AS LIVE. A card carries a countdown, a like control and
   a price CTA; someone walking in expecting a struck-through price that
   finished an hour ago becomes the shop's problem. These are quiet rows with
   the time stated. */
.sm-later{display:flex;flex-direction:column;gap:2px;}
/* ⚠ THE TIME IS A THIRD LINE IN THE TEXT COLUMN, NOT A RIGHT-HAND COLUMN.
   As a flex sibling it competed for horizontal space with a nowrap title, and
   on a narrow screen it was pushed past the row's edge and clipped — "ended
   1:2…" — while the fixed Editor's Pick tab covered whatever survived. Right
   padding was a guess at how much room the tab needs and it was not enough.
   Stacking removes the competition entirely: nothing can be pushed out of a
   column, so this cannot recur at any width or with any title length.
   ⚠ overflow:hidden as a backstop, so nothing can escape the row even if a
   future child forgets to shrink. */
/* ⚠ SAME DEFAULT, SAME BITE. This row was the other element reporting
   min-width:auto in the same measurement. It is a flex CONTAINER inside a grid
   item, so it inherits the problem one level down: its own overflow:hidden can
   only clip once it is allowed to be narrower than its contents. */
.sm-later-row{display:flex;align-items:center;gap:12px;padding:9px 20px 9px 2px;border-bottom:1px solid var(--line);text-decoration:none;color:inherit;overflow:hidden;min-width:0;}
.sm-later-row:last-child{border-bottom:0;}
.sm-later-img{flex:0 0 auto;width:56px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;background:var(--bg-sand2,#f2efe8);}
.sm-later-img img{width:100%;height:100%;object-fit:cover;display:block;}
.sm-later-txt{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;overflow:hidden;}
.sm-later-txt > span{min-width:0;max-width:100%;}
.sm-later-shop{font-size:12px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sm-later-title{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Now a stacked line rather than a competing column. */
.sm-later-when{font-size:12px;color:var(--ink-soft);white-space:nowrap;margin-top:2px;}
/* ⚠ THE FADE IS WHAT SAYS "FINISHED" — Too Good To Go greys its sold-out cards
   for the same reason. Applied to the row, not the text, so the photo dims too:
   a full-colour photo beside faded words still reads as available. */
/* ⚠ NOT FADED LIKE THE ENDED ROWS. A finished deal is dimmed because it is
   over; one opening later is the opposite — it is the reason to come back, and
   the only live thing on an otherwise empty board. Full opacity, and the time
   carries the brand green so the eye lands on WHEN. */
.sm-later--soon .sm-later-when{color:var(--brand);font-weight:600;}
.sm-later--done .sm-later-row{opacity:.55;}
.sm-later--done .sm-later-when{font-weight:600;}
.sm-later--done .sm-later-img{filter:grayscale(.35);}
/* ⚠ SEPARATED FROM THE LIST. With 12px it sat tight under the final row and
   read as another entry in it rather than a prompt about the list. The rule
   above already removes the last row's border, so the gap is the only thing
   distinguishing them. */
.sm-later-cta{margin:18px 0 0;padding-top:14px;border-top:1px solid var(--line);font-size:13.5px;text-align:center;}
.sm-later-listed{margin:22px 0 0;padding-top:14px;border-top:1px solid var(--line);font-size:13px;color:var(--ink-soft);text-align:center;}
/* ⚠ 56px OF PADDING TOP AND BOTTOM WAS 112px OF NOTHING inside a box whose
   whole job is to explain a quiet moment. On a phone the message plus its own
   chrome filled the screen, so "Opening later" — the one genuinely useful thing
   on an empty board — sat below the fold. Halved, and it loses no legibility:
   the dashed border already separates the box from the page. */
.sm-empty{text-align:center;padding:26px 20px;border:1px dashed var(--line);border-radius:16px;background:var(--paper2);}
/* Sans, not the Fraunces serif. An empty state reports SYSTEM STATE — it isn't
   the page's editorial voice, and the nav badge directly above it says almost
   the same words in sans. Two typefaces for one message on one screen read as
   an accident rather than a system. */
.sm-empty-title{font-family:'Inter Tight',system-ui,sans-serif;font-size:21px;font-weight:800;letter-spacing:-.02em;color:var(--ink);margin-bottom:8px;}
/* Left-aligned inside a centred column: multi-line text is easier to scan with
   a straight left edge than centred (the eye has to re-find each line start),
   but the COLUMN itself is centred so it doesn't float against one side of a
   wide panel. */
.sm-empty p{font-size:14px;color:var(--ink-soft);max-width:46ch;margin:0 auto 8px;line-height:1.5;text-align:left;}
/* DESKTOP: the panel is much wider than the text needs, so a 38ch column left
   a narrow strip adrift in a big box. Tighten the vertical padding (56px top
   and bottom was a lot for three short paragraphs) and let the measure breathe
   a little wider. */
@media(min-width:901px){
  .sm-empty{padding:44px 32px;}
  .sm-empty-title{font-size:23px;margin-bottom:12px;}
  .sm-empty p{max-width:52ch;font-size:14.5px;}
  .sm-empty p:last-of-type{margin-bottom:0;}
}
.sm-empty p:last-of-type{margin-bottom:0;}
.sm-empty-lead{color:var(--ink) !important;font-weight:500;}
/* :not(.btn) — this rule used to catch the "Tell me when something's on" BUTTON
   as well as the text links. ".sm-empty p a" is one class plus two elements,
   which outranks ".btn-green"'s single class, so the button's label was painted
   var(--green) ON a green button and vanished. The :hover rule below set it to
   ink, which is why pressing it made the text appear — the one state where it
   was legible was the one you had to discover by tapping.
   Same family as the inline-style and :active-rotation collisions: the value
   was never wrong, a more specific rule was winning. */
.sm-empty p a:not(.btn){color:var(--green);font-weight:600;text-decoration:underline;text-underline-offset:2px;}
.sm-empty p a:not(.btn):hover{color:var(--ink);}
/* Belt and braces: state the button's own colours at a specificity that can't
   lose to a future .sm-empty descendant rule. */
.sm-empty p a.btn-green,.sm-empty p a.btn-green:hover{color:var(--on-brand, #fff);text-decoration:none;}
.sm-shops{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;}
.sm-shop{border:1px solid var(--line);border-radius:12px;padding:12px 14px;background:var(--bg-card, #fff);}
.sm-shop-name{display:block;font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:15px;color:var(--ink);}
.sm-shop-meta{display:block;font-size:12px;color:var(--ink-soft);margin-top:2px;}
@media(max-width:520px){.sm-shops{grid-template-columns:1fr;}}
/* shop spotlight */
.spot-card{display:flex;gap:18px;align-items:flex-start;background:var(--bg-card, #fff);border:1px solid var(--line);border-radius:18px;padding:22px;text-decoration:none;color:var(--ink);transition:border-color .18s,transform .18s;}
.spot-card:hover{border-color:var(--ink);transform:translateY(-2px);}
.spot-avatar{width:72px;height:72px;border-radius:14px;background:var(--green);flex-shrink:0;display:flex;align-items:center;justify-content:center;overflow:hidden;}
.spot-avatar img{width:100%;height:100%;object-fit:cover;}
.spot-avatar span{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:26px;color:var(--on-brand, #fff);}
.spot-name{font-family:'Fraunces',serif;font-size:22px;font-weight:500;letter-spacing:-.01em;}
.spot-verified{color:var(--green2);font-size:16px;}
.spot-meta{font-size:13px;color:var(--ink-soft);margin-top:3px;}
.spot-desc{font-size:14px;color:var(--ink-soft);line-height:1.55;margin-top:10px;}
.spot-link{display:inline-block;margin-top:12px;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:14px;color:var(--green);}
/* new on sastamal */
.newshops{position:relative;}
.newshop{text-decoration:none;color:var(--ink);}
.newshop:hover{border-color:var(--ink);transform:translateY(-2px);}
.newshop-avatar{width:52px;height:52px;border-radius:12px;background:var(--green);margin:0 auto 10px;display:flex;align-items:center;justify-content:center;overflow:hidden;}
.newshop-avatar img{width:100%;height:100%;object-fit:cover;}
.newshop-avatar span{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:19px;color:var(--on-brand, #fff);}
.newshop-name{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:14px;line-height:1.2;}
.newshop-meta{font-size:12px;color:var(--ink-soft);margin-top:3px;}
@media(max-width:640px){.newshops{grid-template-columns:repeat(2,1fr);}.spot-card{flex-direction:column;}}
/* My interests */
/* ⚠ NO BOX. Desktop draws these inline; the card was mobile-only and made a
   row of two dropdowns look like a panel of settings. Inline matches the
   desktop and gives the row back its full width. */
/* Tight to the row below, so the two read as one block of furniture rather
   than two separate bands each claiming their own space. */
.sm-interests{margin:0 0 2px;border:0;border-radius:0;background:none;}
/* Two side-by-side dropdowns: [Category ▾] [Area ▾]. Each opens its own panel;
   panels are absolutely positioned so the row stays compact. */
.sm-int-dropdowns{display:flex;align-items:center;gap:8px;padding:0;flex-wrap:wrap;}
.sm-dd{position:relative;}
/* ⚠ THE WHOLE FILTER AREA IS SECONDARY FURNITURE. It sits above the deals and
   is used once, then ignored — so it should be legible and quiet, not a
   headline. Two rows of chunky controls pushed the first card most of the way
   down the screen, which costs the thing people actually came for. */
.sm-dd-btn{display:inline-flex;align-items:center;gap:6px;font-size:13.5px;font-weight:600;font-family:inherit;color:var(--text-body, #40382e);background:transparent;border:0;border-radius:999px;padding:5px 8px;cursor:pointer;white-space:nowrap;transition:background .12s,color .12s;}
.sm-dd-btn:hover{background:var(--green-wash, #eaf5ee);}
.sm-dd-btn.is-active{background:var(--green);color:var(--on-brand, #fff);}
.sm-dd-btn.is-active .sm-dd-caret{color:var(--on-brand, #fff);}
.sm-dd-caret{font-size:10px;color:var(--ink-soft);transition:transform .15s;}
.sm-dd-btn[aria-expanded="true"] .sm-dd-caret{transform:rotate(180deg);}
.sm-dd-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:40;width:min(320px,86vw);max-height:60vh;overflow-y:auto;background:var(--bg-card, #fff);border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px rgba(10,8,6,.16);padding:12px;}
[data-theme="dark"] .sm-dd-panel{box-shadow:0 10px 30px rgba(0,0,0,.5);}
.sm-area-list{display:flex;flex-direction:column;gap:2px;}
.sm-dd-clear{display:block;width:100%;margin-top:10px;padding:9px 12px;border:0;border-top:1px solid var(--line);background:none;font-family:'Inter Tight',sans-serif;font-size:13px;font-weight:600;color:var(--green);cursor:pointer;text-align:center;border-radius:0 0 8px 8px;}
.sm-dd-clear:hover{background:var(--green-wash, #eaf5ee);}
.sm-interests.is-idle{border-color:transparent;background:none;margin-bottom:10px;}
.sm-interests.is-idle .sm-int-dropdowns{padding:2px 0;}
.sm-int-panel-extra:empty{display:none;}
.sm-int-panel-extra{padding:0 12px 10px;}
/* With no filter set and nothing hidden, the bar has nothing to report — it's
   only an entry point to the panel. A full bordered box for the word "All" is
   a line of screen paying no rent, and on a 667px phone that's a card's worth
   of space above the first deal. Go quiet: no border, no background, just the
   tap target. It reasserts itself the moment it has something to say. */
.sm-interests.is-idle{border-color:transparent;background:none;margin-bottom:10px;}
.sm-filter-back{font-size:13px;font-weight:600;color:var(--green);white-space:nowrap;}
.sec-head--filtered{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px;}
/* Names the active filter. Quiet — it reports state, it is not a headline. */
.sm-filter-what{font-family:'Inter Tight',system-ui,sans-serif;font-size:14px;font-weight:700;
  color:var(--ink);letter-spacing:-.01em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.sm-int-edit,.sm-int-clear{flex:0 0 auto;white-space:nowrap;font-size:12px;font-weight:600;border:1.5px solid var(--line);background:var(--bg-card, #fff);color:var(--ink);border-radius:999px;padding:6px 12px;cursor:pointer;}
.sm-int-edit{border-color:var(--green);color:var(--green);}
.sm-int-edit:hover{background:var(--green);color:var(--on-brand, #fff);}
.sm-int-panel{border-top:1px solid var(--line);padding:14px;}
.sm-int-help{font-size:13px;color:var(--ink-soft);margin:0 0 12px;line-height:1.5;}
.sm-int-group{margin-bottom:12px;}
.sm-int-label{display:block;font-family:'Inter Tight',system-ui,sans-serif;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:6px;}
.sm-int-note{display:inline-block;margin-left:8px;font-family:'Inter Tight',system-ui,sans-serif;font-size:11px;color:var(--ink-soft);opacity:.75;}
.sm-int-chips{display:flex;flex-wrap:wrap;gap:6px;}
.sm-int-chip{font-size:13px;font-weight:500;border:1.5px solid var(--line);background:var(--bg-card, #fff);color:var(--ink-soft);border-radius:999px;padding:6px 12px;cursor:pointer;transition:all .12s;}
.sm-int-chip:hover{border-color:var(--green);}
.sm-int-chip.on{background:var(--green);border-color:var(--green);color:var(--on-brand, #fff);}
.sm-cat-tree{display:flex;flex-direction:column;gap:0;margin-bottom:14px;}
.sm-filter-divider{height:1px;background:var(--border-color, #e5e0d6);margin:18px 0 14px;}
.sm-filter-sectionlbl{font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--green, #189554);margin:0 0 10px;}
.sm-cat-group{border:0;border-radius:0;overflow:hidden;}
/* REVERTED to the original compact height. A 44px min-height was tried here
   for the standard touch-target minimum and it was wrong for this control: the
   panel holds 5 parents plus up to 6 children, so 44px rows pushed the list
   past the 60vh panel cap and turned a glanceable menu into a scroll. Density
   IS the feature in a filter menu — you're comparing options, not aiming at
   one. The row is still a wide full-width target, which is the dimension that
   actually matters on a phone; it's short, not small. */
.sm-cat-head{display:flex;align-items:center;gap:10px;width:100%;padding:8px 4px;background:transparent;border:0;cursor:pointer;text-align:left;font-family:'Inter Tight',system-ui,sans-serif;}
.sm-cat-head:hover{background:var(--green-wash);border-radius:8px;}
.sm-cat-head-icon{flex:0 0 auto;color:var(--green);display:inline-flex;}
.sm-cat-head-lbl{flex:1;font-weight:700;font-size:15px;color:var(--ink);}
.sm-cat-caret{flex:0 0 auto;color:var(--ink-soft);font-size:12px;transition:transform .18s;}
.sm-cat-head[aria-expanded="true"] .sm-cat-caret{transform:rotate(180deg);}
/* padding-left 34px is not arbitrary: head padding 4 + icon 20 + gap 10. It
   lines each child's tick box up with the START OF THE PARENT'S LABEL, so the
   indent reads as "contained by" rather than as a random inset. If the icon
   size or head gap ever changes, this must change with it. */
.sm-cat-children{display:flex;flex-direction:column;gap:2px;padding:2px 4px 8px 34px;background:transparent;}
.sm-cat-children[hidden]{display:none;}
.sm-cat-child{display:flex;align-items:center;gap:9px;padding:5px 0;cursor:pointer;font-size:14px;color:var(--ink);}
/* The parent tick sits on the RIGHT of its row while child ticks sit on the
   left, so the two don't form a single column. That's deliberate, not an
   oversight: this checkbox means "select all six", a different action from the
   ones below it, and giving it its own position stops it reading as just
   another item in the list.
   No min-height here either — as a flex child it sets the floor for the whole
   parent row, so padding it out silently made every heading taller. */
.sm-cat-parent-tick{flex:0 0 auto;display:inline-flex;align-items:center;margin-right:2px;cursor:pointer;}
.sm-cat-cb{position:absolute;opacity:0;width:0;height:0;}
.sm-cat-tick-box{flex:0 0 auto;width:19px;height:19px;border:var(--control-border-w, 1.5px) solid var(--control-border, var(--line));border-radius:5px;display:inline-flex;align-items:center;justify-content:center;transition:background .12s,border-color .12s;}
.sm-cat-cb:checked + .sm-cat-tick-box{background:var(--green);border-color:var(--green);}
.sm-cat-cb:checked + .sm-cat-tick-box::after{content:"";width:5px;height:9px;border:solid var(--bg-card, #fff);border-width:0 2px 2px 0;transform:rotate(45deg);margin-top:-2px;}
.sm-cat-cb:indeterminate + .sm-cat-tick-box{background:var(--green-wash);border-color:var(--green);}
.sm-cat-cb:indeterminate + .sm-cat-tick-box::after{content:"";width:9px;height:2px;background:var(--green);}
.sm-cat-cb:focus-visible + .sm-cat-tick-box{outline:2px solid var(--green);outline-offset:2px;}
/* ── Deal peek sheet ──────────────────────────────────────
   A look before committing to a page load. Reuses the rail sheet's mechanics
   (hidden-before-fade, safe-area, cubic-bezier) so the two feel identical.
   Content is fetched on tap: inlining every deal's detail would reintroduce
   the payload problem that small cards exist to solve. */
.sm-peek-back{position:fixed;inset:0;background:var(--scrim, rgba(0,0,0,.5));opacity:0;transition:opacity .35s cubic-bezier(0.16,1,0.3,1);z-index:940;}
.sm-peek-back.is-up{opacity:1;}
.sm-peek{position:fixed;left:0;right:0;bottom:0;z-index:950;background:var(--surface-raised, var(--paper));border-radius:20px 20px 0 0;
  max-height:82vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:0 20px calc(20px + env(safe-area-inset-bottom));
  transform:translateY(100%);transition:transform .35s cubic-bezier(0.16,1,0.3,1);box-shadow:var(--sheet-shadow, 0 -10px 40px rgba(0,0,0,.22));border-top:1px solid var(--peek-edge, transparent);}
.sm-peek.is-up{transform:translateY(0);}
.sm-peek.is-dragging{transition:none;}
@media(min-width:601px){
  .sm-peek{left:50%;right:auto;width:min(560px,92vw);transform:translate(-50%,100%);}
  .sm-peek.is-up{transform:translate(-50%,0);}
}
.sm-peek-grab{position:sticky;top:0;z-index:2;background:var(--surface-raised, var(--paper));padding:10px 0 6px;display:flex;justify-content:center;cursor:grab;}
.sm-peek-grab i{display:block;width:40px;height:4px;border-radius:2px;background:var(--grab-handle, #ccc);}
/* Body is the positioning context for the faint cover wallpaper. The photo panel
   is opaque and sits above the veil, so the dim cover only reads through the
   lower text area, exactly the intended "bottom half" treatment. */
.sm-peek-body{position:relative;}
/* No wallpaper behind the sheet body — see the note in the peek builder. The
   photo panel above carries the imagery; the content below stays on the solid
   sheet so every line is crisp at full contrast. */
/* Photo panel on top of the sheet: full-bleed to the sheet edges, fixed aspect,
   crisp image with nothing over it but the close button. Text lives below on the
   solid sheet, so legibility never fights the photo. */
.sm-peek-photo{position:relative;margin:0 -20px 14px;height:200px;overflow:hidden;background:var(--bg-sand4, #f5f2ec);}
.sm-peek-photo img{width:100%;height:100%;object-fit:cover;display:block;}
.sm-peek-head{display:flex;align-items:flex-start;gap:12px;padding-bottom:0;}
/* WARM NEUTRAL, matching .card-avatar--init. This green only ever shows when
   a shop has no logo, and at 44px on the peek it was the loudest thing on the
   sheet — a placeholder outshouting the deal itself. */
.sm-peek-logo{width:44px;height:44px;border-radius:50%;background:var(--bg-sand2, #f2efe8);color:var(--text-muted, #6a6050);display:flex;align-items:center;justify-content:center;
  font-family:'Inter Tight',sans-serif;font-weight:700;font-size:15px;flex:0 0 auto;overflow:hidden;text-decoration:none;}
.sm-peek-logo img{width:100%;height:100%;object-fit:cover;}
.sm-peek-shop{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:15px;color:var(--ink);}
/* Identity column: name row, address directly beneath. Let it shrink so a
   long shop name truncates rather than running under the corner buttons. */
.sm-peek-idwrap{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;}
.sm-peek-nameline{display:flex;align-items:baseline;gap:10px;min-width:0;}
.sm-peek-nameline .sm-peek-shop{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.sm-peek-area{font-size:13px;color:var(--ink-soft);}
/* The name itself is now also a link when we have one (see the peek builder in
   home.js) — "Visit shop" stays as the explicit, unmissable CTA next to it, this
   is just the same destination the logo above also links to now. Colour and
   weight are unchanged from the plain-text version, so this doesn't read as
   an obvious "link" the way a blue underline would; underline-on-hover is
   the only tell. */
/* No color here — .sm-peek-shop already sets color:var(--ink), which is
   correctly dark-mode-aware (tokens.css redefines --text-main in
   :root[data-theme="dark"]). Setting color:inherit here overrode that with
   whatever this element's ancestors resolve to instead, which is how the
   name went dim in dark mode — the exact class of bug --ink-soft has hit
   elsewhere in this project on the alerts and feedback screens. */
.sm-peek-shop--link{text-decoration:none;}
.sm-peek-shop--link:hover{text-decoration:underline;}
/* Report, Pin, Like, Follow, Mute all share this base circular treatment.
   One grouped rule rather than a separate shared class on the markup — each
   selector here is still just ONE class, so a state class alone
   (.reacted, .is-on) is automatically more specific and wins regardless of
   file order, the same tie-break the cascade already handled for the old
   photo-corner buttons, just without needing a second class to engineer it. */
/* ALL SIX ARE BARE ICONS NOW — no circle, no border, no fill. He asked for
   Pin and the alert bells to match the save heart; taking only those two out
   would have left flag and share circled and the row looking half-finished,
   so the whole strip went. The 34px box stays as tap area, so nothing moves.
   STATE IS CARRIED BY COLOUR AND FILL instead of by an inverted pill — see
   the .is-on rules further down, which now recolour the icon and fill its
   path rather than painting a disc behind it. */
/* .sm-peek-share is no longer in this list: share moved out of the strip and
   onto the primary row as a real button. A selector for an element that no
   longer exists is how the next person 'fixes' a button that isn't there. */
.sm-peek-report-open,.sm-peek-pin,.sm-peek-likes,.sm-peek-follow,.sm-peek-mute{
  width:34px;height:34px;border-radius:8px;
  border:0;background:none;color:var(--ink-soft);
  display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;
  padding:0;cursor:pointer;flex:0 0 auto;position:relative;
}
/* Strip sits on the solid panel below the photo, not on the photo itself —
   see the peek builder in home.js for why (contrast on an unpredictable
   photo used to need its own --onimg treatment; on the panel it never does).
   space-between rather than flex-end: the left side holds the shop's live
   busy chip when there's one (see .sm-peek-strip-left below), the right
   side is the action group — matches where these lived when Like/Pin/Report
   were still floating in the top-right corner. flex-wrap as a safety net,
   though five 34px circles plus a short busy chip fit with room to spare on
   any phone width this ships to. */
/* Shop badges in the peek. Deliberately quieter than the action strip below
   — these are facts about the place, not things to tap, so no border and a
   flat tint rather than the raised pill treatment used for buttons. */
.sm-peek-fac{display:flex;flex-wrap:wrap;gap:7px;padding:0 var(--peek-pad,18px) 2px;}
/* Icon-only tiles. Square rather than pill-shaped so they read as a set of
   markers rather than as tappable chips — nothing here is a button. The emoji
   is set at 19px inside a 34px tile: big enough to identify at a glance, which
   is the whole reason the labels came off. */
/* min-width, not width. Most marks are a single emoji and stay square, but
   some are a WORD — Halal is حلال, the mark people actually look for — and
   four Arabic glyphs at 19px overflow a fixed 34px box. Letting the tile grow
   keeps both cases right without a special case for either. */
/* NO TILE. The box was doing the work of saying 'these are a set', but the
   row already reads as a set from its spacing alone — and six filled boxes
   under the shop name competed with the action strip right below them.
   min-width and padding stay: Halal is a WORD (حلال), not an emoji, and it
   still needs room to sit level with the square marks beside it. */
/* EXPLICIT COLOUR, and it matters for exactly one badge. Every other mark
   here is an emoji, which carries its own colours and is unaffected by the
   `color` property — but Halal is the Arabic word حلال, real text, so it was
   the only tile whose legibility depended on whatever it happened to inherit.
   With the tile background gone it had nothing behind it and went almost
   invisible. --ink follows the theme, so it is readable in both modes. */
/* ⚠ TEXT PILLS NOW, NOT ICON TILES. The peek carries three DECIDING facts —
   dietary and cuisine — and they have to be readable without a hover, which is
   the thing icon-only could never deliver on a phone.
   ⚠ QUIET ON PURPOSE. These are facts about the place, not offers and not
   buttons: a hairline ring and the body ink, so they never compete with the
   price or the green CTA below them. The offer badges keep their accent fills. */
.sm-peek-fac-i{display:inline-flex;align-items:center;justify-content:center;
  padding:4px 10px;box-sizing:border-box;border-radius:999px;
  background:none;border:1px solid var(--rule);color:var(--ink-soft);
  font-family:'Inter Tight',system-ui,sans-serif;font-weight:600;
  font-size:12.5px;line-height:1.35;letter-spacing:.01em;
  white-space:nowrap;cursor:default;}
/* WORD MARKS (cuisines) sit at 12.5px, not 19px. An emoji is one glyph wide
   whatever the size; "Middle Eastern" at 19px would be ~130px and push every
   other badge off the row on its own. */
.sm-peek-fac-i--word{font-size:12.5px;font-weight:700;letter-spacing:.01em;
  border:1px solid var(--rule);border-radius:6px;padding:0 6px;}
.sm-peek-strip{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;margin:0 0 10px;}
/* .sm-peek-strip-left is ALWAYS rendered, even empty — an empty flex child
   costs nothing to lay out, and without it space-between would collapse to
   a single child (the action group) and left-align it instead, which is
   the one thing this layout must never do. */
.sm-peek-strip-left{display:flex;min-width:0;}
.sm-peek-strip-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;}
/* Wraps just the flag trigger + its popover in their own relatively-positioned
   box, so the popover anchors under THAT specifically (top:100%) regardless
   of where the flag sits in the row — not under the strip's own right edge,
   which is a different spot once Report isn't the rightmost button.
   Centred (left:50% + translateX(-50%)) rather than edge-anchored to
   either side — Report is the LEFTMOST of the five strip buttons, so a
   right:0 anchor (extending the box leftward from there) ran the popover
   off the left edge of the screen, which is exactly the bug reported.
   Centring divides any overflow risk evenly between both sides instead of
   concentrating it on one, regardless of exactly where Report ends up
   sitting in the row on a given screen width. */
.sm-peek-report{position:relative;flex:0 0 auto;}
.sm-peek-report-pop{position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);z-index:12;
  width:240px;max-width:calc(100vw - 48px);
  background:var(--bg-card, #fff);border:1px solid var(--rule);border-radius:12px;
  box-shadow:0 10px 30px rgba(10,8,6,.16);padding:8px;}
[data-theme="dark"] .sm-peek-report-pop{box-shadow:0 10px 30px rgba(0,0,0,.5);}
.sm-peek-report-reason{display:block;width:100%;text-align:left;background:none;border:0;
  padding:8px 10px;border-radius:8px;font:inherit;font-size:13.5px;color:var(--ink);cursor:pointer;}
/* Same never-defined token as the code chip — it pinned this hover to a light
   literal in dark mode too, so hovering a report reason flashed a near-white
   block behind light text. */
.sm-peek-report-reason:hover{background:var(--bg-sand4, #f5f2ec);}
.sm-peek-report-note-row{display:flex;gap:6px;padding:2px;}
.sm-peek-report-note{flex:1;min-width:0;font:inherit;font-size:13px;padding:8px 9px;
  border:1px solid var(--rule);border-radius:8px;background:var(--bg-card, #fff);color:var(--ink);}
.sm-peek-report-send{flex:0 0 auto;background:var(--color-brand, #189554);color:#fff;border:0;
  border-radius:8px;padding:0 12px;font-family:'Inter Tight',sans-serif;font-weight:700;
  font-size:13px;cursor:pointer;}
.sm-peek-report-done{margin:0;padding:8px 10px;font-size:13px;color:var(--ink-soft);}
/* Sans, matching the feed card title. Fraunces is the brand display face and

   stays on page headings, but at component size it just reads as "a different
   font", and having the modal in serif while the card behind it is sans made
   the two look like different products. */
.sm-peek-title{font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:20px;
  line-height:1.25;letter-spacing:-.015em;margin:6px 0 8px;color:var(--ink);}
/* Score only — Report/Pin/Like/Follow/Mute all moved to .sm-peek-strip above.
   Still its own row: a value signal, not an action, so it stays visually
   separate from the buttons. margin-left:auto keeps it flush right, matching
   the strip above it, even though it's now the only thing in this row. */
.sm-peek-engage{display:flex;align-items:center;gap:10px;margin:0 0 8px;}
/* Solid fill, not a pale tint, on all four toggle-able buttons above — a
   colour swap alone is hard to read as "selected" on a small 34px circle
   without a visible count or label to lean on, the way the old text pills
   had. Follow fills brand green (a positive, opt-IN state); Mute fills
   saffron instead (a suppression state), so the two read as different
   kinds of "on" even though both are circles in the same row. */
/* Green at rest, not just on hover — Like isn't a safety-valve action like
   Report or Mute, it's the lowest-friction, most-wanted-often one, and
   waiting for a hover/tap before it looks like anything worth tapping works
   against that. Border+icon only, not a fill, so .reacted below still reads
   as a distinct "done" state rather than the two looking the same. */
/* The heart is green at rest — the one control in the strip that invites the
   tap. Sizing now comes from the shared rule above, which it rejoined once the
   whole strip went bare. */
.sm-peek-likes{color:var(--color-brand-deep, #12814A);}
.sm-peek-likes:hover{color:var(--color-brand, #189554);}
/* SAVED = the heart fills, identical to the card. */
.sm-peek-likes.reacted{color:var(--color-brand-deep, #12814A);}
.sm-peek-likes.reacted svg{fill:currentColor;}
.sm-peek-likes[disabled]{opacity:.6;cursor:default;}
/* ON = the icon turns brand green and its path FILLS. With no disc to invert,
   fill is the only thing left that reads as "done" at a glance, and it is the
   same signal the save heart uses — so one rule of thumb covers the strip. */
.sm-peek-pin.is-on{color:var(--color-brand-deep, #12814A);}
.sm-peek-pin.is-on svg{fill:currentColor;}
.sm-peek-follow.is-on{color:var(--color-brand-deep, #12814A);}
.sm-peek-follow.is-on svg{fill:currentColor;}
.sm-peek-follow[disabled]{opacity:.6;cursor:default;}
/* Mute keeps SAFFRON rather than green: it is the one control here that turns
   something OFF, and colouring it like the others would make muting look like
   another kind of following. */
.sm-peek-mute.is-on{color:var(--color-accent, #e86000);}
.sm-peek-mute.is-on svg{fill:currentColor;}
.sm-peek-mute[disabled]{opacity:.6;cursor:default;}
.sm-peek-score{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-family:'Inter Tight',sans-serif;font-weight:700;font-size:15px;color:var(--color-brand-deep, #12814A);}
.sm-peek-score-lbl{font-size:11px;font-weight:600;color:var(--ink-soft);text-transform:uppercase;letter-spacing:.04em;}
/* --surface-sunken IS NEVER DEFINED, anywhere in the theme — so this always
   fell back to the literal #f6f3ee in BOTH modes, giving a near-white box.
   Meanwhile --ink-soft correctly flips light in dark mode, so the text went
   pale grey on near-white and the code became unreadable. --bg-sand4 is the
   equivalent token that actually flips (#f5f2ec / #2b2620). */
.sm-peek-code{display:inline-block;font-size:13px;color:var(--ink-soft);background:var(--bg-sand4, #f5f2ec);border:1px dashed var(--rule);border-radius:8px;padding:6px 12px;margin:4px 0;}
.sm-peek-code b{color:var(--ink);font-family:'Inter Tight',sans-serif;letter-spacing:.02em;}
/* Badges inside the peek sit on the sheet (not over a photo), and the default
   tint pills wash out on the dark sheet. Give them a solid, high-contrast fill. */
.sm-peek-badges .card-badge{background:var(--color-accent, #e86000);color:#fff;}
.sm-peek-badges .card-badge--pct{background:var(--color-brand, #189554);color:#fff;}
.sm-peek-badges .card-ends{color:var(--ink-soft);}
/* Matches the dark notice strip on shop profiles — same signal, same look.
   Deliberately not saffron: this is operational information, not urgency. */
/* Operational warning (closing early / card machine down). This is the one
   thing that can invalidate the whole deal, so it reads as a WARNING, saffron
   tint with a solid left rule, rather than the neutral dark block it used to
   be, which looked like a design element instead of an alert. */
.sm-peek-notice{display:flex;align-items:flex-start;gap:9px;
  background:var(--bg-tint, #fdf0e6);color:var(--color-accent-ink, #a04600);
  border-left:3px solid var(--color-accent-deep, #c85000);
  border-radius:8px;padding:11px 13px;margin:10px 0 4px;font-size:13.5px;
  font-weight:500;line-height:1.5;}
[data-theme="dark"] .sm-peek-notice{background:var(--bg-tint, #3a2517);color:var(--color-accent-ink, #ffb98a);}
.sm-peek-badges{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;}
.sm-peek-price{display:flex;align-items:baseline;gap:10px;margin:8px 0;}
.sm-peek-now{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:26px;color:var(--color-accent-deep, #c85000);}
/* var(--light) has no fallback and is never actually DEFINED anywhere in
   tokens.css or this file (it appears in a handful of other spots too, all
   with the same gap) — an unset custom property with no fallback makes the
   declaration invalid, so this was inheriting whatever ancestor color it
   landed on rather than anything intentional, which is how it read as too
   dark in dark mode. --ink-soft is the token this actually wants: same
   muted-but-legible role, and it's what .sm-peek-area (the address line)
   already uses elsewhere in this same sheet, so it's proven to read fine
   here too. */
.sm-peek-was{font-size:15px;color:var(--ink-soft);text-decoration:line-through;}
.sm-peek-off{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:14px;color:var(--color-accent-deep, #c85000);}
.sm-peek-desc{font-size:14px;line-height:1.6;color:var(--ink-soft);margin:8px 0;}
.sm-peek-meta{font-size:13px;color:var(--ink-soft);margin:8px 0;display:flex;flex-wrap:wrap;gap:6px 14px;}
.sm-peek-acts{display:flex;flex-direction:column;gap:8px;margin-top:14px;}
.sm-peek-mini{display:flex;gap:8px;}
/* SECONDARY actions. Deliberately quiet — transparent fill, muted text — so
   they read as options rather than competing with the primary CTA below. */
.sm-peek-ico{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px;border-radius:11px;
  border:1px solid var(--rule);background:transparent;color:var(--ink-soft);
  font-family:'Inter Tight',sans-serif;font-weight:600;font-size:12.5px;text-decoration:none;cursor:pointer;
  transition:border-color .15s,color .15s;}
.sm-peek-ico svg{width:16px;height:16px;}
.sm-peek-ico:hover{border-color:var(--color-brand-deep, #12814A);color:var(--color-brand-deep, #12814A);}
.sm-peek-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:11px;border-radius:11px;border:1px solid var(--rule);
  background:var(--bg-card, #fff);color:var(--ink);font-family:'Inter Tight',sans-serif;font-weight:700;font-size:13px;text-decoration:none;cursor:pointer;}
/* PRIMARY CTA — the one thing we want tapped. Taller, heavier and solid brand
   green so it clearly outranks the secondary row above it. */
.sm-peek-btn--go{background:var(--color-brand-deep, #12814A);border-color:var(--color-brand-deep, #12814A);
  color:var(--on-brand, #fff);padding:14px;font-size:14.5px;font-weight:800;letter-spacing:-.01em;
  box-shadow:0 4px 14px -4px rgba(18,129,74,.5);transition:background .15s,transform .15s;}
.sm-peek-btn--go:hover{background:var(--color-brand, #189554);transform:translateY(-1px);}
/* 2 : 1. flex-basis 0 with grow 2 and 1 splits the row by RATIO rather than by
   percentage, so the 8px gap comes out of both sides proportionally instead of
   pushing the total past 100%. */
.sm-peek-go-row{display:flex;gap:8px;}
.sm-peek-go-row .sm-peek-btn--go{flex:2 1 0;min-width:0;}
/* SECONDARY, but it still has to look like a BUTTON. The first version was a
   transparent box with a brand-green border, which on the dark sheet measures
   3.3:1 — an edge you can't really see, so it read as loose text sitting
   beside a solid block rather than as a control.
   Now a FILLED TINT: the fill gives it a body and an obvious tap target, while
   staying far lighter than the primary so "See full deal" is still the only
   thing that reads as THE action. A visible border on top of the tint gives
   the edge the transparent version never had. */
.sm-peek-btn--share{flex:1 1 0;min-width:0;padding:14px 8px;font-size:13.5px;font-weight:700;
  background:var(--color-brand-tint, #eaf5ee);
  border-color:var(--color-brand, #189554);
  color:var(--color-brand-deep, #12814A);cursor:pointer;
  transition:background .15s,color .15s,border-color .15s;}
/* Dark mode: --color-brand-tint becomes a deep green wash, so the text has to
   come UP to stay readable on it rather than staying at the deep brand green. */
:root[data-theme="dark"] .sm-peek-btn--share{color:var(--color-green-brt, #47d685);border-color:var(--color-brand, #1e8b4f);}
.sm-peek-btn--share:hover{background:var(--color-brand, #189554);color:var(--on-brand, #fff);border-color:var(--color-brand, #189554);}
/* Confirmation after a copy — colour only, no fill, so it can't momentarily
   outweigh the primary button beside it. */
.sm-peek-btn--share.is-copied{color:var(--color-brand, #189554);border-color:var(--color-brand, #189554);}
.sm-peek-state{padding:28px 0;text-align:center;color:var(--ink-soft);font-size:14px;}
/* The peek now paints from the CARD before the fetch lands, so this line
   replaces the spinner: the sheet is already useful and only says that more is
   coming. ⚠ IT MUST BE QUIET. A prominent loader over real content reads as a
   fault; the point of painting early is that there is nothing to wait for. */
.sm-peek-more{padding:10px 0 2px;font-size:12.5px;color:var(--ink-soft);opacity:.75;}
.sm-peek-spin{width:22px;height:22px;border:2px solid var(--rule);border-top-color:var(--color-brand-deep, #12814A);border-radius:50%;margin:0 auto 10px;animation:smspin .7s linear infinite;}
@keyframes smspin{to{transform:rotate(360deg);}}
[hidden]{display:none;}

.card.sm-hidden{display:none;}

/* ── Swipe-to-hide (shopper-side, device-local, expires with the deal) ──
   Hidden cards use their OWN class, never .sm-hidden, the filter's apply()
   clears .sm-hidden wholesale and would otherwise resurrect a hidden card. */
.card.sm-dismissed{display:none;}
/* ⚠ THE BACKFILL BUFFER. Rendered but not shown; home.js reveals one each time
   a card is dismissed, so the board keeps its size instead of thinning.
   ⚠ IT MUST BE IN EVERY :not() THAT COUNTS "VISIBLE" in home.js — the empty
   state, the per-row heading visibility and the rail recount all count cards,
   and a buffered card is present in the DOM. That is the documented trap: any
   new hiding class has to be added to every counting selector, or the board
   reports cards nobody can see. */
.card.sm-buffer{display:none;}
/* Quiet by design: it is a way onward, not a call to action, and it sits below
   a board that is already the point of the page. */
.sm-more-wrap{margin:22px 0 0;text-align:center;}
.sm-more{font-family:'Inter Tight',system-ui,sans-serif;font-size:13.5px;font-weight:600;
  color:var(--green,#12814A);background:none;border:1px solid var(--rule);
  border-radius:999px;padding:9px 20px;cursor:pointer;transition:background .15s,border-color .15s;}
.sm-more:hover{background:var(--green-wash,#eaf5ee);border-color:var(--green,#12814A);}
.sm-more[disabled]{opacity:.55;cursor:default;}
/* ⚠ RESTORED 2026-08-18 AFTER BEING SILENTLY REVERTED THREE TIMES.
   This block shipped with the tap-feedback batch, but every LATER batch
   rebuilt page-home.php from the morning's zip rather than from the version
   already deployed — so each deploy quietly removed it again, and only the
   style.css copy survived. This page DEQUEUES style.css, so the homepage was
   the one place with no pressed state at all: the page it matters most on.
   The lesson is the drift rule, applied to Claude's own working copies as much
   as to the site: build from what is DEPLOYED, not from the last zip received. */
/* ==========================================================================
   TAP FEEDBACK
   ==========================================================================

   ⚠ NOTHING ON THIS SITE HAD AN :active STATE and -webkit-tap-highlight-color
   was set nowhere, so on iOS a card tap painted the system grey box and then
   nothing happened for the ~1s the peek takes to fetch. That gap is most of
   why the site reads as unresponsive next to a native app.

   ⚠ THE GREY BOX IS REPLACED, NOT JUST REMOVED. Setting tap-highlight to
   transparent on its own makes things WORSE — the only feedback there was
   disappears. Every element that loses it gets a real pressed state below.

   ⚠ 120ms AND scale(.985), deliberately small. The press must land in the same
   frame as the finger; anything slower reads as lag, and anything larger reads
   as a bouncy toy rather than a considered product.

   ⚠ (hover: hover) GUARDS THE HOVER RULES. On touch devices :hover sticks after
   a tap, which is why a tapped card can stay "lifted" until you tap elsewhere.
   ========================================================================== */

/* Kill the system highlight everywhere it would fire. */
a, button, .card, .sm-cat-head, .sm-cat-child, .sm-area-row, .rail-tab,
.sm-vt-btn, .foot-share, .sm-peek-go, .btn, .btn-green, .btn-sec,
[role="button"], input[type="submit"] {
  -webkit-tap-highlight-color: transparent;
}

/* The pressed state that replaces it. */
.card:active,
.rail-pick:active,
.rail-shop:active,
.sm-biz-card:active {
  transform: scale(.985);
  transition: transform .12s ease;
}

button:active,
.btn:active,
.btn-green:active,
.btn-sec:active,
.sm-vt-btn:active,
.rail-tab:active,
.foot-share:active,
[role="button"]:active {
  transform: scale(.97);
  transition: transform .12s ease;
}

/* Rows are wide targets — a scale on a full-width row looks like the page
   moving, so they dim instead. */
.sm-cat-head:active,
.sm-cat-child:active,
.sm-area-row:active,
.sm-con-list li:active,
.site-footer__links a:active,
.foot-col a:active {
  opacity: .55;
  transition: opacity .1s ease;
}

/* ⚠ RESPECT reduce-motion. Someone who has asked the OS for less movement
   should still get feedback — so the transform is dropped and the dim kept,
   rather than removing the response altogether. */
@media (prefers-reduced-motion: reduce) {
  .card:active, .rail-pick:active, .rail-shop:active, .sm-biz-card:active,
  button:active, .btn:active, .btn-green:active, .btn-sec:active,
  .sm-vt-btn:active, .rail-tab:active, .foot-share:active,
  [role="button"]:active {
    transform: none;
    opacity: .7;
  }
}

.card{touch-action:pan-y;position:relative;overflow:hidden;}  /* vertical scroll always wins; hint clips to radius */
.card.sm-swiping{transition:none;will-change:transform;}
.card.sm-settle{transition:transform .22s ease;}
.card.sm-hiding{transition:opacity .28s ease,transform .28s ease;opacity:0;transform:translateX(-60%);}
.card-swipe-hint{position:absolute;inset:0 0 0 auto;width:96px;display:flex;align-items:center;justify-content:center;gap:6px;
  background:linear-gradient(90deg,rgba(10,8,6,0) 0%,rgba(10,8,6,.06) 40%,rgba(10,8,6,.10) 100%);
  color:var(--ink-soft);font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:12px;
  border-radius:inherit;opacity:0;pointer-events:none;transition:opacity .12s;}
/* Only the hint for the direction actually being dragged. Both were showing
   at once the moment right-swipe existed, which reads as "this card does two
   things at random" rather than "left hides, right likes". */
.card.sm-swiping:not(.sm-swipe-r) .card-swipe-hint{opacity:1;}
/* Mirror of .card-swipe-hint, pinned left, and green rather than grey: this is
   the affirmative half of the gesture and it should not look like the Hide
   affordance in a different position. */
.card-swipe-like{position:absolute;inset:0 auto 0 0;width:96px;display:flex;align-items:center;justify-content:center;gap:6px;
  background:linear-gradient(270deg,rgba(18,129,74,0) 0%,rgba(18,129,74,.08) 40%,rgba(18,129,74,.16) 100%);
  color:var(--brand,#12814A);font-family:'Inter Tight',system-ui,sans-serif;font-weight:700;font-size:12px;
  border-radius:inherit;opacity:0;pointer-events:none;transition:opacity .12s;}
.card.sm-swiping.sm-swipe-r .card-swipe-like{opacity:1;}

/* Undo bar — the safety net. Without this, a mis-swipe is silent and
   unrecoverable, which is the real risk of hide-with-no-account. */
.sm-undo{position:fixed;left:50%;bottom:calc(18px + env(safe-area-inset-bottom));transform:translateX(-50%) translateY(140%);
  display:flex;align-items:center;gap:14px;padding:11px 14px 11px 16px;border-radius:999px;
  background:var(--surface-invert);color:var(--on-surface-invert);font-family:'Inter Tight',system-ui,sans-serif;font-size:13px;font-weight:600;
  box-shadow:0 8px 28px rgba(0,0,0,.28);z-index:900;transition:transform .28s cubic-bezier(0.16,1,0.3,1);max-width:calc(100vw - 32px);}
.sm-undo.is-up{transform:translateX(-50%) translateY(0);}
.sm-undo button{background:none;border:0;color:#6fcf97;font:inherit;font-weight:700;cursor:pointer;padding:2px 4px;}
.sm-undo button:hover{text-decoration:underline;}
[hidden]{display:none;}                    /* explicit display rules defeat [hidden] */

/* Restore control — appears only once something is hidden. */
.sm-restore{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;white-space:nowrap;background:none;border:0;padding:0;
  font-family:'Inter Tight',system-ui,sans-serif;font-size:12px;font-weight:600;color:var(--ink-soft);cursor:pointer;}
/* This rule's own display:inline-flex sits AFTER the generic [hidden] rule
   above and was overriding it, so "Show hidden (0)" rendered permanently even
   with the attribute set. Specific beats ordering. */
.sm-restore[hidden]{display:none;}
.sm-restore:hover{color:var(--color-brand-deep, #12814A);text-decoration:underline;}
.sm-social{display:flex;gap:10px;margin-top:16px;}
.sm-social__icon{width:34px;height:34px;border-radius:50%;border:1px solid var(--line);display:inline-flex;align-items:center;justify-content:center;color:var(--ink-soft);transition:color .18s,border-color .18s,background .18s;}
.sm-social__icon svg{width:16px;height:16px;}
.sm-social__icon:hover{color:var(--on-brand, #fff);background:var(--green);border-color:var(--green);}
