/* ═══════════════════════════════════════════════════════════════════════════
   SYFT V2 — CONSUMER APP
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --amber:       #F59E0B;
  --amber-light: #FCD34D;
  --amber-deep:  #D97706;
  --cream:       #FFFBF0;
  --warm-white:  #FEF9EE;
  --text:        #1C0A00;
  --muted:       #92400E;
  --soft:        #B45309;
  --surface:     #FFFFFF;
  --border:      #FDE68A;
  --border-soft: #FEF3C7;
  --success:     #16A34A;
  --danger:      #DC2626;
  --pill:        #FEF3C7;

  --nav-h:  76px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  --font-num: 'Space Grotesk', system-ui, sans-serif;
}

* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  background: var(--warm-white);
  font-family: 'DM Sans', system-ui, sans-serif;
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -webkit-user-select: none;
  user-select: none;
}

button, input, select, textarea { font-family: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; }

/* ── Screens ──────────────────────────────────────────────────────────────── */
.screen {
  position: fixed;
  inset: 0;
  bottom: calc(var(--nav-h) + var(--safe-b));
  display: none;
  flex-direction: column;
  background: var(--warm-white);
}
.screen.active { display: flex; }

.screen-scroll {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0 20px 24px;
}

.screen-header {
  padding: calc(var(--safe-t) + 20px) 20px 12px;
  background: var(--warm-white);
}
.screen-title {
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
}
.screen-sub {
  font-size: 13px;
  color: var(--muted);
  margin-top: 2px;
}

/* ── Full-screen overlays (onboarding, auth, QR, feedback) ────────────────── */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: none;
  flex-direction: column;
  background: var(--warm-white);
  padding: calc(var(--safe-t) + 28px) 24px calc(var(--safe-b) + 28px);
  overflow-y: auto;
}
.overlay.active { display: flex; }

/* ═══════════════════════════════════════════════════════════════════════════
   MAP
   ═══════════════════════════════════════════════════════════════════════════ */
/* The map fills the screen. No deal list underneath — a map with a list
   stapled to the bottom is two screens fighting for the same space. Pins,
   then tap. The List tab is where the list lives. */
#map {
  position: absolute;
  inset: 0;
  background: var(--cream);
}

.map-top {
  position: absolute;
  top: calc(var(--safe-t) + 14px);
  left: 16px;
  right: 16px;
  z-index: 500;
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: none;
}
.map-top > * { pointer-events: auto; }

.gps-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 9px 14px;
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 2px 10px rgba(120, 53, 15, 0.10);
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.gps-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--success);
  flex-shrink: 0;
}
.gps-dot.searching { background: var(--amber); animation: pulse 1.4s infinite; }
.gps-dot.denied    { background: var(--danger); }

@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

.map-recenter {
  position: absolute;
  right: 16px;
  bottom: 20px;
  z-index: 500;
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 2px 10px rgba(120, 53, 15, 0.12);
  display: grid;
  place-items: center;
  color: var(--muted);
}

