/* [hidden] must win. The UA rule is `[hidden]{display:none}` — specificity 0,1,0 —
   so any `.thing { display: flex }` silently un-hides it. Three separate bugs in
   this stylesheet were that, so it is settled here once. */
[hidden] { display: none !important; }

/* ============ Pakistan360 design system ============ */
:root {
  --pine-900: #14342B;
  --pine-700: #1E5245;
  --pine-500: #2D6A4F;
  --pine-100: #D8EBE1;
  --mist: #EDF6F1;
  --paper: #FAFAF7;
  --card: #FFFFFF;
  --ink: #1D2A26;
  --ink-soft: #55675F;
  --line: #DCE7E1;
  --amber: #E9B44C;
  /**
   * THE STAR IS A GRAPHICAL OBJECT AND HAS TO CLEAR 3:1 AGAINST WHAT IS BEHIND IT.
   *
   * It was #F2A93B, which measures 2.00:1 on a white card and 1.91:1 on paper — the single
   * most-scanned element in a directory, and it was failing the graphical minimum by a third.
   * On a phone in daylight the rating simply was not there.
   *
   * #BD7F0B is 3.38:1 on card and 3.24:1 on paper, still unmistakably gold, and still the
   * warmer sibling of --amber rather than a second brand colour. --amber itself is unchanged
   * and needs no change: everywhere it carries text it does so on the dark pine topbar, where
   * it measures 7.13:1.
   */
  --star: #BD7F0B;

  /**
   * --ok AND --danger WERE FAILING AS TEXT, AND --ok IS THE WORD "OPEN NOW".
   *
   * Found the same way the star was: by measuring instead of looking. #2D8A4E is 4.32:1 on a
   * white card, 4.13 on paper and 3.92 on mist — under the 4.5:1 minimum on every surface the
   * site has. It is the colour that tells somebody whether the shop is open before they make
   * the trip. #C4483A cleared white and paper and failed on mist at 4.40.
   *
   * Both are darkened along their own hue — 92% and 98% — so nothing about the palette changes
   * except that the words can be read: --ok is now 4.97 / 4.75 / 4.51, --danger 5.00 / 4.78 /
   * 4.53. `php tools/contrast.php` recomputes all of it from this file.
   */
  --ok: #297F48;
  --danger: #C04739;
  --info: #3D6B99;

  /* Disabled controls are exempt from 1.4.3 by the spec itself. This is the ONLY token below
     the text floor, it is named so nobody reaches for it by accident, and live text never
     uses it. There is no third live ink level: between --ink-soft at 5.46:1 on the worst
     surface and the 4.5:1 floor there is a fifth of a stop, so a "muted" grey either fails or
     is indistinguishable from --ink-soft. Recede by getting smaller, or by being deleted. */
  --ink-dis: #8FA098;

  /**
   * TWO BORDER TOKENS, BECAUSE A DIVIDER AND A CONTROL EDGE ARE DIFFERENT OBLIGATIONS.
   *
   * --line is decoration between rows: WCAG 1.4.11 does not apply and 1.27:1 is a deliberate
   * whisper. --line-ui is the edge of something you operate, where 1.4.11 requires 3:1.
   *
   * Every input on this site — register, sign in, search, the business editor — was bordered
   * with --line at 1.27:1. It looked clean. It was a control whose boundary a person with
   * ordinary eyesight in daylight cannot locate.
   */
  --line-ui: #868D89;          /* 3.40 card / 3.25 paper / 3.08 mist */

  /* A green ring on a green button is not a focus indicator. This is the one colour on the
     site whose whole job is to NOT belong to the palette. */
  --focus: #0F62C4;

  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 18px;
  --radius-pill: 999px;
  --grad: linear-gradient(135deg, #2D6A4F 0%, #1F7A63 100%);

  /**
   * THE ELEVATION BUDGET IS THREE SHADOWS AND THEY ALL MEAN "THIS FLOATS ABOVE THE MAP".
   *
   * Not "this is important" — importance is size, weight and position. A blurred shadow costs
   * paint proportional to blur radius x area, and thirty of them in a scrolling result list is
   * what drops frames on the mid-range Android this site is for. Flat plus a 1px hairline is
   * the default; the test is whether the element is physically above the map.
   *
   * Note the negative Y on the sheet. A downward shadow on a bottom-anchored panel is a tell.
   */
  --shadow-sheet: 0 -2px 16px rgba(20, 52, 43, .16);
  --shadow-float: 0 2px 8px rgba(20, 52, 43, .16);
  --shadow-over:  0 4px 16px rgba(20, 52, 43, .12);

  /* --shadow and --shadow-lg predate the budget and are still named in ~49 rules. They are
     now hairline-weight rather than deleted: flattening in one edit and chasing the fallout
     across 2,400 lines is how a refresh becomes a regression. Anything that genuinely floats
     names one of the three above. */
  --shadow: 0 1px 2px rgba(20, 52, 43, .06);
  --shadow-lg: var(--shadow-over);

  --topbar-h: 58px;
  --sheet-peek: 214px;
  --ease: cubic-bezier(.32,.72,.28,1);

  /**
   * SPACING: base 4, nine steps, and the holes are deliberate.
   *
   * There was no spacing scale at all. Measured across the stylesheet: 824 uses off the 4px
   * grid against 513 on it, with 5, 6, 7, 9, 10, 11 and 13 all in regular service — which is
   * not a system, it is a year of nudging.
   *
   * 20, 28, 40 and 56 are absent on purpose. JUMP A STEP, NEVER INVENT A VALUE BETWEEN TWO:
   * if 16 is tight the answer is 24. If 24 is then loose, the real problem is that the two
   * things belong to different groups than you assumed, or one of them needs a weight change
   * rather than a gap change.
   *
   * THE PROXIMITY RULE, which is the whole of "does this look ordered": the gap separating
   * two groups must be at least twice the largest gap inside either group.
   */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  /* Line boxes are multiples of 4 so text sits on the same grid as the spacing. */
  --t-page: 28px;    --lh-page: 36px;
  --t-section: 22px; --lh-section: 28px;
  --t-card: 18px;    --lh-card: 24px;
  --t-body: 16px;    --lh-body: 24px;
  --t-meta: 14px;    --lh-meta: 20px;
  --t-label: 13px;   --lh-label: 16px;

  /* The emoji faces are named last so a stack that lists none cannot leave the choice to
     last-resort fallback. On Windows this changes nothing — Chrome already resolved to Segoe
     UI Emoji — but a stack that states its intent does not depend on that being true
     everywhere. A font is chosen per character, so these are only ever reached for
     codepoints the faces above genuinely lack. */
  --font: "Segoe UI", system-ui, -apple-system, Roboto, "Helvetica Neue", Arial, sans-serif,
          "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", "Segoe UI Symbol";
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  /* 16px, not 15. Every reference system consulted lands on 16 for a public reading surface;
     15 is a dashboard number, and this page is read outdoors on a cheap screen. The 1px buys
     roughly one line of legibility per paragraph and costs one result per screenful, which is
     the right side of that trade for somebody squinting at a shop name in sunlight. */
  margin: 0; font-family: var(--font); color: var(--ink);
  background: var(--paper); font-size: var(--t-body); line-height: 1.5;
}
a { color: var(--pine-500); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; }

/* 1.25 clips the i'jam — the dots above and below Naskh letterforms — on any heading that
   wraps. 1.3 is the floor that does not, and it is imperceptible in Latin. The Urdu block
   further down raises it again. */
h1, h2, h3 { line-height: 1.3; color: var(--pine-900); }
input, select, textarea, button { font: inherit; color: inherit; }

/**
 * THE FOCUS RING IS NOT THE BRAND COLOUR, AND THAT IS THE POINT.
 *
 * It was --pine-500: a green ring, drawn most often around a green button, on a green-tinted
 * page. An indicator that borrows the palette it is meant to stand out from is an indicator
 * that cannot be seen exactly where it matters most. --focus is deliberately the one colour
 * on this site that belongs to nothing else.
 *
 * NEVER transition outline. A focus ring that fades in is not there when the key is pressed.
 */
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
:focus:not(:focus-visible) { outline: none; }
/* On the dark topbar and on any filled control, double it so it reads against either ground. */
.topbar :focus-visible,
.btn-primary:focus-visible { outline-color: var(--card); box-shadow: 0 0 0 6px var(--focus); }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  html { scroll-behavior: auto; }
}