.map-count {
  position: absolute;
  left: 16px;
  bottom: 20px;
  max-width: calc(100% - 90px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  z-index: 500;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 8px 14px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  box-shadow: 0 2px 10px rgba(120, 53, 15, 0.10);
}

/* ── Deal pins ──────────────────────────────────────────────────────────────
   A pin has to answer "is that worth going to" without being tapped, so it
   carries four things: who it is, what the offer is worth, what it applies to,
   and — only when it is nearly gone — how many are left.

   "50%" alone is a fragment. "Lily's Bistro / 50% off / alcohol" is a decision.

   ── Why it is this small ────────────────────────────────────────────────────
   Everything here is squeezed on purpose. The first version had a 2.5px white
   border, 5-6px of padding, a divider rule and the count as a separate pill
   floating above — four pieces of chrome around two lines of content, and at
   ordinary zoom in a dense strip the pins collided with each other.

   The chrome went, not the content. Thinner border, tighter padding, no
   divider, and the count moved into a corner badge where it costs no height at
   all. Same four pieces of information in roughly half the area, which is what
   decides how many restaurants fit on screen before anything has to collapse
   into a cluster.

   The wrapper is transform-positioned rather than relying on Leaflet's
   iconAnchor, because the pin's width changes with its text and a fixed pixel
   anchor would drift off the actual location. */
.pin-wrap {
  position: relative;
  transform: translate(-50%, -100%);
  display: flex;
  flex-direction: column;
  align-items: center;
}

.deal-pin {
  position: relative;
  background: var(--amber);
  color: #fff;
  border: 2px solid #fff;
  border-radius: 9px;
  text-align: center;
  white-space: nowrap;
  box-shadow: 0 2px 7px rgba(120, 53, 15, 0.28);
  line-height: 1.1;
}

/* ── Two columns ────────────────────────────────────────────────────────────
   The discount stacked on the left, the picture on the right.

   The stacking is the whole trick. "50%" over "OFF" is two short lines, which
   is the same height as a 21px picture beside it — so the picture can be big
   enough to read as food rather than as a character, and the pin does not grow
   at all. As one long line it would leave the icon floating in whitespace or
   push the pin taller.

   Padding lives here rather than on the pin, so the name band above can run
   edge to edge regardless of what padding the body happens to have. */
.pin-body {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 4px 8px 5px;
}

.pin-icon { font-size: 21px; line-height: 1; }

/* ── The name band ──────────────────────────────────────────────────────────
   A header across the top of the pin, not another line of text inside it.

   As a plain line it disappeared: same colour, same background, sitting
   directly above the number, so the eye read "LILY'S BISTRO $5 OFF ENTRÉES" as
   one run-on phrase. A darker band gives it a job — who, then what — and makes
   the pin scan in two beats instead of three lines.

   Runs edge to edge with the pin's own top corners, which is why the padding
   sits on .pin-body rather than on .deal-pin. */
.pin-name {
  display: block;
  padding: 2px 7px;
  background: rgba(0, 0, 0, 0.20);
  border-radius: 7px 7px 0 0;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.96;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pin-amount {
  font-family: var(--font-num);
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.01em;
  text-align: center;
}
.pin-amount i {
  display: block;
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.05em;
  opacity: 0.95;
}

/* ── The footer ─────────────────────────────────────────────────────────────
   Only at close zoom, and it is what stops the fully zoomed-in pin being the
   same pin larger. What it applies to, and when it stops.

   "When it stops" is the deliberate half. At this zoom someone is deciding
   whether to go NOW, and the end of the window is the only fact on the pin
   that changes that answer — distance they can already judge, because they are
   looking at a map with their own dot on it. */
.pin-foot {
  display: block;
  padding: 0 8px 4px;
  font-size: 8px;
  font-weight: 600;
  opacity: 0.92;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pin-foot.condition { font-weight: 700; opacity: 1; }
.pin-subject {
  display: block;
  font-size: 8px;
  font-weight: 600;
  opacity: 0.9;
  max-width: 82px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Top three matches. Bigger, darker — the ranking has to be visible on the
   map, not only in the list. */
.deal-pin.top-match { background: var(--amber-deep); }
.deal-pin.top-match .pin-body { padding: 5px 10px 6px; gap: 8px; }
.deal-pin.top-match .pin-amount { font-size: 13.5px; }
.deal-pin.top-match .pin-icon   { font-size: 23px; }
.deal-pin.top-match .pin-foot   { font-size: 8.5px; padding: 0 10px 5px; }
.deal-pin.top-match .pin-amount  { font-size: 13.5px; }
.deal-pin.top-match .pin-subject { font-size: 8.5px; max-width: 92px; }
.deal-pin.top-match .pin-name    { font-size: 8.5px; padding: 2px 9px; }

/* ── Scarcity ───────────────────────────────────────────────────────────────
   The corner badge everyone already knows — the red dot on a phone icon. It
   needs no label and no legend, and overlapping the corner means it costs no
   width and no height at all.

   It never goes away, at any zoom. How many are left is the only thing on a
   pin that is a reason to act NOW. What the deal applies to tells you whether
   it is relevant, the name tells you where to go, and both can wait — "two
   left" cannot, because by the time someone has zoomed in to find that out it
   may not be true.

   A bare count. Everyone reads a small red number as "running out", and
   spelling out "3 left" costs width the map cannot spare. The card and the
   deal sheet still say it in full, where there is room. */
.pin-scarce {
  position: absolute;
  top: -7px;
  left: 100%;
  margin-left: -12px;
  background: #DC2626;
  color: #fff;
  border: 1.5px solid #fff;
  border-radius: 999px;
  min-width: 18px;
  padding: 0 5px;
  font-family: var(--font-num);
  font-size: 9.5px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  white-space: nowrap;
  box-shadow: 0 1px 4px rgba(153, 27, 27, 0.45);
  animation: scarce-pulse 2.4s ease-in-out infinite;
}
@keyframes scarce-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.11); }
}

/* ── Clearance for the count ────────────────────────────────────────────────
   The badge is anchored to the pin's right EDGE and grows outward, overlapping
   by a fixed 12px whatever it says. That matters because the label changes: a
   bare "3" when zoomed out, the full "3 left" when zoomed right in. Anchored
   the other way — hanging a fixed distance off the edge — a longer label would
   reach further and further back over the text, and by "3 left" it would be
   sitting on top of the restaurant name.

   So the clearance is a constant, and it applies to BOTH the name band and the
   body, because the badge sits at the top of the pin and the band is what is
   up there.

   Both sides equally. Padding only the right clears the badge but leaves every
   word sitting left of centre, which looks like a bug even when nothing is
   covered.

   Only applied when there is actually a count — otherwise every pin on the map
   carries clearance for a badge that is not there. */
.deal-pin.has-count           .pin-body { padding-left: 16px; padding-right: 16px; }
.deal-pin.has-count           .pin-name { padding-left: 16px; padding-right: 16px; }
.deal-pin.has-count           .pin-foot { padding-left: 16px; padding-right: 16px; }
.deal-pin.top-match.has-count .pin-body { padding-left: 18px; padding-right: 18px; }
.deal-pin.top-match.has-count .pin-name { padding-left: 18px; padding-right: 18px; }
.deal-pin.top-match.has-count .pin-foot { padding-left: 18px; padding-right: 18px; }
.deal-pin.compact.has-count   .pin-body { padding-left: 15px; padding-right: 15px; }

/* ── Compact pins ───────────────────────────────────────────────────────────
   Below the detail zoom the subject line is dropped. At city scale a hundred
   two-line cards is unreadable noise; the number alone still says "there is
   something here", and zooming in fills in what it is. */
/* City zoom. Smaller, but it still carries the whole offer — the discount AND
   the picture. "$5 off" alone does not say off what, so it cannot be acted on,
   and a map of fragments makes someone tap every pin to find out. The picture
   is what does that job here, and it is also the only thing legible enough at
   this scale to tell one pin from another without reading.

   The name goes, because at city scale it is unreadable and fifty of them
   collide into noise. */
.deal-pin.compact { border-radius: 7px; }
.deal-pin.compact .pin-body   { padding: 3px 6px 4px; gap: 6px; }
.deal-pin.compact .pin-amount { font-size: 11px; }
.deal-pin.compact .pin-amount i { font-size: 8px; }
.deal-pin.compact .pin-icon   { font-size: 18px; }
.deal-pin.compact .pin-name   { display: none; }
.deal-pin.compact .pin-scarce  { top: -6px; margin-left: -11px; min-width: 16px; padding: 0 4px; font-size: 8.5px; line-height: 14px; }

/* Pin tail — points at the actual coordinate. */
.deal-pin::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -6px;
  transform: translateX(-50%);
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 6px solid #fff;
}

.user-dot {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: #2563EB;
  border: 3px solid #fff;
  box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.18);
}

/* ═══════════════════════════════════════════════════════════════════════════
   DEAL PIECES  (sheets, coupons)
   ═══════════════════════════════════════════════════════════════════════════
   Shared by the deal sheet, the business sheet and the wallet. The feed no
   longer uses these — it has its own row — but the sheets still do. */
.deal-badge {
  flex-shrink: 0;
  width: 52px; height: 52px;
  border-radius: 13px;
  background: var(--pill);
  display: grid;
  place-items: center;
  font-family: var(--font-num);
  font-weight: 700;
  font-size: 15px;
  color: var(--amber-deep);
  line-height: 1;
  text-align: center;
}
.deal-badge small { display: block; font-size: 9px; font-weight: 600; margin-top: 1px; }

.deal-body { flex: 1; min-width: 0; }
.deal-biz {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.deal-title {
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  margin: 3px 0 5px;
}
.deal-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  font-size: 11px;
  color: var(--soft);
}
.deal-tag {
  background: var(--pill);
  color: var(--amber-deep);
  border-radius: 999px;
  padding: 3px 8px;
  font-weight: 600;
}
.deal-tag.reason { background: #DCFCE7; color: #15803D; }
.deal-tag.scarce { background: #FEE2E2; color: #B91C1C; }
.deal-tag.more   { background: #EDE9FE; color: #6D28D9; }

/* "+2 more here" — a pill floating above the pin, and the last thing to appear
   as you zoom in.

   Purple, not red. The two badges say opposite things and must never be
   confused: red is a warning that this is nearly gone, purple is an invitation
   that there is more to look at. Same colour would turn "3 left" and "2 more
   deals" into the same glance.

   Above rather than in a corner because it is about the restaurant, not this
   deal — it sits outside the offer rather than attached to it. It also only
   appears at close zoom, where there is room for words: "+2" alone reads as an
   unread count rather than an invitation. */
.pin-more {
  background: #6D28D9;
  color: #fff;
  border: 1.5px solid #fff;
  border-radius: 999px;
  padding: 1px 7px 2px;
  margin-bottom: 3px;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  box-shadow: 0 1px 5px rgba(76, 29, 149, 0.35);
}

/* ═══════════════════════════════════════════════════════════════════════════
   DEAL CARDS  (the feed)
   ═══════════════════════════════════════════════════════════════════════════
   Two columns of photos, with one wide card at the top for the pick.

   The grid is the point. A single column of equal rows marches — you read one,
   then the next, then the next, and by the fourth you have stopped. Two columns
   let the eye zigzag and take four cards in the time one row used to cost, so
   fifty deals feels like browsing instead of like a list.
   ═══════════════════════════════════════════════════════════════════════════ */
.deal-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding-bottom: 6px;
}

.deal-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 13px;
  overflow: hidden;
  cursor: pointer;
  display: flex;
  flex-direction: column;
}
.deal-card:active { transform: scale(0.985); }

/* The pick gets the full width and a taller photo. That size difference is the
   only place the ranking is visible — sorted order alone is invisible, and a
   label on an identical card is a claim rather than a demonstration. */
.deal-card.hero { grid-column: 1 / -1; margin-bottom: 14px; border-color: var(--amber); }
.deal-card.hero .card-photo { height: 165px; }
.deal-card.hero .card-offer   { font-size: 30px; }
.deal-card.hero .card-offer i { font-size: 30px; }
/* The hero's caption is twice the width of a grid card's but was sized like
   one, which left the deal and the restaurant name huddled against the left
   edge of a mostly empty bar. Scaled to the room they actually have, while
   staying under the offer -- the amount is still the loudest thing in the
   row, which is the one piece of hierarchy that must not flip. */
/* The pick spans the full width, so its column is wide enough that none of
   this applies -- nothing is cut there even at 30px. */
.deal-card.hero .card-what  { font-size: 30px; }
.deal-card.hero .card-biz   { font-size: 16px; }
.deal-card.hero .card-facts { font-size: 13px; }
.deal-card.hero .card-facts { font-size: 13px; }

/* Saved deals outside their window still show — a saved happy hour that is
   dark at 2pm tells you to come back at four — but they should not compete
   with what is running right now. */
.deal-card.not-live { opacity: 0.55; }

/* ── The photo ───────────────────────────────────────────────────────────────
   A fixed height rather than an aspect ratio, so the two columns always line
   up no matter what shape a restaurant uploaded. `cover` crops rather than
   letterboxes: tried showing the whole photo (`contain`) with a soft
   background behind it, but across a real mix of restaurant photos -- not
   just the two well-matched ones it was checked against -- it read as
   broken and inconsistent rather than deliberate, not worth what it cost.
   Back to a tight, deliberate crop that looks the same on every card. */
.card-photo {
  position: relative;
  height: 120px;
  overflow: hidden;
  display: grid;
  place-items: center;
}
/* Absolutely positioned, not just `height: 100%`.

   The box is a centring grid (for the fallback glyph), and a percentage height
   on a centred grid item resolves against a row that is being sized BY that
   item — circular, so the browser gives up and uses the image's own aspect
   ratio instead. An 800x600 photo in a 348px-wide card came out 261px tall
   inside a 132px box, overflowed, and got clipped to whatever happened to be
   in the top half. Taking it out of flow removes the circularity. */
.card-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The bottom tier of the fallback chain, and the one that runs before any
   photos exist at all. Tinted rather than grey — a grey box reads as a failed
   load, a coloured one reads as a design. */
.card-fallback { font-size: 34px; line-height: 1; }
.deal-card.hero .card-fallback { font-size: 46px; }

/* ── The offer, in the caption ───────────────────────────────────────────────
   Used to sit on the photo, bottom right, on a dark scrim -- but food
   photography is exactly the kind of busy, unpredictable background that can
   swallow white-on-black, scrim or not. Moving it into the caption puts it on
   a plain, known surface every time, so it is never a contrast gamble against
   whatever a restaurant uploaded.

   Split big-over-small, the way the map pin already splits it (splitAmount) --
   "50%" stacked over "OFF" runs bigger without taking more width than the
   longer of the two words would need on its own, which is also what leaves
   room for the business name and hours beside it instead of crowding them
   into an ellipsis. */
/* Big and small are the same size on purpose -- William's note after seeing
   the first pass was that OFF should take up as much width as the number
   above it, not read as a caption underneath it. Centered in its own third
   of the row rather than the card's edge, so it reads as a block. */
.card-offer {
  font-family: var(--font-num);
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--amber-deep);
  line-height: 0.95;
  text-align: center;
}
.card-offer i {
  display: block;
  font-style: normal;
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.01em;
  opacity: 0.92;
}