/* ---------- Topbar ---------- */
.topbar {
  position: sticky; top: 0; z-index: 500; height: var(--topbar-h);
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 0 18px; padding-left: max(18px, env(safe-area-inset-left)); padding-right: max(18px, env(safe-area-inset-right));
  background: var(--pine-900); color: #fff;
  box-shadow: 0 1px 0 rgba(255,255,255,.06), var(--shadow);
}
@supports (backdrop-filter: blur(10px)) {
  .topbar { background: rgba(18, 46, 38, .86); backdrop-filter: blur(10px) saturate(1.3); -webkit-backdrop-filter: blur(10px) saturate(1.3); }
}
.brand { display: flex; align-items: center; gap: 9px; color: #fff; flex-shrink: 0; }
.brand:hover { text-decoration: none; }
.brand-mark { color: var(--pine-100); }
.brand-name { font-size: 1.25rem; font-weight: 700; letter-spacing: .2px; }
.brand-name span { color: var(--amber); }
.topnav { display: flex; align-items: center; gap: 18px; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.topnav::-webkit-scrollbar { display: none; }
.topnav a { color: #E4EFE9; font-weight: 500; white-space: nowrap; }
.topnav a:hover { color: #fff; text-decoration: none; }
.nav-user { position: relative; display: flex; align-items: center; padding-left: 14px;
            border-left: 1px solid rgba(255,255,255,.18); }
.nav-user-btn { display: inline-flex; align-items: center; gap: 8px; background: transparent;
                border: 1px solid rgba(255,255,255,.22); border-radius: 999px; padding: 3px 10px 3px 3px;
                color: var(--amber); font: inherit; font-weight: 600; cursor: pointer; }
.nav-user-btn:hover, .nav-user.open .nav-user-btn { background: rgba(255,255,255,.12); }
.nav-user-name { color: var(--amber); font-weight: 600; max-width: 12ch;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-user-caret { font-size: .7rem; opacity: .8; }
/* Fixed, not absolute, and that is the whole reason this works.
   `.topnav` sets `overflow-x: auto` so a crowded nav scrolls rather than crops — and the
   moment one axis is `auto` the other stops being `visible` too, so an absolutely
   positioned dropdown is clipped to the height of the bar. It reports its full size to
   JavaScript and paints as a 6px sliver. A fixed element is not clipped by an ancestor's
   overflow, so it escapes cleanly and the nav keeps scrolling. */
.nav-user-menu { position: fixed; top: calc(var(--topbar-h) + 6px); inset-inline-end: 14px;
                 min-width: 220px; z-index: 950;
                 background: var(--card); border: 1px solid var(--line); border-radius: 12px;
                 box-shadow: var(--shadow-lg); padding: 6px; }
.nav-user-head { margin: 0; padding: 8px 12px 10px; border-bottom: 1px solid var(--line); }
.nav-user-head b { display: block; color: var(--ink); }
.nav-user-head span { display: block; font-size: .78rem; color: var(--ink-soft);
                      overflow: hidden; text-overflow: ellipsis; }
.nav-user-menu a, .nav-user-out { display: block; width: 100%; text-align: start; padding: 9px 12px;
                 border-radius: 8px; color: var(--ink) !important; font-weight: 500;
                 background: transparent; border: 0; font: inherit; cursor: pointer; }
.nav-user-menu a:hover, .nav-user-out:hover { background: var(--mist); text-decoration: none; }
.nav-user-out { color: var(--danger) !important; border-top: 1px solid var(--line);
                border-radius: 0 0 8px 8px; margin-top: 4px; }
/* ---------- The nav's overflow group ----------
   Same fixed-position trick as .nav-user-menu above, and for the same reason: `.topnav` is
   `overflow-x: auto`, so an absolutely positioned dropdown is clipped to the height of the
   bar and paints as a sliver. Anchored to the START edge rather than the end, because this
   button sits in the middle of the row and a menu pinned to the far right would open a long
   way from the thing that was pressed. */
.nav-more { position: relative; display: flex; align-items: center; flex-shrink: 0; }
.nav-more-btn { display: inline-flex; align-items: center; gap: 5px; background: transparent;
                border: 0; color: #E4EFE9; font: inherit; font-weight: 500; cursor: pointer;
                white-space: nowrap; padding: 6px 8px; border-radius: 9px; }
.nav-more-btn:hover, .nav-more.open .nav-more-btn { background: rgba(255,255,255,.12); color: #fff; }
.nav-more-menu { position: fixed; top: calc(var(--topbar-h) + 6px);
                 min-width: 220px; z-index: 950;
                 background: var(--card); border: 1px solid var(--line); border-radius: 12px;
                 box-shadow: var(--shadow-lg); padding: 6px; }
.nav-more-menu a { display: block; width: 100%; padding: 9px 12px; border-radius: 8px;
                   color: var(--ink) !important; font-weight: 500; white-space: nowrap; }
.nav-more-menu a:hover { background: var(--mist); text-decoration: none; }

.inline-form { display: inline; }
.linklike { background: none; border: 0; color: #E4EFE9; cursor: pointer; padding: 0; font-weight: 500; white-space: nowrap; }
.linklike:hover { color: #fff; }

/* ---------- Buttons, badges, forms ---------- */
/* 11px, not 9px, and the reason is arithmetic rather than taste: at a 16px line 9px of
   padding computes to a 41px control, three pixels under the 44px every touch guideline
   asks for. One value, and it moves 218 buttons at once. */
.btn {
  display: inline-block; border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 11px 18px; font-weight: 600; cursor: pointer;
  /* Was five properties including box-shadow and filter. `filter` forces a new compositing
     layer for the whole button on every hover, and box-shadow is the most expensive property
     there is to animate. Background and border are what actually change. */
  transition: background .15s, border-color .15s;
  background: var(--mist); color: var(--pine-700); text-align: center;
  -webkit-tap-highlight-color: transparent;
}
/* Hover is gated: it is dead code on a touch device and it strands the last-tapped button
   looking hovered, because there is no pointer to move away. */
@media (hover: hover) {
  .btn:hover { text-decoration: none; background: var(--pine-100); }
  .btn-primary:hover { background: var(--pine-700); }
}
/* Press is NOT gated. On touch it is the only feedback there is, and it confirms the tap
   before the network has answered — which on a slow connection is most of a second. */
.btn:active { transform: scale(.97); }
.btn-primary:active { background: var(--pine-900); }
.btn-primary { background: var(--grad); color: #fff; border: 0; box-shadow: var(--shadow); }
.btn-amber { background: var(--amber); color: var(--pine-900); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-ghost { background: transparent; border-color: var(--line-ui); }
/* .btn-ghost is an outlined BUTTON, so its edge is a control boundary at 3:1, not a divider.
   It was --line at 1.27:1 — an outlined button with an outline you cannot see. */
.btn-sm { padding: 8px 13px; font-size: .9rem; border-radius: var(--radius-sm); }
.btn[disabled], .btn:disabled { background: var(--mist); color: var(--ink-dis); cursor: not-allowed; box-shadow: none; }
.btn-block { display: block; width: 100%; }
/* `opacity: .55` lived here and is gone: opacity fades the LABEL as well as the fill, so a
   disabled primary button's white text landed somewhere near 2:1 on its own background and
   the word became decoration. Disabled controls are exempt from the contrast minimum, but
   the exemption is permission to be quiet, not permission to be unreadable — a person still
   has to know WHICH button is unavailable. The rule above states the two colours instead. */

.badge { display: inline-block; border-radius: 999px; padding: 2px 10px; font-size: .78rem; font-weight: 600; }
.badge-green { background: var(--pine-100); color: var(--pine-700); }
.badge-amber { background: #FBEED3; color: #8a6414; }
.badge-red   { background: #F6DDD9; color: var(--danger); }
.badge-gray  { background: #E9ECEA; color: var(--ink-soft); }
/* Fifth badge, for "in progress" — a state that is neither good news nor bad and was being
   drawn amber, which on a payments queue reads as "something is wrong". */
.badge-blue  { background: #DEE9F4; color: var(--info); }

.field { margin-bottom: 14px; }
.field label { display: block; font-weight: 600; margin-bottom: 5px; color: var(--pine-900); }
/* THE BASE RULE, which never existed. Eighty-six views write class="hint" for the quieter
   line under a label, a value or a heading, and until now only `.field .hint` was styled — so
   on all the others the text rendered at body colour and body size, which is the opposite of
   what the word means. Placed here, above the @media override further down, so that override
   still wins at its breakpoint; the descendant rules that follow are more specific and are
   unaffected either way. */
/* 14px, the floor. .85rem was 13.6px at a 16px root and the @media block below took it to
   .8rem = 12.8px on small screens — smaller text on the smaller screen, which is backwards
   and was the largest single group of under-14px text on the site. */
.hint { font-size: var(--t-meta); color: var(--ink-soft); }
.field .hint { font-size: var(--t-meta); color: var(--ink-soft); margin-top: var(--s1); }
/**
 * THE EDGE OF A CONTROL IS NOT DECORATION — WCAG 1.4.11 WANTS 3:1 FOR IT.
 *
 * This was `1.5px solid var(--line)`, and --line is #DCE7E1: 1.27:1 against the white fill
 * inside it. Every form on the site — register, sign in, search, claim a business, the whole
 * business editor — drew its fields with a boundary that a person with ordinary eyesight in
 * daylight cannot locate. It photographs beautifully, which is exactly why it survived.
 *
 * --line-ui is 3.40:1. 1px rather than 1.5 because a hairline that can actually be seen does
 * not also need to be thick, and 1.5px lands on a half-pixel at 1x.
 */
.input, textarea.input, select.input {
  width: 100%; border: 1px solid var(--line-ui); border-radius: var(--radius-sm);
  padding: 10px 12px; background: #fff; outline: none; transition: border-color .15s;
  min-height: 44px;
}
.input::placeholder, textarea.input::placeholder {
  /* A placeholder is text. Every reference system consulted set this below 4.5:1 — the
     lightest of them at 2.19:1 — and it is the one string in a form that a first-time user
     most needs to read. */
  color: var(--ink-soft); opacity: 1;
}
.input:focus { border-color: var(--pine-500); box-shadow: 0 0 0 3px rgba(45,106,79,.12); }
textarea.input { min-height: 110px; resize: vertical; }
.checkbox { display: flex; gap: 8px; align-items: flex-start; font-size: .95rem; }
.form-error { color: var(--danger); font-size: .9rem; margin-top: 4px; }

/* ---------- Cards & layout ---------- */
.page { max-width: 1080px; margin: 0 auto; padding: 28px 18px 60px; }
.page-narrow { max-width: 460px; margin: 0 auto; padding: 40px 18px 70px; }
.center { text-align: center; }
/* The border was a hardcoded #E8F0EB one shade off --line, which is how a palette acquires a
   second hairline nobody chose. Flat plus a real hairline is the default now; --shadow is a
   1px hint rather than the 14px halo it was, so the card still separates from --paper. */
.card { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: var(--s5); }
.card + .card { margin-top: 16px; }
.card-title { margin: 0 0 12px; font-size: 1.1rem; }

/* ---------- Flash ---------- */
.flash-stack { position: fixed; top: calc(var(--topbar-h) + 12px); left: 50%; transform: translateX(-50%); z-index: 900; display: flex; flex-direction: column; gap: 8px; width: min(520px, 92vw); }
.flash { padding: 11px 16px; border-radius: 10px; box-shadow: var(--shadow-lg); font-weight: 500; background: var(--card); border-left: 4px solid var(--info); animation: flash-in .25s ease; }
.flash-ok { border-left-color: var(--ok); }
.flash-warn { border-left-color: var(--amber); }
.flash-error { border-left-color: var(--danger); }
@keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } }

/* ---------- Explore (map home) ---------- */
/**
 * The map page is exactly one screen tall, and it is the SHELL that guarantees it.
 *
 * This used to be `height: calc(100dvh - var(--topbar-h))`, which subtracts the top bar and
 * nothing else. The moment a conditions notice appears — "Heavy rush expected this weekend",
 * 57 pixels — the page became 57 pixels taller than the screen, and the bottom of the sheet
 * hung off the end of it. With a 296px peek that was merely untidy. Collapse the sheet to a
 * 58px strip and the strip itself is below the fold: the control that opens the list is
 * invisible until you scroll, on a page that is not supposed to scroll.
 *
 * Laying the shell out as a flex column makes the map take whatever is left over, whatever
 * is above it, without anything having to know the height of anything else.
 */
body.page-home { display: flex; flex-direction: column; min-height: 100vh; min-height: 100dvh;
                 max-height: 100dvh; overflow: hidden; }
body.page-home > main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.explore { display: flex; flex: 1 1 auto; min-height: 0;
           height: calc(100vh - var(--topbar-h)); height: calc(100dvh - var(--topbar-h));
           overflow: hidden; }
body.page-home .explore { height: auto; }
.panel { width: 400px; min-width: 330px; display: flex; flex-direction: column; background: var(--card); border-right: 1px solid #E8F0EB; box-shadow: 4px 0 18px rgba(20,52,43,.05); z-index: 10; }
.sheet-grip { display: none; }
.sheet-toggle { display: none; }
.panel-head { padding: 12px 14px 0; background: var(--card); }
.searchbox { position: relative; display: flex; align-items: center; gap: 9px; border: 1.5px solid transparent; border-radius: 999px; padding: 11px 16px; background: var(--mist); color: var(--ink-soft); transition: background .15s, border-color .15s, box-shadow .15s; }
.searchbox:focus-within { border-color: var(--pine-500); background: #fff; box-shadow: 0 0 0 4px rgba(45,106,79,.10), var(--shadow); }
.searchbox input { border: 0; outline: 0; background: transparent; flex: 1; font-size: .98rem; color: var(--ink); min-width: 0; }
.suggest { position: absolute; top: calc(100% + 6px); left: 0; right: 0; background: #fff; border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow-lg); overflow: hidden; z-index: 60; }
.suggest-item { display: block; width: 100%; text-align: left; padding: 11px 13px; border: 0; background: none; cursor: pointer; }
.suggest-item:hover { background: var(--mist); }
.suggest-item small { color: var(--ink-soft); }

/* Chip rail: one scrollable row, edge fades + arrows, never crops silently */
.chip-rail { position: relative; margin: 0 -14px; }
.chips { display: flex; gap: 7px; overflow-x: auto; padding: 10px 14px 12px; scrollbar-width: none; scroll-behavior: smooth; overscroll-behavior-x: contain; scroll-snap-type: x proximity; cursor: grab; }
.chips.dragging { cursor: grabbing; scroll-behavior: auto; scroll-snap-type: none; }
.chips::-webkit-scrollbar { display: none; }
.chip {
  flex: 0 0 auto; scroll-snap-align: start; border: 1px solid transparent; background: var(--mist); color: var(--pine-700);
  border-radius: 999px; padding: 8px 15px; min-height: 38px; font-size: .87rem; font-weight: 600; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s, transform .1s, box-shadow .15s;
  -webkit-tap-highlight-color: transparent; user-select: none;
}
.chip:hover { background: #fff; border-color: var(--pine-500); }
.chip:active { transform: scale(.95); }
.chip.active { background: var(--grad); color: #fff; box-shadow: 0 2px 8px rgba(31, 122, 99, .30); }
.rail-fade { position: absolute; top: 0; bottom: 0; width: 42px; pointer-events: none; opacity: 0; transition: opacity .2s; z-index: 2; }
.rail-fade.left  { left: 0;  background: linear-gradient(90deg,  var(--card) 18%, transparent); }
.rail-fade.right { right: 0; background: linear-gradient(270deg, var(--card) 18%, transparent); }
.chip-rail.can-left  .rail-fade.left  { opacity: 1; }
.chip-rail.can-right .rail-fade.right { opacity: 1; }
.rail-btn {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--line);
  background: #fff; color: var(--pine-700); cursor: pointer; box-shadow: var(--shadow);
  display: none; align-items: center; justify-content: center; padding: 0; line-height: 1; font-size: 1rem;
}
.rail-btn.left { left: 4px; } .rail-btn.right { right: 4px; }
.rail-btn:hover { background: var(--mist); }
/**
 * THE ARROWS ARE VISIBLE WHENEVER THERE IS SOMEWHERE TO GO, NOT ONLY ON HOVER.
 *
 * They used to require `:hover` on the rail. Measured on a 1440px desktop: the chip row is
 * 1,862px of content in a 399px box, `scrollbar-width: none` hides the scrollbar, and the
 * arrows were `display: none` — so a mouse user saw a row of categories cut off mid-word with
 * NO affordance of any kind. It scrolled perfectly by wheel and by drag; nothing on screen
 * said so. "The filters are not moving" is what that looks like from the outside, and it is
 * the correct reading: a control you cannot discover does not work.
 *
 * Hover still darkens them. What hover no longer does is decide whether they exist.
 */
@media (hover: hover) and (pointer: fine) {
  .chip-rail.can-left  .rail-btn.left  { display: flex; }
  .chip-rail.can-right .rail-btn.right { display: flex; }
  /* Quiet until pointed at, so a permanent control does not become permanent noise. */
  .rail-btn { opacity: .78; transition: opacity .15s, background .15s; }
  .chip-rail:hover .rail-btn { opacity: 1; }
}

.panel-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 16px; font-size: .85rem; color: var(--ink-soft); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
#result-count { font-weight: 700; color: var(--pine-700); }
.sort-sel { border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink-soft); font-size: .84rem; font-weight: 600; padding: 4px 6px; cursor: pointer; }
.sort-sel:focus { border-color: var(--pine-500); outline: none; }

.chip-toggle { border-style: dashed; }
.chip-toggle.active { background: var(--pine-700); border-color: var(--pine-700); border-style: solid; color: #fff; }

.open-badge { font-size: .72rem; font-weight: 700; border-radius: 999px; padding: 1px 8px; }
.open-badge.is-open { background: var(--pine-100); color: var(--pine-700); }
.open-badge.is-closed { background: #F1E4E2; color: var(--danger); }
.panel-list { flex: 1; overflow-y: auto; padding: 10px 12px calc(24px + env(safe-area-inset-bottom)); overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }

.result-card {
  position: relative;
  display: flex; gap: 12px; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius);
  background: #fff; margin-bottom: 10px; cursor: pointer;
  /* Only what actually changes. Listing box-shadow here meant every row carried a shadow
     transition it never used, and transform is on :active where it needs no easing at all. */
  transition: border-color .15s, color .15s;
  content-visibility: auto; contain-intrinsic-size: auto 84px;
  -webkit-tap-highlight-color: transparent;
}
/**
 * HOVER IS GATED, PRESS IS NOT.
 *
 * A hover rule outside a query is dead code on a touch device that also leaves the last-tapped
 * row stuck looking hovered, because there is no pointer to leave. And the lift was the
 * expensive kind: `box-shadow` and `transform` transitioning together on every row of a
 * scrolling list. Colour and border only now, 150ms, desktop only.
 *
 * The press state stays ungated: on touch it is the ONLY feedback there is, and it confirms
 * the tap before the network answers.
 */
@media (hover: hover) {
  .result-card:hover, .result-card.hover { border-color: var(--pine-500); }
  .result-card:hover .result-name { color: var(--pine-500); }
}
.result-card.hover { border-color: var(--pine-500); }   /* set by the map, not by a pointer */
.result-card:active { transform: scale(.99); }
.result-thumb { flex: 0 0 60px; height: 60px; border-radius: 14px; background: var(--mist); display: flex; align-items: center; justify-content: center; color: var(--pine-500); font-size: 1.4rem; overflow: hidden; }
.result-thumb img { width: 100%; height: 100%; object-fit: cover; }
.result-body { flex: 1; min-width: 0; }

/**
 * THE PROXIMITY RULE, WHICH IS THE WHOLE OF "DOES THIS LOOK ORDERED".
 *
 * The gap separating two groups must be at least twice the largest gap inside either group.
 * Here: name -> 4 -> what and where -> 8 -> rating. The single 8px gap is what tells the eye
 * the name owns the line beneath it and not the one below that. Before, every gap in the card
 * was 3px, which is not a hierarchy — it is a stack.
 */
.result-name { font-size: var(--t-card); font-weight: 700; color: var(--pine-900); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.result-sub { font-size: var(--t-meta); color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: var(--s1); }
/* HOW FAR, FIRST IN THE LINE AND IN THE INK COLOUR.
   Distance is the reason somebody chose this sort, so it leads the meta row rather than
   trailing it, and it is the one part of that row set in --ink rather than --ink-soft. It is
   also the only figure on the card that changes with where the reader is standing. */
.result-dist { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The sentence shown when a location is being asked for or was refused. It sits under the
   count row rather than replacing it, so the results stay on screen while it is read. */
.sort-note { margin: var(--s1) 0 0; max-width: 46ch; }
.result-rating { display: flex; align-items: center; gap: 5px; font-size: var(--t-meta); margin-top: var(--s2); }
.stars { color: var(--star); letter-spacing: 1px; font-size: .9rem; }
/* Ratings line up in a scrolling list, and proportional figures make the column jitter as
   rows pass under the thumb. One property, and the numbers hold still. */
.rating-num, .rating-count { font-variant-numeric: tabular-nums; }
.rating-num { font-weight: 700; }
.rating-count { color: var(--ink-soft); }
.result-card.sponsored { background: linear-gradient(160deg, #FFFDF4, #FDF6E3); border-color: #EFDFB4; position: relative; }
/* 12px floor, not .68rem (10.9px at a 16px root). "Sponsored" is a DISCLOSURE — the one label
   on a result a reader is entitled to be able to read, and it was the smallest text on the
   page. Padding raised with it so the pill still looks like a pill. */
.sponsored-tag { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: #8a6414; background: #FBEED3; padding: 2px 8px; border-radius: 999px; }
/*
 * THE SITE'S OWN MESSAGE LOOKS LIKE THE SITE, NOT LIKE AN ADVERTISEMENT.
 *
 * Amber-on-cream is the colour this site uses for "somebody paid for this" and readers learn
 * it fast — which is exactly why "From Pakistan360" must not wear it. In pine it reads as a
 * note from the site, and it is not shouted: sentence case rather than uppercase, because a
 * disclosure is shouted and this is not a disclosure.
 */
.sponsored-tag.is-house {
  color: var(--pine-700); background: var(--mist);
  text-transform: none; letter-spacing: 0;
}
.empty-note { text-align: center; color: var(--ink-soft); padding: 30px 10px; }

/* Skeleton loaders (perceived speed on slow networks) */
.sk-card { display: flex; gap: 12px; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius); background: #fff; margin-bottom: 10px; }
/**
 * THE SKELETON IS STATIC ON PURPOSE, AND THIS IS A REVERSAL.
 *
 * It shimmered: a gradient swept across every placeholder row on a 1.15s infinite loop. The
 * intent was right — perceived speed on a slow network — but an INFINITE animation never lets
 * the compositor idle, and it is running at exactly the moment the device is busiest, on
 * exactly the mid-range Android this site is built for. Twenty rows of it is twenty layers
 * being recomposited every frame while the network request it is apologising for is in flight.
 *
 * A flat block at the true dimensions of the content buys most of the perceived speed — the
 * value is in reserving the space, not in the sparkle — and costs nothing.
 */
.sk { position: relative; overflow: hidden; background: #EDF2EF; border-radius: 8px; }
.sk-thumb { flex: 0 0 58px; height: 58px; border-radius: 10px; }
.sk-line { height: 12px; margin: 5px 0; }
.sk-body { flex: 1; }

.mapwrap { flex: 1; position: relative; background: var(--mist); }
#map { position: absolute; inset: 0; }
/* No positioning here on purpose. The button is handed to MapLibre as a control
   (MVMap.adoptControl) and stacked with the zoom and compass buttons — it used to be
   pinned at `bottom: 104px`, which landed it squarely on top of the `+` on every desktop
   size, because the compact attribution notice lifts that whole stack. */
.mv-ctrl { margin: 0 10px 10px 0; }
.locate-btn {
  width: 44px; height: 44px;
  border-radius: 12px; border: 1px solid var(--line); background: #fff; color: var(--pine-700);
  cursor: pointer; box-shadow: var(--shadow); display: flex; align-items: center; justify-content: center;
  transition: background .15s, transform .1s;
}
.locate-btn:hover { background: var(--mist); }
.locate-btn:active { transform: scale(.93); }
.locate-btn.active { color: #2A7FFF; border-color: #2A7FFF; }
.locate-btn.locating { animation: loc-spin 1s linear infinite; }
.locate-btn.loc-error { color: var(--danger); border-color: var(--danger); }
@keyframes loc-spin { to { transform: rotate(360deg); } }

/* User location: unmistakable pulsing blue dot, always above every pin */
.user-dot {
  width: 18px; height: 18px; border-radius: 50%;
  background: #2A7FFF; border: 3.5px solid #fff;
  box-shadow: 0 0 0 1.5px rgba(42,127,255,.55), 0 2px 8px rgba(20,52,43,.35);
  position: relative;
}
.user-dot::before {
  content: ""; position: absolute; inset: -3.5px; border-radius: 50%;
  background: rgba(42,127,255,.35);
  animation: user-pulse 2.4s ease-out infinite;
  z-index: -1;
}
@keyframes user-pulse {
  0%   { transform: scale(1);    opacity: .9; }
  70%  { transform: scale(3.1);  opacity: 0; }
  100% { transform: scale(3.1);  opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .user-dot::before { animation: none; opacity: .35; transform: scale(1.6); } }

/* Map popup */
.maplibregl-popup-content { border-radius: 16px !important; box-shadow: var(--shadow-lg) !important; padding: 12px 14px !important; font-family: var(--font) !important; min-width: 200px; }
.popup-name { font-weight: 700; color: var(--pine-900); margin-bottom: 2px; }
.popup-cat { font-size: .8rem; color: var(--ink-soft); margin-bottom: 6px; }
.popup-actions { display: flex; gap: 8px; margin-top: 8px; }
.marker-pin { cursor: pointer; }

/* ---------- Auth & simple pages ---------- */
.auth-card { margin-top: 26px; }
.auth-title { margin: 0 0 18px; }
.auth-alt { text-align: center; margin-top: 16px; font-size: .93rem; color: var(--ink-soft); }
.err-code { font-size: 5rem; margin: 40px 0 0; color: var(--pine-100); -webkit-text-stroke: 2px var(--pine-500); }
.err-msg { font-size: 1.1rem; color: var(--ink-soft); margin-bottom: 26px; }

/* ---------- Business page ---------- */
.crumbs { font-size: .88rem; color: var(--ink-soft); margin-bottom: 12px; }
.biz-page { max-width: 1150px; }
.biz-head { display: flex; gap: 20px; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; }
.biz-name { margin: 0 0 4px; font-size: 1.65rem; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.biz-sub { color: var(--ink-soft); margin-bottom: 10px; }
.biz-ratings { display: flex; gap: 22px; flex-wrap: wrap; margin-bottom: 14px; }
.rating-block { display: flex; align-items: center; gap: 6px; }
.rating-block .stars { font-size: 1.05rem; }
.rating-google { opacity: .8; }
.biz-actions { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 16px; }
/* The phone number is an outlined primary, not a ghost: it is the second-most-wanted action
   on a listing and the number itself is the label, because on a directory it is data as well
   as a button. Pine, so it reads as the same family as Directions rather than a third idea. */
.btn-call { background: var(--card); color: var(--pine-700); border-color: var(--pine-500); font-weight: 700; }
.btn-call:hover { background: var(--mist); }

/* Inline notice — sits in the page flow, unlike the floating .flash toasts. */
.notice { padding: 12px 14px; border-radius: 10px; background: var(--card); border: 1px solid var(--line);
          border-left: 4px solid var(--info); margin-bottom: 14px; font-size: .93rem; }
.notice-warn { border-left-color: var(--amber); background: #FFFBF2; }
html[dir="rtl"] .notice { border-left: 1px solid var(--line); border-right: 4px solid var(--info); }
html[dir="rtl"] .notice-warn { border-right-color: var(--amber); }
.biz-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; max-width: 260px; }
.biz-photos img { width: 100%; height: 90px; object-fit: cover; border-radius: 10px; }
.biz-cols { display: flex; gap: 18px; margin-top: 18px; align-items: flex-start; }
.biz-main { flex: 1.6; min-width: 0; }
.biz-side { flex: 1; min-width: 280px; max-width: 380px; position: sticky; top: calc(var(--topbar-h) + 14px); }
.biz-side .card + .card, .biz-side .card + div .card { margin-top: 14px; }
.map-card { padding: 10px; }
#mini-map { height: 260px; border-radius: 10px; overflow: hidden; }
.route-info { padding: 10px 4px 0; font-size: .95rem; }
.hours-table { width: 100%; border-collapse: collapse; font-size: .93rem; }
.hours-table td { padding: 5px 0; border-bottom: 1px dashed var(--line); }
.hours-table td:last-child { text-align: right; color: var(--ink-soft); }
.hours-table tr.today td { font-weight: 700; color: var(--pine-700); }
/* The "next day" marker on an after-midnight closing time. Smaller and quieter than the
   time itself — it is a clarification, not a second fact — and on its own line at narrow
   widths so it never pushes the time out of the column. */
.hours-plus { display: inline-block; margin-inline-start: 6px; font-size: .82em; opacity: .8; white-space: nowrap; }
@media (max-width: 380px) { .hours-plus { display: block; margin-inline-start: 0; } }
.contact-list { list-style: none; margin: 0; padding: 0; }
.contact-list li { padding: 5px 0; }
.service-list { list-style: none; margin: 0; padding: 0; }
.service-list li { display: flex; justify-content: space-between; gap: 14px; padding: 10px 0; border-bottom: 1px dashed var(--line); }
.service-list li:last-child { border-bottom: 0; }
.service-price { font-weight: 700; color: var(--pine-700); white-space: nowrap; }
.service-price small { font-weight: 500; color: var(--ink-soft); }
hr.soft { border: 0; border-top: 1px solid var(--line); margin: 20px 0; }

/* ---- Catalog (tailored offerings) ---- */
.cat-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.cat-kind-form { display: flex; align-items: center; gap: 7px; }
.cat-sec-title { margin: 18px 0 4px; font-size: 1.02rem; color: var(--pine-700); }
.cat-sections, .cat-suggest { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; margin: 10px 0 0; }
.cat-addsec { display: flex; gap: 5px; }
.cat-addsec .input { max-width: 150px; }
.chip-static { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 999px; background: var(--card); font-size: .88rem; }
.chip-x { border: 0; background: none; cursor: pointer; color: var(--ink-soft); padding: 0 2px; font-size: .82rem; }
.chip-x:hover { color: var(--danger); }
.chip-ghost { padding: 5px 11px; border: 1px dashed var(--line); border-radius: 999px; background: none; cursor: pointer; font-size: .86rem; color: var(--ink-soft); }
.chip-ghost:hover { border-color: var(--pine-500); color: var(--pine-700); }
.item-off { opacity: .55; }
.badge-soft { display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--pine-100); color: var(--pine-700); font-size: .78rem; font-weight: 600; }
.menu-list li { padding: 8px 0; }
.product-grid, .listing-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 10px; margin: 8px 0 4px; }
.listing-grid { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.product-card, .listing-card { border: 1px solid var(--line); border-radius: 12px; padding: 12px; background: var(--card); display: flex; flex-direction: column; gap: 6px; }
.listing-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.listing-meta { display: flex; gap: 12px; flex-wrap: wrap; color: var(--ink-soft); font-size: .88rem; }
.result-match { font-size: .82rem; color: var(--pine-700); font-weight: 600; margin: 1px 0; }

/* ---- Collections ---- */
.col-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 14px; }
.col-card { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; text-decoration: none; color: var(--ink); }
.col-card:hover { text-decoration: none; border-color: var(--pine-500); box-shadow: var(--shadow-lg); transform: translateY(-2px); transition: all .15s; }
.col-emoji { font-size: 1.8rem; }
.col-title { font-size: 1.05rem; color: var(--pine-900); }
.col-hero { display: flex; gap: 14px; align-items: center; margin: 6px 0 4px; }
.col-hero-emoji { font-size: 2.6rem; }
.col-intro { color: var(--ink-soft); margin: 10px 0 16px; }
.col-item { display: flex; gap: 14px; align-items: flex-start; position: relative; }
.col-rank {
  flex: 0 0 28px; height: 28px; border-radius: 50%; background: var(--grad); color: #fff;
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: .95rem;
}
.col-blurb { margin: 6px 0 0; color: var(--ink); }

/* ---- Leaderboards (/top) ---- */
.top-hero { margin-bottom: 14px; }
.top-board { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.top-row { display: flex; gap: 14px; align-items: flex-start; }
.top-rank {
  flex: 0 0 30px; height: 30px; border-radius: 50%; background: var(--mist); color: var(--pine-700);
  display: inline-flex; align-items: center; justify-content: center; font-weight: 800; font-size: .95rem;
}
.top-rank-1 { background: linear-gradient(135deg,#F6D365,#E9B44C); color: #4A3608; }
.top-rank-2 { background: linear-gradient(135deg,#E3E7EA,#C3CBD1); color: #37424A; }
.top-rank-3 { background: linear-gradient(135deg,#E5C0A0,#C98F63); color: #33200E; }  /* 5.6:1; #4A2E15 was 4.5 */
.top-row-podium { box-shadow: var(--shadow-lg); }
.top-score { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin: 3px 0 2px; }
/* 0.8rem bold is NOT "large text" under WCAG, so these pills need the full 4.5:1. */
.trend { font-size: .8rem; font-weight: 700; padding: 1px 7px; border-radius: 999px; white-space: nowrap; }
.trend-up { background: #E4F3E9; color: #1E6538; }      /* 6.2:1 — var(--ok) was only 3.8:1 at this size */
.trend-down { background: #FBE9E7; color: #A33326; }    /* 5.9:1 — var(--danger) was 4.1:1 */
.trend-flat { background: var(--mist); color: var(--ink-soft); }
.rating-recent { margin: 8px 0 0; font-size: .9rem; color: var(--ink-soft); display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }

/* ---- Personal lists ---- */
.list-row { display: flex; gap: 14px; align-items: center; }
.list-emoji { flex: 0 0 44px; height: 44px; border-radius: 12px; background: var(--mist);
              display: flex; align-items: center; justify-content: center; font-size: 1.4rem; }
.list-note-form { display: flex; gap: 6px; margin-top: 8px; }
.list-note-form .input { flex: 1; min-width: 0; }
.list-item-tools { display: flex; flex-direction: column; gap: 4px; flex: 0 0 auto; }
.share-row { display: flex; flex-wrap: wrap; gap: 8px; }
.share-wa:hover { border-color: #25D366; color: #128C4A; }
.share-fb:hover { border-color: #1877F2; color: #1877F2; }
.share-x:hover  { border-color: #111; color: #111; }

/* Add-to-list opens in place, no dropdown scripting, so it survives a strict CSP. */
.add-to-list { margin-top: 12px; }

/**
 * THE TWO DISCLOSURES SIT IN THE ACTION ROW, NOT UNDER IT.
 *
 * They used to be siblings after `.biz-actions`, and a <details> is a block, so each took a
 * whole line: five buttons, then "Add to a list" alone on a row, then "Add to a route" alone
 * on another. They are actions on the place like every other button there, and they now live
 * inside the row — which means undoing the standalone margin that used to separate them.
 */
/**
 * THE ICON SET — see `icon()` in Core/helpers.php for why these are SVG and not emoji.
 *
 * `flex: none` because these live inside buttons that are already inline-flex, and an icon
 * that can be squeezed by a long label is an icon that turns into an oval in Urdu.
 *
 * `vertical-align` matters only where one lands in flowing text rather than a flex button —
 * the signed-out "Add to a list" sentence is exactly that case. -.15em puts the optical
 * centre of the glyph on the text's own centre rather than its baseline.
 */
.ico { flex: none; vertical-align: -.15em; }

/* The refusal that opens out of a queue row. Closed it is a button the width of its words, so
   the table stays one line per listing; open it is a labelled box wide enough to write a real
   sentence in, because the sentence is what gets sent to the person who submitted it. */
.reject-inline > summary { list-style: none; cursor: pointer; display: inline-flex; }
.reject-inline > summary::-webkit-details-marker { display: none; }
.reject-body { display: flex; flex-wrap: wrap; align-items: end; gap: 6px; margin-top: 8px;
               padding: 10px; border: 1px solid var(--line); border-radius: 10px;
               background: var(--paper); min-width: 320px; }
.reject-body .input { flex: 1 1 240px; min-width: 0; }
.reject-body .lbl { flex: 1 0 100%; font-size: .82rem; color: var(--ink-soft); }
/* Never let an icon carry the meaning alone: it is aria-hidden, so a control with no visible
   words would be announced as nothing at all. This is a note, not a rule the browser applies. */

.biz-actions > .add-to-list { margin-top: 0; }
/* Closed, it is a button and should be sized like one rather than stretched by the row. */
.biz-actions > .add-to-list:not([open]) { flex: 0 0 auto; }
/**
 * OPEN, IT TAKES THE WHOLE ROW. The panel underneath holds a select, a text input and a
 * submit; in a shared row it would be squeezed into whatever width was left over beside a
 * button. `order: 1` moves the opened one below the buttons rather than shoving them along,
 * so the row a person was just looking at does not rearrange itself under the tap.
 */
.biz-actions > .add-to-list[open] { flex: 1 0 100%; order: 1; }
/* The signed-out line is a sentence, not a button, so it gets its own row and no more. */
/* `.add-to-list-guest` no longer exists in the markup — see the note in business.php: a
   signed-out visitor is offered one way to keep a place, not two. The selectors are kept
   because `.add-to-list-guest` is still the right class for whatever surface introduces lists
   to a guest next, and a rule that describes a component correctly costs nothing. */
/*
 * THE THIN-LISTING BLOCK — the page's honest state on 83% of this directory.
 *
 * Deliberately NOT a card. It sits inside the header card, under the name, above the actions,
 * because it is a statement about the thing the page is about rather than a separate object on
 * the page. A card here would make it read as an advertisement or a warning; a tinted panel
 * with a rule down its inline start reads as a note.
 *
 * Amber, not red. Nothing is wrong — the listing is real, the location is right, there is just
 * not much of it yet. Red would tell a shopkeeper their page is broken, and the first thing
 * they would do is ring somebody about it.
 */
.biz-thin {
  margin: 14px 0 4px; padding: 14px 16px;
  background: var(--mist); border-radius: 12px;
  border-inline-start: 3px solid var(--amber, #C77700);
}
.biz-thin-said    { margin: 0; font-weight: 600; }
.biz-thin-missing { margin: 4px 0 0; color: var(--ink-soft); }
.biz-thin .btn    { margin-top: 12px; }
/* A hint under a button is a footnote to it, so it is tied to the button rather than floating
   below the panel. */
.biz-thin-note    { margin: 8px 0 0; }

.biz-actions > .add-to-list-guest { flex: 1 0 100%; order: 1; margin: 4px 0 0; }
.add-to-list > summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; }
.add-to-list > summary::-webkit-details-marker { display: none; }
.add-to-list-body { margin-top: 10px; padding: 12px; border: 1px solid var(--line);
                    border-radius: 12px; background: var(--mist); display: flex; flex-direction: column; gap: 6px; }
.add-to-list-new { margin-top: 8px; }
.add-to-list-row { display: flex; gap: 6px; }
.add-to-list-row .input { flex: 1; min-width: 0; }
.add-to-list-guest { margin: 12px 0 0; }

/* ---- Data desk: everything here is about removing keystrokes ---- */
.bulk-fill { border: 1px solid var(--line); border-radius: 12px; background: var(--mist); padding: 12px 14px; margin-bottom: 14px; }
.bulk-fill-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.bulk-fill-row .input { flex: 0 1 200px; }
.bulk-presets { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 10px; }
.bulk-presets .chip { cursor: pointer; font-size: .84rem; }
.hours-cell { display: flex; gap: 5px; align-items: center; }
.hours-cell .input { flex: 1; min-width: 0; }
.copy-above { flex: 0 0 auto; padding-inline: 9px; }
/* A standalone icon link is a target in its own right, not a word inside a sentence,
   so WCAG 2.2's 24x24 minimum applies to it. It was 18x17. */
.lookup { display: inline-flex; align-items: center; justify-content: center;
          min-width: 24px; min-height: 24px; text-decoration: none;
          margin-inline-start: 2px; vertical-align: middle; }
/* A row whose value the parser refused: keep it visible and keep the typed text. */
.row-rejected > td { background: #FFF7ED; }
.reject-note { margin-top: 5px; color: var(--danger); }
.report-list { margin-top: 14px; }
.report-list > summary { cursor: pointer; }
/* Google's content gets a visible edge of its own, which is both clearer for the
   reader and what Google's attribution rules ask for. */
.google-block { border: 1px solid var(--line); border-left: 4px solid #4285F4; }
.google-attrib { border-top: 1px solid var(--line); padding-top: 10px; }
/* WCAG 2.2 target size (2.5.8): these anchors sit in flex/grid rows, so they are not
   "links inline in a sentence" and the 24px minimum applies. They measured 23px. */
.saved-name a, .google-block li > a, .list-row .saved-name a {
  display: inline-flex; align-items: center; min-height: 24px;
}
html[dir="rtl"] .google-block { border-left: 1px solid var(--line); border-right: 4px solid #4285F4; }
.col-map-pin {
  width: 26px; height: 26px; border-radius: 50%; border: 2px solid #fff; cursor: pointer;
  background: var(--pine-500); color: #fff; font-weight: 700; font-size: .82rem;
  display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 6px rgba(20,52,43,.4);
}
.col-map-pin:hover { background: var(--pine-700); transform: scale(1.15); }
.col-admin-item { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; border-bottom: 1px dashed var(--line); }
.col-admin-item:last-of-type { border-bottom: 0; }
.col-blurb-form { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.col-blurb-form .input { flex: 1; min-width: 160px; }
.col-admin-actions { display: flex; gap: 5px; }

/* ---- Filter row (Open now / Deals above categories) ---- */
.filter-row { display: flex; gap: 8px; align-items: center; margin: 10px 0 2px; flex-wrap: wrap; }
.filter-label { font-size: .82rem; font-weight: 600; color: var(--ink-soft); flex-shrink: 0; }
.filter-row .chip { flex-shrink: 0; }
.filter-sep { width: 1px; align-self: stretch; background: var(--line); flex-shrink: 0; margin: 4px 2px; }
.price-dd { position: relative; flex-shrink: 0; }
.price-dd-menu {
  position: absolute; top: calc(100% + 6px); inset-inline-start: 0; z-index: 80;
  min-width: 200px; background: #fff; border: 1px solid var(--line); border-radius: 12px;
  box-shadow: var(--shadow-lg); padding: 6px; display: flex; flex-direction: column; gap: 2px;
}
.price-opt {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: start;
  border: 0; background: none; cursor: pointer; padding: 9px 10px; border-radius: 8px; font-size: .95rem;
}
.price-opt:hover { background: var(--mist); }
.price-opt .price-check { margin-inline-start: auto; opacity: 0; color: var(--pine-700); font-weight: 700; }
.price-opt.active { background: var(--pine-100); }
.price-opt.active .price-check { opacity: 1; }
.price-dd-menu[hidden] { display: none; }   /* author rule must beat our own display:flex */
.price-dd-menu { max-height: min(70vh, 460px); overflow-y: auto; overscroll-behavior: contain; }
.price-dd-menu.dd-up { top: auto; bottom: calc(100% + 6px); }   /* flipped when there is no room below */
.price-dd-foot { display: flex; gap: 6px; align-items: center; border-top: 1px solid var(--line); margin-top: 4px; padding-top: 5px; }
.price-opt-clear { justify-content: center; color: var(--ink-soft); flex: 1; }
.price-dd-done, .facet-dd-done { flex-shrink: 0; }
.dd-caret { font-size: .8em; opacity: .7; }
/* Facet menu: same control, but grouped (Food type / Good for / Facilities) and taller */
.facet-dd-menu { min-width: 232px; max-height: min(62vh, 420px); overflow-y: auto; overscroll-behavior: contain; }
.facet-dd-menu .dd-grp { font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--ink-soft); padding: 8px 10px 3px; }
.facet-dd-menu .dd-grp:first-child { padding-top: 3px; }
.facet-dd-menu .price-opt { padding: 8px 10px; }
.facet-dd-menu .price-glyph { color: inherit; width: 1.2em; text-align: center; }
.facet-dd-menu .price-dd-foot { position: sticky; bottom: -6px; background: #fff; }
#facet-dd[hidden] { display: none; }
@media (max-width: 860px) { .facet-dd-menu { max-height: 50vh; } }

/* ---- Facets (cuisine / type / facilities) ---- */
.facet-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.facet-chip { display: inline-flex; align-items: center; gap: 5px; padding: 4px 11px; border-radius: 999px;
  background: var(--pine-100); color: var(--pine-700); font-size: .84rem; font-weight: 600; white-space: nowrap; }
.facet-chip:hover { background: var(--pine-500); color: #fff; text-decoration: none; }
.facet-list { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px 14px; }
.facet-list li { display: flex; align-items: center; gap: 8px; font-size: .94rem; }
.facet-ico { width: 1.3em; text-align: center; }
.facet-grp-title { margin: 12px 0 6px; font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--ink-soft); }
.check-item { display: flex; align-items: center; gap: 8px; padding: 6px 9px; border: 1px solid var(--line); border-radius: 9px; cursor: pointer; font-size: .93rem; background: var(--card); }
.check-item:hover { border-color: var(--pine-500); }
.check-item input { accent-color: var(--pine-700); flex-shrink: 0; }
.check-item:has(input:checked) { background: var(--pine-100); border-color: var(--pine-500); font-weight: 600; }

/* ---- Price level (₨ … ₨₨₨₨) ---- */
.price-glyph { color: var(--ok); font-weight: 700; letter-spacing: .5px; white-space: nowrap; }
.biz-price { display: flex; gap: 7px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
.price-pick { display: flex; gap: 7px; }
.price-pick input { position: absolute; opacity: 0; pointer-events: none; }
.price-pick label.chip { cursor: pointer; user-select: none; }
.price-pick input:checked + label.chip { background: var(--grad); color: #fff; border-color: transparent; box-shadow: 0 2px 8px rgba(31,122,99,.30); }
.price-pick input:focus-visible + label.chip { outline: 2.5px solid var(--pine-500); outline-offset: 2px; }

/* ---- Promotions / deals ---- */
.deal-badge { display: inline-block; padding: 1px 8px; border-radius: 999px; background: #FBEED3; color: #8a5a14; font-size: .76rem; font-weight: 700; }
.promo-banner { display: flex; gap: 12px; align-items: flex-start; background: linear-gradient(135deg, #FFF9EE 0%, #FFF3DC 100%); border-color: #F2DEB4; }
.promo-flame { font-size: 1.6rem; line-height: 1.2; }
.promo-title { font-size: 1.05rem; color: var(--pine-900); }
.promo-meta { display: flex; gap: 8px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
.promo-old { color: var(--ink-soft); }
.promo-row { display: flex; gap: 12px; align-items: center; padding: 9px 0; border-bottom: 1px dashed var(--line); }
.promo-row:last-of-type { border-bottom: 0; }
.deal-card { display: flex; gap: 14px; align-items: center; text-decoration: none; color: var(--ink); }
.deal-card:hover { text-decoration: none; border-color: var(--amber); box-shadow: var(--shadow-lg); transform: translateY(-1px); transition: all .15s; }
.deal-card .saved-body { display: flex; flex-direction: column; gap: 2px; }

/* ---- Suggest an edit ---- */
.sug-card summary { cursor: pointer; font-weight: 600; color: var(--ink-soft); list-style: none; }
.sug-card summary::-webkit-details-marker { display: none; }
.sug-card summary:hover { color: var(--pine-700); }
.sug-card[open] summary { color: var(--pine-700); margin-bottom: 4px; }

/* ---- Saved places (♥) ---- */
.fav-heart {
  position: absolute; top: 4px; inset-inline-end: 6px; z-index: 2;
  border: 0; background: none; cursor: pointer;
  /* 20x18 was below the 24x24 minimum — a thumb aiming at it hit the card and
     navigated instead. The glyph is unchanged; only the hit area grew. */
  min-width: 34px; min-height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.05rem; line-height: 1; color: var(--ink-soft); opacity: .55;
  transition: transform .12s, color .12s, opacity .12s;
}
/* On touch there is no hover to reveal it, so it must always be legible. */
@media (hover: none) { .fav-heart { opacity: .8; } }
.result-card:hover .fav-heart, .fav-heart.is-saved { opacity: 1; }
.fav-heart:hover { transform: scale(1.2); color: var(--danger); }
.fav-heart.is-saved { color: var(--danger); }
.fav-btn .fav-ico { color: var(--danger); }
.fav-btn.is-saved { background: #F6DDD9; }
.fav-pop.is-saved { color: var(--danger); }
.saved-card { display: flex; gap: 14px; align-items: center; }
.saved-thumb { flex: 0 0 64px; height: 64px; border-radius: 12px; background: var(--mist); overflow: hidden; display: flex; align-items: center; justify-content: center; }
.saved-thumb img { width: 100%; height: 100%; object-fit: cover; }
.saved-emoji { font-size: 1.5rem; }
.saved-body { flex: 1; min-width: 0; }
.saved-name { font-weight: 700; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Map credit: collapsed to a small ⓘ until tapped (OSM licence requires the credit to remain reachable) */
.maplibregl-ctrl-attrib.maplibregl-compact:not(.maplibregl-compact-show) .maplibregl-ctrl-attrib-inner { display: none; }
.maplibregl-ctrl-attrib.maplibregl-compact { min-height: 44px; background: rgba(255,255,255,.75); }
/*
 * THE ⓘ IS THE ONLY WAY TO THE LICENCE, SO IT HAS TO BE HITTABLE.
 *
 * MapLibre draws this button at 24x24. That clears WCAG 2.5.8's 24px minimum and misses this
 * project's own 44px bar, and it was reported on all four map pages. It matters more than an
 * ordinary undersized control does: collapsing the credit to a symbol is only defensible
 * because tapping the symbol opens it, and a 24px target on a moving map beside a pan gesture
 * is a tap most people will miss at least once.
 *
 * The glyph is left at its own size; only the box grows, so nothing about the map's corner
 * looks different.
 */
.maplibregl-ctrl-attrib-button {
  width: 44px; height: 44px;
  background-position: center; background-size: 20px 20px;
}
/*
 * AND THE CREDIT INSIDE IT, which is the link the licence is actually about.
 *
 * MapLibre sets its attribution text at 12px and the OpenStreetMap link measured 85x14 on
 * every page with a live map -- 104 of the site's 118 undersized targets were this one link,
 * repeated. Same argument as the landing page's credit: an attribution nobody can read or hit
 * is not much of an attribution, and this is the one piece of text on the site that is there
 * because of a licence rather than because somebody wanted it there.
 *
 * The hit box grows by padding and gives the space back by margin, so the corner plate does
 * not change size.
 */
.maplibregl-ctrl-attrib-inner { font-size: 14px; }
.maplibregl-ctrl-attrib-inner a {
  display: inline-block; padding: 15px 0; margin: -15px 0;
}

/* ---- Admin analytics ---- */
.filter-bar { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.filter-bar .chip { text-decoration: none; padding: 6px 13px; font-size: .88rem; }
.filter-bar .chip.active { background: var(--grad); color: #fff; }
.bar-pct { display: block; font-weight: 500; color: var(--ink-soft); font-size: .78rem; }
.heat-stage { position: relative; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: #fff; }
.heat-stage iframe { display: block; width: 100%; border: 0; pointer-events: none; }  /* preview only — never interactive */
.heat-stage canvas { position: absolute; inset: 0; pointer-events: none; }
.heat-legend { display: flex; align-items: center; gap: 8px; font-size: .85rem; color: var(--ink-soft); margin-top: 8px; }
.heat-scale { width: 160px; height: 10px; border-radius: 999px;
  background: linear-gradient(90deg, rgba(45,106,79,.45), rgba(56,161,105,.7), rgba(233,180,76,.85), rgba(224,123,57,.9), rgba(196,72,58,.95)); }

.range-links { display: flex; gap: 6px; }
.range-links .chip { padding: 5px 12px; font-size: .88rem; text-decoration: none; }
.range-links .chip.active { background: var(--grad); color: #fff; }
.legend-key { display: inline-block; width: 10px; height: 10px; border-radius: 3px; vertical-align: middle; }
.bar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; font-size: .92rem; }
.bar-label { flex: 0 0 40%; color: var(--ink); }
.bar-track { flex: 1; height: 10px; background: var(--mist); border-radius: 999px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--grad); border-radius: 999px; }
.bar-val { flex: 0 0 42px; text-align: end; font-weight: 700; color: var(--pine-700); }
.tag-cloud { display: flex; flex-wrap: wrap; gap: 7px; }
.tag-cloud .chip b { color: var(--pine-700); }

/* ---- Owner analytics ---- */
.stat-line { font-size: .88rem; color: var(--pine-700); font-weight: 600; margin-top: 3px; }
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 14px; }
.stat-tile { border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px; background: var(--mist); }
.stat-num { font-size: 1.15rem; font-weight: 700; color: var(--pine-900); }
.stat-label { font-weight: 600; font-size: .88rem; }
.stat-chart svg { display: block; }

.review { padding: 16px 0; border-bottom: 1px solid var(--line); }
.review:last-child { border-bottom: 0; }
.review-head { display: flex; gap: 11px; align-items: center; margin-bottom: 8px; }
/* .avatar lives with the rest of the avatar rules further down — it was defined twice,
   40px here and sized-by-modifier there, and only source order decided which won. */
.review-meta { display: flex; gap: 10px; align-items: center; font-size: .85rem; color: var(--ink-soft); }
.review-title { margin: 6px 0 2px; font-size: 1.02rem; }
.review-body { margin: 6px 0; }
.owner-reply { background: var(--mist); border-left: 3px solid var(--pine-500); border-radius: 8px; padding: 10px 13px; margin: 10px 0 0 18px; font-size: .95rem; }
.owner-reply p { margin: 5px 0 0; }
.review-actions { display: flex; gap: 16px; margin-top: 8px; }
.linklike-dark { background: none; border: 0; color: var(--pine-700); font-weight: 600; cursor: pointer; padding: 0; font-size: .88rem; }
.linklike-dark:hover { text-decoration: underline; }
.reply-form, .flag-form { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.reply-form .btn, .flag-form .btn { align-self: flex-start; }

/* Star rating input (RTL trick: reversed order + ~) */
.star-input { display: inline-flex; flex-direction: row-reverse; gap: 3px; font-size: 1.9rem; }
.star-input input { position: absolute; opacity: 0; pointer-events: none; }
.star-input label { color: #CFD9D3; cursor: pointer; transition: .1s; }
.star-input label:hover, .star-input label:hover ~ label,
.star-input input:checked ~ label { color: var(--star); }

.sponsored-card { background: #FFFDF4; border-color: #EAD9A8; }
.sponsored-body { display: block; color: inherit; margin-top: 6px; }
.sponsored-body:hover { text-decoration: none; }
.sponsored-body img { border-radius: 8px; margin-bottom: 8px; }
.sponsored-body p { margin: 4px 0 0; font-size: .9rem; color: var(--ink-soft); }
/*
 * SMALL, BUT NEVER HIDDEN -- AND 11.5px WAS CLOSER TO HIDDEN THAN TO SMALL.
 *
 * The original decision stands and is right: an advertisement that will not say why it is
 * there is the thing people install blockers to be rid of, so this link is quiet rather than
 * prominent. What was not decided was the number. `.74rem` computed to 11.84px, and the
 * variant inside `.sponsored-wrap` to 11.52px -- measured across seventeen pages at 320 and
 * 390px, they were 24 of the site's 24 text nodes under 14px.
 *
 * 14px is this project's own floor, set in tools/uxaudit and applied everywhere else. It
 * leaves the link the smallest thing on any page it appears on -- body text is 16-17px -- and
 * it is a disclosure a person can read on a mid-range Android handset held at arm's length,
 * which is the entire point of a disclosure. 13px was tried first and kept the tool reporting
 * it: a measuring instrument that always has something to say is one people stop reading, so
 * either the number meets the bar or the exception is written into the tool. This one meets
 * the bar. The hit area was already handled -- see the padding rule in the 920px block below.
 */
.ad-why { display: inline-block; margin-top: 8px; font-size: 14px; color: var(--ink-soft); text-decoration: underline; }
.ad-why:hover { color: var(--pine-700); }

@media (max-width: 920px) {
  /**
   * `align-items: stretch` IS THE FIX, and the bug it fixes is worth writing down.
   *
   * `.biz-cols` carries `align-items: flex-start` so that the sticky aside does not stretch to
   * the height of the main column. Correct for a ROW. But this block turns the container into
   * a COLUMN, and in a column the cross axis is the HORIZONTAL one — so `flex-start` stopped
   * meaning "do not stretch to full height" and started meaning "shrink to content WIDTH".
   *
   * `.biz-side` escaped it because it sets `width: 100%` explicitly. `.biz-main` did not, so
   * below 920px the description, the menu and the whole review section rendered in a
   * shrink-to-fit column — 316px inside a 724px page — while the map card underneath ran the
   * full width. Nothing errored; it just looked like two different pages stacked.
   *
   * `flex: 1.6` on .biz-main does nothing here either: flex-grow acts along the MAIN axis,
   * which in a column is height.
   */
  .biz-cols { flex-direction: column; align-items: stretch; }
  .biz-side { position: static; max-width: none; width: 100%; }
}

/* ---------- Dashboards (admin/owner/ads) ----------
   .admin-nav — the strip that used to sit at the top of every admin screen — is gone. It
   carried fourteen of thirty-seven links and wrapped onto three lines on a laptop, and it is
   replaced by .admin-rail at the end of this file, which carries all of them and folds into
   the same strip shape below 1100px. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin-bottom: 18px; }
.stat-card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; box-shadow: var(--shadow); color: inherit; }
.stat-card:hover { text-decoration: none; }
a.stat-card:hover { border-color: var(--pine-500); }
.stat-card b { font-size: 1.5rem; color: var(--pine-700); }
.stat-card span { font-size: .84rem; color: var(--ink-soft); }
.stat-warn { border-color: var(--amber); background: #FFFDF4; }
.admin-table { width: 100%; border-collapse: collapse; font-size: .92rem; }
.admin-table th { text-align: left; padding: 8px 10px; border-bottom: 2px solid var(--line); color: var(--ink-soft); font-size: .8rem; text-transform: uppercase; letter-spacing: .4px; }
.admin-table td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.admin-table tr:last-child td { border-bottom: 0; }
.table-card { overflow-x: auto; }
.input-sm { padding: 5px 8px; border-radius: 7px; font-size: .9rem; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 14px; }
.hours-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0 10px; }
.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px; }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.photo-cell { position: relative; }
.photo-cell img { width: 100%; height: 105px; object-fit: cover; border-radius: 10px; }
.photo-del { position: absolute; top: 6px; inset-inline-end: 6px; background: rgba(0,0,0,.55); color: #fff; border: 0; border-radius: 50%; width: 26px; height: 26px; cursor: pointer; }
.photo-del:hover { background: var(--danger); }
@media (max-width: 720px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }

/* ---------- Footer ---------- */
.footer { background: var(--pine-900); color: #B9CFC4; margin-top: 0; }
.footer-inner { max-width: 1080px; margin: 0 auto; padding: 18px; display: flex; justify-content: space-between; gap: 14px; flex-wrap: wrap; font-size: .88rem; }
.footer a { color: #DCEAE2; margin-left: 14px; }
body:has(.explore) .footer { display: none; }

/* ---------- Mobile: full-bleed map + draggable bottom sheet ---------- */
@media (max-width: 1400px) {
  .topbar { gap: 10px; padding: 0 12px; }
  .brand-name { font-size: 1.12rem; }
  .topnav { gap: 13px; }
  .topnav a:not(.btn) { font-size: .92rem; }
  .nav-user { padding-left: 10px; gap: 8px; }

  .explore { position: relative; display: block; }
  .mapwrap { position: absolute; inset: 0; }

  .panel {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 30;
    width: 100%; min-width: 0; height: 88%; max-height: 88%;
    border-right: 0; border-top: 1px solid var(--line);
    /* 14 rather than 24: a 24px radius on a full-width sheet clips the first result row's
       corners and reads as a modal that failed to open, rather than as a panel that lives
       here. And --shadow-sheet rather than a fourth hand-written shadow — it is the same
       intent, from the budget, with the Y already negative. */
    border-radius: var(--radius) var(--radius) 0 0; box-shadow: var(--shadow-sheet);
    transform: translateY(calc(100% - var(--sheet-peek)));
    /* 240ms, not 300: this is the most-repeated interaction on the site and the last 60ms is
       felt as lag rather than as smoothness once you have done it twenty times. */
    transition: transform .24s var(--ease);
    touch-action: none;
  }
  .panel.dragging { transition: none; }
  .panel.half { transform: translateY(52%); }
  .panel.full { transform: translateY(0); }
  .panel-list, .panel-meta, .panel-head { touch-action: auto; }

  .sheet-grip { display: block; padding: 9px 0 2px; cursor: grab; touch-action: none; }
  .sheet-grip::before { content: ""; display: block; width: 52px; height: 5px; border-radius: 999px; background: #CBDAD2; margin: 0 auto; }

  .panel-head { padding-top: 4px; }
  .chip { padding: 9px 16px; min-height: 40px; }

  /* Keep map controls visible above the sheet peek. Scoped to .mapwrap on
     purpose: unscoped, this lifted the zoom buttons off the bottom of every
     other map on the site — including the one on a business page, which has no
     sheet under it at all. */
  .mapwrap .maplibregl-ctrl-bottom-right { bottom: calc(var(--sheet-peek) + 2px) !important; }
  .mapwrap .maplibregl-ctrl-bottom-left  { bottom: calc(var(--sheet-peek) + 2px) !important; }
}
/* A phone on its side. The peek still has to clear the search box — a clipped input is
   worse than a tall strip — but the count row can go, since the map is showing the same
   answer right behind it. */
@media (max-width: 860px) and (max-height: 620px) {
  :root { --sheet-peek: 96px; }
  .panel:not(.half):not(.full) .panel-meta { display: none; }
}
.btn-wa { color: #128C50; border-color: #BFE3CF; }
.btn-wa:hover { background: #E9F7EF; }
.biz-name .open-badge { font-size: .74rem; vertical-align: middle; }

/* ---------- Review photos + lightbox + search-area ---------- */
.review-photos { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 2px; }
.review-photo { position: relative; display: inline-block; }
.review-photo img { width: 86px; height: 86px; object-fit: cover; border-radius: 10px; cursor: zoom-in; display: block; border: 1px solid var(--line); }
.biz-photos img { cursor: zoom-in; }

.lightbox {
  position: fixed; inset: 0; z-index: 1200; background: rgba(10, 22, 17, .93);
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px;
  animation: flash-in .18s ease;
}
.lightbox > img { max-width: 94vw; max-height: 84vh; border-radius: 10px; box-shadow: var(--shadow-lg); }
.lb-cap { color: #DCEAE2; font-size: .92rem; max-width: 90vw; text-align: center; }
.lb-btn {
  position: absolute; width: 46px; height: 46px; border-radius: 50%; border: 0; cursor: pointer;
  background: rgba(255,255,255,.12); color: #fff; font-size: 1.25rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.lb-btn:hover { background: rgba(255,255,255,.25); }
/* LOGICAL, NOT PHYSICAL, and the glyphs come along for free: U+2039 and U+203A are
   Bidi_Mirrored, so on an Urdu page "previous" sits on the right AND points right without a
   second rule. Written as `left: 12px` this control read "next" to an Urdu speaker while
   doing "previous". */
.lb-close { top: 14px; inset-inline-end: 14px; }
.lb-prev { inset-inline-start: 12px; top: 50%; transform: translateY(-50%); }
.lb-next { inset-inline-end: 12px; top: 50%; transform: translateY(-50%); }

.search-area-btn {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 25;
  border: 1px solid var(--line); border-radius: 999px; background: #fff; color: var(--pine-700);
  font-weight: 700; font-size: .9rem; padding: 10px 18px; cursor: pointer; box-shadow: var(--shadow-lg);
  animation: flash-in .2s ease;
}
.search-area-btn:active { transform: translateX(-50%) scale(.96); }

/* ---------- Urdu / RTL ---------- */
.lang-switch {
  color: var(--amber) !important; font-weight: 700; border: 1px solid rgba(233,180,76,.45);
  border-radius: 999px; padding: 3px 12px; font-size: .88rem;
}
.lang-switch:hover { background: rgba(233,180,76,.15); text-decoration: none; }

/**
 * THE ONE WEBFONT THIS SITE SERVES, AND WHY IT SERVES IT ITSELF.
 *
 * Every font stack here used to be a hope that the reader's device already owned the face.
 * Urdu survives that on Pakistani Windows and Android. Sindhi, Saraiki, Pashto and Balochi do
 * not — their extra letters (ڄ ٺ ڏ ڙ ݙ ڳ ې ۏ) are missing from most Latin-first system faces,
 * and a missing glyph is an empty box the reader never reports. They conclude the site is
 * broken and leave.
 *
 * SELF-HOSTED, NOT LINKED FROM GOOGLE. `font-src 'self'` in bootstrap.php forbids a third-party
 * origin, and widening it is the exact objection that ruled out reCAPTCHA: a request to
 * somebody else's server on every page, for every visitor, forever. A font is not worth that.
 *
 * WHY THIS FILE. Noto Naskh Arabic was already in the repository — ShareCard has been drawing
 * Urdu share images with it for months — under the SIL Open Font License, which permits
 * redistribution with the software. `tools/font_check.php` reads its character map off the
 * disk and confirms it draws every letter Urdu, Sindhi, Saraiki, Pashto, Balochi and Shahmukhi
 * Punjabi need: 57 of 57. Subset to the Arabic ranges it is 43 KB, and it is fetched only by
 * readers who are actually on a page in one of those languages.
 *
 * unicode-range IS WHAT MAKES THAT TRUE. Without it the browser downloads the font on every
 * page including the English ones. With it, the file is requested only when a character in
 * these blocks is about to be drawn.
 *
 * font-display: swap — text appears immediately in a fallback and reflows when the font
 * arrives. On a Pakistani mobile connection the alternative is a paragraph of nothing.
 *
 * NOT NASTA'LIQ, and that is a real limitation rather than an oversight. Urdu is conventionally
 * set in Nasta'liq and this is Naskh, so Urdu readers get a correct but plainer face than a
 * Nasta'liq one would give them. Sindhi is conventionally Naskh and is well served. SIL Lateef
 * would be better again for Sindhi specifically — it is drawn for the language — and it is OFL,
 * so it can be added here later. See assets/fonts/README.md.
 */
@font-face {
  font-family: "Noto Naskh Arabic";
  /* RELATIVE TO THIS STYLESHEET, not absolute from the domain root. `/assets/fonts/...` is
     correct in production, where the docroot IS html/ — and a 404 in every subdirectory
     install, including the development one this was written on, where the site sits at
     /murreeview/html/. The font would simply never arrive, Urdu would fall back to a system
     face, and nothing would say so. `../fonts/` resolves against the stylesheet's own URL and
     is right in both. */
  src: url("../fonts/noto-naskh-arabic.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+200C-200F;
}

/**
 * WHY `[lang|="ur"]` AND NOT `[lang="ur"]`.
 *
 * `=` is an EXACT match. The layout now emits BCP 47 tags — `ur-PK`, `sd-PK`, `pa-PK` — because
 * a bare `pa` is read as Gurmukhi Punjabi almost everywhere, which is a different script and a
 * different direction. The moment that changed, `[lang="ur"]` stopped matching anything and
 * every rule below silently switched off: the Naskh face, the 2.0 line-height, the 17px, the
 * letter-spacing reset. Urdu would have gone on rendering — in the Latin body font, at Latin
 * metrics, with the i'jam colliding — and nothing would have errored.
 *
 * `|=` matches the value exactly or the value followed by a hyphen. It is the selector the
 * spec added for language subtags, and it is the only one that is correct for both `ur` and
 * `ur-PK`. e2e_lang asserts that no exact-match language selector comes back.
 */
/**
 * URDU IS NOT A TRANSLATION LAYER OVER AN ENGLISH DESIGN. It is half the site, and Naskh has
 * different metrics from any Latin face. Four rules, none of them preferences.
 */
html[lang|="ur"] body {
  /* The same emoji tail as --font. This declaration REPLACES the family outright, so
     without it every emoji would render correctly in English and badly in Urdu. */
  font-family: "Noto Naskh Arabic", "Urdu Typesetting", "Segoe UI", Tahoma, sans-serif,
               "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", "Segoe UI Symbol";
  /* 2.0 rather than 1.9: at even font sizes 1.9 lands off the 4px grid (17 x 1.9 = 32.3)
     while 2.0 lands on it, so Urdu text aligns to the same rhythm as the spacing scale.
     17px because Naskh puts more of the em box into ascenders, descenders and the i'jam, so
     it reads about a step smaller than Latin at the same nominal size. */
  line-height: 2; font-size: 17px;
}
/* 1.6 is fine; 2.0 on a heading looks like a fault, and below 1.5 the descender dots of one
   line collide with the line above. This is the floor, stated so nobody "tidies" it to 1.3. */
html[lang|="ur"] h1, html[lang|="ur"] h2, html[lang|="ur"] h3 { line-height: 1.6; }

/**
 * AN ENGLISH DOCUMENT INSIDE A RIGHT-TO-LEFT PAGE.
 *
 * The privacy notice is served in English to every reader on purpose (Core\Lang::ENGLISH_ONLY
 * says why). Marking it `lang="en" dir="ltr"` gets the direction right on its own, but not the
 * type: the two rules above are `html[lang|="ur"] body ...`, so they apply to every descendant
 * of body regardless of its own lang, and English would be set in Naskh at 17px on a 2.0
 * leading — a Latin paragraph wearing an Arabic face, which reads as a font that failed to
 * load. These three declarations put it back, and nothing else: same family, same rhythm, same
 * size as the rest of the site in English.
 */
.en-doc { font-family: var(--font); line-height: 1.5; font-size: var(--t-body); }
.en-doc h1, .en-doc h2, .en-doc h3 { line-height: 1.25; }
/* The one line above it stays in the reader's language and the page's own direction, so it is
   deliberately OUTSIDE .en-doc and inherits everything. */
.notice.english-only { margin-bottom: var(--s5); }

/**
 * LETTER-SPACING IS NOT A STYLE CHOICE IN ARABIC SCRIPT — IT IS DAMAGE.
 *
 * Naskh is cursive: letters JOIN. Tracking pulls the joins apart, so the word does not read
 * as loose, it reads as broken — as though the font failed to load correctly. Every
 * negative-tracking rule in every design system worth reading is, silently, a Latin-only rule.
 *
 * This was scoped to .btn and .chip. There were fourteen letter-spacing declarations in this
 * file and the other twelve applied to Urdu unchallenged. One rule, no exceptions, and it
 * cannot be forgotten the next time a component wants tighter tracking.
 */
html[lang|="ur"] *:not(.stars) { letter-spacing: normal !important; }
/* .stars is exempt because it is not script. It is a row of ★ glyphs, and the 1px that
   separates them is doing the same job there as it would in any language. A blanket rule that
   also damages the one legitimate case is a rule people start writing exceptions around. */

/**
 * NO CASE TRANSFORM AS A HIERARCHY DEVICE. Arabic script has no case, so `text-transform:
 * uppercase` is a silent no-op: the label tier keeps its differentiation in English and loses
 * it completely in Urdu, while an English-only review of the page sees nothing wrong. Labels
 * are told apart here by size, weight and colour — all of which exist in both scripts.
 */
html[lang|="ur"] * { text-transform: none !important; }

/* A fixed height plus a longer Urdu label is a clipped descender. Controls state a minimum. */
html[lang|="ur"] .btn, html[lang|="ur"] .chip, html[lang|="ur"] .input { height: auto; }

/**
 * NUMBERS INSIDE URDU PROSE MUST BE ISOLATED OR THEY COME OUT SCRAMBLED.
 *
 * A phone number, an hours range like 9:00-17:00, a house number or a price is left-to-right
 * content inside a right-to-left paragraph, and the bidi algorithm will reorder the pieces
 * around it. `unicode-bidi: isolate` is the fix in CSS; <bdi> is the fix in markup. The site
 * already does this by hand in eleven places with `direction: ltr` and `unicode-bidi: embed`
 * — embed is the older, weaker form that does not stop the surrounding text from reordering.
 *
 * This is the single most common bilingual directory bug and it is invisible to anybody
 * reading the English.
 */
html[lang|="ur"] .phone, html[lang|="ur"] .tel, html[lang|="ur"] time,
html[lang|="ur"] .hours-time, html[lang|="ur"] .price, html[lang|="ur"] .num,
html[lang|="ur"] bdi { unicode-bidi: isolate; }

html[dir="rtl"] .panel { border-right: 0; border-left: 1px solid var(--line); }
html[dir="rtl"] .flash { border-left: 0; border-right: 4px solid var(--info); }
html[dir="rtl"] .flash-ok { border-right-color: var(--ok); }
html[dir="rtl"] .flash-warn { border-right-color: var(--amber); }
html[dir="rtl"] .flash-error { border-right-color: var(--danger); }
html[dir="rtl"] .nav-user { border-left: 0; border-right: 1px solid rgba(255,255,255,.18); padding-left: 0; padding-right: 14px; }
html[dir="rtl"] .footer a { margin-left: 0; margin-right: 14px; }
html[dir="rtl"] .owner-reply { border-left: 0; border-right: 3px solid var(--pine-500); margin: 10px 18px 0 0; }
html[dir="rtl"] .suggest-item { text-align: right; }
html[dir="rtl"] .admin-table th { text-align: right; }
html[dir="rtl"] .hours-table td:last-child { text-align: left; }
html[dir="rtl"] .service-price { direction: ltr; }
html[dir="rtl"] .reply-form .btn, html[dir="rtl"] .flag-form .btn { align-self: flex-end; }
/* numbers/phones/URLs stay left-to-right inside RTL text */
html[dir="rtl"] .rating-num, html[dir="rtl"] .rating-count, html[dir="rtl"] .stars { direction: ltr; unicode-bidi: embed; }

/* ---------- Directions panel (Google-style modes) ---------- */
.dir-panel {
  background: #fff; border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow-lg);
  padding: 12px 14px; font-size: .92rem;
}
.mapwrap > .dir-panel { position: absolute; top: 12px; left: 12px; z-index: 26; width: min(300px, calc(100% - 24px)); }
html[dir="rtl"] .mapwrap > .dir-panel { left: auto; right: 12px; }
.map-card .dir-panel { margin-top: 10px; box-shadow: none; }
.dir-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 8px; }
.dir-from { color: var(--ink-soft); font-size: .84rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dir-close { border: 0; background: var(--mist); color: var(--pine-700); width: 26px; height: 26px; border-radius: 50%; cursor: pointer; flex-shrink: 0; line-height: 1; }
.dir-close:hover { background: var(--pine-100); }
.dir-modes { display: flex; gap: 8px; margin-bottom: 8px; }
.dir-mode {
  flex: 1; border: 1.5px solid var(--line); background: #fff; border-radius: 10px; padding: 7px 10px;
  font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;
  transition: border-color .15s, background .15s; -webkit-tap-highlight-color: transparent;
}
.dir-mode:hover { border-color: var(--pine-500); }
.dir-mode.active { border-color: var(--pine-500); background: var(--mist); color: var(--pine-900); }
.dir-mode[disabled] { opacity: .45; cursor: not-allowed; }
.dir-time { font-size: .88rem; }
.dir-detail { color: var(--ink); font-weight: 600; min-height: 1.3em; }
html[dir="rtl"] .dir-detail, html[dir="rtl"] .dir-time { unicode-bidi: plaintext; }
@media (max-width: 860px) {
  .mapwrap > .dir-panel { top: 10px; left: 10px; right: 10px; width: auto; }
}

/* Elevation profile in directions panel */
.dir-elev { margin-top: 8px; }
.dir-elev svg { display: block; width: 100%; height: 34px; border-radius: 6px; }
.dir-elev-stats { display: flex; gap: 12px; font-size: .8rem; font-weight: 700; margin-bottom: 4px; }
.dir-elev-stats .up { color: var(--danger); }
.dir-elev-stats .down { color: var(--pine-500); }
.dir-elev-stats .alt { color: var(--ink-soft); font-weight: 600; margin-left: auto; }
html[dir="rtl"] .dir-elev-stats .alt { margin-left: 0; margin-right: auto; }
html[dir="rtl"] .dir-elev-stats { unicode-bidi: plaintext; }
html[dir="rtl"] .panel { box-shadow: -4px 0 18px rgba(20,52,43,.05); }

/* ---- Admin settings (email / SMTP) ---- */
.mail-ok  { border-color: var(--ok); background: #F4FBF6; }
.mail-bad { border-color: var(--danger); background: #FEF6F5; }
.smtp-log { margin: 8px 0 0; padding: 10px 12px; background: #14342B; color: #D7E8DF; border-radius: 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow: auto; }
.test-mail-form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.test-mail-form .input { flex: 1; min-width: 220px; }

/* ---- Collapsed navigation: links fold into the ☰ menu ---- */
/* 860px was chosen when the bar held eight links. Adding Guides made it eleven, and the
   row began overflowing at about 1150px — "Sign up" and the language switch pushed off
   the right edge, and on a laptop that reads as links having been REMOVED rather than
   run out of room. The breakpoint follows the content: measure before changing it. */
/* THE FOLD BREAKPOINT IS 1400px, AND IT IS MEASURED, NOT CHOSEN.
   With the overflow group in place the bar needs about 1,300px: ~650px of links (Map 44,
   Cities 57, Service providers 170, Top rated 113, Admin 66, Advertising 127, wallet 75),
   ~65 for the More button, ~186 for the account chip, ~66 for the language switch, nine
   18px gaps, and ~170 for the brand and the topbar's own padding.

   IT WAS 1200, WHICH IS THE BUG: a 1,366px laptop — the commonest screen there is — got the
   horizontal bar and could not hold it, so the row scrolled sideways and BOTH ends were
   clipped. A horizontal scrollbar inside a navigation bar is a thing almost nobody finds.

   1,400 rather than 1,360 on purpose. 1,360 would just fit a 1,366 screen, with about 65px
   to spare — and 65px is one longer translated label away from the same fault coming back.
   Folding a hair earlier costs that screen nothing: the stacked menu is the same links, one
   tap away, and it was built for exactly this width.

   THE SAME NUMBER LIVES IN core.js (`matchMedia`), because the folded account and overflow
   menus have to have their `hidden` attribute removed by script — CSS cannot do that half.
   Change one and change the other, and measure again before you do.
   It was 1200, which is why a 1,366px laptop got the horizontal bar and could not fit it.
   THE SAME NUMBER LIVES IN core.js (`matchMedia`), because the folded account and overflow
   menus have to have their `hidden` attribute removed by script — CSS cannot do that half.
   Change one and change the other, and measure again before you do. */
.nav-toggle { display: none; }
@media (max-width: 1400px) {
  .nav-toggle {
    display: flex; align-items: center; justify-content: center;
    width: 46px; height: 46px; margin-inline-start: auto; flex-shrink: 0;
    background: transparent; border: 0; border-radius: 12px; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .nav-toggle:active { background: rgba(255,255,255,.12); }
  .nav-burger, .nav-burger::before, .nav-burger::after {
    display: block; width: 22px; height: 2px; border-radius: 2px; background: #fff;
    transition: transform .22s var(--ease), opacity .18s;
  }
  .nav-burger::before, .nav-burger::after { content: ""; position: absolute; }
  .nav-burger::before { transform: translateY(-7px); }
  .nav-burger::after  { transform: translateY(7px); }
  .nav-toggle[aria-expanded="true"] .nav-burger { background: transparent; }
  .nav-toggle[aria-expanded="true"] .nav-burger::before { transform: rotate(45deg); }
  .nav-toggle[aria-expanded="true"] .nav-burger::after  { transform: rotate(-45deg); }

  .topnav {
    position: fixed; top: var(--topbar-h); inset-inline: 0; z-index: 490;
    display: block; overflow-y: auto; overflow-x: hidden;
    max-height: calc(100dvh - var(--topbar-h)); gap: 0;
    background: var(--pine-900); padding: 6px 10px 14px;
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    box-shadow: 0 14px 30px rgba(20,52,43,.35);
    transform: translateY(-8px); opacity: 0; visibility: hidden; pointer-events: none;
    /* visibility must flip INSTANTLY on open (delayed only on close) — animating it
       is a classic "menu never appears" bug when the tab is throttled. */
    transition: transform .2s var(--ease), opacity .18s, visibility 0s linear .18s;
  }
  .topnav.open {
    transform: none; opacity: 1; visibility: visible; pointer-events: auto;
    transition: transform .2s var(--ease), opacity .18s, visibility 0s linear 0s;
  }
  .topnav a, .topnav .linklike {
    display: flex; align-items: center; width: 100%; min-height: 50px;
    padding: 0 14px; border-radius: 12px; font-size: 1.02rem; font-weight: 600;
    border-bottom: 1px solid rgba(255,255,255,.07); white-space: normal;
  }
  .topnav a:last-child { border-bottom: 0; }
  .topnav a:active, .topnav .linklike:active { background: rgba(255,255,255,.10); }
  .topnav .btn { justify-content: center; margin-top: 8px; border-bottom: 0; }
  /* In the folded menu there is nothing to drop the account menu out of, so it stops
     floating and becomes part of the list. It is also shown open: on a phone the whole
     nav is already a deliberate act, and hiding four more links behind a second tap is
     how Log out became unfindable in the first place. */
  .nav-user { display: block; padding: 0; margin-top: 6px; border: 0; position: static;
    border-top: 1px solid rgba(255,255,255,.16); padding-top: 6px; }
  .nav-user-btn { display: none; }
  .nav-user-menu { position: static; display: block; background: transparent; border: 0;
    box-shadow: none; padding: 0; min-width: 0; }
  /* No `[hidden]` override here: line 4 of this file is `[hidden]{display:none!important}`,
     deliberately, and no later author rule can beat it. The folded menu is opened by
     removing the attribute in core.js instead of arguing with it in CSS. */
  .nav-user-head { border-bottom-color: rgba(255,255,255,.16); padding-inline-start: 0; }
  .nav-user-head b, .nav-user-menu a, .nav-user-out { color: #fff !important; }
  .nav-user-head span { color: rgba(255,255,255,.66); }
  .nav-user-menu a, .nav-user-out { padding-inline-start: 0; border-radius: 0; }
  .nav-user-menu a:hover, .nav-user-out:hover { background: rgba(255,255,255,.10); }
  .nav-user-out { color: #FFB4A8 !important; border-top-color: rgba(255,255,255,.16); }
  /* The overflow group folds the same way: below this width the whole nav is already a
     deliberate tap, so its links become ordinary rows and the "More" button disappears
     rather than hiding five items behind a second tap inside a menu. core.js removes the
     `hidden` attribute for the same reason it does for the account menu. */
  .nav-more { display: block; position: static; }
  .nav-more-btn { display: none; }
  .nav-more-menu { position: static; display: block; background: transparent; border: 0;
    box-shadow: none; padding: 0; min-width: 0; }
  .nav-more-menu a { color: #fff !important; padding-inline-start: 0; border-radius: 0; }
  .nav-more-menu a:hover { background: rgba(255,255,255,.10); }

  .topnav .inline-form { display: block; }
  .topnav .linklike { justify-content: flex-start; text-align: start; }
  .lang-switch { justify-content: center; margin-top: 8px;
    background: rgba(255,255,255,.10); border-bottom: 0 !important; }
  body.nav-open { overflow: hidden; }
}
/* Heading for a GROUP of controls (price pair, date range, radio group) — a <label>
   that points at nothing is worse for screen readers than a plain heading. */
.group-label { display: block; font-weight: 600; margin-bottom: 5px; color: var(--pine-900); }

/* ---- Touch targets: everything a thumb can hit is at least ~44px on phones ---- */
.footer nav a { display: inline-block; padding: 6px 0; }
@media (max-width: 860px) {
  .brand { min-height: 44px; padding-block: 6px; }
  .sort-sel { min-height: 40px; }
  .footer nav a { min-height: 44px; display: inline-flex; align-items: center; padding: 0 4px; }
  .crumbs a, .crumbs span { display: inline-flex; align-items: center; min-height: 36px; }
  /* MapLibre's own controls ship at 29px — too small for a thumb on a hillside */
  .maplibregl-ctrl button { width: 38px !important; height: 38px !important; }
  .maplibregl-ctrl button .maplibregl-ctrl-icon { background-size: 20px 20px; }
  .open-badge, .biz-name .open-badge { font-size: .78rem; }
  .facet-dd-menu .dd-grp { font-size: .78rem; }
  .facet-chip { min-height: 36px; padding-block: 6px; }
  .checkbox input[type=checkbox], .field input[type=checkbox], .field input[type=radio] { width: 20px; height: 20px; }
  .checkbox { min-height: 40px; align-items: center; }
  .photo-del { width: 36px; height: 36px; font-size: 1rem; }
  .checkbox input[type=checkbox], .field input[type=checkbox], .field input[type=radio] { min-width: 20px; }
  /* Buttons and inline actions get a full thumb-sized hit area */
  .btn { min-height: 40px; display: inline-flex; align-items: center; justify-content: center; }
  .btn-block { display: flex; }
  .linklike, .linklike-dark, .js-reply, .js-flag { min-height: 36px; display: inline-flex; align-items: center; }
  /* `.hint { font-size: .8rem }` lived here — 12.8px, i.e. the small screen got the SMALLER
     text. Backwards, and it was the biggest single group of under-14px text on the site.
     Removed rather than adjusted: the base rule is already at the 14px floor and there is no
     reason for a narrow screen to want less of it. */
}

/* ---- Wallet top-up ---- */
.wallet-balance { color: var(--pine-700); }
.topup-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.topup-opt { display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer;
  border: 1.5px solid var(--line); border-radius: 12px; padding: 14px 10px; font-weight: 700; background: var(--card); }
.topup-opt input { accent-color: var(--pine-700); }
.topup-opt:hover { border-color: var(--pine-500); }
.topup-opt:has(input:checked) { background: var(--pine-100); border-color: var(--pine-500); }
hr.soft { border: 0; border-top: 1px solid var(--line); margin: 16px 0; }

/* ---- Offline payments ------------------------------------------------------------------
   The screens between "add funds" and a receipt. Two of them are read standing up with a
   banking app open in the other hand, which is what every size below is for: the reference
   and the account number are the largest text on the page, monospaced, never wrapped, and
   selectable in one gesture. A wrapped account number is a mistyped account number. */

/* Choosing a method: one tall row each, because the second line is a bank name or the first
   words of the instructions and a chip cannot hold it. */
.pay-methods { display: grid; gap: 8px; margin-bottom: 4px; }
.pay-method { display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  border: 1.5px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--card); }
.pay-method input { accent-color: var(--pine-700); margin-top: 3px; }
.pay-method:hover { border-color: var(--pine-500); }
.pay-method:has(input:checked) { background: var(--pine-100); border-color: var(--pine-500); }
.pay-method-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pay-method-body b { font-weight: 700; }
.pay-method-body .hint { margin: 0; }

/* The reference. Everything else on the payment page is secondary to this string. */
.pay-ref-card { text-align: center; }
.pay-ref { display: flex; align-items: center; justify-content: center; gap: 8px;
  margin: 6px 0 10px; flex-wrap: wrap; }
.pay-ref-code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: clamp(22px, 6vw, 30px); font-weight: 700; letter-spacing: .06em;
  color: var(--pine-900); background: var(--mist); border-radius: 10px;
  padding: 8px 14px; user-select: all; }
.pay-acct { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 1.12rem; font-weight: 700; letter-spacing: .04em; user-select: all;
  white-space: nowrap; }

/* Label/value pairs. A two-column grid down to 420px, then stacked — the value must never
   be squeezed into a column narrower than an account number. */
.pay-details { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px;
  margin: 0 0 14px; align-items: baseline; }
.pay-details dt { font-size: var(--t-label); font-weight: 600; color: var(--ink-soft); }
.pay-details dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.pay-details dd .hint { display: block; margin: 0; }
@media (max-width: 420px) {
  .pay-details { grid-template-columns: 1fr; gap: 2px; }
  .pay-details dd { margin-bottom: 8px; }
}

/* The payer's screenshot on the desk. Capped, because it is glanced at beside a real bank
   statement and a full-height phone screenshot pushes the decision buttons off the screen. */
.pay-proof { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 14px; }
.pay-proof img { max-height: 160px; border-radius: 10px; border: 1px solid var(--line);
  cursor: zoom-in; }
.pay-decide { margin-top: 4px; }
.pay-decide .row-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* A card that carries a state. The leading edge does the work so the card still reads as a
   card at a glance rather than as a coloured box.

   border-inline-start, not border-left: on the Urdu site the page runs right to left, and a
   stripe pinned to the left is a stripe on the far side of the card from where the eye
   starts. e2e_lang asserts that every new rule here takes a side logically. */
.card-green { border-inline-start: 4px solid var(--ok); }
.card-amber { border-inline-start: 4px solid var(--amber); }
.card-red   { border-inline-start: 4px solid var(--danger); }

/* Something a person wrote or an instruction they must follow, set apart from our own prose
   so it is obvious which is which. */
.quoted-note { margin: 10px 0; background: var(--mist); border-radius: 8px;
  padding: 10px 13px; white-space: pre-line; }
.notice-line { border-inline-start: 3px solid var(--amber); padding-inline-start: 10px; margin: 0 0 14px; }
.minor-head { font-size: var(--t-card); margin: 16px 0 6px; }
.plain-list { list-style: none; padding: 0; margin: 8px 0 0; display: grid; gap: 6px; }
.plain-list li { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* Any table that can outgrow its card scrolls in its own box rather than widening the page.
   Tables are the one thing on this site allowed to scroll sideways. */
.table-wrap { overflow-x: auto; }

/* The copy button's acknowledgement. Colour AND a changed glyph, because the glyph is what
   a person who cannot distinguish the colour is reading. */
.btn.is-copied { color: var(--ok); border-color: var(--ok); }

/* ---- Paid placements: always visibly labelled ---- */
.featured-tag { display: inline-block; background: var(--amber); color: var(--pine-900);
  border-radius: 999px; padding: 1px 9px; font-size: .72rem; font-weight: 700; vertical-align: middle; }
.boosted-tag { display: inline-flex; align-items: center; gap: 4px; background: var(--amber);
  color: var(--pine-900); border-radius: 999px; padding: 2px 10px; font-size: .76rem; font-weight: 700; }
.placement-note { font-size: .82rem; color: var(--ink-soft); }

/* ============================================================================
   Mobile density pass — the sheet was spending 275px of a 214px opening on
   controls, so no result was ever visible. Measured on 412x860.
   ============================================================================ */

/* The ⚙ pinned at the head of the category rail. It does not scroll with the
   chips: a filter control that slides out of reach is a filter control nobody
   uses. */
.rail-filter { display: none; }
.filter-scrim { display: none; }
.filter-sheet-head { display: none; }
.filter-sort { display: none; }

@media (max-width: 860px) {
  /**
   * Collapsed, the sheet is a STRIP — the grip and one line saying what is under it.
   *
   * It used to rest at 296px, which on a 360x640 phone is nearly half the screen given
   * over to a search box and a row of chips before the map gets a look in. The map IS the
   * product on this page; everything else is a drawer over it. So the resting state now
   * shows the grip and the result count, and one tap opens the rest.
   *
   * The count row is REORDERED above the search box rather than duplicated into a second
   * element, because two elements showing the same number are two elements that can
   * disagree.
   */
  /**
   * ...AND THE SEARCH FIELD IS PART OF THAT STRIP.
   *
   * It was not, and that was the single most expensive thing on the phone. The whole of
   * `.panel-head` sat below the fold, so on arrival the screen offered a map, a hamburger
   * and a drawer handle — and a visitor arriving at /search?q=pizza read a map that said
   * "0 places in view" while their own words sat in an input inside the closed drawer.
   * The site's one verb was the one thing you could not see.
   *
   * So the peek now carries the search box AND the count: grip, search, count. It costs
   * about 110px of an 844px screen — the map still has seven-eighths of it, and the
   * argument above (the map IS the product) survives intact. The chip rail and the filter
   * row stay below the fold, because those refine an answer and this is the question.
   */
  :root { --sheet-peek: 112px; }
  /* A floor only. The real height is measured and set inline by home.js the moment the strip
     is filled, because how tall two lines beside a button come out depends on the font and the
     language — this value existing at all is so there is something sane for the single frame
     before that measurement lands. */
  .panel.has-pick { --sheet-peek: 146px; }

  .panel > .sheet-grip { order: 1; }
  .panel > .panel-head { order: 2; }
  .panel > .panel-pick { order: 3; }
  .panel > .panel-meta { order: 4; }
  .panel > .panel-list { order: 5; }

  /* The one-result strip. It takes the count row's place rather than sitting beside it: at
     rest there is one line of room and it should say the most specific true thing, which
     once somebody has chosen a place is that place and not how many are in view. */
  .panel-pick {
    display: flex; align-items: center; gap: 10px;
    /* The home indicator lives in the bottom 34px of a notched phone, and this strip is the
       one row of the sheet that rests against it. Every other bottom-anchored surface here
       already accounts for it; this one is new and would have been the exception. */
    padding: 8px 14px calc(10px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--line);
  }
  .panel.has-pick > .panel-meta { display: none; }
  .pick-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
  .pick-body:hover { text-decoration: none; }
  .pick-name {
    font-weight: 700; color: var(--pine-900); font-size: .98rem;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .pick-sub {
    font-size: .875rem; color: var(--ink-soft);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .pick-sub .rating-num { color: var(--ink); font-weight: 700; }
  .panel-pick .btn { flex: none; }
  .pick-x {
    flex: none; border: 0; background: none; cursor: pointer; padding: 0;
    color: var(--ink-soft); font-size: 1.5rem; line-height: 1;
    width: 32px; height: 32px; position: relative;
  }
  .pick-x::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }

  /* Collapsed, the head is the search box and nothing else. Opening the sheet reveals the
     rest in place — no second element, no duplicated state. */
  .panel:not(.half):not(.full) .chip-rail { display: none; }

  .panel-meta { border-top: 1px solid var(--line); border-bottom: 0; padding: 4px 16px 8px; touch-action: none; cursor: pointer; }
  .panel.half .panel-meta, .panel.full .panel-meta { touch-action: auto; }

  .sheet-toggle { display: inline-flex; align-items: center; gap: 6px; background: transparent;
                  border: 0; padding: 4px 2px; font: inherit; font-weight: 600;
                  color: var(--pine-700); cursor: pointer; }
  .sheet-toggle::after { content: "\25B2"; font-size: .7rem; transition: transform .25s var(--ease); }
  .sheet-toggle [data-close] { display: none; }
  .panel.half .sheet-toggle [data-open], .panel.full .sheet-toggle [data-open] { display: none; }
  .panel.half .sheet-toggle [data-close], .panel.full .sheet-toggle [data-close] { display: inline; }
  .panel.half .sheet-toggle::after, .panel.full .sheet-toggle::after { transform: rotate(180deg); }

  /* Give the grip a job: it is 16px of pure decoration otherwise. */
  .sheet-grip { padding: 7px 0 3px; }

  .searchbox { padding: 11px 14px; }
  .panel-head { padding: 4px 14px 8px; border-top: 0; }

  /* One row, not three. */
  .filter-sheet {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    background: var(--card); border-radius: 20px 20px 0 0;
    box-shadow: 0 -10px 40px rgba(20,52,43,.26);
    padding: 4px 16px calc(18px + env(safe-area-inset-bottom));
    transform: translateY(101%); transition: transform .26s var(--ease);
    max-height: 82vh; overflow-y: auto;
  }
  .filter-sheet.open { transform: translateY(0); }
  .filter-sheet-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px; padding: 10px 0 6px; font-size: 1.05rem; color: var(--pine-900);
    position: sticky; top: 0; background: var(--card);
  }
  .filter-scrim { position: fixed; inset: 0; z-index: 59; background: rgba(12,30,24,.44); }
  body.no-scroll { overflow: hidden; }

  /* Inside the sheet there is room to breathe, so let it wrap and drop the
     label and the divider that were forcing the wrap out on the panel. */
  .filter-sheet .filter-row { flex-wrap: wrap; gap: 10px; margin: 4px 0 2px; }
  .filter-sheet .filter-label,
  .filter-sheet .filter-sep { display: none; }
  .filter-sheet .price-dd { position: static; }
  .filter-sheet .price-dd-menu { position: static; width: 100%; box-shadow: none; border-radius: 12px; margin-top: 8px; }
  .filter-sort { display: block; padding: 14px 0 2px; border-top: 1px solid var(--line); margin-top: 12px; }
  .filter-sort .sort-sel { width: 100%; min-height: 44px; font-size: 1rem; }

  .chip-rail { display: flex; align-items: center; gap: 6px; margin: 0 -14px; padding-inline-start: 14px; }
  .chip-rail .chips { padding: 8px 14px 9px 4px; flex: 1; }
  .rail-filter {
    display: inline-flex; align-items: center; justify-content: center; position: relative;
    flex: 0 0 auto; width: 44px; height: 44px; border-radius: 14px;
    border: 1px solid var(--line); background: var(--card); color: var(--pine-700); cursor: pointer;
  }
  .rail-filter.has-filters { background: var(--pine-700); border-color: var(--pine-700); color: #fff; }
  .rail-filter-dot {
    position: absolute; top: -5px; right: -5px; min-width: 19px; height: 19px; padding: 0 5px;
    border-radius: 999px; background: var(--amber, #C77700); color: #fff;
    font-size: .72rem; font-weight: 800; line-height: 19px; text-align: center;
    border: 2px solid var(--card);
  }
  html[dir="rtl"] .rail-filter-dot { right: auto; left: -5px; }
  /* The arrows are a mouse affordance; a thumb just swipes. */
  .chip-rail .rail-btn { display: none; }

  /* Count only — the sort moved into the sheet, so this is one short line. */
  .panel-meta { padding: 4px 16px 5px; font-size: .8rem; border-top: 0; }
  .panel-list { padding-top: 6px; }
}

/* A phone on its side. The peek still has to clear the search box — a clipped input is
   worse than a tall strip — but the count row can go, since the map is showing the same
   answer right behind it. */
@media (max-width: 860px) and (max-height: 620px) {
  :root { --sheet-peek: 96px; }
  .panel:not(.half):not(.full) .panel-meta { display: none; }
}

/* ---------- Directions: the map is the answer, the numbers are the caption --
   A route drawn 260px tall inside three nested borders is a diagram of a route,
   not a route. When directions open, the card becomes one surface: map to the
   edges, readout docked over the bottom of it. */
.map-card.dir-open { padding: 0; overflow: hidden; position: relative; }
.map-card.dir-open #mini-map { height: min(62vh, 560px); min-height: 330px; border-radius: 0; }
.map-card.dir-open .map-addr { display: none; }
.map-card.dir-open .dir-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 5; margin: 0;
  border: 0; border-radius: 18px 18px 0 0;
  background: rgba(255,255,255,.97);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  box-shadow: 0 -6px 26px rgba(20,52,43,.18);
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
}
@supports not (backdrop-filter: blur(4px)) { .map-card.dir-open .dir-panel { background: #fff; } }

/* Same idea on the explore map: docked low, so it never covers the pin you
   just tapped or the popup that opened next to it. */
@media (max-width: 860px) {
  .mapwrap > .dir-panel {
    top: auto; bottom: calc(var(--sheet-peek) + 12px);
    left: 10px; right: 10px; width: auto;
  }
  html[dir="rtl"] .mapwrap > .dir-panel { left: 10px; right: 10px; }
}

@media (max-width: 860px) {
  .map-card.dir-open .maplibregl-ctrl-bottom-right,
  .map-card.dir-open .maplibregl-ctrl-bottom-left { bottom: 136px !important; }
}

/* ============================================================================
   Navigation view — the whole screen is the map
   Someone pressing Directions is about to go somewhere. The route is the thing
   they are holding the phone for; the distance and the time are a caption on
   it, and the rest of the listing waits behind the ⓘ.
   ============================================================================ */
body.nav-open { overflow: hidden; }

.navview { position: fixed; inset: 0; z-index: 1100; background: var(--mist); }
.nav-map { position: absolute; inset: 0; }
.nav-map .maplibregl-ctrl-bottom-left,
.nav-map .maplibregl-ctrl-bottom-right { display: none; }   /* nothing competes with the route */

.nav-top {
  position: absolute; top: 0; left: 0; right: 0; z-index: 4;
  display: flex; align-items: center; gap: 10px;
  padding: calc(10px + env(safe-area-inset-top)) 12px 22px;
  background: linear-gradient(180deg, rgba(9,26,20,.72) 0%, rgba(9,26,20,.38) 55%, transparent);
  color: #fff; pointer-events: none;
}
.nav-top > * { pointer-events: auto; }
.nav-icon {
  flex: 0 0 auto; width: 42px; height: 42px; border-radius: 50%; border: 0; cursor: pointer;
  background: rgba(255,255,255,.94); color: var(--pine-900);
  font-size: 1.3rem; line-height: 1; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(9,26,20,.3);
}
.nav-icon:hover { background: #fff; }
.nav-back { font-size: 1.9rem; padding-bottom: 4px; }
html[dir="rtl"] .nav-back { transform: scaleX(-1); }
.nav-dest { flex: 1; min-width: 0; text-shadow: 0 1px 3px rgba(9,26,20,.6); }
.nav-dest b { display: block; font-size: 1.02rem; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-from { font-size: .82rem; color: #D8EAE1; }

/* The next manoeuvre. Only while navigating, and only once it is close enough
   to be about to happen — a banner that is always on is wallpaper. */
.nav-banner {
  position: absolute; z-index: 5; left: 12px; right: 12px; top: calc(70px + env(safe-area-inset-top));
  display: flex; align-items: center; gap: 12px;
  background: var(--pine-900, #14342B); color: #fff;
  border-radius: 14px; padding: 12px 16px; box-shadow: 0 6px 22px rgba(9,26,20,.34);
}
.nav-arrow { font-size: 1.7rem; line-height: 1; flex: 0 0 auto; }
.nav-banner-txt { min-width: 0; }
.nav-instr { display: block; font-size: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-instr-dist { font-size: .85rem; color: #9FD0BC; }
.navview.arrived .nav-banner { background: #1E6538; }

.nav-recenter {
  position: absolute; z-index: 5; right: 12px; bottom: calc(var(--nav-sheet-h, 190px) + 14px);
  border: 0; border-radius: 999px; padding: 10px 16px; min-height: 40px; cursor: pointer;
  background: #fff; color: var(--pine-700); font-weight: 700; font-size: .9rem;
  box-shadow: 0 3px 14px rgba(9,26,20,.28);
}
html[dir="rtl"] .nav-recenter { right: auto; left: 12px; }

.nav-sheet {
  position: absolute; z-index: 6; left: 0; right: 0; bottom: 0;
  background: #fff; border-radius: 18px 18px 0 0;
  padding: 14px 16px calc(16px + env(safe-area-inset-bottom));
  box-shadow: 0 -6px 26px rgba(20,52,43,.2);
}
.nav-modes { display: flex; gap: 10px; margin-bottom: 10px; }
.nav-mode {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; border: 1.5px solid var(--line); border-radius: 14px;
  background: var(--card); cursor: pointer; font-size: 1rem; font-weight: 700; color: var(--ink);
}
.nav-mode.active { border-color: var(--pine-500); background: var(--mist); color: var(--pine-900); }
.nav-detail { font-weight: 700; color: var(--ink); min-height: 1.35em; }
.nav-elev { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 8px; font-size: .82rem; font-weight: 700; }
.nav-elev .up { color: #A33326; }
.nav-elev .down { color: #1E6538; }
.nav-elev svg { display: block; width: 100%; height: 30px; border-radius: 6px; }
.nav-start { width: 100%; margin-top: 12px; min-height: 50px; font-size: 1.05rem; }

.nav-live { display: flex; align-items: center; gap: 12px; margin-top: 10px; }
.nav-live-nums { flex: 1; min-width: 0; }
.nav-left { display: block; font-size: 1.25rem; color: var(--pine-900); }
.nav-eta { font-size: .86rem; color: var(--ink-soft); }
.nav-stop { min-height: 46px; padding: 0 22px; }
.navview.weak-fix .nav-eta { opacity: .55; }

/* While navigating the numbers matter less than the road, so the sheet shrinks. */
.navview.navigating .nav-modes,
.navview.navigating .nav-elev { display: none; }
.navview.navigating .nav-detail { font-size: .86rem; font-weight: 600; color: var(--ink-soft); }

/* Everything else about the place, behind one button. */
.nav-infosheet {
  position: absolute; z-index: 7; left: 0; right: 0; bottom: 0; max-height: 78%;
  overflow-y: auto; background: var(--paper);
  border-radius: 18px 18px 0 0; box-shadow: 0 -10px 40px rgba(20,52,43,.3);
  padding: 16px 14px calc(24px + env(safe-area-inset-bottom));
  animation: nav-rise .22s var(--ease);
}
@keyframes nav-rise { from { transform: translateY(14%); opacity: .4; } to { transform: none; opacity: 1; } }
.nav-infosheet .card { margin-bottom: 12px; }
.nav-infosheet .biz-actions { display: none; }     /* the actions are on the page behind this */
.navview.info-open .nav-sheet { display: none; }

/* The dot, and the cone showing which way the phone is pointing.
 *
 * Named `navview-me`, NOT `nav-user`. It was `nav-user` — which is also the signed-in
 * person's name and picture in the top bar, three hundred lines further up this file. The
 * turn-by-turn marker was written later, so it won, and every signed-in visitor had their
 * name squeezed into a 26-pixel box and clipped. Two unrelated things sharing a class name
 * is the same failure as two unrelated things sharing a variable name: the later one wins
 * silently and the damage appears somewhere nobody was looking. */
.navview-me { width: 26px; height: 26px; position: relative; }
.navview-me-dot {
  position: absolute; inset: 4px; border-radius: 50%;
  background: #2B6CB0; border: 3px solid #fff; box-shadow: 0 0 0 1px rgba(20,52,43,.25);
}
.navview-me-cone { display: none; }
.navview-me.has-heading .navview-me-cone {
  display: block; position: absolute; left: 50%; top: 50%; width: 0; height: 0;
  border-left: 13px solid transparent; border-right: 13px solid transparent;
  border-bottom: 26px solid rgba(43,108,176,.34);
  transform: translate(-50%, -100%) rotate(var(--hdg, 0deg));
  transform-origin: 50% 100%;
}

@media (min-width: 861px) {
  /* On a desktop a full-bleed sheet across 1400px is silly; keep it a column. */
  .nav-sheet, .nav-infosheet { left: auto; right: 24px; width: 400px; bottom: 24px; border-radius: 18px; }
  .nav-banner { left: 24px; right: auto; width: 420px; }
  .nav-recenter { right: 448px; bottom: 34px; }
  html[dir="rtl"] .nav-sheet, html[dir="rtl"] .nav-infosheet { right: auto; left: 24px; }
}

/* ---------- Area pages ---------------------------------------------------
   Server-rendered, unlike the map: this is the page that answers "hotels in
   Bhurban" in HTML rather than in JavaScript. */
.area-page { max-width: 880px; }
.area-head h1 { margin: 6px 0 2px; }
.area-cats { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 4px; }
.area-cats .chip { text-decoration: none; }
.area-cats .chip b { color: var(--pine-700); margin-inline-start: 3px; }
.area-cats .chip .hint { margin-inline-start: 5px; font-size: .8rem; }
.area-map-card { padding: 0; overflow: hidden; margin: 14px 0 20px; }
#area-map { height: 300px; }

.area-group { margin: 26px 0; }
.area-group > h2 { font-size: 1.1rem; color: var(--pine-900); margin: 0 0 10px;
                   scroll-margin-top: calc(var(--topbar-h) + 12px); }
.area-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.area-item { display: flex; align-items: stretch; gap: 8px; background: var(--card);
             border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.area-item:hover { border-color: var(--pine-500); box-shadow: var(--shadow); }
.area-item-main { flex: 1; min-width: 0; display: flex; gap: 12px; padding: 12px 14px;
                  text-decoration: none; color: inherit; align-items: flex-start; }
.area-item-thumb { flex: 0 0 56px; width: 56px; height: 56px; border-radius: 12px; overflow: hidden;
                   background: var(--mist); display: flex; align-items: center; justify-content: center; }
.area-item-thumb img { width: 100%; height: 100%; object-fit: cover; }
.area-item-body { min-width: 0; }
.area-item-name { display: block; font-weight: 700; color: var(--pine-900); }
.area-item-rating { display: flex; align-items: center; gap: 6px; font-size: .88rem; margin-top: 2px; }
.area-item-desc { display: block; margin-top: 3px; }
.area-item-call { align-self: center; margin-right: 12px; min-width: 44px; min-height: 44px;
                  display: flex; align-items: center; justify-content: center; text-decoration: none; }
html[dir="rtl"] .area-item-call { margin-right: 0; margin-left: 12px; }
.area-foot { margin-top: 28px; }

.area-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
/* .area-tile is defined once, with the picture layout, further down. */
.area-tile b { display: block; color: var(--pine-900); font-size: 1.05rem; }
.area-tile:hover { border-color: var(--pine-500); box-shadow: var(--shadow); }

@media (max-width: 860px) {
  #area-map { height: 240px; }
  .area-item-desc { display: none; }   /* the name and the rating are the decision */
}

/* ---------- Conditions strip ---------------------------------------------
   Above everything, on every page, because "is the road open" is asked before
   anyone starts browsing. Colour carries severity but never alone — the icon
   and the words say it too. */
.conditions {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px; font-size: .93rem; line-height: 1.35;
  border-bottom: 1px solid rgba(0,0,0,.08);
}
.conditions-info   { background: #EAF3FB; color: #14344E; }
.conditions-warn   { background: #FFF6E5; color: #5A3D00; }
.conditions-danger { background: #FBEAE7; color: #7A1F12; }
.conditions-ico { font-size: 1.15rem; flex: 0 0 auto; }
.conditions-txt { flex: 1; min-width: 0; }
.conditions-txt b { display: block; color: inherit; }
.conditions-txt span { opacity: .88; }
.conditions-more {
  flex: 0 0 auto; color: inherit; font-weight: 700; text-decoration: underline;
  min-height: 24px; display: inline-flex; align-items: center; padding: 0 4px;
}
.conditions-x {
  flex: 0 0 auto; border: 0; background: transparent; color: inherit; cursor: pointer;
  width: 30px; height: 30px; border-radius: 50%; font-size: .95rem; line-height: 1;
  display: flex; align-items: center; justify-content: center; opacity: .7;
}
.conditions-x:hover { opacity: 1; background: rgba(0,0,0,.07); }

@media (max-width: 860px) {
  .conditions { padding: 9px 12px; font-size: .88rem; gap: 9px; }
  .conditions-txt span { display: none; }   /* the headline is the alert */
}

/* ---------- Conditions page ---------- */
.cond-item { display: flex; gap: 14px; padding: 16px; align-items: flex-start; }
.cond-item .cond-ico { font-size: 1.5rem; flex: 0 0 auto; }
.cond-item h2 { margin: 0 0 4px; font-size: 1.05rem; color: var(--pine-900); }
.cond-when { font-size: .84rem; color: var(--ink-soft); margin-top: 6px; display: block; }
.cond-item h2 { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.cond-sev { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: .74rem;
            font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
.cond-sev-info   { background: #EAF3FB; color: #14344E; }
.cond-sev-warn   { background: #FFF6E5; color: #5A3D00; }
.cond-sev-danger { background: #FBEAE7; color: #7A1F12; }
.cond-past { opacity: .72; }

/* Honeypot: hidden from a person, still present for a bot -- not display:none, because some
   password managers and autofillers skip hidden inputs and would defeat it.
 *
 * IT USED TO BE `left: -9999px`, AND ON EVERY URDU PAGE THAT MEANT 10,000 PIXELS OF SIDEWAYS
 * SCROLL. In a left-to-right document, content pushed off the left edge is clipped and adds
 * nothing to `scrollWidth`. In a right-to-left one the left IS the overflow direction, so the
 * same declaration made `documentElement.scrollWidth` 10,784px against a 785px viewport --
 * every page carrying a captcha or a suggest-an-edit form, which is every business page.
 *
 * Nothing threw. The page looked perfect. It was found by opening one in Urdu and running
 * tools/page_audit.js, and it is the reason that file exists. Worth naming the wrong reasoning
 * too: "-9999px is off-screen in both directions" is true and irrelevant -- off-screen in the
 * inline-start direction is clipped, off-screen in the inline-end direction is scrollable, and
 * which one `left` means depends on the language. The clip technique has no direction at all. */
.hp-field { 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; }

/* ---------- Photo moderation queue + visitor upload ---------- */
.photo-queue { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.photo-q-item { padding: 0; overflow: hidden; }
.photo-q-item > img { width: 100%; height: 190px; object-fit: cover; display: block; cursor: zoom-in; background: var(--mist); }
.photo-q-body { padding: 12px 14px 14px; }
.photo-q-actions { display: flex; gap: 8px; margin-top: 10px; }

.photo-add { margin-top: 12px; }
.photo-add summary { cursor: pointer; font-weight: 700; color: var(--pine-700); min-height: 32px;
                     display: inline-flex; align-items: center; gap: 6px; }
.photo-add .field { margin-top: 10px; }
.photo-pending-note { font-size: .84rem; color: var(--ink-soft); margin: 8px 0 0; }
.visit-badge { display: inline-flex; align-items: center; gap: 4px; font-size: .8rem; font-weight: 700;
               color: var(--pine-700); background: var(--mist); border-radius: 999px; padding: 2px 9px; }
.empty-note .empty-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 0; justify-content: center; }
.empty-note .empty-actions .btn { text-decoration: none; }

/* ---------- Admin: area pictures ---------- */
.area-admin { display: flex; gap: 16px; padding: 0; overflow: hidden; margin-bottom: 14px; }
.area-admin-pic { flex: 0 0 210px; position: relative; background: var(--mist); min-height: 150px; }
.area-admin-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.area-admin-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
                    color: var(--ink-soft); font-size: .88rem; }
.area-admin-tag { position: absolute; inset-inline-start: 8px; bottom: 8px; background: rgba(9,26,20,.78); color: #fff;
                  font-size: .72rem; font-weight: 700; padding: 3px 8px; border-radius: 999px; }
.area-admin-form { flex: 1; min-width: 0; padding: 14px 16px 16px; }
.area-admin-form h2 { margin: 0 0 10px; font-size: 1.05rem; color: var(--pine-900); }
.area-admin-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; align-items: center; }

/* ---------- Area page hero ---------- */
.area-hero { position: relative; border-radius: var(--radius); overflow: hidden; margin: 12px 0 18px;
             background: var(--mist); }
.area-hero img { width: 100%; height: 260px; object-fit: cover; display: block; }
.area-hero-credit { position: absolute; right: 8px; bottom: 8px; background: rgba(9,26,20,.7); color: #E7F2EC;
                    font-size: .72rem; padding: 3px 9px; border-radius: 999px; max-width: 70%;
                    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.area-hero-credit a { color: #fff; }
html[dir="rtl"] .area-hero-credit { right: auto; left: 8px; }
.area-intro { font-size: 1rem; color: var(--ink); margin: 6px 0 14px; max-width: 68ch; }
@media (max-width: 860px) {
  /* The hero is the first thing on the page now, so it must not eat the whole screen —
     a picture you have to scroll past is no better than a wall of chips. */
  .area-hero { margin: 8px 0 12px; }
  .area-hero img { height: 168px; }
  .area-head h1 { font-size: 1.45rem; line-height: 1.22; margin: 0 0 4px; }
  /* One swipeable row instead of thirteen stacked lines. */
  .area-cats { flex-wrap: nowrap; overflow-x: auto; scroll-snap-type: x proximity;
               -webkit-overflow-scrolling: touch; scrollbar-width: none;
               margin-inline: calc(var(--page-pad, 16px) * -1); padding-inline: var(--page-pad, 16px);
               padding-bottom: 4px; }
  .area-cats::-webkit-scrollbar { display: none; }
  .area-cats .chip { flex: 0 0 auto; scroll-snap-align: start; }
  .area-admin { flex-direction: column; }
  .area-admin-pic { flex: none; height: 160px; }
}
.area-tile { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
/* Every tile reserves the same picture band, so an area that has no photograph yet still
   lines up with the ones that do instead of leaving a ragged grid. */
.area-tile-pic { display: block; height: 118px; background: var(--mist); }
.area-tile-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.area-tile:not(.has-pic) .area-tile-txt { padding-top: 16px; }
.area-tile-pic-empty { display: block; height: 118px; background:
    linear-gradient(160deg, var(--mist) 0%, #DCE9E1 60%, #CFE0D6 100%);
    position: relative; }
.area-tile-pic-empty::after { content: "⛰"; position: absolute; inset: 0; display: flex;
    align-items: center; justify-content: center; font-size: 1.8rem; opacity: .35; }
.area-tile-txt { display: block; padding: 14px 16px; }

/* ---------- Guides ---------- */
.guide-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; margin-top: 16px; }
.guide-card { display: flex; flex-direction: column; padding: 0; overflow: hidden; text-decoration: none; color: inherit; }
.guide-card:hover { border-color: var(--pine-500); box-shadow: var(--shadow); }
.guide-card-pic { display: block; height: 150px; background: var(--mist); }
.guide-card-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.guide-card-txt { display: block; padding: 14px 16px; }
.guide-card-txt b { display: block; color: var(--pine-900); font-size: 1.05rem; line-height: 1.3; margin-bottom: 4px; }
.guide-card-lead { display: block; color: var(--ink-soft); font-size: .92rem; margin-bottom: 6px; }
.guide-tag { display: inline-block; background: var(--pine-100); color: var(--pine-700);
             border-radius: 999px; padding: 2px 10px; font-size: .74rem; font-weight: 700;
             margin-bottom: 6px; }
.guide-tag.is-stale { background: #FBEED3; color: #8a6414; }

/**
 * A GUIDE IS THE ONE PLACE ON THIS SITE THAT IS READ RATHER THAN SCANNED.
 *
 * Everything else here is a directory: dense, short lines, answer-first. A guide is prose,
 * and prose wants a measure, a rhythm and space above its headings. Three rules, and the
 * third is the one that makes it work in both languages.
 */
.guide-page h1 { font-size: 1.9rem; line-height: 1.3; margin: var(--s3) 0 var(--s1); }
.guide-meta { color: var(--ink-soft); font-size: var(--t-meta); margin: 0 0 var(--s4); }
.guide-lead { font-size: 1.12rem; line-height: 1.55; color: var(--ink); margin: 0 0 var(--s5); }

/* 1. A MEASURE, in px rather than ch. `ch` is the advance width of the digit zero, which
      means nothing at all for Naskh — a 68ch column is a different width in each language,
      and the wrong one in one of them. 680px Latin, 640px Urdu. */
.guide-body { font-size: 1.02rem; line-height: 1.68; max-width: 680px; }
html[lang|="ur"] .guide-body { max-width: 640px; }

/* 2. PARAGRAPH SPACING SCALES WITH LEADING, NOT WITH FONT SIZE. The gap between paragraphs
      has to clearly beat the gap between lines or the block turns to mush. Latin at 1.68
      leaves ~11px between lines, so 20px reads as a paragraph break. Urdu at 2.0 leaves
      ~17px, and that same 20px is barely more than a line gap — hence 32. The rule is
      `margin-block-end >= 1.5 x (line-height - font-size)`, and it is why a single value
      cannot serve both scripts. */
.guide-body p { margin: 0 0 var(--s5); }
html[lang|="ur"] .guide-body p { margin-bottom: var(--s6); }

/* 3. SPACE ABOVE A HEADING IS ROUGHLY DOUBLE THE SPACE BELOW IT. This is the most-violated
      rule in hand-written CSS and the cheapest fix for prose that reads as undifferentiated:
      a heading belongs to what follows it, so it sits near it. 26/10 was close; 32/12 is the
      scale, and it obeys the proximity rule the rest of the site now follows. */
.guide-body h2 { font-size: 1.25rem; line-height: 1.35; margin: var(--s6) 0 var(--s3); color: var(--pine-900); }
.guide-body h3 { font-size: 1.08rem; line-height: 1.4; margin: var(--s5) 0 var(--s2); color: var(--pine-900); }

.guide-body .guide-list { margin: 0 0 var(--s4); padding-inline-start: 22px; }
.guide-body .guide-list li { margin-bottom: var(--s2); }

/* border-inline-start, not border-left: a pull quote with a physical border is a bug that
   appears only in Urdu, which means it appears only in production. */
.guide-body blockquote {
  margin: var(--s6) 0; padding-inline-start: var(--s5);
  border-inline-start: 3px solid var(--pine-500);
  font-size: 1.15rem; line-height: 1.5; font-weight: 600; color: var(--ink);
}
.guide-body a { color: var(--pine-700); }
/* A dated claim about a road has to look dated. */
.guide-stale { background: #FBEED3; color: #6d4f11; border-radius: var(--radius);
               padding: 11px 14px; font-size: .93rem; margin: 0 0 16px; }
.guide-stale a { color: #6d4f11; font-weight: 600; }
.guide-draft { background: var(--mist); border: 1px dashed var(--pine-500); border-radius: var(--radius);
               padding: 10px 14px; font-size: .9rem; margin: 0 0 14px; }
.guide-places { margin-top: 30px; }
.guide-places h2 { font-size: 1.2rem; margin-bottom: 10px; }
.guide-foot { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 28px; }

.guide-form label { display: block; margin-bottom: 14px; font-weight: 600; }
.guide-form label .hint { display: block; font-weight: 400; margin: 2px 0 4px; }
.guide-form input[type=text], .guide-form input[type=url], .guide-form textarea,
.guide-form select { width: 100%; }
.guide-form textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92rem; }
.guide-form-row { border: 0; padding: 0; margin: 0 0 6px; display: grid;
                  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 0 14px; }
.guide-form-row label.check { font-weight: 400; align-self: end; }
.guide-form-pic { max-width: 320px; border-radius: var(--radius); display: block; }
.guide-form-actions { display: flex; gap: 10px; align-items: center; margin-top: 6px; }
@media (max-width: 860px) {
  .guide-page h1 { font-size: 1.5rem; }
  .guide-lead { font-size: 1.02rem; }
  .guide-card-pic { height: 132px; }
}

/* ---------- Rich text editor ---------- */
.mv-ed { border: 1px solid var(--line); border-radius: var(--radius); background: #fff; overflow: hidden; }
.mv-ed-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; padding: 6px 8px;
             background: var(--mist); border-bottom: 1px solid var(--line); }
.mv-ed-btn { background: transparent; border: 1px solid transparent; border-radius: 7px;
             padding: 5px 9px; font-size: .88rem; font-weight: 600; color: var(--pine-900);
             cursor: pointer; line-height: 1.1; }
.mv-ed-btn:hover { background: #fff; border-color: var(--line); }
.mv-ed-btn.is-bold { font-weight: 800; }
.mv-ed-btn.is-italic { font-style: italic; }
.mv-ed-btn.is-underline { text-decoration: underline; }
.mv-ed-btn.is-strike { text-decoration: line-through; }
/* Already applied. Without this the only way to know whether you are inside a list is to
   look at the words, which is the sort of small uncertainty that makes an editor tiring. */
.mv-ed-btn.is-on { background: var(--pine-700); border-color: var(--pine-700); color: #fff; }
.mv-ed-style { border: 1px solid var(--line); border-radius: 7px; background: #fff;
               padding: 5px 8px; font-size: .88rem; font-weight: 600; color: var(--pine-900);
               cursor: pointer; font-family: inherit; min-width: 132px; }
.mv-ed-style.is-other { color: var(--ink-soft); font-style: italic; }
.mv-ed-sep { width: 1px; height: 18px; background: var(--line); margin: 0 5px; }
/* The HTML view reuses the textarea the form already submits, so there is nothing to keep
   in step — but it has to stop looking like a hidden field and start looking like a pane. */
.mv-ed.is-source .mv-ed-source { display: block; width: 100%; border: 0; border-radius: 0;
               min-height: 320px; padding: 14px 16px; font-family: ui-monospace, Menlo, Consolas, monospace;
               font-size: .85rem; line-height: 1.6; resize: vertical; outline: none; }
.mv-ed-count { margin-inline-start: auto; color: var(--ink-soft); font-size: .8rem; font-weight: 400; }
.mv-ed-area { min-height: 320px; max-height: 70vh; overflow-y: auto; padding: 16px 18px;
              font-size: 1.02rem; line-height: 1.65; font-weight: 400; outline: none; }
.mv-ed-area.rtl { min-height: 200px; }
.mv-ed-area:focus { box-shadow: inset 0 0 0 2px var(--pine-100); }
.mv-ed-area h2 { font-size: 1.28rem; margin: 20px 0 8px; }
.mv-ed-area h3 { font-size: 1.1rem; margin: 16px 0 6px; }
.mv-ed-area p { margin: 0 0 12px; }
.mv-ed-area ul, .mv-ed-area ol { margin: 0 0 12px; padding-inline-start: 24px; }
.mv-ed-area blockquote { margin: 0 0 12px; padding-inline-start: 14px;
                         border-inline-start: 3px solid var(--pine-500); color: var(--ink-soft); }
.mv-ed-area figure { margin: 14px 0; }
.mv-ed-area figure img { max-width: 100%; border-radius: 8px; display: block; }
.mv-ed-area figcaption { font-size: .84rem; color: var(--ink-soft); margin-top: 4px; }
.mv-ed-area a { color: var(--pine-700); }
.mv-ed-panel { border-top: 1px solid var(--line); background: var(--mist); padding: 12px 14px; }
.mv-ed-panel-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mv-ed-status { color: var(--ink-soft); font-size: .85rem; }
.mv-ed-lib { margin: 12px 0 8px; font-size: .85rem; color: var(--ink-soft); }
.mv-ed-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 8px;
              max-height: 260px; overflow-y: auto; }
.mv-ed-thumb { padding: 0; border: 2px solid transparent; border-radius: 8px; background: none;
               cursor: pointer; overflow: hidden; line-height: 0; }
.mv-ed-thumb:hover { border-color: var(--pine-500); }
.mv-ed-thumb img { width: 100%; height: 72px; object-fit: cover; display: block; }
.guide-form label:has(> .mv-ed) { font-weight: 600; }

/* Figures inside a published guide */
.guide-body figure { margin: 22px 0; }
.guide-body figure img { max-width: 100%; height: auto; border-radius: var(--radius); display: block; }
.guide-body figcaption { font-size: .85rem; color: var(--ink-soft); margin-top: 6px; }
.guide-body blockquote { margin: 0 0 16px; padding-inline-start: 16px;
                         border-inline-start: 3px solid var(--pine-500); color: var(--ink-soft); }
.guide-body h3 { font-size: 1.1rem; margin: 20px 0 8px; color: var(--pine-900); }
.guide-body ol { margin: 0 0 16px; padding-inline-start: 22px; }
.guide-body img { max-width: 100%; height: auto; }

/* ---------- Picture library ---------- */
.media-upload { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px 14px; align-items: end; }
.media-upload label { display: block; font-weight: 600; font-size: .9rem; }
.media-upload input { width: 100%; }
.media-search { display: flex; gap: 8px; margin: 16px 0; }
.media-search input { flex: 1; }
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; }
.media-item { margin: 0; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.media-item img { width: 100%; height: 120px; object-fit: cover; display: block; }
.media-item figcaption { padding: 8px 10px; font-size: .85rem; }
.guide-converted { background: var(--mist); border-inline-start: 3px solid var(--pine-500);
                   border-radius: 8px; padding: 9px 13px; font-size: .88rem; font-weight: 400;
                   color: var(--ink-soft); margin: 0 0 8px; }

/* ---------- Areas desk ---------- */
.area-new { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 14px; align-items: end; }
.area-new > b { grid-column: 1 / -1; }
.area-new label { display: block; font-weight: 600; font-size: .88rem; }
.area-new input { width: 100%; }
.area-table td { vertical-align: middle; }
.area-row-pic { width: 62px; }
.area-row-pic img { width: 54px; height: 40px; object-fit: cover; border-radius: 6px; display: block; }
.area-row-nopic { display: flex; width: 54px; height: 40px; align-items: center; justify-content: center;
                  background: var(--mist); border-radius: 6px; opacity: .5; }
.area-form label { display: block; margin-bottom: 12px; font-weight: 600; }
.area-form label .hint { display: block; font-weight: 400; margin: 2px 0 4px; }
.area-form input[type=text], .area-form input[type=url], .area-form input[type=number],
.area-form textarea { width: 100%; }
.area-places { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.area-place { display: flex; align-items: center; gap: 12px; background: #fff;
              border: 1px solid var(--line); border-radius: var(--radius); padding: 8px 12px; }
.area-place-pic { flex: 0 0 auto; width: 48px; height: 48px; border-radius: 8px; background: var(--mist);
                  display: flex; align-items: center; justify-content: center; overflow: hidden; }
.area-place-pic img { width: 100%; height: 100%; object-fit: cover; }
.area-place-body { flex: 1 1 auto; min-width: 0; }
.area-place-body b { display: block; }
.area-place-body .hint { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.area-place-acts { display: flex; gap: 6px; flex-wrap: wrap; flex: 0 0 auto; }
.file-btn { position: relative; overflow: hidden; cursor: pointer; }
.file-btn input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
@media (max-width: 700px) {
  .area-place { flex-wrap: wrap; }
  .area-place-acts { width: 100%; }
}

/* ---------- Roles ---------- */
.role-card { margin-bottom: 16px; }
.role-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.role-name { font-size: 1.05rem; font-weight: 700; border: 1px solid transparent; background: transparent;
             padding: 3px 6px; border-radius: 6px; width: 100%; max-width: 300px; }
.role-name:hover, .role-name:focus { border-color: var(--line); background: #fff; }
.role-key { display: block; margin-left: 6px; }
.role-count { flex: 0 0 auto; background: var(--pine-100); color: var(--pine-700); border-radius: 999px;
              padding: 3px 12px; font-size: .8rem; font-weight: 700; white-space: nowrap; }
.role-desc { width: 100%; margin: 8px 0 4px; font-size: .9rem; }
.role-locked { background: var(--mist); border-radius: 8px; padding: 10px 13px; font-size: .88rem;
               color: var(--ink-soft); margin: 10px 0 4px; }
.role-caps { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; margin: 12px 0 6px; }
.role-group { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px 10px; margin: 0; }
.role-group legend { font-size: .76rem; text-transform: uppercase; letter-spacing: .5px;
                     color: var(--ink-soft); padding: 0 5px; }
.role-cap { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 9px; font-size: .9rem; cursor: pointer; }
.role-cap input { margin-top: 3px; flex: 0 0 auto; }
.role-cap .hint { font-size: .8rem; line-height: 1.35; }

/* ---------- Field app (phone first) ---------- */
.page-field { background: var(--mist); }
.field-app { max-width: 560px; margin: 0 auto; padding: 12px 14px 90px; }
.field-top h1 { font-size: 1.4rem; margin: 6px 0 2px; }
.field-back { display: inline-block; font-size: .9rem; margin-bottom: 4px; }
.field-sub { color: var(--ink-soft); font-size: .92rem; margin: 0 0 12px; }
.field-where { display: flex; align-items: flex-start; gap: 10px; background: #fff; border: 1px solid var(--line);
               border-inline-start: 4px solid var(--ink-soft); border-radius: var(--radius);
               padding: 12px 14px; font-size: .95rem; line-height: 1.45; margin-bottom: 12px; }
.field-where.is-ok   { border-inline-start-color: var(--ok); }
.field-where.is-weak { border-inline-start-color: var(--amber); }
.field-where.is-out  { border-inline-start-color: var(--danger); }
.field-where.is-bad  { border-inline-start-color: var(--danger); }
.field-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; margin-top: 6px;
             background: var(--ink-soft); }
.field-where.is-ok .field-dot { background: var(--ok); animation: fieldpulse 2s infinite; }
.field-where.is-weak .field-dot { background: var(--amber); }
.field-where.is-out .field-dot, .field-where.is-bad .field-dot { background: var(--danger); }
@keyframes fieldpulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
.field-card { background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
              padding: 14px 16px; margin-bottom: 10px; }
.field-card-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.field-card-head b { font-size: 1.08rem; }
.field-pill { background: var(--pine-100); color: var(--pine-700); border-radius: 999px;
              padding: 3px 12px; font-size: .82rem; font-weight: 700; }
.field-meta { color: var(--ink-soft); font-size: .86rem; margin: 6px 0 0; line-height: 1.5; }
.field-note { background: var(--mist); border-radius: 8px; padding: 8px 11px; font-size: .88rem; margin: 8px 0 0; }
.field-bar { height: 6px; background: var(--mist); border-radius: 999px; margin-top: 10px; overflow: hidden; }
.field-bar span { display: block; height: 100%; background: var(--pine-500); }
.field-empty b { display: block; margin-bottom: 4px; }
.field-empty p { margin: 0; color: var(--ink-soft); font-size: .92rem; }
.field-go { display: block; width: 100%; text-align: center; background: var(--pine-700); color: #fff;
            border: 0; border-radius: 12px; padding: 15px; font-size: 1.05rem; font-weight: 700;
            text-decoration: none; margin: 14px 0; cursor: pointer; }
.field-go[disabled] { background: var(--line); color: var(--ink-soft); cursor: not-allowed; }
.field-h2 { font-size: 1.05rem; margin: 22px 0 8px; }
.field-list { list-style: none; margin: 0; padding: 0; }
.field-item { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; background: #fff;
              border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin-bottom: 7px; }
.field-item-name { font-weight: 600; }
.field-item-meta { grid-column: 1; color: var(--ink-soft); font-size: .82rem; }
.field-tag { grid-row: 1 / span 2; align-self: center; border-radius: 999px; padding: 2px 10px;
             font-size: .76rem; font-weight: 700; }
.field-tag-pending  { background: #FBEED3; color: #8a6414; }
.field-tag-accepted { background: var(--pine-100); color: var(--pine-700); }
.field-tag-rejected { background: #F6DDD9; color: var(--danger); }
.field-form label { display: block; font-weight: 600; margin-bottom: 14px; font-size: .95rem; }
.field-form input[type=text], .field-form input[type=tel], .field-form select,
.field-form textarea, .field-form input[type=file] { width: 100%; font-size: 1rem; padding: 11px 12px; }
.field-opt { display: block; font-weight: 400; color: var(--ink-soft); font-size: .82rem; margin: 2px 0 4px; }
.field-check { display: flex; gap: 9px; align-items: flex-start; font-weight: 400; font-size: .9rem; }
.field-check input { margin-top: 3px; }
.field-dupes { background: #FBEED3; color: #6d4f11; border-radius: var(--radius); padding: 11px 14px;
               font-size: .9rem; margin-bottom: 12px; }
.field-dupes ul { margin: 6px 0; padding-inline-start: 20px; }
.field-dupes li span { color: #8a6414; font-size: .84rem; }
.field-dupes p { margin: 6px 0 0; font-size: .86rem; }

/* Supervisor desk */
.field-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 14px 0 22px; }
.field-stat { background: #fff; border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; }
.field-stat b { display: block; font-size: 1.7rem; line-height: 1.1; color: var(--pine-900); }
.field-stat span { color: var(--ink-soft); font-size: .85rem; }
.field-stat.is-hot { border-color: var(--amber); background: #FFFCF4; }
.field-review { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 10px; }
.field-review-pic { flex: 0 0 auto; width: 74px; height: 74px; border-radius: 10px; background: var(--mist);
                    display: flex; align-items: center; justify-content: center; overflow: hidden; }
.field-review-pic img { width: 100%; height: 100%; object-fit: cover; }
.field-review-body { flex: 1 1 auto; min-width: 0; }
.field-review-acts { display: flex; gap: 6px; flex-wrap: wrap; flex: 0 0 auto; }
.field-warn { display: inline-block; background: #FBEED3; color: #8a6414; border-radius: 999px;
              padding: 1px 9px; font-size: .76rem; font-weight: 700; margin-inline-start: 4px; }
.admin-table tr.is-done td { opacity: .55; }
@media (max-width: 700px) {
  .field-review { flex-wrap: wrap; }
  .field-review-acts { width: 100%; }
}

/* Mail health banner — the one failure that is invisible from the outside. */
.mail-alarm { border-radius: var(--radius); padding: 14px 16px; margin: 14px 0 18px;
              border-inline-start: 4px solid var(--danger); background: #FDF2F0; }
.mail-alarm-warn { border-inline-start-color: var(--amber); background: #FFFBF2; }
.mail-alarm b { display: block; margin-bottom: 4px; }
.mail-alarm p { margin: 0 0 10px; font-size: .93rem; line-height: 1.5; }

/* "Which ones" — folded away by default, because the banner's job is to say something is
   wrong and this is the answer to the second question, not the first. */
.mail-fail-log { margin: 0 0 10px; }
.mail-fail-log summary { cursor: pointer; font-size: .9rem; font-weight: 600;
                         padding: 4px 0; }
.mail-fail-log summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.mail-fail-log .table-card { margin: 8px 0; max-height: 46vh; overflow: auto; }
.mail-fail-log table { font-size: .86rem; }
.mail-fail-log .hint { font-size: .82rem; }

/* ---------- Profile pictures ---------- */
.avatar { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
          border-radius: 50%; overflow: hidden; background: var(--mist); vertical-align: middle;
          font-weight: 700; line-height: 1; user-select: none; }
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-initials { background: hsl(var(--avatar-hue, 160) 38% 88%); color: hsl(var(--avatar-hue, 160) 45% 28%); }
.avatar-sm { width: 26px; height: 26px; font-size: .68rem; }
.avatar-md { width: 40px; height: 40px; font-size: .95rem; }
.avatar-lg { width: 96px; height: 96px; font-size: 2.1rem; }
.avatar-edit { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 8px; }
.avatar-edit-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.rev-who { display: flex; align-items: center; gap: 10px; }

/* ---------- Advertising: the slot catalogue and the bid queues ---------- */
.slot-card { margin-bottom: 16px; }
.slot-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
/*
 * THE HEADING IS THE LINK TO THE SLOT, so it is a target and not just a title.
 *
 * Each advertising slot on /advertise is a card whose heading — "On the front page", "In the
 * search results" — is the only way into it. Set at 24px, the link's box is exactly its
 * line-height: 32px, which is what a 24px line is, and 12px short of a thumb. Padding with the
 * margin given back, so the card's spacing is unchanged and only the hit area grows.
 *
 * Found because the measuring tool stopped absolving 24px headings under the "inline link in a
 * sentence" exemption, which is for prose and was never meant to cover a card title.
 */
.slot-head h2 a {
  color: inherit;
  /* 7px, not 6: a 24px heading's line box computes to 31.2px here, so 6 left it at 43 — one
     pixel short, which is the kind of near-miss that makes a bar look arbitrary. */
  display: inline-block; padding-block: 7px; margin-block: -7px;
}
/* The heading and its blurb are one block beside the button, so the heading carries no
   margin of its own and the blurb sits close under it rather than half way to the next
   card. Both used to be inline styles on every slot on the price list. */
.slot-head h2 { margin: 0; }
.slot-head .hint { margin: 6px 0 0; max-width: 58ch; }
.slot-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin: 16px 0 0; }
.slot-facts-wide { margin: 18px 0 10px; }
.slot-facts > div { background: var(--mist); border-radius: 10px; padding: 10px 12px; }
/* 1.2 line-height clips Naskh; the figure is a number in either language but the label under
   it is a sentence. The label was .78rem = 12.5px — 32 instances of it across the advertise
   pages, and it is the half of the pair that says what the number MEANS. */
.slot-facts b { display: block; font-size: 1.22rem; line-height: 1.35; font-variant-numeric: tabular-nums; }
.slot-facts span { font-size: var(--t-meta); color: var(--ink-soft); }
.slot-leader { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px;
               background: #FFFDF4; border: 1px solid #EAD9A8; font-size: .93rem; }
.slot-leader-empty { background: var(--mist); border-color: var(--line); color: var(--ink-soft); }
.auction-formula { background: var(--mist); border-radius: 10px; padding: 12px 14px;
                   font-weight: 600; text-align: center; }
/* Your own bid, so you can find yourself in a long queue without reading every row. */
.admin-table tr.bid-mine td { background: #F3FAF6; }
.admin-table .inline-form { display: inline-block; margin: 0 2px 0 0; }
.bid-form label { display: block; font-weight: 600; margin-bottom: 10px; }
.bid-form label .hint { font-weight: 400; display: block; }
.bid-form input, .bid-form select { width: 100%; }
.bid-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.bid-preview { margin: 6px 0 14px; padding: 12px 14px; border-radius: 10px;
               background: var(--mist); font-size: .93rem; min-height: 46px; }
.bid-win { color: var(--ok); }
.bid-lose { color: var(--danger); }

/* ── Advertising: the furniture every one of these screens needs ───────────────
   An advertising screen is prose, a table and a form rather than a map, so they all
   want the same three things: a measure narrower than the site's 1080px, a heading
   that does not push against the top of a padded card, and notes that sit a fixed
   distance from the thing they explain. Those shipped as inline `style=` on sixty-odd
   elements. They belong here, once, where the RTL overrides and the phone breakpoint
   below can actually see them. */
.page-ads.is-wide { max-width: 980px; }
.page-ads.is-mid  { max-width: 820px; }
.page-ads.is-slim { max-width: 640px; }
.page-ads .card > :first-child { margin-top: 0; }
/* A measure in characters, not pixels: 640px stops meaning "a readable line" the
   moment somebody's browser is set to 18px. */
.page-ads .measure       { max-width: 66ch; }
.page-ads .measure-tight { max-width: 52ch; }
.page-ads .center .measure, .page-ads .center .measure-tight { margin-inline: auto; }
.page-ads .note-after { margin: 12px 0 0; }
.page-ads .note-far   { margin: 20px 0 0; }
.page-ads .flush      { margin: 0; }
.page-ads .flush-end  { margin-bottom: 0; }
.page-ads .field-spaced { margin-top: 14px; }
/* A sentence that qualifies the figures above it belongs to those figures, so it sits
   closer to them than to the paragraph after it. */
.page-ads .market-note { margin-top: -4px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* "Open an account" — one short column, centred, with room to breathe around it. */
.card-invite { padding: 28px; }
.card-invite p { margin: 0 auto 16px; }
.card-invite form { max-width: 420px; margin-inline: auto; text-align: start; }
.creative-row { display: flex; gap: 14px; justify-content: space-between; flex-wrap: wrap; }
.creative-body { margin: 4px 0; }
.creative-reject { color: var(--danger); }
.creative-thumb { width: 100px; height: 70px; object-fit: cover; border-radius: 8px; }
.creative-add-head { margin: 0 0 10px; }

/* ── Advertising: which market, and where a bid may run ────────────────────────
   There is no longer one market, so the price list has to ask which one before it can
   quote anything. The picker is a real GET form with a submit button — adscope.js only
   saves the press — so it is laid out as a row that wraps rather than as a control bar
   that assumes the room it has. */
.market-picker { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.market-picker label { margin: 0; }
.market-picker select.input { width: auto; flex: 1 1 220px; min-width: 220px; max-width: 100%; }
/* The sentence explaining what the chosen market includes takes the whole second line:
   it is the answer to "so what am I looking at?", not a hint clipped beside a button. */
.market-picker .hint { flex-basis: 100%; margin: 8px 0 0; }

/* Where the money may run. Radios rather than a dropdown because the two answers are
   not the same kind of thing — one is the whole country, the other is a list you have
   to build — and each carries two lines of explanation the radio must align with the
   TOP of, not the middle. The descendant selectors are deliberate: `.bid-form label`
   is (0,1,1) and would otherwise win over a bare class and put these back in a block. */
.bid-scope { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; margin: 0 0 16px; }
.bid-scope > legend { font-weight: 600; padding: 0 6px; color: var(--pine-900); }
.bid-scope > .bid-scope-choice { display: flex; gap: 10px; align-items: flex-start;
                                 font-weight: 400; margin: 0 0 10px; cursor: pointer; }
.bid-scope > .bid-scope-choice b { color: var(--pine-900); }
.bid-scope .bid-scope-choice input[type="radio"] { width: auto; flex: 0 0 auto; margin-top: 4px;
                                                   accent-color: var(--pine-700); }
/* The chosen answer, said in weight rather than in colour alone. */
.bid-scope > .bid-scope-choice:has(input:checked) span { color: var(--ink); }
.bid-scope > .bid-scope-choice:has(input:checked) { color: var(--ink); }

/* Ninety cities in one control. The search box is what makes that usable at all, and
   the list keeps a fixed height so the form below it does not move as options are
   filtered out — a page that jumps while you are choosing is a page you misclick. */
.city-picker { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.city-picker input[type="search"] { margin-bottom: 8px; }
.city-picker select[multiple] { border: 1.5px solid var(--line); border-radius: 10px;
                                padding: 4px; background: #fff; height: 15rem; }
.city-picker select[multiple]:focus { border-color: var(--pine-500); box-shadow: 0 0 0 3px rgba(45,106,79,.12); outline: none; }
.city-picker select[multiple] option { padding: 5px 8px; border-radius: 6px; }
.city-picker select[multiple] option:checked { background: var(--pine-100); color: var(--pine-700); font-weight: 600; }
.city-picker .hint { margin: 8px 0 0; }
/* Before a city is picked the summary has nothing to say, and an empty paragraph with a
   margin is a gap nobody can explain. */
.city-picker .hint:empty { display: none; }

/* What it would take to win, in the one figure the bidder can act on — so it reads as
   a figure rather than as another paragraph of the same weight. */
.bid-need { background: var(--mist); border-radius: 10px; padding: 12px 14px; margin: 0 0 16px; }

/* National or local, at a glance. Somebody who has just been outbid is asking whether it
   was a neighbour or a national brand, and a column of city names only answers that
   after it has been read. The tag answers it before. Never colour alone: the two tags
   differ in their words first and in their tint second. */
.bid-where { min-width: 9rem; }
.scope-tag { display: inline-block; border-radius: 999px; padding: 1px 9px; font-size: .74rem;
             font-weight: 700; letter-spacing: .3px; text-transform: uppercase; white-space: nowrap; }
.scope-tag.is-national { background: var(--pine-100); color: var(--pine-700); }
.scope-tag.is-local    { background: #FBEED3; color: #8a6414; }
.scope-cities { display: block; font-size: .82rem; color: var(--ink-soft); margin-top: 2px; }

/* Where one bid's money actually went, city by city, on the row under the bid itself.
   A named city that has earned nothing is shown at zero rather than left out — that
   absence is the fact somebody who paid for Gilgit came here to find. */
.bid-spread > td { padding-top: 0; }
.bid-spread-list { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: baseline;
                   vertical-align: top; }
.bid-spread-list .badge { font-weight: 600; white-space: nowrap; }
.bid-spread-list .badge .hint { font-size: .74rem; font-weight: 400; }

/* RTL. A city name in Urdu next to "Rs 12.00" is bidirectional text, and left to the
   paragraph's direction the browser drags the number to the wrong end of the run.
   `plaintext` lets each line answer for itself — the same treatment this stylesheet
   already gives distances, times and star ratings. */
html[dir="rtl"] .city-picker select[multiple] option,
html[dir="rtl"] .bid-spread-list .badge,
html[dir="rtl"] .scope-cities,
html[dir="rtl"] .slot-leader,
html[dir="rtl"] .bid-need { unicode-bidi: plaintext; }
/* The border a fieldset legend sits in is symmetric, but the picker's separator and the
   invitation card's form are not: both are keyed to the start of the line. */
html[dir="rtl"] .card-invite form { text-align: right; }
html[dir="rtl"] .creative-reject { text-align: right; }

@media (max-width: 700px) {
  /* A ten-row list is most of a phone screen, and the search box above it is the part
     that has to stay in view while you type. */
  .city-picker select[multiple] { height: 11rem; }
  .market-picker select.input { flex-basis: 100%; min-width: 0; }
  .market-picker .btn { width: 100%; }
  /* Pills that run off the side of a table cell cannot be read at all, so the spread
     becomes a list. */
  .bid-spread-list { display: grid; gap: 4px; }
  .bid-spread-list .badge { white-space: normal; }
  .bid-where { min-width: 0; }
  .card-invite { padding: 20px; }
}
/* A week of conditions reads as a timeline, not a pile: one heading per day. */
.cond-day { margin: 18px 0 6px; font-size: .82rem; font-weight: 700; color: var(--ink-soft);
            text-transform: uppercase; letter-spacing: .5px; }
.cond-day:first-of-type { margin-top: 6px; }

/* "over the last 27.3 km" — the scope of the climb, beside the figures it qualifies and
   never louder than them. One rule for both panels; a class that means two things in two
   places is the bug that hid the Log out button. */
.dir-elev-stats .scope,
.nav-elev .scope { color: var(--ink-soft); font-weight: 400; font-size: .8rem; flex: 0 1 auto; min-width: 0; }

/* ─────────────────────────────────────────────────────── Wallets and the ledger
   Money on a screen has one job: to be read correctly at a glance, including by
   somebody who is worried. So the sign is never carried by colour alone (a red
   number and a green number are the same number to eight per cent of men), the
   amounts are tabular so columns line up on the decimal point, and the balance
   after each row sits in its own quiet column rather than competing with it. */
/* The advertiser column has to fit "Name — Rs 10,437.60". At 180px it cut the balance off
   mid-number, which on a page about money is the one thing it must not do. */
.wallet-adjust { display: grid; grid-template-columns: minmax(260px, 1.1fr) 120px minmax(220px, 1.6fr) auto;
  gap: 12px; align-items: end; }
.wallet-adjust select.input { text-overflow: ellipsis; }
.wallet-adjust .field { margin: 0; min-width: 0; }
.wallet-adjust .req { color: var(--ink-soft); font-weight: 400; font-size: .78rem; }
.wallet-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Subtract is not a destructive-red button — it is an ordinary, reversible operation
   and dressing it as a delete makes every other red button on the site mean less. It
   is distinguished from Add by weight, not by alarm. */
.btn-danger-soft { background: transparent; color: var(--danger); border: 1px solid var(--danger); }
.btn-danger-soft:hover { background: var(--danger); color: #fff; }

.wallet-filters { display: flex; gap: 12px; align-items: end; flex-wrap: wrap; }
.wallet-filters .field { margin: 0; min-width: 140px; }

.wallet-totals { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 16px 0; }
.wallet-totals .up { color: var(--ok); }
.wallet-totals .down { color: var(--danger); }

.wallet-table td, .wallet-table th { vertical-align: top; }
.wallet-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.wallet-table .nowrap { white-space: nowrap; }
/* A money figure that wraps between "Rs" and the number reads as two things. On a 380px
   screen both the balance tiles and the amount column were doing it. */
.wallet-amt { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.wallet-totals .stat-card b { white-space: nowrap; }
.wallet-amt.up { color: var(--ok); }
.wallet-amt.down { color: var(--danger); }
.wallet-note { margin-top: 3px; font-size: .86rem; color: var(--ink); max-width: 46ch; }

.card-head-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.card-head-row .card-title { margin: 0; }
.pager { display: flex; align-items: center; justify-content: center; gap: 14px; margin: 16px 0; }

@media (max-width: 760px) {
  .wallet-adjust { grid-template-columns: 1fr; }
  .wallet-actions { display: grid; grid-template-columns: 1fr 1fr; }
  .wallet-actions .btn { width: 100%; }
  /* On a phone the balance-after column is the first thing worth losing: the amount and
     the reason are what somebody is scanning for, and a five-column table on a 380px
     screen is a five-column table nobody reads. */
  .wallet-table thead th:last-child, .wallet-table tbody td:last-child { display: none; }
}

/* ============ The admin confirmation dialog ============
   Built by assets/js/admin.js rather than sitting in every view, because a dialog that is
   in the markup of forty screens is a dialog that is subtly different on four of them. */
.confirm-dialog {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 0;
  max-width: 30rem;
  width: calc(100vw - 2rem);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
.confirm-dialog::backdrop { background: rgba(20, 52, 43, .42); }
.confirm-inner { display: flex; flex-direction: column; gap: 12px; padding: 22px; margin: 0; }
.confirm-title { margin: 0; font-size: 1.16rem; line-height: 1.3; }
.confirm-detail { margin: 0; color: var(--ink-soft); font-size: .93rem; }
.confirm-list {
  margin: 0; padding: 12px 14px 12px 30px; font-size: .9rem; color: var(--ink-soft);
  background: var(--mist); border-radius: var(--radius);
}
.confirm-list li { margin: 2px 0; }
.confirm-typebox { display: flex; flex-direction: column; gap: 6px; }
.confirm-typelabel { font-size: .84rem; color: var(--ink-soft); }
.confirm-typeinput { font-family: ui-monospace, Menlo, Consolas, monospace; }
.confirm-typeinput.is-ok { border-color: var(--ok); }
.confirm-actions { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; }
.confirm-actions .btn { min-width: 6.5rem; }
.confirm-go[disabled] { opacity: .5; cursor: not-allowed; }
html[dir="rtl"] .confirm-list { padding: 12px 30px 12px 14px; }
html[dir="rtl"] .confirm-actions { justify-content: flex-start; }
@media (prefers-reduced-motion: no-preference) {
  .confirm-dialog[open] { animation: confirm-in .16s var(--ease); }
  @keyframes confirm-in { from { opacity: 0; transform: translateY(6px) scale(.985); } }
}

/* ============ The admin desk: index, row actions, history ============ */
.desk-groups { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.desk-group { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 16px 18px 18px; }
.desk-group h3 { margin: 0 0 4px; font-size: .78rem; text-transform: uppercase; letter-spacing: .6px; color: var(--ink-soft); }
/* The city you are standing in leads, and is marked as yours rather than as one more
   category — it is the whole of a regional person's job in one box. */
.desk-group.is-city { border-color: var(--pine-500); box-shadow: var(--shadow); }
.desk-group.is-city h3 { color: var(--pine-700); }
.desk-group p.desk-why { margin: 0 0 12px; font-size: .86rem; color: var(--ink-soft); }
.desk-links { display: flex; flex-direction: column; gap: 2px; }
.desk-link {
  display: flex; align-items: baseline; gap: 8px; padding: 7px 9px;
  border-radius: 10px; color: var(--ink); font-size: .95rem;
}
.desk-link:hover, .desk-link:focus-visible { background: var(--mist); }
.desk-link .desk-what { flex: 1; }
.desk-link .desk-sub { display: block; font-size: .8rem; color: var(--ink-soft); line-height: 1.35; }
/* Also used on its own beside a filter box and in the nav strip, not only inside a desk link. */
.desk-count {
  font-variant-numeric: tabular-nums; font-size: .82rem; color: var(--ink-soft);
  background: #E9ECEA; border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.desk-count.is-waiting { background: var(--amber); color: #4A3510; font-weight: 600; }
.desk-link .desk-count {
  font-variant-numeric: tabular-nums; font-size: .82rem; color: var(--ink-soft);
  background: #E9ECEA; border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.desk-link .desk-count.is-waiting { background: var(--amber); color: #4A3510; font-weight: 600; }

/* One row of verbs at the end of a table row. Kept to icons-plus-words, because an icon
   alone is a guess and this column is where the destructive things live. */
.row-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.row-actions form { display: inline; }
html[dir="rtl"] .row-actions { justify-content: flex-start; }
.admin-table td.actions { text-align: right; white-space: nowrap; }
html[dir="rtl"] .admin-table td.actions { text-align: left; }

/* The change history shown beside a record, and on the log screen. */
.history { display: flex; flex-direction: column; gap: 0; }
.history-row { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.history-row:last-child { border-bottom: 0; }
.history-when { font-size: .82rem; color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
.history-what { font-size: .93rem; }
.history-who { grid-column: 2; font-size: .82rem; color: var(--ink-soft); }
.history-diff { grid-column: 2; margin: 4px 0 0; font-size: .85rem; }
.history-diff table { border-collapse: collapse; }
.history-diff th { text-align: left; padding: 2px 12px 2px 0; font-weight: 600; color: var(--ink-soft); font-size: .8rem; }
.history-diff td { padding: 2px 12px 2px 0; }
.history-diff .was { color: var(--danger); text-decoration: line-through; }
.history-diff .now { color: var(--ok); }
html[dir="rtl"] .history-diff th, html[dir="rtl"] .history-diff td { padding: 2px 0 2px 12px; }

.filter-bar { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px; }
.filter-bar label { display: flex; flex-direction: column; gap: 3px; font-size: .78rem; color: var(--ink-soft); }
.filter-bar .grow { flex: 1; min-width: 12rem; }

.empty-state { text-align: center; padding: 34px 18px; color: var(--ink-soft); }
.empty-state b { display: block; color: var(--ink); font-size: 1.02rem; margin-bottom: 4px; }

/* ================================================================= THE ADMIN DESK
   A shell around every screen under Views/admin: the menu down the left, the work to the
   right of it. Stamped by View::render from the view's own name, so it cannot drift.

   WIDTH IS A PROPERTY OF THE CONTENT, NOT OF THE SECTION. Every admin screen used to sit in
   the 1080px column chosen for reading articles on the public site, so a twelve-column table
   scrolled sideways with half the monitor unused beside it. But uncapped is the wrong
   correction — a settings form stretched across 2560px is worse than a narrow one, because
   the eye cannot get from a label on the left to its field on the right. Hence three widths
   and one decision per screen. */
.admin-shell { display: grid; grid-template-columns: 244px minmax(0, 1fr); align-items: start; }
.admin-work  { min-width: 0; }
.admin-work > .page { max-width: 1680px; padding-top: 18px; }
/* One record, or a column of labelled fields. Opted into by the view, in one word.
   Anchored to the rail rather than centred in the leftovers: a narrow column floating in the
   middle of a wide desk reads as a page that failed to load the rest of itself, and the eye
   should travel from the menu to the work without crossing an empty gap first. */
.admin-work > .page.is-form  { max-width: 840px; margin-left: 0; }
/* A form beside a live preview or a list. */
.admin-work > .page.is-split { max-width: none; }

/* A LABELLED FIELD DOES NOT GET WIDER JUST BECAUSE THE PAGE DID.
   The desk went from a 1080px column to 1680px, and a single-line input took the whole of it
   — a "Timeout (seconds)" box six hundred pixels wide holding the number 12, with its label
   so far to the left the eye loses the pairing. `is-form` fixes the pages that are ONLY a
   form; this fixes the form that sits on a page which legitimately wants the room for a table
   beside it. Scoped to `.field >` on purpose: a search box, an editor and a table cell are
   laid out by their own rules and none of them is a labelled field. */
.admin-work .field > .input,
.admin-work .field > select.input,
.admin-work .field > textarea.input { max-width: 46rem; }
/* Two fields side by side already share a row; capping each again would strand them. */
.admin-work .grid-2 .field > .input,
.admin-work .grid-3 .field > .input,
.admin-work .grid-2 .field > select.input,
.admin-work .grid-3 .field > select.input { max-width: none; }

/* ---------- the rail ---------- */
.admin-rail {
  position: sticky; top: var(--topbar-h); align-self: start;
  max-height: calc(100vh - var(--topbar-h)); overflow-y: auto; overscroll-behavior: contain;
  padding: 14px 10px 40px; border-right: 1px solid var(--line); background: var(--card);
}
.rail-group {
  margin: 16px 0 4px; padding: 0 10px; font-size: .68rem; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--ink-soft); opacity: .75;
}
.rail-group.is-city { color: var(--pine-700); opacity: 1; }
.rail-item, .rail-home {
  display: flex; align-items: center; gap: 9px; padding: 6px 10px; border-radius: 8px;
  font-size: .92rem; font-weight: 500; color: var(--ink-soft); line-height: 1.3;
}
.rail-home { font-weight: 700; color: var(--ink); margin-bottom: 2px; }
.rail-item:hover, .rail-home:hover { background: var(--mist); color: var(--pine-700); text-decoration: none; }
.rail-item.on, .rail-home.on { background: var(--pine-700); color: #fff; font-weight: 600; }
.rail-item.on:hover, .rail-home.on:hover { background: var(--pine-700); color: #fff; }
.rail-item .ic { width: 18px; text-align: center; flex: none; font-size: .95rem; }
.rail-item .lb { min-width: 0; }
.rail-n {
  margin-left: auto; flex: none; font-size: .72rem; font-weight: 700; border-radius: 9px;
  padding: 1px 7px; background: var(--mist); color: var(--ink-soft);
}
.rail-n.is-waiting { background: var(--amber); color: #3A2A05; }
.rail-item.on .rail-n { background: rgba(255,255,255,.9); color: var(--pine-700); }

/* ---------- breadcrumbs ---------- */
.admin-crumbs {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: .84rem; color: var(--ink-soft); padding: 14px 18px 0;
  max-width: 1680px; margin: 0 auto;
}
.admin-crumbs a { color: var(--ink-soft); }
.admin-crumbs a:hover { color: var(--pine-700); }
.admin-crumbs .crumb-group { color: var(--ink-soft); }
.admin-crumbs .crumb-here { color: var(--ink); font-weight: 600; }
.admin-work > .page > h1:first-child { margin-top: 6px; }

/* ---------- narrow: the rail folds back into the strip it replaced ----------
   Below 1100px a 244px rail is a quarter of the screen for a menu, so the desk goes back to
   one column and the old strip — which is the right shape for a phone and is still rendered
   by /admin itself. */
/* ---------- narrow: the rail becomes ONE ROW THAT SCROLLS ----------
   Below 1100px a 244px column is a quarter of the screen for a menu, so the rail goes
   horizontal. The first attempt wrapped it and capped the height, and measuring that on a
   real phone is what killed it: at 320px it was 193px tall — a THIRD of the screen — showing
   five of thirty-seven links with the fifth clipped through the middle of a word. A menu box
   you scroll inside a page you also scroll reads as a broken region, not as a menu.

   One row, scrolled sideways, is a pattern this site already uses for category chips and that
   people already know. Every screen stays reachable, nothing is hidden, and it costs 56px.

   THE GROUP HEADINGS GO at this width, and that is the real trade: seven headings cannot sit
   in one row without becoming noise between the things they label. The order still carries the
   grouping, and /admin — the first link in the row — is the full grouped index, which is what
   it was built to be. */
@media (max-width: 1100px) {
  .admin-shell { grid-template-columns: 1fr; }
  .admin-rail {
    position: sticky; top: var(--topbar-h); z-index: 20;
    border-right: 0; border-bottom: 1px solid var(--line);
    padding: 7px 10px; display: flex; flex-wrap: nowrap; gap: 6px; align-items: center;
    max-height: none;
    overflow-x: auto; overflow-y: hidden;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  .admin-rail::-webkit-scrollbar { display: none; }
  .rail-group { display: none; }
  .rail-item, .rail-home {
    flex: none; white-space: nowrap; min-height: 44px; padding: 6px 13px;
    border: 1px solid var(--line); border-radius: 999px; font-size: .9rem;
    scroll-margin-inline: 12px;
  }
  .rail-home { border-color: var(--pine-500); }
  .rail-item.on, .rail-home.on { border-color: var(--pine-700); }
  .rail-item .lb { overflow: visible; }
  .admin-crumbs { padding: 12px 14px 0; }
}
@media (max-width: 560px) {
  /* The label alone; the icon is doing no work in a row this tight. */
  .admin-rail { padding: 6px 8px; }
  .rail-item, .rail-home { font-size: .86rem; padding: 6px 11px; }
}
@media print { .admin-rail, .admin-crumbs { display: none; } .admin-shell { display: block; } }


/* ================================================================ TOUCH TARGETS
   Measured across 17 pages at 320px and 390px: 146 distinct controls under 44px, and the
   pattern was not random — it inverted the value of the action. The phone number on a
   listing was 20px tall. Saving a place was a 34px heart. The grip that opens the sheet —
   the primary gesture on the map — was 28px. Everything that was not a .btn was an inline
   link inside a sentence, which reads as restraint on a desktop and as a dare on a phone.

   TWO TECHNIQUES, AND THE CHOICE BETWEEN THEM IS NOT TASTE.

   For anything that already has a box — a button, a chip — `min-height` states the rule
   directly. Reaching for padding instead means computing 44 from a font size and a line
   height that both change per component, which is how .btn-sm ended up at 35px while .btn
   was at 41: two rules trying to express one requirement in a unit that does not express it.

   For an icon whose SIZE IS THE DESIGN — the heart on a card, a map control — growing the
   box would move the layout. There a transparent 44px ::after catches the touch the ink is
   too small to catch. The hit area grows; nothing looks heavier.

   GUARDED BY (hover: none) ON PURPOSE. On a mouse a 20px link is a perfectly good target
   and the desktop design is not being asked to change; this applies only where a finger is
   the pointer. `pointer: coarse` catches the same devices, but hover is the property that
   actually distinguishes them — a touchscreen laptop reports both. */
@media (hover: none) {

  /* --- things with a box: say the number --- */
  .btn, .btn-sm, .chip, .facet-chip, .sheet-toggle, .sort-sel, .rail-filter {
    min-height: 44px;
  }

  /**
   * A <label> IS A TAP TARGET — tapping it focuses its field — and 28 of them across the
   * forms measured 24px tall. WCAG 2.5.8 arguably exempts them, since the input directly
   * below is the larger equivalent target, but the exemption is about not FAILING; it is not
   * an argument that a 24px target is good. A tap that lands on the label and does nothing
   * visible reads as an unresponsive form.
   *
   * Padding rather than min-height, so the label grows downward INTO the 5px gap it already
   * had above its input instead of pushing every field apart: the negative margin gives the
   * space back. Net layout change on a phone is nil; net hit area is 44px.
   */
  /* `.lbl` and the bare `label[for]` outside a .field are the same object under other
     markup — the four that survived the first pass were all one of those two.
     Excluded by their `for` rather than by a class, because the star and price-level labels
     carry none. Those two ARE the control — a row of star labels is the rating widget — and
     they are sized by their own rules. Catching them here gave them a 44px height and left
     their 1px width, which is not a bigger target, it is a taller sliver. */
  /* .sr-only is excluded because it is not a target — it is a 1px clipped label that exists
     only to be announced. Sizing it to 44px produced an invisible 44px tap target sitting on
     top of the landing page's search box, which is the same fault as inflating the honeypot
     and was caught the same way. A label nobody can see cannot be aimed at. */
  .field label:not(:empty), .checkbox, label.lbl,
  label[for]:not(:empty):not(.sr-only):not(.chip):not(.check):not([for^="star"]):not([for^="pl"]) {
    min-height: 44px; display: flex; align-items: center; flex-wrap: wrap;
  }
  .field label.sr-only { min-height: 0; }
  /* :not(:empty) is not defensive tidiness — it found something. One <label> on the listing
     page has no content at all: 1px wide, nothing to read, nothing announced to a screen
     reader. Inflating it to 44px would have made an invisible 44px target sitting in a form,
     which is worse than leaving it alone. An empty label is a markup question, not a sizing
     one, and it is written down in docs/DESIGN.md rather than hidden by a rule. */
  .field label { align-items: flex-end; padding-bottom: 5px; margin-bottom: 0; }
  /* A label wrapping its own hint is a block, not a row — restore it. */
  .field label:has(.hint) { display: block; min-height: 0; padding-bottom: 0; margin-bottom: 5px; }
  /* min-height on an inline-block does nothing; these need to lay out as boxes to obey it. */
  .btn, .btn-sm, .chip, .facet-chip {
    display: inline-flex; align-items: center; justify-content: center;
  }
  .btn-block { display: flex; }

  /* --- icons whose size is the design: grow the hit area, not the ink --- */
  .fav-heart, .rail-btn, .photo-del, .locate-btn, .chip-x,
  .maplibregl-marker, .maplibregl-ctrl-group button { position: relative; }

  .fav-heart::after, .rail-btn::after, .photo-del::after, .locate-btn::after,
  .chip-x::after, .maplibregl-marker::after, .maplibregl-ctrl-group button::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
    z-index: 0;
  }

  /* --- the verbs a directory lives on --- */
  /* Calling a restaurant, saving it, adding a photo, correcting the hours: 17-23px of text
     inside prose. They get a line of their own with room around them. */
  #biz-contact a, .add-to-list > summary, .photo-add > summary, .sug-card > summary {
    display: inline-flex; align-items: center; min-height: 44px;
  }

  /**
   * THE SAME VERBS, FOR A VISITOR WHO IS NOT SIGNED IN — WHICH IS MOST OF THEM.
   *
   * The rule above was written against the signed-in markup and every selector in it is right
   * for that. The listing page renders DIFFERENT ELEMENTS to a guest: `.photo-add` holds a
   * `<summary>` when you can upload and a plain `<a>` to the login page when you cannot, and
   * "add to a list" becomes `.add-to-list-guest` entirely. So the three actions a first-time
   * visitor is most likely to want were the three still measuring 19-21px, on the majority
   * path, months after the touch pass that fixed the signed-in ones.
   *
   * Worth stating as a rule rather than a fix: a responsive audit run while signed in tests
   * the minority. Measured on a phone at 375px — "Add a photo" 21px, "Add to a list" 19px.
   *
   * `.add-to-list-guest` is no longer rendered — the guest is now offered Save alone, see
   * business.php — and the selector stays here for the same reason the layout rule above it
   * does: it is a correct description of a component, not a workaround for one.
   */
  .photo-add > a, .add-to-list-guest > a, a.rating-count {
    display: inline-flex; align-items: center; min-height: 44px;
  }

  /**
   * "No reviews yet — be the first!" is the invitation to write the FIRST review on a site
   * that has none at all. It measured 21px. Of everything on this page it is the one target
   * whose whole purpose is to be tapped by somebody who has never tapped anything here.
   */

  /**
   * `.ad-why` STAYS SMALL, AND ITS HIT AREA DOES NOT.
   *
   * The note beside its rule says it deliberately: an advertisement that will not say why it
   * is there is the thing people install blockers to be rid of, so the link is small but never
   * hidden. Small TEXT was the decision; an 18px tap target was not — nobody chose that, it
   * just followed. Padding with the margin given back, so the ink is unchanged and the layout
   * does not move.
   */
  .ad-why { padding: 13px 0; margin-top: -5px; margin-bottom: -13px; }

  /**
   * A ROW OF LINKS THAT IS NAVIGATION, NOT PROSE.
   *
   * "See also: Collections · Shared lists · Deals" is set in the hint style, so it reads as a
   * quiet footnote — and its links measured 19px. WCAG 2.5.8 does exempt a link inline in a
   * sentence, and these are technically inline; but the exemption is about not FAILING, and
   * these are how somebody moves between the four things the site actually has.
   *
   * Padding rather than min-height, and a line-height that gives the padding room, so three
   * links on one line grow their hit areas without overlapping each other. Marked with a class
   * in the view instead of a selector: `p.hint > a` would also inflate every link inside a
   * real sentence, which is the case the exemption is genuinely for.
   */
  .alsonav { line-height: 2.6; }
  /*
   * AND WIDE ENOUGH, WHICH THE FIRST PASS FORGOT. Padding-block gave these a 52px height and
   * left "Deals" 34px wide — a thumb misses in either axis. `min-width` rather than
   * padding-inline so the separator dots stay where they are and a long link is not stretched.
   */
  .alsonav a {
    display: inline-block; padding-block: 8px;
    min-width: 44px; text-align: center;
  }

  /* The map's attribution stays as it is. It is required to be present, it is not an action
     anybody is trying to take, and inflating it would put a 44px target over the map in the
     corner a thumb rests in. Left small deliberately, not overlooked. */
}

/* ---------- /cities: what is open, and what is still looking ---------- */
/* A whole card is the link now, not a button inside one: the card IS the thing you want, and
   a 44px button inside a 120px card that does the same job is a smaller target for no reason. */
.listing-card.is-open-city { display: flex; flex-direction: column; gap: 4px; color: inherit; }
.listing-card.is-open-city:hover { text-decoration: none; border-color: var(--pine-500); box-shadow: var(--shadow); }

/* A grid, not an absolutely positioned marker: the heading wraps to two lines on a phone and
   a chevron pinned to a fixed offset lands on top of the second one. Its own column cannot
   collide with anything. */
.city-waiting > summary {
  cursor: pointer; list-style: none;
  display: grid; grid-template-columns: minmax(0, 1fr) 24px; gap: 3px 10px; align-items: start;
}
.city-waiting > summary::-webkit-details-marker { display: none; }
.city-waiting > summary b { color: var(--pine-900); font-size: 1.02rem; }
.city-waiting > summary::after {
  content: "\203A"; grid-column: 2; grid-row: 1 / span 2; align-self: center; justify-self: end;
  font-size: 1.5rem; line-height: 1; color: var(--ink-soft);
  transform: rotate(90deg); transition: transform .18s var(--ease);
}
.city-waiting[open] > summary::after { transform: rotate(-90deg); }
/* Text links, not cards. Seventy cards that all say the same thing read as a broken product;
   seventy links read as an index, which is what this is. */
.city-waiting-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 2px 14px; margin: 14px 0 16px;
}
.city-waiting-list a {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 44px; padding: 4px 2px; border-bottom: 1px solid var(--line);
}

/* ==================================================================== PHONES
   THE BREAKPOINT THAT DID NOT EXIST.
   
   Before this the smallest was 700px, so a 320px iPhone SE and a 699px tablet rendered from
   exactly the same rules. Everything that worked on a phone worked because the 860px layout
   happens to survive being squeezed — which it largely does, and that is a credit to how it
   was built. But nothing was DESIGNED for the width most visitors hold, and there was
   nowhere to put a rule that should only apply to a handset. This is that place.

   560px, not 480: a 540px foldable and a 430px Pro Max are the same problem, and the 860px
   rules already handle everything wider. */
/**
 * THE PINNED LISTING BAR IS A PHONE THING, AND THE STYLESHEET IS WHAT SAYS SO.
 *
 * Every rule that makes `.biz-dock` a bar — `position: fixed`, `display: flex`, its own
 * `[hidden]` handling — lives in the phone block below. The JavaScript that reveals it does
 * not know about the breakpoint: an IntersectionObserver on the action row sets
 * `dock.hidden = false` the moment that row scrolls off, at any width.
 *
 * On a laptop that produced a real, visible fault. Scrolling to the bottom of a listing
 * un-hid an element with NO styling at all, which then rendered in normal page flow — the
 * business name in bold and a green Directions button, loose above the footer, looking like
 * a half-finished template. It only appeared AFTER a scroll, so the page looked correct
 * every time anybody checked the top of it.
 *
 * ONE LINE, AND THE BREAKPOINT STAYS IN ONE PLACE. Gating the observer in JavaScript would
 * have worked too, and would have put the same 560px in a second file to drift from this
 * one. Hidden by default here, shown by the media query below: the CSS decides where the bar
 * exists, the script only decides when.
 */
.biz-dock { display: none; }

@media (max-width: 560px) {

  /* Secondary text stops being a squint. .hint is 0.85rem — 13.6px — which reads as
     "quieter" on a monitor and as "not for you" on a phone in daylight. 160 nodes on the
     cities page alone were under 14px. */
  /* .panel-meta is in this list because on a phone it is no longer a footnote: it now sits
     directly under the search box in the sheet's resting state, and "3 places in view" is the
     line that tells somebody their search did something. */
  .hint, .result-sub, .rating-count, .desk-sub, .biz-sub,
  .panel-meta, .chip { font-size: .875rem; }

  /* The rating row is four things — stars, a number, a source, a badge — and at 390px the
     source wrapped onto a second line and collided with the badge. The count is the part
     that can go: "4.7 ★★★★☆ · Open" is the whole of what a person scanning a list needs,
     and the exact number of Google reviews is on the listing itself. Kept in the DOM for
     the wider layouts and for anything reading the page aloud. */
  .result-rating { flex-wrap: nowrap; gap: 4px; min-width: 0; }
  /* ONLY when a number stands beside it. The same class carries "No reviews yet" as a row's
     whole content — hiding that leaves an empty line saying nothing, which is worse than the
     collision this fixes. The adjacent-sibling selector is the difference. */
  .result-rating .rating-num + .rating-count { display: none; }
  .result-badge, .result-rating .badge { margin-inline-start: auto; flex: none; }

  /* Urdu carries more vertical detail than Latin at the same nominal size, so a shared
     0.85rem is a harsher cut in Urdu than it is in English. */
  html[lang|="ur"] .hint,
  html[lang|="ur"] .result-sub,
  html[lang|="ur"] .rating-count { font-size: .95rem; }

  /* A phone reads one column. Two 150px stat tiles side by side leave neither room to
     breathe, and the third wraps alone onto its own row looking like a mistake. */
  .stat-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .stat-card b { font-size: 1.35rem; }

  /* Page furniture: less of it, so more of the page is the page. */
  .page { padding: 18px 14px 48px; }
  h1 { font-size: 1.72rem; line-height: 1.18; }

  /* The pinned action bar. Only here, in the phone block: on a laptop the action row has not
     scrolled anywhere and a permanent bar would be furniture. 64px because that is a
     comfortable thumb row without eating a chunk of a short screen — and the blur is
     functional rather than decorative, so text scrolling underneath stays separable from
     text sitting on top of it. */
  .biz-dock {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
    display: flex; align-items: center; gap: 12px;
    min-height: 64px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--card) 88%, transparent);
    -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
    border-top: 1px solid var(--line); box-shadow: var(--shadow-sheet);
  }
  .biz-dock[hidden] { display: none; }
  .biz-dock-what { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
  .biz-dock-what b {
    font-size: .98rem; color: var(--pine-900);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .biz-dock-what .open-badge { align-self: flex-start; }
  .biz-dock .btn { flex: none; }
  /* The page has to end above the bar, or the last review is unreadable behind it. */
  body:has(.biz-dock:not([hidden])) .page { padding-bottom: 96px; }

  /* Actions on a listing stop being ragged: two equal columns, so the eye reads pairs
     rather than a staircase. */
  /* Two equal columns, so the eye reads pairs rather than the staircase six buttons of
     different widths made. The two that matter take a full row each and sit first. */
  .biz-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .biz-actions > .is-lead { grid-column: 1 / -1; }
  .biz-actions > .btn-block, .biz-actions > .is-wide { grid-column: 1 / -1; }
  /* Same reasoning as the desktop rule: a closed disclosure is one of the two columns, an
     open one needs both, and the guest sentence is a sentence. `order` does nothing useful
     in a two-column grid, so these only claim the width. */
  .biz-actions > .add-to-list[open] { grid-column: 1 / -1; }
  .biz-actions > .add-to-list-guest { grid-column: 1 / -1; }
  /* A number is not a sentence: let it shrink rather than wrap mid-digits. */
  .btn-call { white-space: nowrap; font-size: .95rem; }
}
/* ============================================================================
   THE LANDING PAGE

   Written phone-first: every rule below is the small-screen rule, and the two
   media queries at the bottom only ADD to it. That is the opposite of the rest
   of this stylesheet, which grew desktop-first and then walked back — and it is
   why the landing page needs no `max-width` override to be right on a phone.

   Three layers, and the z-order is the whole design:
     0  .landing-map     the map. Scenery. Never touched, never read out.
     1  .landing-scrim   makes the text legible and eats every pointer event.
     2  .landing-fore    the search box and everything a person uses.
   ========================================================================= */

.landing {
  position: relative;
  /* The topbar is sticky and outside this element, so the landing fills what is
     left. `dvh` rather than `vh` because on a phone `vh` is the viewport with
     the browser chrome RETRACTED — using it here puts the search box under the
     address bar on first paint, which is exactly when it matters most. */
  min-height: calc(100dvh - var(--topbar-h));
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--mist);
}

/* ---------- layer 0: the map ---------- */

.landing-map {
  position: absolute; inset: 0; z-index: 0;
  /* Belt and braces with `interactive: false` on the map itself. A MapLibre
     canvas binds its own wheel and touch handlers, and a painted div on top of
     it does not stop a wheel event reaching the element underneath. */
  pointer-events: none;
  opacity: 0; transition: opacity .6s var(--ease);
}
.landing.has-map .landing-map { opacity: 1; }
/* A style that failed to load leaves a grey rectangle claiming to be a map. */
.landing-map.is-dead { display: none; }

/* ---------- layer 1: the scrim ---------- */

.landing-scrim {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;                 /* nothing here is clickable either way */
  /**
   * TWO LAYERS, AND THE FIRST VERSION HAD ONE.
   *
   * It was a top-down gradient, densest at the top — the obvious shape, and wrong, because
   * the content is CENTRED. It put its heaviest wash over the empty band above the heading
   * and left the map washed to nothing exactly where there was nothing else to look at,
   * while the text sat lower down over the thinner part. Measured on a 390px phone: the
   * first 600px of the page was a pale gradient with no map and no words in it.
   *
   * So the density follows the TEXT, not the viewport. A soft ellipse centred on the middle
   * of the page carries the heading and the search box, and a gentle flat wash underneath
   * keeps the whole thing calm without erasing the map. The map now reads as a map at the
   * top and the bottom, which is where it has room to.
   */
  background:
    radial-gradient(115% 58% at 50% 44%,
      rgba(250, 250, 247, .95) 0%,
      rgba(250, 250, 247, .88) 34%,
      rgba(250, 250, 247, .40) 68%,
      rgba(250, 250, 247, .12) 100%),
    linear-gradient(180deg,
      rgba(250, 250, 247, .20) 0%,
      rgba(250, 250, 247, .16) 60%,
      rgba(250, 250, 247, .30) 100%);
}

/* ---------- layer 2: what a person uses ---------- */

.landing-fore {
  position: relative; z-index: 2;
  flex: 1; display: flex; flex-direction: column;
  padding: var(--s5) var(--s4) var(--s4);
  padding-inline: max(var(--s4), env(safe-area-inset-left)) max(var(--s4), env(safe-area-inset-right));
}

.landing-mid { flex: 1; display: flex; flex-direction: column; justify-content: center; max-width: 640px; width: 100%; margin-inline: auto; }

.landing-h1 {
  font-size: 1.75rem; line-height: 1.25; margin: 0 0 var(--s2);
  color: var(--pine-900); text-wrap: balance;
}
.landing-sub {
  font-size: var(--t-body); line-height: 1.5; color: var(--ink-soft);
  margin: 0 0 var(--s5); max-width: 44ch;
}

/* ---------- the search box: the one control that matters ---------- */

/**
 * THE FIELD AND THE BUTTON ARE BOTH PILLS, AND THAT IS THE THIRD ATTEMPT.
 *
 * First attempt: an 8px button centred in a 14px field with 4px of padding — it floated in a
 * white gutter with corners that disagreed with the ones around it.
 * Second attempt: inner radius = outer - padding (14 - 5 = 9), stretched to the inner height.
 * The arithmetic was right and it still read as a sticker, which is the useful thing this
 * taught: CONCENTRIC IS NOT THE SAME AS RESOLVED. Two rounded rectangles with different radii
 * are two shapes however carefully their curves are matched, and at 56px tall the difference
 * between a 14px corner and a 9px one is large enough to see and small enough to look like a
 * mistake rather than a decision.
 *
 * A pill has no radius to disagree about. `999px` on both means the inner shape is the outer
 * shape at every size, at every height, in every language, with no arithmetic to keep in step
 * when the padding changes. It also puts the search cluster in the same family as the category
 * chips six pixels below it, which are pills already - so the front of the site is now one
 * shape rather than three.
 *
 * The rules that survived from attempt two and still matter:
 *   - `align-items: stretch` on the row, so the gutter is equal on all four sides rather than
 *     6px at the ends and 12px top and bottom.
 *   - No shadow on the button: it is inside a raised field, it is not raised again.
 */
.landing-search {
  display: flex; align-items: stretch; gap: var(--s2);
  background: var(--card);
  border: 1px solid var(--line-ui);
  border-radius: var(--radius-pill);
  padding: 6px;
  padding-inline-start: var(--s5);
  /* The ONE place on this page a shadow is correct: it genuinely floats above
     the map, which is the whole test the elevation budget applies. */
  box-shadow: var(--shadow-float);
}
.landing-go {
  flex: none; align-self: stretch;
  border-radius: var(--radius-pill);
  padding-inline: var(--s6);
  min-height: 0;              /* the field's own height governs the row */
  display: flex; align-items: center; justify-content: center;
  box-shadow: none;           /* it is inside a raised field; it is not raised again */
}
.landing-search:focus-within { border-color: var(--pine-500); box-shadow: var(--shadow-float), 0 0 0 3px rgba(45,106,79,.14); }

/**
 * THE LEADING MAGNIFIER IS HIDDEN ON A PHONE, AND THAT IS THE FIX FOR A REAL FAULT.
 *
 * With the icon, the padding and a labelled Search button, the input had about 250px on a
 * 390px screen and the placeholder rendered as "Search restaurants, hotels, sho" — the one
 * string on the page whose whole job is to say what can be typed, cut off mid-word.
 *
 * Something had to go, and the icon is the right thing: it is decoration that duplicates the
 * button beside it. Dropping the BUTTON instead would have left the action to a keyboard
 * "search" key that not every Android keyboard shows. 36px back, and the placeholder fits.
 */
.landing-search-icon { display: none; color: var(--ink-soft); flex: none; align-items: center; }
@media (min-width: 560px) { .landing-search-icon { display: flex; } }

.landing-input {
  flex: 1; min-width: 0;
  border: 0; outline: 0; background: transparent;
  font: inherit; font-size: var(--t-body); color: var(--ink);
  /* 48px, not 64. Inside a 100dvh page every pixel here is a pixel the heading
     and chips do not get, and 48 already clears the touch floor with padding. */
  min-height: 48px;
  /* Stops iOS Safari zooming the whole page in on focus, which it does below
     16px and which then leaves the visitor on a page they have to pinch back. */
  font-size: max(16px, var(--t-body));
}
.landing-input::placeholder { color: var(--ink-soft); opacity: 1; }
/* The native clear button is a 16px target that overlaps our submit button. */
.landing-input::-webkit-search-cancel-button { -webkit-appearance: none; }

/**
 * A LATER `.landing-go { flex: none; min-height: 44px; }` USED TO LIVE HERE, AND IT IS WHY
 * TWO ROUNDS OF FIXING THE BUTTON CHANGED NOTHING ON SCREEN.
 *
 * It was left behind from the version before the button stretched. Being later in the file it
 * won the cascade over the `min-height: 0` set with the rest of the button's geometry, so the
 * careful rules above were correct and inert at the same time - the hardest kind of CSS bug to
 * see, because reading the block you just edited shows you the right answer.
 */

/* ---------- category chips ---------- */

.landing-chips {
  display: flex; gap: var(--s2); margin-top: var(--s4);
  /* One scrolling row rather than a wrapped block — the same decision, and for
     the same reason, as the admin rail on a narrow screen. */
  overflow-x: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--s1);
  scroll-padding-inline: var(--s4);
}
.landing-chips::-webkit-scrollbar { display: none; }
.landing-chip {
  flex: none; white-space: nowrap;
  background: var(--card); border: 1px solid var(--line-ui); color: var(--pine-700);
  text-decoration: none;
}
.landing-chip-more { background: var(--pine-100); border-color: transparent; font-weight: 700; }

/**
 * THE ROW SCROLLS. NOTHING ON A DESKTOP SAID SO, TWICE.
 *
 * Measured at 1440px: nine category pills plus "Or open the map" is about 1,050px of content
 * in a 640px column, `scrollbar-width: none` hides the bar, and the row ended flush at the
 * column edge with a pill sliced mid-word. It scrolls by wheel, by trackpad and by drag — and
 * a mouse user has no way to find that out, so from the outside the row is simply broken.
 *
 * This is the same fault the /explore chip rail had, fixed the same way, and the fact that it
 * had to be fixed twice is the lesson: the landing page grew its own copy of the chip row
 * (`.landing-chips`) rather than reusing `.chip-rail`, so the fix to one could not reach the
 * other. Two rows that look identical and behave differently is what a duplicate costs.
 *
 * The buttons are a POINTER affordance and appear only for a mouse: a thumb already knows a
 * row like this swipes, and two 36px circles parked over the ends of a phone-width row would
 * cover two of the nine things they are meant to help you reach.
 */
.landing-rail { position: relative; margin-top: var(--s4); }
.landing-rail .landing-chips { margin-top: 0; }

/**
 * A FADE AT THE SCROLLING END, WHICH IS THE HALF OF THIS THE ARROWS DO NOT COVER.
 *
 * The arrows above are gated on `(hover: hover) and (pointer: fine)`, and the argument for
 * that is right — two 36px circles parked over the ends of a phone-width row would cover two
 * of the nine things they are meant to help you reach. But it leaves a phone with NO signal at
 * all, and "a thumb already knows a row like this swipes" turned out to be doing more work
 * than it could carry: at 390px the third pill is guillotined through the middle of its first
 * letter by the viewport edge, which reads as a layout fault, not as an invitation.
 *
 * A MASK RATHER THAN A COLOUR GRADIENT, and that is forced by where this row sits. `.chip-rail`
 * fades to `var(--card)` because it is on a card. This row is over a live map under a scrim and
 * has no single colour behind it — the same reason the arrows here are filled rather than
 * outlined. A mask fades to whatever is actually there.
 *
 * `can-left` and `can-right` are set from `Math.abs(scrollLeft)` in landing.js, so despite the
 * names they mean START and END, not physical left and right. Hence the RTL pair below: on the
 * Urdu site the inline end of the row is its left edge, and fading the wrong side would put a
 * soft edge where the row stops and a hard one where it continues.
 */
.landing-rail .landing-chips {
  --fade: 40px;
  -webkit-mask-image: none;
          mask-image: none;
}
.landing-rail.can-right .landing-chips {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
}
.landing-rail.can-left .landing-chips {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent);
}
.landing-rail.can-left.can-right .landing-chips {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to right, transparent, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent);
}
html[dir="rtl"] .landing-rail.can-right .landing-chips {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to left, #000 calc(100% - var(--fade)), transparent);
}
html[dir="rtl"] .landing-rail.can-left .landing-chips {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
}
/*
 * BOTH ENDS, IN RTL, AND IT HAS TO BE REPEATED RATHER THAN INHERITED.
 *
 * The gradient itself is symmetric, so direction is irrelevant here — but specificity is not.
 * `html[dir="rtl"] .landing-rail.can-right .landing-chips` outweighs
 * `.landing-rail.can-left.can-right .landing-chips`, so on the Urdu site a row scrolled to the
 * middle would have taken the one-ended mask and faded only one side. Written out rather than
 * reordered, because reordering would leave the same trap for whoever adds the next rule.
 */
html[dir="rtl"] .landing-rail.can-left.can-right .landing-chips {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent);
          mask-image: linear-gradient(to right, transparent, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent);
}

/**
 * FILLED, NOT OUTLINED, AND THAT IS ABOUT WHAT IT SITS ON TOP OF.
 *
 * The arrow overlaps the pill at the end of the row — it has to, because the row is as wide as
 * the column and there is nowhere outside it to stand at narrow widths. A white circle with a
 * hairline border overlapping a white pill with a hairline border is two of the same object,
 * half on top of each other, and it reads as a rendering fault rather than as a control.
 *
 * Filled in pine with a card-coloured ring, it is unmistakably a different KIND of thing from
 * the chips, it separates cleanly from whatever it covers, and it needs no colour-matching to
 * the background — which matters here because the background is a live map under a scrim and
 * has no single colour to match.
 */
.landing-rail-btn {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 36px; height: 36px; border-radius: var(--radius-pill);
  border: 2px solid var(--card); background: var(--pine-700); color: #fff;
  cursor: pointer; padding: 0; line-height: 1;
  box-shadow: var(--shadow-float);
  display: none; align-items: center; justify-content: center;
}
.landing-rail-btn.left  { inset-inline-start: -4px; }
.landing-rail-btn.right { inset-inline-end: -4px; }
.landing-rail-btn:hover { background: var(--pine-900); }
/**
 * AN SVG PATH DOES NOT KNOW WHICH WAY THE PAGE READS, AND THAT IS THE WHOLE BUG.
 *
 * `inset-inline-end` puts the "next" button on the LEFT of the Urdu site, correctly — the
 * inline end of a right-to-left row is its left edge, and that is the direction more chips
 * live in. The chevron inside it is a hand-drawn path pointing right, and no logical property
 * touches it. So on the Urdu site the button that scrolls left was drawn pointing right: the
 * one thing an arrow is for, wrong, on the half of the site least likely to be tested.
 */
html[dir="rtl"] .landing-rail-btn svg { transform: scaleX(-1); }
.landing-rail-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

@media (hover: hover) and (pointer: fine) {
  .landing-rail.can-left  .landing-rail-btn.left  { display: flex; }
  .landing-rail.can-right .landing-rail-btn.right { display: flex; }
  /* Quiet until pointed at, so a permanent control does not become permanent noise. */
  .landing-rail-btn { opacity: .82; transition: opacity .15s, background .15s; }
  .landing-rail:hover .landing-rail-btn { opacity: 1; }
  /* Room at the ends so an arrow never sits on top of the pill it is offering to reveal. */
  .landing-rail.can-left  .landing-chips { padding-inline-start: var(--s6); }
  .landing-rail.can-right .landing-chips { padding-inline-end: var(--s6); }
}

/* A chip that opens the sub-filters below it is showing a state, so it looks selected. */
.landing-chip[aria-expanded="true"] {
  background: var(--pine-700); border-color: var(--pine-700); color: #fff;
}

/**
 * ---------- the sub-filter drawer ----------
 *
 * WHY A CATEGORY CHIP NO LONGER JUST NAVIGATES.
 *
 * Pressing "Restaurants" used to leave the landing page for a category listing, which is a
 * fine page and the wrong answer to what the press meant. Nobody wants "every restaurant in
 * the city" — they want a kind of restaurant, near them, and the tags that say which kind
 * (cuisine, meal, amenity) already exist on every listing. They were reachable from exactly
 * one place: a dropdown on the map page, after you had already searched.
 *
 * So the chip opens them here instead. Pick the kind, press the button, and the search that
 * runs is BUILT FROM THE FILTERS rather than from a typed phrase — which is the difference
 * between "biryani" (a word that has to appear in a listing) and Restaurants + Pakistani (a
 * fact recorded about a listing). The second is the one that returns the right places.
 *
 * The chips stay real links to real category pages underneath all of this. With the script
 * blocked, or from a crawler, pressing one navigates exactly as it always did — the drawer is
 * an enhancement over a working page, never the only way through.
 */
.landing-subs {
  margin-top: var(--s3);
  background: var(--card);
  border: 1px solid var(--line-ui);
  border-radius: var(--radius);
  box-shadow: var(--shadow-over);
  padding: var(--s3) var(--s4) var(--s4);
  /* On a short phone in landscape this drawer plus the heading plus the search box is more
     than the viewport. It scrolls rather than pushing the button off the bottom. */
  max-height: min(46vh, 340px); overflow-y: auto;
}
.landing-subs[hidden] { display: none; }
.landing-subs-grp + .landing-subs-grp { margin-top: var(--s3); }
.landing-subs-lead {
  display: block; font-size: var(--t-label); font-weight: 700;
  color: var(--ink-soft); text-transform: none; margin-bottom: var(--s2);
}
.landing-subs-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.landing-sub-chip {
  flex: none; background: var(--card); border: 1px solid var(--line-ui); color: var(--pine-700);
  border-radius: var(--radius-pill); padding: 8px 15px; min-height: 38px;
  font-size: var(--t-meta); font-weight: 600; cursor: pointer;
}
.landing-sub-chip[aria-pressed="true"] {
  background: var(--pine-700); border-color: var(--pine-700); color: #fff;
}
.landing-sub-chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The action row is sticky to the bottom of a scrolling drawer: the button is the point of
   the drawer, and a button you have to scroll to find is a button half the people miss. */
.landing-subs-go {
  display: flex; align-items: center; gap: var(--s3);
  margin-top: var(--s4); padding-top: var(--s3);
  border-top: 1px solid var(--line);
  position: sticky; bottom: calc(var(--s4) * -1);
  background: var(--card);
}
.landing-subs-btn { flex: 1; min-height: 44px; }
.landing-subs-clear {
  flex: none; border: 0; background: none; padding: var(--s2) var(--s3);
  min-height: 44px; color: var(--ink-soft); font: inherit; font-size: var(--t-meta);
  font-weight: 600; cursor: pointer; text-decoration: underline;
}
.landing-subs-clear:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.landing-subs-clear[hidden] { display: none; }

/* ---------- the "now showing" strip ---------- */

.landing-now {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s2) var(--s3);
  box-shadow: var(--shadow-float);
  max-width: 640px; width: 100%; margin-inline: auto;
  min-height: 56px;
}
/* The caption half of the strip: the dot and the link, wrapped together so the whole caption
   can be aria-hidden without taking the button beside it out of the accessibility tree too.
   The wrapper is layout only — it inherits the row it replaced. */
.landing-now-cap {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--s2);
}
.landing-now-dot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok);
  /* No pulse. A dot that throbs forever beside a map that is already moving is
     two animations competing for the same glance, and one of them is free. */
}
.landing-now-link {
  flex: 1; min-width: 0; text-decoration: none; color: inherit;
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  /* 43px measured, and 43 is not 44. The strip's own min-height does not reach the link
     inside it, so the one thing on this page a person actually taps was a pixel short. */
  min-height: 44px;
}
.landing-now-link b {
  font-size: var(--t-meta); font-weight: 700; color: var(--pine-900);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.landing-now-link span {
  /* --t-label is 13px and this is a sentence, not a label: "Banks & ATMs · Mall Road · 3.8★"
     is the line that says what the place IS. 14 is the floor and it applies here. */
  font-size: var(--t-meta); color: var(--ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/**
 * ATTRIBUTION, WHICH A DECORATIVE MAP STILL OWES.
 *
 * The tour map is built with `attributionControl: false`, because MapLibre's own control is a
 * floating pill with a toggle button and this map takes no interaction. That removes the
 * control; it does not remove the obligation. These are OpenStreetMap-derived tiles under the
 * ODbL, and the credit is required whether or not the map is scenery — "it is only a
 * background" is not a licence term.
 *
 * So it is rendered as ordinary text by the view, and shown only when a map is actually
 * running. Quiet, in the corner, and a real link.
 */
.landing-credit {
  /* Top corner, not bottom: the "now showing" strip is full width and pinned to the bottom, so
     either bottom corner puts the credit underneath it — on the pause button in RTL. The top
     of the map is empty in both directions. */
  position: absolute; z-index: 2; top: 4px; inset-inline-end: 8px;
  /*
   * 14px, NOT 12 -- AND THE REASON IS THE LICENCE, NOT THE AUDIT.
   *
   * This is the ODbL credit for the tiles. It appeared on twelve of the seventeen measured
   * pages as the site's most common piece of sub-14px text, and its link measured 84x16.
   * "Quiet, in the corner" was the intention and is preserved; an attribution set small enough
   * that a reader cannot make out the word OpenStreetMap is not quieter, it is closer to not
   * being an attribution at all. The colour and the translucent plate keep it out of the way.
   */
  font-size: 14px; color: var(--ink-soft);
  background: rgba(250,250,247,.72); border-radius: 4px; padding: 1px 6px;
  display: none;
}
.landing.has-map .landing-credit { display: block; }
/*
 * A 44px HIT BOX WITHOUT MOVING THE INK, which is the same trick `.ad-why` uses: padding to
 * make the target, negative margin to give the space back, so the credit sits exactly where it
 * sat and the plate around it does not grow.
 */
.landing-credit a {
  color: inherit; text-decoration: underline;
  display: inline-block; padding: 13px 0; margin: -13px 0;
}
/**
 * THE PAUSE CONTROL, WHICH NOW SAYS WHAT IT IS.
 *
 * It was a 44x44 square holding two bars and nothing else. An icon-only control is a guess
 * even when the icon is a standard one — pause what, the map or something on it — and this
 * particular one sat inside an `aria-hidden` element, so the guess was all anybody got.
 *
 * The word costs about 40px of a 640px strip and removes the guess. `white-space: nowrap`
 * because "Pause" is one word in English and "روکیں" is one word in Urdu, and neither should
 * ever be allowed to become two lines inside a 44px-high button.
 */
.landing-now-pause {
  flex: none; background: transparent; border: 0; cursor: pointer;
  color: var(--ink-soft);
  min-width: 44px; min-height: 44px; padding-inline: var(--s2);
  display: flex; align-items: center; justify-content: center; gap: 6px;
  border-radius: var(--radius-sm);
  font: inherit; font-size: var(--t-meta); white-space: nowrap;
}
.landing-now-pause:hover { background: var(--mist); }
.landing.is-paused .landing-now-pause { color: var(--pine-700); background: var(--pine-100); }
/* One icon is shown at a time and the label changes with it, so the button reads the same
   way twice over. The word is swapped in JS because it is translated; the glyph is swapped
   here because CSS already knows the state from the root class. */
.landing-now-pause .i-play  { display: none; }
.landing.is-paused .landing-now-pause .i-pause { display: none; }
.landing.is-paused .landing-now-pause .i-play  { display: block; }

/* ---------- the geo notice ---------- */

.landing-geo {
  margin: var(--s3) auto 0; max-width: 640px; width: 100%;
  font-size: var(--t-meta); color: var(--ink-soft);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: var(--s2) var(--s3);
}

/* ---------- the fallback list ---------- */

/* Present in the HTML always, hidden once a map is actually running. On a
   device that refused the map this IS the page's content, so it is styled as
   content rather than as an apology. */
.landing-list {
  position: relative; z-index: 2;
  padding: 0 var(--s4) var(--s6);
  max-width: 640px; width: 100%; margin-inline: auto;
}
.landing.has-map .landing-list { display: none; }
.landing-list-head { font-size: var(--t-meta); color: var(--ink-soft); font-weight: 600; margin: 0 0 var(--s2); }
.landing-list ul { list-style: none; margin: 0; padding: 0; }
.landing-list li + li { border-top: 1px solid var(--line); }
.landing-list a {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s3) 0; text-decoration: none; color: inherit;
  min-height: 44px; justify-content: center;
}
.landing-list a b { font-size: var(--t-body); color: var(--pine-900); }
.landing-list a span { font-size: var(--t-meta); color: var(--ink-soft); }

/* ---------- the ping ---------- */

.landing-ping { position: relative; width: 14px; height: 14px; pointer-events: none; }
.landing-ping-dot {
  position: absolute; inset: 0; border-radius: 50%;
  background: var(--pine-500); border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(20,52,43,.4);
}
.landing-ping-ring {
  position: absolute; top: 50%; left: 50%;
  width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; border: 2px solid var(--pine-500);
  /* ONE run, not infinite. The ring marks an arrival; a ring that keeps pulsing
     until the map leaves is decoration competing with the thing it points at,
     and it never lets the compositor idle. --ping-ms is set inline from the
     admin setting so the ring always finishes inside the rest period. */
  animation: landing-ping var(--ping-ms, 1100ms) cubic-bezier(.22,.61,.36,1) 1 both;
}
@keyframes landing-ping {
  from { transform: scale(1);  opacity: .85; }
  to   { transform: scale(6);  opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .landing-ping-ring { animation: none; opacity: .5; }
}

/* ---------- landscape phone: the heading is what gives ---------- */

@media (max-height: 520px) and (max-width: 900px) {
  .landing-h1 { font-size: 1.35rem; margin-bottom: var(--s1); }
  .landing-sub { display: none; }      /* the H1 already says it */
  .landing-now { display: none; }      /* and the map has nowhere to breathe */
  .landing-fore { padding-block: var(--s3); }
}

/* ---------- from a tablet up ---------- */

@media (min-width: 700px) {
  .landing-fore { padding: var(--s7) var(--s5) var(--s5); }
  .landing-h1 { font-size: 2.5rem; }
  .landing-sub { font-size: 1.06rem; margin-bottom: var(--s6); }
  .landing-search { padding-inline-start: var(--s4); }
  .landing-input { min-height: 56px; }
  .landing-scrim {
    /* Wider screen, narrower text column: the ellipse tightens horizontally and sits
       toward the inline start, where the copy is. */
    /* Lighter than the phone's, and considerably lighter than the first attempt at this
       breakpoint. On a 1440px screen the radial covers an enormous area, so the same alphas
       that carry text on a 390px phone erase the whole map on a laptop — which is what
       happened, and what "I can't see the map" meant. The text column is narrow here, so the
       wash can be tight around it and let the rest of the map be a map. */
    background:
      radial-gradient(52% 64% at 32% 46%,
        rgba(250,250,247,.95) 0%,
        rgba(250,250,247,.86) 40%,
        rgba(250,250,247,.28) 76%,
        rgba(250,250,247,0) 100%),
      linear-gradient(180deg,
        rgba(250,250,247,.14) 0%,
        rgba(250,250,247,.10) 60%,
        rgba(250,250,247,.22) 100%);
  }
}

@media (min-width: 1100px) {
  .landing-h1 { font-size: 3rem; }
  .landing-mid { max-width: 720px; }
  .landing-now, .landing-geo { max-width: 720px; }
}

/* ---------- three small things the landing work needed and the sheet lacked ---------- */

/* A label that exists for a screen reader and not for the eye. The landing search box has a
   visible placeholder and no visible label, and a placeholder is NOT a label: it disappears
   the moment somebody types, and it is not announced as the field's name. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Wide content scrolls inside its own box; the page body never scrolls sideways. This is the
   rule docs/DESIGN.md states and there was no class implementing it — every wide admin table
   was relying on the table simply fitting. */
.tw { overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: var(--s5); }

/* /admin/landing's number rows. */
.landing-set-row { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.landing-set-input { width: 8.5rem; flex: none; font-variant-numeric: tabular-nums; }
.landing-set-unit { font-size: var(--t-meta); color: var(--ink-soft); }
/* The consequence line sits under the control it belongs to, tight to it, and set apart from
   the next field by the field's own margin — the proximity rule, applied to a form. */
.landing-set-cost { margin: var(--s1) 0 0; max-width: 62ch; }

/* ---------- the refine strip: sub-filters offered on a text search ---------- */

/**
 * Sits between the count row and the results, so it reads as a comment on THOSE results
 * rather than as another permanent filter bar. Hidden entirely unless it has something to
 * offer, because an empty affordance is worse than none: it teaches people to ignore that
 * part of the page.
 */
.refine {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s4);
  overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--line);
  background: var(--mist);
}
.refine::-webkit-scrollbar { display: none; }
.refine[hidden] { display: none; }
.refine-lead {
  flex: none; font-size: var(--t-meta); color: var(--ink-soft); font-weight: 600;
  white-space: nowrap;
}
.refine-chip { flex: none; background: var(--card); border-color: var(--line-ui); }
/* Selected sub-filters read the same as a selected category chip, because they do the same
   job — and pressing one again clears it, so the strip is the way out as well as in. */
.refine-chip.active { background: var(--pine-700); border-color: var(--pine-700); color: #fff; }

/* "All places" leads the landing row and is the only one that widens the directory rather
   than narrowing it, so it reads as the filled default the others sit beside. */
.landing-chip-all { background: var(--pine-100); border-color: transparent; font-weight: 700; }

/* ============================================================================
   SERVICE PROVIDERS — people who come to you
   ============================================================================
   Almost nothing new. The tiles are `.col-card`, the rows are `.area-item`, the pills are
   `.chip`: a provider is a different KIND of thing from a listing but it is not a different
   kind of page, and giving it its own visual language would have been a second design for
   the same job. What is here is the handful of pieces that have no equivalent anywhere else.
   ============================================================================ */

.prov-card-blurb { font-size: var(--t-meta); color: var(--ink-soft); line-height: 1.45; }
/* A zero is stated, not styled out. On a new section an empty kind is a fact worth showing —
   it says the site intends to have plumbers — so it is quieter than a count, never hidden. */
.prov-card-n.is-none { color: var(--ink-dis); }

/**
 * WHICH LIST IS ON SCREEN, said in a strip that cannot be mistaken for decoration.
 *
 * Two different questions are answered by /providers/{type} — "who reaches you" and "everyone
 * in this city" — and the fallback between them is silent unless the page says so. Somebody
 * ringing a driver who does not come to their valley is the failure this strip prevents, so
 * it is not a `.hint`: it has a background, and the narrowed case is coloured differently
 * from the un-narrowed one.
 */
.prov-scope {
  margin: 0 0 var(--s4); padding: var(--s3) var(--s4);
  border-radius: var(--radius); background: var(--mist);
  color: var(--ink-soft); font-size: var(--t-meta); line-height: 1.5;
}
.prov-scope.is-near { background: var(--pine-100); color: var(--pine-900); font-weight: 600; }

.prov-head { display: flex; gap: var(--s4); align-items: flex-start; }
.prov-head-pic {
  flex: none; width: 72px; height: 72px; border-radius: var(--radius);
  background: var(--mist); display: flex; align-items: center; justify-content: center;
  font-size: 2rem; overflow: hidden;
}
.prov-head-pic img { width: 100%; height: 100%; object-fit: cover; }
.prov-head-txt { min-width: 0; }
.prov-head-txt h1 { margin: 0 0 var(--s1); font-size: var(--t-page); line-height: 1.2; }
.prov-head-sub { margin: 0; color: var(--ink-soft); font-size: var(--t-body); }
.prov-checked { margin: var(--s2) 0 0; color: var(--ok); font-size: var(--t-meta); font-weight: 600; }
.prov-avail { margin: var(--s1) 0 0; font-size: var(--t-meta); color: var(--ink-soft); }
.prov-avail.is-free { color: var(--ok); font-weight: 600; }

/* The number is why the page exists, so it is a full-width control near the top rather than a
   line of text somebody has to select and copy on a phone. */
.prov-actions { display: flex; gap: var(--s3); margin: var(--s4) 0; flex-wrap: wrap; }
.prov-call { flex: 1; min-width: 200px; min-height: 48px; font-size: var(--t-body); }
.prov-wa { flex: none; min-height: 48px; }

.prov-rate { display: block; }
.prov-rate-note { display: block; margin-top: var(--s2); }
.prov-about { margin: 0; line-height: 1.6; }

.prov-covers-radius { margin: 0 0 var(--s3); }
.prov-areas { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2); }
/* An area the directory has no page for is still a true fact about where this person goes.
   It is shown, and it is flat, because a chip that looks like a link and is not is worse
   than plain text. */
.chip.is-flat { background: var(--mist); border-color: transparent; cursor: default; }
.prov-foot { margin-top: var(--s5); }

/* ---- a provider's face, and the ways to reach them ----------------------------------
   The head picture is bigger than an avatar anywhere else on the site and that is the whole
   argument for it: somebody rings a number off this page and then stands at a roadside
   waiting for a stranger, and the picture is how they know which car stopped. */
.prov-head-pic { background: none; width: auto; height: auto; font-size: inherit; }
.prov-head-pic .avatar-lg { width: 84px; height: 84px; font-size: 1.9rem; }

/* Two numbers, and the second says why it is there. An unlabelled pair reads as a mistake. */
.prov-call-alt { flex: 1; min-width: 200px; background: var(--card); border: 1px solid var(--line-ui);
                 color: var(--pine-700); display: flex; flex-direction: column;
                 justify-content: center; gap: 1px; line-height: 1.2; padding-block: var(--s2); }
.prov-call-num { font-weight: 700; }
.prov-call-alt .hint { font-size: var(--t-label); font-weight: 500; }

/* Email and website are a quiet line, not two more buttons. On a phone the tap that dials is
   what somebody came for, and five equal buttons make them read all five to find it. */
.prov-contacts { display: flex; flex-wrap: wrap; gap: var(--s4); margin: 0 0 var(--s4);
                 font-size: var(--t-meta); }
.prov-contacts a { color: var(--pine-700); }

/* The "this is what a caller sees" panel on both edit screens. */
.prov-me { display: flex; align-items: center; gap: var(--s4); margin: var(--s3) 0 var(--s4); }
.prov-me .hint { max-width: 46ch; }
.prov-is-me { position: absolute; inset-inline-start: 4px; bottom: 4px; }
.prov-promote { position: absolute; inset-inline-start: 4px; bottom: 4px; }

/* ======================================================================================
   REGISTRATION WIZARD, AND THE CHECKLIST THAT FINISHES WHAT IT STARTED
   ======================================================================================

   These two are one idea rendered twice. The wizard asks the three questions an entry cannot
   exist without and offers the rest; the checklist, on the person's own page afterwards, is
   where the rest comes back — no longer as an obligation blocking the door but as a line
   saying what each missing thing is costing them.

   The whole visual argument is RESTRAINT. A registration flow that looks expensive looks
   long. One card, one question in it, a hairline of progress above, and nothing else on the
   screen competing for the eye.
   -------------------------------------------------------------------------------------- */

/* Progress is a fact, not a demand: the count is --t-label and the bar is 4px. A thick bar in
   the brand colour reads as "you are 2/6 of the way through an ordeal"; this reads as a
   position. */
.wiz-head { margin-bottom: var(--s4); }
.wiz-count { margin: 0 0 var(--s2); color: var(--ink-soft);
             font-size: var(--t-label); line-height: var(--lh-label); font-weight: 600;
             letter-spacing: .02em; text-transform: uppercase; }
.wiz-bar { height: 4px; background: var(--mist); border-radius: var(--radius-pill); overflow: hidden; }
.wiz-bar span { display: block; height: 100%; background: var(--pine-500);
                border-radius: var(--radius-pill); }

.wiz-card { padding: var(--s5) var(--s5) var(--s4); }

/* The question is the page's h1 and is sized as one. The sentence under it is the reason this
   is a wizard at all, so it is body-sized and NOT a .hint — shrinking it to 13px would file
   the answer to "why are you asking me this" under small print. */
.wiz-q { margin: 0 0 var(--s2); font-size: var(--t-section); line-height: var(--lh-section); }
.wiz-why { margin: 0 0 var(--s5); color: var(--ink-soft);
           font-size: var(--t-body); line-height: var(--lh-body); max-width: 52ch; }

/* Back sits to the start, Next takes the rest of the row. Both are logical properties by way
   of flex order, so RTL needs no rule of its own. */
/* ARROWS ARE NOT BIDI-MIRRORED. Unicode mirrors brackets and a handful of maths relations
   and nothing else, so "← Back" goes on pointing left in Urdu — where back is to the right,
   and where the button itself has already moved to the other side of the row. Wrapping the
   glyph does for a text arrow what scaleX() does for the SVG chevron on the landing rail. */
.dir-arrow { display: inline-block; }
html[dir="rtl"] .dir-arrow { transform: scaleX(-1); }

.wiz-nav { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s5); }
.wiz-nav .btn-primary { flex: 1; }
.wiz-nav > form { flex: 1; }

/* Skip is centred, below, and on its own line — the three-across arrangement makes it a peer
   of Next and people press it by accident. */
.wiz-skip { margin: var(--s3) 0 0; text-align: center; }

/* Three fields in the whole flow carry this, and it is the only place the word appears. If it
   were on every label it would stop being information. */
.wiz-req { color: var(--danger); font-size: var(--t-label); font-weight: 600; }

.wiz-more { margin-top: var(--s4); border-top: 1px solid var(--line); padding-top: var(--s3); }
.wiz-more > summary { cursor: pointer; color: var(--pine-700); font-weight: 600;
                      font-size: var(--t-meta); }
.wiz-more[open] > summary { margin-bottom: var(--s2); }

/* ---- the summary ----------------------------------------------------------------------
   Every answer, blanks included. A summary that hides the blanks is a summary that hides
   exactly the rows somebody came to this screen to check. */
.wiz-sum { margin: 0 0 var(--s4); border-top: 1px solid var(--line); }
.wiz-sum-row { display: grid; grid-template-columns: 11rem 1fr; gap: var(--s2) var(--s4);
               align-items: baseline; padding: var(--s3) 0; border-bottom: 1px solid var(--line); }
.wiz-sum dt { color: var(--ink-soft); font-size: var(--t-meta); line-height: var(--lh-meta); }
.wiz-sum dd { margin: 0; display: flex; gap: var(--s3); align-items: baseline;
              justify-content: space-between; overflow-wrap: anywhere; }
.wiz-sum dd .linklike-dark { flex: none; }

/* #FDF2F0 is the tint already used for the danger panel further up this file rather than a
   second, near-identical pink: --danger measures 4.55:1 on it, and the lighter shade I reached
   for first came out at 4.45 — under the floor, and invisible to the eye that picked it. */
.wiz-blocked { background: #FDF2F0; border: 1px solid var(--danger); border-radius: var(--radius-sm);
               color: var(--danger); padding: var(--s3) var(--s4); margin: 0 0 var(--s4);
               font-size: var(--t-meta); line-height: var(--lh-meta); }
.wiz-blocked b { color: var(--danger); }

.wiz-dupe { background: var(--mist); border-radius: var(--radius); padding: var(--s4);
            margin: 0 0 var(--s4); }
.wiz-dupe .hint { margin: var(--s1) 0 var(--s3); }
.wiz-dupe .prov-areas { margin: 0; }

/* Outside the card, because leaving and starting over are not answers to the question above
   them. Quiet, and the two sit on one line where there is room. */
.wiz-foot { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
            gap: var(--s3); margin-top: var(--s4); }
.wiz-foot .hint { margin: 0; }

@media (max-width: 560px) {
  .wiz-sum-row { grid-template-columns: 1fr; gap: var(--s1); }
  .wiz-card { padding: var(--s4); }
}

/* An empty span whose only job is to be jumped to. Height zero so it changes no layout, and
   the scroll margin clears the fixed topbar — without it "Add it" lands the field under the
   bar and looks like a link that did nothing. */
.anchor { display: block; height: 0; overflow: hidden;
          scroll-margin-top: calc(var(--topbar-h) + var(--s4)); }

/* ---- finish your page -----------------------------------------------------------------
   The checklist is deliberately NOT styled as a warning even though two of its rows can be.
   It is the last card somebody sees on their own page every time they visit it, and a panel
   that shouts on every visit is a panel that gets ignored on the visit that mattered. */
.chk { margin-bottom: var(--s5); }
.chk-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); }
.chk-count { color: var(--ink-soft); font-size: var(--t-meta); font-weight: 700; white-space: nowrap; }
.chk-bar { height: 4px; background: var(--mist); border-radius: var(--radius-pill);
           overflow: hidden; margin: var(--s3) 0 var(--s3); }
.chk-bar span { display: block; height: 100%; background: var(--pine-500);
                border-radius: var(--radius-pill); }
.chk-intro { margin: 0 0 var(--s4); max-width: 60ch; }

.chk-list { list-style: none; margin: 0; padding: 0; }
.chk-item { display: grid; grid-template-columns: 1.25rem 1fr; gap: var(--s3);
            padding: var(--s3) 0; border-top: 1px solid var(--line); }
.chk-item:first-child { border-top: 0; }

/* The mark is the row's whole state, so it is a real character and not a coloured dot: a tick,
   an exclamation or a middle dot survives a screen reader, a high-contrast mode and a
   greyscale print, and none of the three needs the colour to be understood. */
.chk-mark { font-weight: 700; text-align: center; color: var(--ink-dis); line-height: var(--lh-body); }
.chk-item.is-done .chk-mark { color: var(--ok); }
.chk-item.is-hard .chk-mark { color: var(--danger); }
.chk-item.is-hard > .chk-body > b { color: var(--danger); }

/* Done rows recede rather than disappear. Struck-through text would be unreadable, and hiding
   them would make the list shorter every visit until the count made no sense. */
.chk-item.is-done > .chk-body > b { color: var(--ink-soft); font-weight: 600; }

.chk-body { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s1) var(--s3);
            line-height: var(--lh-body); }
.chk-body > b { flex: none; }
.chk-why { color: var(--ink-soft); font-size: var(--t-meta); line-height: var(--lh-meta);
           flex: 1 1 16ch; min-width: 0; }

/* AFTER the .chk-why shorthand above, not beside the wizard's media query further up: `flex:
   1 1 16ch` is a shorthand that rewrites flex-basis, so an override written earlier in the
   file loses to it however well the media query matches. On a phone the consequence takes its
   own full-width line — beside the label, "You are listed, and no search can find you" breaks
   into six ragged lines against a wall of white space and stops looking like a sentence. */
@media (max-width: 560px) {
  .chk-why { flex-basis: 100%; }
}

/* ======================================================================================
   THE BADGE SCREEN
   ======================================================================================
   Everything here is chrome around one 264x72 picture that is not ours to restyle — it is an
   <img> the owner is about to paste into their own site, and the preview has to be that exact
   picture rather than an HTML approximation of it. So the job of this stylesheet is to give it
   somewhere honest to sit and to make choosing between five of them easy on a phone.
   -------------------------------------------------------------------------------------- */

/* A checked ground, so a light badge on a light card does not read as edgeless. It is the only
   place on the site with a pattern, and that is the point: it says "this is a picture, and this
   is where it ends". */
.badge-stage { display: flex; flex-direction: column; align-items: center; gap: var(--s3);
               background:
                 linear-gradient(45deg, var(--mist) 25%, transparent 25%, transparent 75%, var(--mist) 75%) 0 0/16px 16px,
                 linear-gradient(45deg, var(--mist) 25%, transparent 25%, transparent 75%, var(--mist) 75%) 8px 8px/16px 16px,
                 var(--card); }
.badge-stage img { display: block; max-width: 100%; height: auto; }
.badge-stage .hint { margin: 0; text-align: center; max-width: 46ch; }

/* One column on a phone, two where there is room. Never three: the sample line inside each is
   what makes the choice, and a third column squeezes it to nothing. */
.badge-styles { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
                gap: var(--s2); }
.badge-style { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--s3);
               align-items: baseline; padding: var(--s3) var(--s4);
               border: 1px solid var(--line-ui); border-radius: var(--radius-sm);
               cursor: pointer; background: var(--card); }
.badge-style:hover { border-color: var(--pine-500); }
.badge-style.is-on { border-color: var(--pine-500); box-shadow: inset 0 0 0 1px var(--pine-500);
                     background: var(--mist); }
.badge-style input { grid-row: 1 / span 3; align-self: center; }
.badge-style-name { font-weight: 700; }
/* The sample sentence, quoted at the size it will not be — deliberately smaller than the badge
   draws it, because this is a description of the badge and not a second badge. */
.badge-style-say { grid-column: 2; font-size: var(--t-meta); line-height: var(--lh-meta);
                   color: var(--ink-soft); overflow-wrap: anywhere; }
.badge-style-say b { color: var(--ink); }
.badge-style-not { grid-column: 2; font-size: var(--t-label); color: var(--danger); }

.badge-themes { display: flex; gap: var(--s2); flex-wrap: wrap; }
.badge-theme { display: flex; align-items: center; gap: var(--s2); padding: var(--s2) var(--s4);
               border: 1px solid var(--line-ui); border-radius: var(--radius-pill);
               cursor: pointer; min-height: 44px; }
.badge-theme.is-on { border-color: var(--pine-500); background: var(--mist); font-weight: 600; }

/* Monospace, because this is code and a proportional font hides a missing quote mark. LTR even
   in Urdu: it is HTML, and bidi reordering of an attribute makes it unreadable and uncopyable. */
.badge-code { font-family: ui-monospace, "Cascadia Mono", Consolas, "Courier New", monospace;
              font-size: var(--t-label); line-height: 1.55; direction: ltr; text-align: left;
              white-space: pre; overflow-x: auto; }

.badge-nums { display: flex; gap: var(--s6); flex-wrap: wrap; margin-bottom: var(--s3); }
.badge-nums b { display: block; font-size: var(--t-page); line-height: var(--lh-page);
                font-variant-numeric: tabular-nums; }
.badge-nums span { color: var(--ink-soft); font-size: var(--t-meta); }
.badge-h3 { font-size: var(--t-card); margin: var(--s5) 0 var(--s2); }
.badge-hosts .num { text-align: end; font-variant-numeric: tabular-nums; }

/* The card that sends an owner here from their edit screen. */
.badge-cta { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
.badge-cta img { flex: none; }
.badge-cta div { flex: 1 1 16ch; min-width: 0; }
.badge-cta .hint { margin: 2px 0 0; }

/* ======================================================================================
   "IS THIS STILL RIGHT?" — the freshness strip
   ======================================================================================
   The one line on a listing that says how much to trust the rest of it. Quiet by design: it
   sits below the contact details and the hours, where somebody has just read the thing they
   might be about to correct, and it must not compete with the call button above it.
   -------------------------------------------------------------------------------------- */
.fresh { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
         gap: var(--s3) var(--s4); }
.fresh-said { flex: 1 1 22ch; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.fresh-said .hint { margin: 0; max-width: 52ch; }
/* Confirmed recently is stated in the ordinary ink, not in green. A tick that shouts turns a
   fact into a boast, and the same panel has to be able to say "checked five months ago"
   without either reading as an alarm. */
.fresh-ok  { color: var(--ink); }
.fresh-old { color: var(--ink-soft); font-weight: 600; }
.fresh-acts { display: flex; gap: var(--s2); flex-wrap: wrap; flex: none; }

@media (max-width: 560px) {
  /* Full width and side by side: two half-width buttons is a reachable pair on a phone,
     and a stack of two full-width ones makes an answer look like a form. */
  .fresh-acts { width: 100%; }
  .fresh-acts > * { flex: 1 1 0; }
  .fresh-acts .btn { width: 100%; }
}

/* ======================================================================================
   THE CARD ON THE COUNTER
   ======================================================================================
   A page that is also a printable object. The screen version explains itself; the printed
   version is the card and nothing else, because a sheet of A4 with a card and three
   paragraphs of instructions on it gets cut out badly or not at all.
   -------------------------------------------------------------------------------------- */
.rc-sheet { display: flex; justify-content: center; margin: var(--s5) 0; }
.rc-card { display: flex; align-items: center; gap: var(--s5);
           border: 2px solid var(--pine-700); border-radius: var(--radius);
           padding: var(--s5); background: #fff; max-width: 460px; }
.rc-qr { flex: none; line-height: 0; }
.rc-qr svg { width: 148px; height: 148px; display: block; }
.rc-words { min-width: 0; }
.rc-name { display: block; font-size: var(--t-section); line-height: var(--lh-section);
           color: var(--pine-900); overflow-wrap: anywhere; }
/* Both languages, always, on the card itself: the person who printed it chose a language;
   the person picking it up off the counter did not. */
.rc-ask-en { margin: var(--s3) 0 0; font-weight: 700; color: var(--ink); }
.rc-ask-ur { margin: 2px 0 0; font-weight: 700; color: var(--ink); }
.rc-url  { margin: var(--s3) 0 0; font-size: var(--t-label); color: var(--ink-soft);
           overflow-wrap: anywhere; }
.rc-mark { margin: 2px 0 0; font-size: var(--t-label); font-weight: 700; color: var(--pine-700);
           letter-spacing: .04em; }
.rc-steps { margin: 0 0 var(--s4); padding-inline-start: var(--s5); line-height: var(--lh-body); }
.rc-steps li { margin-bottom: var(--s2); }

@media (max-width: 480px) {
  .rc-card { flex-direction: column; text-align: center; }
}

/* WHAT PRINTS IS THE CARD. Everything else on the page is instructions for the person doing
   the printing, and instructions printed onto the card are instructions a customer reads
   instead of scanning. */
@media print {
  .no-print, .topbar, .footer, nav, .flash { display: none !important; }
  body { background: #fff; }
  .rc-sheet { margin: 0; justify-content: flex-start; }
  .rc-card { break-inside: avoid; box-shadow: none; }
  .rc-qr svg { width: 132px; height: 132px; }
}

/* The area pager. Two buttons and a count — no numbered pages, because an area with forty
   pages is an area whose reader wanted the category links at the top, not page 27. */
.area-pager { display: flex; align-items: center; justify-content: center; gap: var(--s4);
              margin: var(--s5) 0; flex-wrap: wrap; }
.area-pager .hint { margin: 0; }

/* ---------- A day out: routes ---------- */
/* Appended at the END of the file on purpose. This stylesheet has already been bitten once by
   an earlier media query losing to a later shorthand; anything added later goes last. */

.route-map { height: 320px; width: 100%; }
@media (max-width: 640px) { .route-map { height: 240px; } }

.route-handoff { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.route-qr { flex: 0 0 auto; line-height: 0; }
.route-qr svg { width: 132px; height: 132px; display: block; }
.route-code { font: 700 1.6rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;
              letter-spacing: .18em; margin: 0; }
.route-code-input { font: 600 1.1rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;
                    letter-spacing: .16em; text-transform: uppercase; max-width: 12ch; }

.route-list { list-style: none; margin: 12px 0 0; padding: 0;
              display: flex; flex-direction: column; gap: 8px; }
.route-stop { display: flex; gap: 12px; align-items: flex-start;
              background: var(--card); border: 1px solid var(--line);
              border-radius: var(--radius); padding: 12px 14px; }
/* The number is the point of the whole screen — it is what makes this a plan rather than a
   set — so it is the first thing in the row and the only thing with a filled background. */
.route-num { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
             background: var(--pine-500); color: #fff; font-weight: 700; font-size: .85rem;
             display: inline-flex; align-items: center; justify-content: center; }
.route-body { flex: 1 1 auto; min-width: 0; }
.route-name { font-weight: 600; }
.route-note { margin-top: 8px; }
.route-note .input { font-size: .9rem; }
.route-stop-note { margin: 6px 0 0; font-size: .92rem; }
.route-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.route-controls { flex: 0 0 auto; display: flex; gap: 2px; align-items: flex-start; }

/**
 * SCOPED TO THE CARD, because `.route-add` is worn by two different things.
 *
 * `partials/route_add.php` is a CARD whose summary is a plain <b>, and `padding: 4px 0` is
 * right for it. The listing page reuses the same class on a <details> whose summary carries
 * `class="btn btn-ghost btn-sm"` — and `.route-add summary` (0,1,1) beats `.btn` (0,1,0), so
 * it was stripping the button's LEFT AND RIGHT PADDING TO ZERO. "Add to a route" rendered
 * with its text touching both borders while "Add to a list", which has no such override,
 * sat correctly beside it.
 *
 * The child combinator and `.card` together mean this can only ever reach the partial's own
 * summary. A button keeps the padding every other button has.
 */
.card.route-add > summary { cursor: pointer; padding: 4px 0; }
.route-add-form { margin-top: 10px; }

/* The numbered pin on the map. A DOM marker, so the number is never dropped by the label
   collision detector the way a symbol layer's text would be. */
.route-pin { width: 24px; height: 24px; border-radius: 50%; background: var(--pine-500);
             color: #fff; font: 700 12px/24px -apple-system, BlinkMacSystemFont, sans-serif;
             text-align: center; border: 2px solid #fff;
             box-shadow: 0 1px 4px rgba(0,0,0,.35); cursor: default; }

/* ---------- The results-panel pager ---------- */
/* Same two-button shape as .area-pager, but it lives INSIDE a scrolling panel rather than at
   the bottom of a document, so it needs its own top rule to separate it from the last card
   and a little breathing room under it on a phone, where the panel ends at the safe area. */
.list-pager { display: flex; align-items: center; justify-content: center; gap: var(--s4);
              flex-wrap: wrap; margin: var(--s5) 0 var(--s4);
              padding-top: var(--s4); border-top: 1px solid var(--line); }
.list-pager .hint { margin: 0; white-space: nowrap; }

/* ---------- The filter bar on /providers/{type} and /areas ---------- */
/* A plain GET form: a search box, two or three selects, two checkboxes and a button. It
   wraps, because on a phone those are six controls and one row of them would either scroll
   sideways or shrink the search box to nothing. `flex: 1 1 220px` on the search box is what
   makes it take the leftover width on a wide screen and a full row of its own on a narrow
   one, without a media query deciding where that happens. */
.prov-filters { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center;
                margin: var(--s4) 0 var(--s5); }
/* `width: auto` is the line that makes this a bar rather than a stack. The site's base
   `.input` is `width: 100%`, which is right for a form where every field owns its row and
   wrong here: a flex item whose basis resolves to `auto` takes its width from `width`, so
   every select demanded the full 784px and the six controls became six rows, 262px tall.
   The basis on each control below is what lets them share a line and wrap in pairs. */
.prov-filters .input { width: auto; min-width: 0; padding: 7px 10px; font-size: .95rem; }
.prov-filters input[type="search"] { flex: 1 1 220px; }
.prov-filters select { flex: 0 1 auto; max-width: 100%; }
/* The two checkboxes read as labels rather than as form furniture, and the whole thing is
   the click target — a 13px box on a touchscreen is not one.
   `.filter-chk` and not `.chk`: that class already belongs to the completeness checklist
   further up this file, where it carries a bottom margin. Reusing it here would have put a
   gap under two controls in a row of eight, and the cause would have been a rule six hundred
   lines away about a different feature. */
.prov-filters .filter-chk { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
                     font-size: .92rem; color: var(--ink-2); padding: 6px 2px; margin: 0; }
.prov-filters .filter-chk input { margin: 0; }
@media (max-width: 560px) {
  /* Below this the selects stop sharing a row sensibly with the search box, so they pair up
     with each other instead: two per row, each half the width, which fits "Any rating" and
     "Best rated" without truncating either. */
  .prov-filters input[type="search"] { flex: 1 1 100%; }
  .prov-filters select { flex: 1 1 44%; }
}

/* ---------- Advertisement slots ---------- */
/* THESE HAD NO RULES AT ALL. `[data-ad-slot]`, `#side-ad-slot` and the `.ad-slot-*` classes
   core.js emits were undefined in this stylesheet, so every slot was a zero-height div that
   became whatever the creative was the moment the fetch resolved. On /area/{slug} the empty
   div sits directly above the map, so the map jumped down the full height of the ad — a
   layout shift on the element the reader was already looking at.

   `min-height` reserves the space before the ad arrives. It is the height of the smallest
   card the renderers can produce (a title, two lines of body and the disclosure), so a slot
   that stays empty leaves a modest gap rather than a hole, and one that fills does not move
   anything below it. */
[data-ad-slot], #side-ad-slot { min-height: 96px; }
[data-ad-slot]:empty, #side-ad-slot:empty { min-height: 0; }

/* An advertiser's picture is theirs and can be any shape, so the SLOT decides the band and
   the picture is cropped into it — `object-fit: cover`, never stretched.

   A DEFINITE HEIGHT, not a max-height. With `max-height` alone an image that has not decoded
   yet contributes ZERO height, so the card grew the moment the picture landed and pushed
   everything under it down — the exact layout shift the reserved space above this was added
   to prevent, reintroduced one line later. A fixed band is also what makes two advertisers'
   creatives sit at the same size instead of one owning twice the column. */
.sponsored-card img, .sponsored-body img {
  height: 180px; width: 100%; object-fit: cover; display: block; background: var(--mist);
}
@media (max-width: 560px) { .sponsored-card img, .sponsored-body img { height: 140px; } }

/* The slot is an advertisement and has to read as one at a glance, in every renderer. The
   pill and the hairline are what make a sponsored card distinguishable from a card the site
   chose — the background gradient alone is too easy to miss on a bright screen outdoors. */
.sponsored-card { border-color: var(--line); position: relative; }
.sponsored-card .ad-why { display: block; margin-top: 6px; }

/* The list advertisement and its explanation, wrapped together so the "why" link is a
   sibling of the card rather than an anchor nested inside one. */
.sponsored-wrap { display: block; margin-bottom: var(--s3); }
.sponsored-wrap .ad-why { margin: 2px 0 0 12px; }  /* size inherited -- see .ad-why */

/* The front page advertisement. It is a sibling of the map and of the fallback list, so it
   needs the list's own centring rather than inheriting it — and a z-index above the scrim,
   which sits over the map and would otherwise swallow the click. */
.landing-ad { position: relative; z-index: 2; max-width: 640px; width: 100%;
              margin-inline: auto; padding: 0 var(--s4) var(--s5); }
.landing-ad:empty { padding: 0; }

/* The money glossary. A <details> because the definitions are reference rather than reading,
   and because a modal would need script that the content security policy will not run. */
.money-words > summary { cursor: pointer; font-size: 1rem; padding: 2px 0; }
.money-words[open] > summary { margin-bottom: 6px; }
.money-words-list { margin: 10px 0 0; display: grid; grid-template-columns: 200px 1fr; gap: 8px 18px; }
.money-words-list dt { font-weight: 700; }
.money-words-list dd { margin: 0; color: var(--ink); max-width: 70ch; }
@media (max-width: 700px) {
  .money-words-list { grid-template-columns: 1fr; gap: 2px 0; }
  .money-words-list dd { margin: 0 0 10px; }
}

/* ---- the admin rail's two levels, and its own search ------------------------------
   A major heading is the subject; a section heading inside it is always Waiting on you,
   then Day to day, then Settings. The section heading is deliberately quieter than the
   major: three levels of equally loud type is a menu nobody can scan. */
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.rail-find { padding: 6px 10px 8px; }
.rail-find input { width: 100%; padding: 6px 9px; font: inherit; font-size: .92rem;
  border: 1px solid var(--line, #dfe5e2); border-radius: 7px; background: #fff; color: inherit; }
.rail-find input:focus { outline: 2px solid var(--brand, #17503a); outline-offset: 1px; }
.rail-none { margin: 2px 12px 10px; color: var(--muted, #6a7570); font-size: .88rem; }
.rail-major { margin-bottom: 2px; }
.rail-sec { margin: 8px 0 1px; padding: 0 12px; font-size: .72rem; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: var(--muted, #8b958f); }
/* Settings sits at the foot of every subject; a hairline says "the list above ended". */
.rail-sec-settings { border-top: 1px solid var(--line, #eceff0); padding-top: 8px; margin-top: 10px; }
.rail-group .rail-n { margin-left: 6px; vertical-align: middle; }

/* The verification code sits beside its button, and is set in a monospaced face at a size
   that can be read off a phone notification and typed without losing your place. */
.verify-code-form { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
.verify-code-form .input { width: 8.5em; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.15rem; letter-spacing: .18em; text-align: center; }

/* ======================================================================================
   THE PICTURE FOR A STORY
   ======================================================================================
   Almost no styling, because there is almost nothing here: a canvas the browser draws the
   whole picture into, and a button under it. The canvas carries its own real dimensions in
   the markup, so the box holds 9:16 before the script runs and the page does not jump.
   -------------------------------------------------------------------------------------- */
.sc-stage { display: flex; justify-content: center; margin: var(--s5) 0; }
/* Capped by HEIGHT rather than by width: a tall picture sized to the column is taller than
   the window it is being looked at in, and an owner who has to scroll to see the bottom of a
   preview cannot tell whether the bottom is right. */
.sc-canvas { max-height: 62vh; max-width: 100%; height: auto; width: auto;
             border-radius: var(--radius); box-shadow: var(--shadow-lg);
             background: var(--pine-900, #14342B); }
/* The button does not exist until there is a picture behind it. A download link with nothing
   attached is worse than no link: it looks like a fault in the site rather than a browser
   that could not draw. */
.sc-acts { display: none; margin: 0 0 var(--s3); text-align: center; }
.sc-acts.is-on { display: block; }
.sc-note { text-align: center; max-width: 52ch; margin-inline: auto; }
/* The two things an owner with no website can still use, named on the badge card. A hint-sized
   line with real links in it, so the links have to stay visible against the muted text. */
.own-no-site { margin-top: var(--s2); }
.own-no-site a { color: var(--pine-700); font-weight: 600; }

/* ==========================================================================
   THE INTERFACE DESK  (/admin/strings)
   ==========================================================================
   A translator works through 2,106 phrases in sittings, so the screen's whole job is to make
   "what is left" visible at a glance and to keep the row they are typing in legible. Three
   colours and nothing else: the bar, the row tint, and the one red for a broken placeholder.
   ========================================================================== */
.strings-cover { padding: var(--s3) var(--s4); }
.cover-bar {
  display: flex; height: 12px; border-radius: 999px; overflow: hidden;
  background: var(--mist); border: 1px solid var(--line);
}
.cover-bar .seg { display: block; height: 100%; }
.cover-bar .is-done    { background: var(--pine-700); }
/* Amber, not green: a phrase that still carries the English word counts as translated by every
   naive measure and reads as English to the person the page is for. It is not done. */
.cover-bar .is-same    { background: var(--amber); }
.cover-bar .is-missing { background: transparent; }
.cover-key {
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  font-size: var(--t-meta); color: var(--ink-soft);
}
.cover-key b { display: inline-block; width: 10px; height: 10px; border-radius: 3px; vertical-align: baseline; }
.cover-key .k-done    { background: var(--pine-700); }
.cover-key .k-same    { background: var(--amber); }
.cover-key .k-missing { background: var(--mist); border: 1px solid var(--line); }
.cover-key .k-fault   { background: var(--danger); }
.cover-key .is-fault  { color: var(--danger); font-weight: 600; }

.strings-find, .strings-io { flex-wrap: wrap; gap: var(--s2); align-items: center; }
.strings-io .inline-form { display: flex; align-items: center; gap: var(--s2); }

.strings-table td { vertical-align: top; }
.strings-table .strings-key { font-size: .78rem; word-break: break-all; }
/* "edited here" — this phrase differs from the committed file. Not a warning: it is how a
   translator finds their own work again after a week, and how anyone can see at a glance what
   lang_export would fold back into the .php file. */
.strings-table .pill {
  display: inline-block; margin-inline-start: 4px; padding: 1px 6px;
  border-radius: 999px; background: var(--pine-100); color: var(--pine-700);
  font-size: .68rem; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
}
/* The English column is prose in Latin script and stays that way on an Urdu admin screen —
   `dir="ltr"` in the markup handles the direction, this handles the measure. */
.strings-en { max-width: 40ch; }
.strings-args { margin-top: var(--s1); display: flex; flex-wrap: wrap; gap: 4px; }
.strings-args code {
  background: #FFF4DA; border: 1px solid var(--line);
  border-radius: 4px; padding: 0 4px; font-size: .72rem;
}
/* A section heading made from the part of the key before the first dot. The keys already carry
   that structure and a translator who can see they are doing "the whole booking flow" makes
   better choices than one going alphabetically through a list. */
.strings-group td {
  background: var(--mist); font-weight: 700; font-size: var(--t-meta);
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft);
}
.strings-table tr.is-missing td:first-child { box-shadow: inset 3px 0 0 var(--line); }
.strings-table tr.is-same    td:first-child { box-shadow: inset 3px 0 0 var(--amber); }
.strings-table tr.is-fault   td:first-child { box-shadow: inset 3px 0 0 var(--danger); }
html[dir="rtl"] .strings-table tr.is-missing td:first-child { box-shadow: inset -3px 0 0 var(--line); }
html[dir="rtl"] .strings-table tr.is-same    td:first-child { box-shadow: inset -3px 0 0 var(--amber); }
html[dir="rtl"] .strings-table tr.is-fault   td:first-child { box-shadow: inset -3px 0 0 var(--danger); }
.strings-fault { margin: var(--s1) 0 0; font-size: var(--t-meta); color: var(--danger); }
.strings-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4); margin-top: var(--s4); }
.strings-foot .pager { margin: 0; }

/* Repeat submissions from one address, on the photo queue. Amber rather than red: two from one
   place is a couple who both photographed the same café, fourteen is a script, and the screen
   shows the number rather than deciding which it is. */
.photo-q-more { display: block; margin-top: 2px; color: var(--ink); }
.photo-q-more b { color: #8A6212; }

/* The rights notice above an upload button, and the agreement line above the register button.
   Both are the small print that has to be READ rather than skipped, so they get the hint size
   and the hint colour but sit hard against the control they belong to — a rights line floating
   two paragraphs from the button is a rights line nobody connects to the act. */
.upload-rights, .auth-terms {
  margin: var(--s2) 0 var(--s3);
  font-size: var(--t-meta); line-height: 1.5; color: var(--ink-soft);
}
.upload-rights a, .auth-terms a { color: var(--pine-700); font-weight: 600; }
.auth-terms { text-align: center; margin-bottom: var(--s3); }

/* The rights tick-box. Its target is the whole label rather than the 13px box — a required
   control that is hard to hit is a form people abandon, and this one sits on the path that
   exists to get photographs onto 39,992 empty listings. */
.rights-tick {
  display: flex; align-items: flex-start; gap: 10px;
  margin: 0 0 var(--s3); padding: 8px 0;
  font-size: var(--t-meta); line-height: 1.45; color: var(--ink);
  cursor: pointer;
}
.rights-tick input { flex: none; width: 18px; height: 18px; margin: 1px 0 0; cursor: pointer; }
.rights-tick span { flex: 1; min-width: 0; }

/* ══════════════════════════════════════════════════════════════════════════════════════════
   Notifications
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* THE BELL. Rendered by the server with the count already in it — see layout.php for why a
   dot that appears a second after the page settles is a dot nobody sees. */
.nav-bell {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 50%; font-size: 17px; line-height: 1;
  text-decoration: none; flex: none;
}
.nav-bell:hover, .nav-bell:focus-visible { background: rgba(255, 255, 255, .14); }
.mv-bell-dot {
  position: absolute; top: 1px; inset-inline-end: 0;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 9px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 17px; text-align: center;
  /* The topbar is dark pine, so the dot needs an edge of its own or it reads as part of the
     bell rather than as a count sitting on top of it. */
  box-shadow: 0 0 0 2px var(--pine-900);
}

.ntf-page h1 { margin-bottom: var(--s3); }
.ntf-tools { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s3); }
.ntf-new { font-weight: 700; color: var(--pine-700); }

.ntf-list { list-style: none; margin: 0 0 var(--s4); padding: 0; }
.ntf-item { border-bottom: 1px solid var(--line); }
.ntf-item:first-child { border-top: 1px solid var(--line); }

/* UNREAD IS A RULE DOWN THE LEADING EDGE, NOT A BACKGROUND WASH.
   A tinted row competes with the card surfaces used everywhere else on this site, and the tint
   that survives both themes and both text directions is no tint at all. `inset-inline-start`
   rather than `left`, so the marker is on the correct side in Urdu. */
.ntf-item.is-unread { box-shadow: inset 3px 0 0 var(--pine-500); }
[dir="rtl"] .ntf-item.is-unread { box-shadow: inset -3px 0 0 var(--pine-500); }

.ntf-link {
  display: grid; gap: 2px; padding: var(--s3) var(--s3) var(--s3) calc(var(--s3) + 6px);
  text-decoration: none; color: inherit;
}
.ntf-link:hover, .ntf-link:focus-visible { background: var(--mist); }
.ntf-title { font-weight: 600; }
.ntf-item.is-unread .ntf-title { font-weight: 700; }
.ntf-body { color: var(--ink-soft); font-size: var(--t-meta); line-height: var(--lh-meta); }
.ntf-when { color: var(--ink-soft); font-size: 12px; font-variant-numeric: tabular-nums; }

/* Something that arrived over the live stream while somebody was looking at the page. It fades
   in from a VISIBLE resting state — an element parked at opacity 0 waiting for a class is an
   element that stays invisible when the animation is suppressed. */
@keyframes ntf-in { from { background: var(--pine-100); } to { background: transparent; } }
.ntf-arrived { animation: ntf-in 1.6s ease-out; }
@media (prefers-reduced-motion: reduce) { .ntf-arrived { animation: none; } }

.ntf-h2 { margin: var(--s5) 0 var(--s2); }
.ntf-group { padding: var(--s3); margin-bottom: var(--s3); }
.ntf-group h3 { margin: 0 0 var(--s2); font-size: 1rem; }

.ntf-table { width: 100%; border-collapse: collapse; }
.ntf-table th[scope="col"] { font-size: 12px; font-weight: 600; color: var(--ink-soft); width: 68px; text-align: center; }
.ntf-table th[scope="row"] { font-weight: 400; text-align: start; padding: var(--s2) 0; }
.ntf-table td { text-align: center; padding: var(--s2) 0; }
.ntf-table tr + tr th, .ntf-table tr + tr td { border-top: 1px solid var(--line); }
.ntf-switch { display: inline-flex; cursor: pointer; }
.ntf-switch input { width: 20px; height: 20px; cursor: pointer; }

.ntf-push { padding: var(--s3); margin-bottom: var(--s3); }
.ntf-push-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); flex-wrap: wrap; }
.ntf-devices { list-style: none; margin: var(--s3) 0 0; padding: 0; display: grid; gap: var(--s2); }
.ntf-devices li { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); }
.ntf-status { min-height: 1.4em; color: var(--pine-700); font-size: var(--t-meta); }


/* ---------- The counter, and the receipt it prints ---------- */
/* Appended at the END, for the reason written above the routes block: this stylesheet has
   already lost an earlier media query to a later shorthand once. */

/* One payer per row: who they are on the left, the amount and the button on the right. It
   wraps to a column on a phone because a clerk on a counter tablet is the likeliest reader
   and a two-column form squeezed to 380px is where the wrong amount gets typed. */
.cash-payer { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s3) var(--s4);
  justify-content: space-between; padding: var(--s3) 0; border-top: 1px solid var(--line); }
.cash-payer:first-of-type { border-top: 0; }
.cash-payer.is-muted { opacity: .6; }
.cash-payer-who { min-width: 0; flex: 1 1 220px; overflow-wrap: anywhere; }
.cash-payer-who .hint { display: block; margin: 2px 0 0; }
.cash-take { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s3); }
.cash-take .field { margin: 0; min-width: 130px; }

/* A cancelled row stays legible rather than being greyed to nothing: a till is balanced by
   reading this list against a drawer, and the cancelled ones are exactly the rows somebody
   is looking for when the two disagree. */
tr.is-void td { color: var(--ink-soft); }
tr.is-void code { text-decoration: line-through; }

/* THE DOCUMENT. Sized to a sheet rather than to the screen, because it exists to be printed
   and handed over — a receipt that reflows to the browser window prints differently on every
   machine in every office. */
.receipt { position: relative; max-width: 620px; margin: var(--s4) 0; padding: var(--s5);
  background: #fff; color: #1a1a1a; border: 1px solid var(--line); border-radius: var(--r2); }
.receipt-head { display: flex; justify-content: space-between; gap: var(--s4); flex-wrap: wrap;
  padding-bottom: var(--s4); border-bottom: 2px solid #1a1a1a; }
.receipt-brand { font-weight: 800; font-size: 1.15rem; letter-spacing: .01em; }
.receipt-office { font-weight: 600; margin-top: 2px; }
.receipt-addr { font-size: var(--t-meta); color: #555; }
.receipt-no { text-align: end; }
.receipt-label { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #555; }
.receipt-num { font-family: var(--mono, ui-monospace, monospace); font-size: 1.1rem; font-weight: 700; }

.receipt-body { display: grid; grid-template-columns: max-content 1fr; gap: var(--s2) var(--s5);
  margin: var(--s4) 0; align-items: baseline; }
.receipt-body dt { font-size: var(--t-label); font-weight: 600; color: #555; }
.receipt-body dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* The one number anybody checks, set so it cannot be misread at arm's length. */
.receipt-amount { font-size: 1.5rem; font-weight: 800; }

.receipt-foot { border-top: 1px solid var(--line); padding-top: var(--s3);
  font-size: var(--t-meta); color: #555; }
.receipt-sign { display: flex; justify-content: space-between; gap: var(--s5);
  margin-top: var(--s6); }
.receipt-sign span { flex: 1; border-top: 1px solid #1a1a1a; padding-top: 4px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }

/* A cancelled receipt is still printable — somebody may need to file the cancelled copy —
   so it is stamped rather than hidden. aria-hidden by being decorative text the document
   already states in words above it. */
.receipt-void { border-color: var(--red-600, #b3261e); }
.receipt-stamp { position: absolute; inset-inline-end: var(--s5); top: 42%;
  transform: rotate(-14deg); font-size: 2.6rem; font-weight: 900; letter-spacing: .12em;
  color: rgba(179, 38, 30, .22); pointer-events: none; user-select: none; }

@media print {
  /* The page chrome is already hidden by the rule further up this file. What is left is to
     stop the card furniture printing as a box around the document. */
  .receipt { border: 0; margin: 0; padding: 0; max-width: none; break-inside: avoid; }
  .receipt-stamp { color: rgba(179, 38, 30, .35); }
  .page > .card { display: none !important; }
  .receipt-body dt, .receipt-addr, .receipt-label, .receipt-foot { color: #000; }
}