/* ── The badge ───────────────────────────────────────────────────────────────
   Top left, one per card at most, and often none. Groupon puts one on every
   card and they cancel out; the value is entirely in being the exception.

   Three tones, three meanings, and they must never be confused: red is running
   out, amber is already yours, green is close enough to walk. */
.card-badge {
  position: absolute;
  left: 6px;
  top: 6px;
  border-radius: 7px;
  padding: 3px 9px;
  font-size: 12.5px;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: #fff;
  white-space: nowrap;
}
.card-badge.urgent { background: #C0392B; }
.card-badge.saved  { background: var(--amber-deep); }
.card-badge.near   { background: #0F7A5A; }
.deal-card.hero .card-badge { font-size: 14.5px; padding: 4px 10px; }

/* ── The caption ─────────────────────────────────────────────────────────────
   Three lines, in the order they are decided: what the food is, who is doing
   it, and the two facts that settle it. The restaurant name is deliberately
   the quietest — it matters after the food is interesting, not before.

   The offer gets a fixed third of the row rather than however much its own
   text needs — a deliberate two-thirds/one-third split, not a flexible one,
   so the amount reads as its own block instead of a label that happens to be
   big. `min-width: 0` on the copy column is what makes the ellipsis rules
   below it actually truncate instead of stretching the grid to fit them. */
.card-text {
  padding: 8px;
  display: grid;
  /* The deal name gets its own row across the whole bar, and only what is
     left underneath is split. Sharing a row with the amount cost it a third
     of the card before it had written a letter -- and the name is the thing
     being decided about, so it was the wrong third to give away. Across the
     full width "Appetizers" fits at a size it never came close to in a
     column.

     The amount gets a fixed third rather than `auto`. Sized to its content it
     was a different width on every card -- and a wide one ("BOGO") helped
     itself to the restaurant's column, because an `fr` track will not shrink
     under nowrap content. A fixed third is also what gives the amount a BOX,
     which is what it is fitted to: same right-hand column on every card, text
     scaled to fill it. */
  grid-template-columns: minmax(0, 1fr) 33%;
  grid-template-areas:
    "title title"
    "meta  offer";
  column-gap: 8px;
  row-gap: 4px;
  /* Top, not bottom. Pinned to the floor, the restaurant block hung below the
     amount and left a band of white directly under the deal name -- the gap
     this layout exists to close. Sitting at the top puts the restaurant
     immediately under the name it belongs to, which is where it reads from. */
  align-items: start;

  /* ── The template ─────────────────────────────────────────────────────────
     These four numbers are the card. Every deal gets the same caption: one
     row for the name, one for the restaurant and the amount, and the same
     total height whatever is written in them. The text is fitted to these
     boxes rather than the boxes grown to the text -- which is the whole
     difference between a coupon book and a stack of receipts.

     Before this the caption ran from 96px to 153px depending on how long a
     restaurant was called, and since grid rows stretch to their tallest
     member, the shorter card in every pair carried a band of dead white along
     its bottom edge. That band is the complaint this design has been chasing
     since the beginning; it was never in one place, it was wherever the
     shorter card happened to be.

     They are read back by fitCardText through getComputedStyle, so the CSS
     stays the one place they are written down. Change them here and the text
     resizes to suit. */
  height: 104px;
  box-sizing: border-box;
  grid-template-rows: 36px 48px;
}
.deal-card.hero .card-text { height: 150px; grid-template-rows: 44px 86px; }

.card-what  { grid-area: title; align-self: center; }
/* The restaurant sits at the top of the row and the distance line at the
   foot of it, with whatever slack there is falling between them. Stacked from
   the top instead, that slack collected under the distance line, so the two
   facts that settle the visit sat at a different height on every card and the
   feed read as ragged even though every card was the same size. A gap under
   the restaurant is invisible; a line that moves is not. */
.card-meta  {
  grid-area: meta; min-width: 0; overflow: hidden;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Centred in the row it shares with the restaurant, so a one-line amount
   ("$5", with no OFF under it) sits level with the name instead of hanging
   from the ceiling of an empty box. */
.card-offer { grid-area: offer; align-self: stretch; display: flex; flex-direction: column; justify-content: center; }

/* The loudest thing on the left, and sized by what actually fits rather than
   by taste. A grid card on a 375px phone gives this column 92px. Measured in
   that column, 21px cuts "Appetizers", "Cocktails" and "Margaritas" to
   "Appeti...", and they are single words, so they cannot wrap out of it --
   letting them break mid-word instead produces "Cocktail / s". 17px is the
   largest size at which no deal name on the seeded feed is cut at all, which
   is the whole point of the line. Still a long way up from the 13.5px it
   replaced. */
/* Also a starting point -- fitCardText raises a short name and lowers a long
   one from here. */
.card-what {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.1;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
/* The condition is the one term Syft cannot enforce — a human reads it off a
   screen and checks a bill — so it never blends into the description. */
.card-what .cond { color: #B91C1C; }

/* The size here is a starting point, not the answer: fitCardText measures the
   box and sets the real one, so "Table 33" comes out large and "Chappy's
   Social House" comes out small and both are whole. The ellipsis stays as the
   backstop for a name longer than the smallest size will take. */
.card-biz {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Set by fitOne on the lines that came out larger wrapped than unbroken.
   The clamp is the backstop, not the plan: the size is chosen so the whole
   text fits inside the budget, so nothing should ever reach the ellipsis. */
.fit-wrap {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
/* "0.7 mi · till 10pm". 11px is not a stylistic choice: the pair has to clear
   92px with a two-digit distance in front of it, and 12px does not. */
.card-facts {
  font-size: 11.5px;
  line-height: 1.3;
  color: var(--soft);
  /* Pushed to the foot of the meta column -- see .card-meta. The gap between
     it and the restaurant above comes from the column's own `gap`, so this
     margin is free to eat the slack and nothing collapses onto anything. */
  margin-top: auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}


/* ── Section labels ─────────────────────────────────────────────────────────
   "For you" is what turns the top card from merely first into chosen.

   The badge slot beside it is where "PROMOTED" goes once that placement is
   sold. Paid placement presented as a recommendation is a disclosure problem,
   not a design preference — the room for the word costs nothing now and means
   nothing has to move later. */
.feed-label {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 4px 0 7px;
}
.feed-label:not(:first-child) { margin-top: 6px; }
.feed-label b {
  font-size: 10.5px;
  font-weight: 800;
  color: var(--amber-deep);
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
.feed-label hr { flex: 1; border: none; border-top: 1px solid var(--border-soft); }
.feed-badge {
  background: #EDE9FE;
  border: 1px solid #C9BFF5;
  color: #5B21B6;
  border-radius: 4px;
  padding: 0 5px;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.05em;
}

/* Business sheet */
.biz-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 4px;
}
.biz-avatar {
  flex-shrink: 0;
  width: 46px; height: 46px;
  border-radius: 13px;
  background: var(--pill);
  color: var(--amber-deep);
  display: grid;
  place-items: center;
  font-family: 'Plus Jakarta Sans', sans-serif;
  font-size: 19px;
  font-weight: 800;
}
.biz-deal {
  display: flex;
  gap: 12px;
  align-items: center;
  width: 100%;
  text-align: left;
  background: var(--cream);
  border: 1px solid var(--border-soft);
  border-radius: 13px;
  padding: 13px 14px;
  margin-bottom: 9px;
}
.biz-deal:active { transform: scale(0.985); }
.biz-deal.off { opacity: 0.55; }
.biz-deal .badge {
  flex-shrink: 0;
  width: 46px; height: 46px;
  border-radius: 11px;
  background: var(--surface);
  display: grid;
  place-items: center;
  font-family: var(--font-num);
  font-size: 13px;
  font-weight: 700;
  color: var(--amber-deep);
  text-align: center;
  line-height: 1.1;
}

/* What the discount applies to, under the headline. The card equivalent of
   the pin's second line. */
.deal-applies {
  font-size: 12.5px;
  color: var(--soft);
  margin: -2px 0 5px;
}
.deal-applies b { color: var(--amber-deep); font-weight: 700; }

/* Conditions read differently from categories on purpose. "over $50" is a
   hurdle, not a description, and should not look like a menu section. */
.deal-applies .cond { color: #B91C1C; font-weight: 600; }
.deal-tag.cond      { background: #FEE2E2; color: #B91C1C; }
.pin-subject.condition { font-weight: 700; }

.condition-banner {
  background: #FEF3C7;
  border-left: 3px solid var(--amber);
  border-radius: 0;
  padding: 11px 14px;
  margin-top: 14px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--amber-deep);
  font-weight: 600;
}

.qr-condition {
  display: inline-block;
  background: #FEE2E2;
  color: #B91C1C;
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 12.5px;
  font-weight: 700;
  margin-bottom: 18px;
}
.deal-until { font-family: var(--font-num); font-weight: 600; }

/* ═══════════════════════════════════════════════════════════════════════════
   BOTTOM NAV
   ═══════════════════════════════════════════════════════════════════════════ */
/* List · Map · [Search] · Wallet · Profile
   Search is the raised centre button — it is the one action that applies to
   every screen rather than being a destination of its own. */
.nav {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 800;
  height: calc(var(--nav-h) + var(--safe-b));
  padding-bottom: var(--safe-b);
  background: var(--surface);
  border-top: 1px solid var(--border-soft);
  display: flex;
  align-items: center;
  justify-content: space-around;
}

.nav-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  color: #C9A227;
  padding: 6px 0;
}
.nav-item svg { width: 23px; height: 23px; }
.nav-item span { font-size: 10px; font-weight: 600; }
.nav-item.active { color: var(--amber-deep); }

.nav-search {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
}
.nav-search-btn {
  width: 54px; height: 54px;
  border-radius: 50%;
  background: var(--amber);
  color: #fff;
  display: grid;
  place-items: center;
  margin-top: -22px;
  border: 4px solid var(--surface);
  box-shadow: 0 4px 14px rgba(245, 158, 11, 0.38);
}
.nav-search-btn svg { width: 24px; height: 24px; }
.nav-search-btn:active { transform: scale(0.94); }

/* ═══════════════════════════════════════════════════════════════════════════
   SHEETS
   ═══════════════════════════════════════════════════════════════════════════ */
.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 850;
  background: rgba(28, 10, 0, 0.42);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease;
}
.sheet-backdrop.open { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 860;
  background: var(--surface);
  border-radius: 24px 24px 0 0;
  padding: 10px 22px calc(var(--safe-b) + 26px);
  transform: translateY(100%);
  transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
  max-height: 88vh;
  overflow-y: auto;
}
.sheet.open { transform: translateY(0); }

.sheet-grab {
  width: 38px; height: 4px;
  border-radius: 999px;
  background: var(--border);
  margin: 0 auto 16px;
}
.sheet-title {
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
}
.sheet-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 18px 0 8px;
}

/* ── Form controls ────────────────────────────────────────────────────────── */
.field {
  width: 100%;
  background: var(--cream);
  border: 1.5px solid var(--border);
  border-radius: 12px;
  padding: 13px 15px;
  font-size: 15px;
  outline: none;
  -webkit-user-select: text;
  user-select: text;
}
.field:focus { border-color: var(--amber); }
.field::placeholder { color: #C9A227; }

.btn {
  width: 100%;
  background: var(--amber);
  color: #fff;
  border-radius: 13px;
  padding: 15px;
  font-size: 15px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.btn:active { transform: scale(0.985); }
.btn:disabled { opacity: 0.5; }
.btn.secondary {
  background: var(--cream);
  color: var(--muted);
  border: 1.5px solid var(--border);
}
/* The label is #B91C1C rather than --danger (#DC2626), and that is measured
   rather than preferred: --danger on this pink sits at 3.95:1, under the 4.5
   a 15px bold label needs. It reads fine to anybody who already knows what the
   button says. This is the third contrast bug in the project and the first two
   both shipped, so verify.js now computes this ratio rather than trusting it.
   --danger is untouched everywhere else; it is only wrong on THIS background.
   Disabled sits a little higher than the global 0.5 for the same reason: the
   state is meant to read as unavailable, not as absent. */
.btn.danger { background: #FEE2E2; color: #B91C1C; }
.btn.danger:disabled { opacity: 0.62; }

.chip {
  background: var(--cream);
  border: 1.5px solid var(--border);
  border-radius: 999px;
  padding: 9px 15px;
  font-size: 13px;
  font-weight: 600;
  color: var(--soft);
}
.chip.selected {
  background: var(--amber);
  border-color: var(--amber);
  color: #fff;
}
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }

.price-row { display: flex; gap: 8px; }
.price-btn {
  flex: 1;
  background: var(--cream);
  border: 1.5px solid var(--border);
  border-radius: 13px;
  padding: 13px 4px;
  text-align: center;
}
.price-btn.selected { background: var(--amber); border-color: var(--amber); }
.price-btn .sym {
  font-family: var(--font-num);
  font-size: 17px;
  font-weight: 700;
  color: var(--amber-deep);
}
.price-btn.selected .sym,
.price-btn.selected .hint { color: #fff; }
.price-btn .hint { font-size: 10px; color: var(--soft); margin-top: 2px; }

.form-error {
  color: var(--danger);
  font-size: 13px;
  font-weight: 600;
  margin: 10px 0 0;
  min-height: 18px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   QR SCREEN
   ═══════════════════════════════════════════════════════════════════════════ */
.qr-wrap { text-align: center; margin: auto 0; }
.qr-card {
  background: #fff;
  border-radius: 22px;
  padding: 26px;
  display: inline-block;
  box-shadow: 0 6px 28px rgba(120, 53, 15, 0.14);
}
.qr-card canvas, .qr-card svg { display: block; }
.qr-card.expired { opacity: 0.28; }

.qr-timer {
  font-family: var(--font-num);
  font-size: 32px;
  font-weight: 700;
  color: var(--amber-deep);
  margin-top: 22px;
  font-variant-numeric: tabular-nums;
}
.qr-timer.low { color: var(--danger); }
.qr-hint { font-size: 13px; color: var(--muted); margin-top: 6px; }

/* ═══════════════════════════════════════════════════════════════════════════
   STARS
   ═══════════════════════════════════════════════════════════════════════════ */
.stars { display: flex; gap: 8px; }
.star {
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  color: var(--border);
}
.star.filled { color: var(--amber); }
.star svg { width: 30px; height: 30px; }

.rating-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 0;
}
.rating-row .label { font-size: 14px; font-weight: 600; }
.rating-row .stars { gap: 3px; }
.rating-row .star { width: 30px; height: 30px; }
.rating-row .star svg { width: 22px; height: 22px; }

/* ═══════════════════════════════════════════════════════════════════════════
   WALLET
   ═══════════════════════════════════════════════════════════════════════════ */
.tabs {
  display: flex;
  gap: 6px;
  background: var(--pill);
  border-radius: 12px;
  padding: 4px;
  margin: 0 20px 16px;
}
.tab {
  flex: 1;
  padding: 9px;
  border-radius: 9px;
  font-size: 13px;
  font-weight: 700;
  color: var(--soft);
  text-align: center;
}
.tab.active { background: var(--surface); color: var(--amber-deep); }

.tab-panel { display: none; }
.tab-panel.active { display: block; }

.coupon-card {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 4px solid var(--amber);
  border-radius: 14px;
  padding: 15px 16px;
  margin-bottom: 11px;
  width: 100%;
  text-align: left;
}
.coupon-card.used,
.coupon-card.expired { border-left-color: #D6D3D1; opacity: 0.55; }

/* A saved deal outside its window is still worth showing — it tells you when
   to come back. Dimmed rather than hidden. */
.deal-card.not-live { opacity: 0.62; }
.deal-card.not-live .deal-badge { background: #F5F0E4; color: #A8A29E; }

.history-row {
  display: flex;
  gap: 12px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 14px;
  padding: 13px 15px;
  margin-bottom: 9px;
  width: 100%;
  text-align: left;
}
.history-date {
  flex-shrink: 0;
  width: 46px;
  text-align: center;
  font-family: var(--font-num);
}
.history-date .d { font-size: 17px; font-weight: 700; line-height: 1; }
.history-date .m { font-size: 10px; color: var(--soft); text-transform: uppercase; }

.needs-review {
  background: #FEF3C7;
  color: var(--amber-deep);
  font-size: 10px;
  font-weight: 700;
  padding: 3px 7px;
  border-radius: 999px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PROFILE
   ═══════════════════════════════════════════════════════════════════════════ */
.syft-code-card {
  background: linear-gradient(135deg, var(--amber) 0%, var(--amber-deep) 100%);
  border-radius: 18px;
  padding: 22px;
  color: #fff;
  margin-bottom: 18px;
}
.syft-code-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.85;
}
.syft-code-value {
  font-family: var(--font-num);
  font-size: 27px;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin: 5px 0 8px;
  -webkit-user-select: text;
  user-select: text;
}
.syft-code-note { font-size: 12px; opacity: 0.9; line-height: 1.5; }

.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 13px;
  padding: 15px 16px;
  margin-bottom: 9px;
  width: 100%;
  text-align: left;
  font-size: 15px;
  font-weight: 600;
}
.list-row .value { font-size: 13px; color: var(--soft); font-weight: 500; }

/* ═══════════════════════════════════════════════════════════════════════════
   MISC
   ═══════════════════════════════════════════════════════════════════════════ */
.empty {
  text-align: center;
  padding: 56px 24px;
  color: var(--muted);
}
.empty-title { font-size: 16px; font-weight: 700; margin-bottom: 5px; }
.empty-text  { font-size: 13px; line-height: 1.6; opacity: 0.8; }

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--nav-h) + var(--safe-b) + 18px);
  transform: translate(-50%, 20px);
  z-index: 950;
  background: var(--text);
  color: var(--cream);
  border-radius: 999px;
  padding: 12px 20px;
  font-size: 13.5px;
  font-weight: 600;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  max-width: 88vw;
  text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.spinner {
  width: 26px; height: 26px;
  border: 3px solid var(--border);
  border-top-color: var(--amber);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  margin: 40px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }

.hidden { display: none !important; }

/* Leaflet overrides — the default blue/grey chrome clashes badly with amber */
.leaflet-container { background: var(--cream); font-family: inherit; }
.leaflet-control-attribution {
  font-size: 9px;
  background: rgba(255, 251, 240, 0.8);
}
.leaflet-marker-icon { border: none; background: none; }


/* ── The owner bar ───────────────────────────────────────────────────────────
   Shown to a restaurant owner browsing the customer app. One line, dismissible
   for the session, and deliberately quiet — they came here to look at deals,
   not to be told where they should be instead. */
.owner-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 800;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(var(--safe-t) + 8px) 14px 8px;
  background: var(--text);
  color: #fff;
  font-size: 12.5px;
  font-weight: 600;
}
.owner-bar.hidden { display: none; }
/* Fixed elements are out of the flow, so the page has to be told to start
   lower. Measured rather than guessed: one line at 12.5px with this padding
   is 35px tall on a phone, and the safe area is already inside the bar. */
body.has-owner-bar .screen { padding-top: 35px; }
.owner-bar a { color: #fff; text-decoration: none; }
.owner-bar button { color: #fff; font-size: 19px; line-height: 1; padding: 0 4px; }

/* ── Delete account ─────────────────────────────────────────────────────────
   Opens in place under its row in settings rather than as a sheet. A sheet
   slides over everything and reads as a step in a flow; this is a drawer that
   was already there and had to be opened on purpose, which is closer to what
   deleting an account actually is. */
.delete-panel {
  margin-top: 10px;
  padding: 16px;
  border: 1px solid #FECACA;
  border-radius: 14px;
  background: #FEF2F2;
}
.delete-lead {
  margin: 0 0 10px;
  font-size: 14px;
  font-weight: 700;
  color: var(--danger);
}
.delete-list {
  margin: 0 0 12px;
  padding-left: 18px;
  font-size: 13.5px;
  color: var(--text);
  line-height: 1.55;
}
.delete-list li { margin-bottom: 3px; }
.delete-note {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}
.delete-label {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.delete-panel .field { margin-bottom: 10px; text-transform: uppercase; }
