/* ── Self-hosted Inter (works on file://, no CORS) ──────────────────────────────────────
   PATHS: `../fonts/...` is CORRECT in THIS build and must stay. A previous version of this
   comment said the opposite - it was carried over from the homepage build, where this CSS was
   INLINED into index.html at the site root, so url()s resolved against the document. Here the
   file is served as assets/site.css, and CSS url()s resolve against the STYLESHEET, so
   `../fonts/` correctly means root/fonts. The preload tags in the pages use `../fonts/` too
   but resolve against the DOCUMENT (/exchange-rates/), landing on the same /fonts/ URL - which
   is what keeps the preload and the CSS request the same file rather than two downloads.

   font-display: BLOCK, changed from `optional` 10 Aug 2026 (client).
   `optional` gives the font ~100ms and, if it misses, uses the fallback FOR THE WHOLE PAGE
   VIEW and never swaps. It was chosen to kill the swap flash, and it did - by permanently
   showing a system font whenever Inter was a moment late, which on a cold cache is most of
   the time. That is the "it loads another font on a fast load" complaint, and it was a
   deliberate setting behaving exactly as specified, not a bug.
   `block` inverts the trade: text waits rather than settling for the wrong font. Safe here
   ONLY because these are same-origin, preloaded, already-subset ~21KB files (232 codepoints),
   so the wait is milliseconds. If the fonts ever move off-origin, grow, or lose their preload
   tags, this becomes a flash of INVISIBLE text and must be revisited.

   ONE VARIABLE FILE, and it is INTER 3.019 - THE SAME BUILD AS THE STATIC FILES IT REPLACED.
   Verified before installing: identical version string AND identical git hash
   (`Version 3.019;git-0a5106e0b`), and the digit 1 measures 0.4645em in both. Not a
   redesign, not a different cut - the same outlines, delivered as a variable font.

   ── THE TRAP, because it cost a full revert on 10 Aug 2026 ──
   The first attempt at this pulled "Inter" from fonts.googleapis.com. **Google now serves
   Inter 4.001, and v4 is a REDESIGN, not a version bump.** It shipped, and the client spotted
   it instantly across the whole site. Every automated check had passed: font loaded, family
   "Inter", weights distinct and monotonic, x-height / cap-height / most advances matching v3
   to within 0.5%. It only failed the eye. The one measurable tell is the **digit 1 - 0.4645em
   in v3, 0.4067em in v4** - which on a page built out of rate numbers is unmissable.
   **ALWAYS check the `name` table version, and measure the digit 1, before swapping a font
   file.** "It loaded and the family is right" is not proof it is the same font.
   Source this from the rsms/inter GitHub release (v3.19), NEVER from Google.

   ── WHAT THIS FIXED ──
   1. THE FAKE WEIGHTS. Five rules pointed at three files: 600 aliased the 500 file, 800
      aliased the 700, so those two silently rendered as their neighbours. Both pages use all
      five. `font-weight: 400 800` is now a genuine continuous range.
   2. ELEVEN MISSING GLYPHS. The static subset lacked characters the pages actually use, so
      they fell back to a system font mid-line: the `->` arrow in EVERY rate row, plus baht,
      won, dong, peso, Indian rupee, Turkish lira, sheqel, c-caron, l-stroke. The rate board
      was rendering its currency symbols in a different typeface from its numerals.
   3. 35 KB in ONE request, against 66 KB across THREE.

   ── BUILD ──
   Subset of `Inter Web/Inter-roman.var.woff2` from the v3.19 release, over the character set
   at scripts/inter-charset.txt - the union of the old 232 codepoints, every character in both
   pages, and the whole U+20A0-20BF currency block as headroom so a new feed currency can
   never arrive without a glyph. wght axis clamped to 400..800; nothing here uses lighter or
   heavier. OpenType features kept: kern, tnum, calt, ccmp, locl, mark - `tnum` is
   load-bearing, it is what aligns the rate columns. Dropped frac/numr/dnom/pnum as unused.
   `liga` does not exist in Inter 3.19 at all, so the `font-feature-settings: 'liga'` further
   down has always been a no-op.

   KNOWN GAP: U+09F3 (Bengali taka) is absent from Inter entirely. It sits in the BDT currency
   metadata, which the feed does not send. Unfixable here - the glyph does not exist upstream. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 800;
  font-display: block;
  src: url('../fonts/inter-var-v3.woff2') format('woff2');
}
:root {
  --gold: #feb900;
  --gold-deep: #e3a600;
  --ink: #000000;
  --ink-soft: #191c20;
  --slate: #4b5563;
  --muted: #8c919a;
  --paper: #ffffff;
  --mist: #f3f4f6;
  --rowbg: #ffffff;
  --line: #e5e7eb;
  /* THE OPEN/CLOSED STATUS LIGHT (client, 10 Aug 2026).

     Deliberately NOT #10b981 any more. That is Tailwind's emerald-500, the default success
     colour in generated UI, and the client read the component built on it as "made by AI"
     without knowing why. This is deeper and less saturated so it sits beside the gold rather
     than shouting over it.

     IT SURVIVES ON ONE SURFACE ONLY: the dot. Green used to run across the dot, the .s-main
     TEXT, the pill fill, the pill rim and the bar's outer border - five surfaces across four
     components - which is what made the whole thing read as a green component dropped into a
     gold site. Everything except the dot is now brand or neutral.

     The brand rule is a single accent, gold. A semantic state light is not an accent and
     keeps its exemption only while it stays this small - so if this ever starts tinting
     panels or text again, it has stopped being a status light and broken the rule. */
  --open: #0f8a5f;
  --maxw: 1432px;
  --r: 24px;
  --rs: 14px;
  --font: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif;
  --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.03), 0 1px 3px rgba(0, 0, 0, 0.02);
  --shadow-float: 0 20px 48px -12px rgba(0, 0, 0, 0.08), 0 8px 16px -8px rgba(0, 0, 0, 0.04);
}
html {
  font-style: normal;
  /* THE CANVAS NEEDS ITS OWN COLOUR, and it is not decorative (client, 16 Aug 2026 - white
     strip appearing under the sticky bar on Android).
     `body` sets `background: var(--paper)` - which is #ffffff - and only then overrides the
     COLOUR to #f8fafc. Background propagation from body to the canvas is enough while the
     viewport is stable, but on Android every scroll-up animates Chrome's URL bar in, the
     visual viewport shrinks, and the strip revealed underneath is painted from the canvas
     before the page repaints into it. With no colour of its own that strip flashes white
     against a #f8fafc page, which reads as a white bar shoving the action bar upwards.
     Setting it here means the worst case is a strip the same colour as the page, i.e.
     invisible. It does NOT stop the action bar moving - that bar is `position: fixed` and
     tracking a shrinking viewport is correct behaviour, not a bug. */
  background-color: #f8fafc;
}
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
body {
  font-family: var(--font);
  font-style: normal;
  color: var(--ink);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* BODY MUST NOT PAINT A BACKGROUND. This is not a tidy-up - it is what keeps the entire
     background system visible, and getting it wrong hides every glow on the site at once.

     Every atmospheric layer here sits at `z-index: -1`: body::before (the three page-wide
     radial tints), .hero::before / .hero::after (the gold and green washes), #rates::before,
     #location::before and .fcta::before. None of those elements creates a stacking context of
     its own, so they all paint in the ROOT one, where the order is:
         1. the html background
         2. negative z-index layers   <- all six glows
         3. block-level backgrounds   <- body's background
         4. content
     While `html` had no background of its own, body's background was PROPAGATED to the canvas
     and body painted nothing at step 3, so the glows at step 2 were visible.
     `html { background-color: #f8fafc }` was added on 16 Aug 2026 to stop a white strip
     flashing under the sticky bar on Android. That is a real fix and it stays - but it ends
     the propagation, so body started painting its own opaque #f8fafc box at step 3, directly
     over all six glow layers. The page went flat everywhere, on every page, in one line.
     Leaving body transparent lets html's colour be the canvas and the glows sit on top of it.
     If a background is ever needed on body again, move the glows out of negative z-index
     first, or they will disappear a second time. */
  background: transparent;
}
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background-image:
    radial-gradient(at 0% 0%, hsla(253,16%,7%,0.05) 0, transparent 50%),
    radial-gradient(at 50% 0%, hsla(225,39%,30%,0.05) 0, transparent 50%),
    radial-gradient(at 100% 0%, hsla(339,49%,30%,0.05) 0, transparent 50%);
  pointer-events: none;
  will-change: transform;
}
a {
  color: inherit;
  text-decoration: none;
}
ul {
  list-style: none;
}
img {
  max-width: 100%;
  display: block;
}
h1, h2, h3, h4 {
  font-family: var(--font);
  font-style: normal;
  color: var(--ink);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
}
h1 {
  font-size: clamp(40px, 6vw, 68px);
  line-height: 1.0;
}
h2 {
  font-size: clamp(30px, 4vw, 48px);
  margin-bottom: 12px;
}
h3 {
  font-size: 19px;
  letter-spacing: -0.015em;
}
p {
  color: var(--slate);
}
/* Material Symbols - self-hosted 3.6KB subset (only the 19 icons the page uses), inlined
   @font-face replacing the render-blocking 446KB Google stylesheet (~3s of mobile FCP/LCP).
   Tiny + same-origin + preloaded, so it loads instantly and font-display:block shows icons
   with no flash and no layout shift (CLS stays 0). Ligature names still work - ZERO markup
   change. REBUILD NOTE: if you add a NEW icon to the page, add its name to NAMES in
   scripts/subset-material-symbols.py and re-run it, then drop the new woff2 in - otherwise the
   new icon shows as text (the subset only contains the 19 icons listed there). */
@font-face {
  font-family: 'Material Symbols Outlined';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url(../fonts/material-symbols-subset.woff2?v=b1b6641d) format('woff2');
}
.material-symbols-outlined {
  font-family: 'Material Symbols Outlined';
  font-weight: normal;
  font-style: normal;
  font-size: 22px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  vertical-align: middle;
  -webkit-font-feature-settings: 'liga';
  font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
}
.wrap {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
}
.tnum {
  font-variant-numeric: tabular-nums;
}
.gold {
  color: var(--gold);
}

/* ── Glass utility classes (used on sections & cards) ── */
.mobnav {
  position: fixed;
  inset: 0;
  z-index: 65;
  background: rgba(255, 255, 255, 0.92);
  /* backdrop-filter removed here too. There are TWO .mobnav blocks in this file and the
     later one only sets the properties it changes - omitting a property does not reset
     it, so this declaration kept applying blur(48px) long after the later block had
     dropped its own. See the full note on the second block. */
  border: 1px solid rgba(255, 255, 255, 0.6);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 24px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), visibility 0.4s;
}
.mobnav.open {
  opacity: 1;
  visibility: visible;
}
/* Close button: deliberately mirrors .burger's exact box (44px, top 14, right 24) so the
   hamburger appears to morph into an X in place rather than jumping down 13px. If the header
   padding ever changes, re-measure the burger and match these two offsets. */
.mobnav-close {
  position: absolute;
  top: 14px;
  right: 24px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--ink);
  z-index: 10;
  /* spins in just after the panel, so the X "arrives" rather than snapping */
  opacity: 0;
  transform: rotate(-90deg) scale(0.7);
  transition: opacity .3s ease, transform .45s cubic-bezier(.16,1,.3,1), background .2s ease;
}
.mobnav.open .mobnav-close {
  opacity: 1;
  transform: rotate(0deg) scale(1);
  transition-delay: .12s;
}
.mobnav-close:hover { background: rgba(255, 255, 255, 0.85); }
@media (prefers-reduced-motion: reduce) {
  .mobnav-close, .mobnav.open .mobnav-close { transform: none; transition: opacity .2s ease; transition-delay: 0s; }
}
.mobnav .nav-links {
  display: flex;
  flex-direction: column;
  gap: 8px;
  text-align: center;
  width: 100%;
  max-width: 360px;
  transform: translateY(24px);
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.mobnav.open .nav-links {
  transform: translateY(0);
}
.mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer) {
  font-size: 28px;
  font-weight: 800;
  color: var(--ink);
  letter-spacing: -0.04em;
  padding: 10px 0;
  display: block;
  border-radius: 16px;
  /* NO transition here. This selector is (0,4,1) and the stagger rules that animate these
     same elements are (0,2,0) and (0,3,0), so a `transition` declared here silently won
     and the entry and exit both SNAPPED - the panel faded but its contents just appeared.
     The colour and background timings moved into the one shared transition below so there
     is a single declaration per element and nothing to lose to. */
}
.mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer):hover {
  color: var(--slate);
  background: rgba(0, 0, 0, 0.04);
}
.mobnav .nav-divider {
  width: 48px;
  height: 2px;
  background: rgba(0, 0, 0, 0.08);
  border-radius: 99px;
  margin: 16px auto;
}
/* The quiet follow-up under the call button. Underlined text, not a second pill: one
   primary action per panel, and this is for the person who wants the map instead. */
.mobnav .nav-links .mobnav-loc {
  align-self: center;
  margin-top: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 6px 4px;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--slate);
  border-bottom: none;
  /* text-decoration propagates to descendants and a child cannot cancel it, so the
     underline ran under the pin as well. The rule moves onto the text span instead. */
  text-decoration: none;
}
/* A 2px gold rule rather than a 1px text underline: it reads as a deliberate mark
   instead of a default link, and it stops at the text where it belongs. Gold in the
   edge, ink in the type - the same rule the route steps and the buy/sell pill follow. */
.mobnav .nav-links .mobnav-loc .mnl-t {
  border-bottom: 2px solid rgba(254,185,0,0.85);
  padding-bottom: 3px;
}
.mobnav .nav-links .mobnav-loc .material-symbols-outlined {
  font-size: 18px;
  color: var(--gold);
}
/* THE VISIT FOLLOW-UPS, SIDE BY SIDE (client asked for the ticket in the menu, 8 Oct 2026).
   "Get a ticket" shares ONE row with "Our store location" instead of adding a line, because
   the panel's one-screen fit has no line to spare. Measured mid-page, offer line showing: a
   stacked ticket link pushed the copyright 15px off a 360x640 screen, where the panel had
   6px left. Side by side it costs no height on anything 340px wide or more; narrower, the row
   wraps - on screens that already scroll. Both stay quiet underlined links: the panel keeps
   ONE primary action, the call button. Ticket first, because it is what you do BEFORE you
   set off. The wrapper takes the location link's place as the panel's 9th child, so the
   stagger timing below is unchanged. */
.mobnav .nav-links .mobnav-follow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 2px 16px;
  margin-top: 16px;
}
.mobnav .nav-links .mobnav-follow .mobnav-loc { margin-top: 0; }

/* Closes the panel out: the useful line first, the copyright as a quiet full stop. */
.mobnav .nav-links .mobnav-meta {
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid rgba(16,24,40,0.07);
  display: flex;
  flex-direction: column;
  gap: 5px;
  text-align: center;
}
.mobnav .mnm-hours { font-size: 13px; font-weight: 500; color: var(--slate); }
.mobnav .mnm-hours b { font-weight: 700; color: var(--ink); }
.mobnav .mnm-copy { font-size: 11.5px; font-weight: 500; color: var(--muted); line-height: 1.45; }
.mobnav .cta-mob {
  /* Was 8px, which sat the button right against Contact's hairline. The removed
     .nav-divider used to supply this separation. */
  margin-top: 22px;
  /* `.mobnav a` puts a hairline under every link and `a:last-of-type` used to cancel it
     here. The store link is now last, so this button has to cancel its own. */
  border-bottom: none;
  background: var(--ink);
  color: #fff !important;
  font-weight: 700;
  font-size: 16px !important;
  letter-spacing: 0 !important;
  padding: 16px 32px;
  border-radius: 9999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  transition: transform 0.2s, box-shadow 0.2s;
}
.mobnav .cta-mob:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
}
.mobnav .cta-mob .material-symbols-outlined {
  color: var(--gold);
  font-size: 20px;
}
.glass-panel {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
}

.glass-dark {
  background: rgba(15, 23, 42, 0.9);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  padding: 14px 28px;
  border-radius: 9999px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: all 0.2s ease;
  min-height: 52px;
}
.btn .material-symbols-outlined {
  font-size: 18px;
}
.btn-gold {
  background: var(--gold);
  color: var(--ink);
  box-shadow: 0 4px 12px rgba(254, 185, 0, 0.3);
}
.btn-gold:hover {
  background: var(--gold-deep);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(254, 185, 0, 0.4);
}
.btn-ink {
  background: var(--ink);
  color: #fff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-ink:hover {
  background: var(--ink-soft);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.btn-ghost {
  background: rgba(255,255,255,0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--ink);
  border: 1px solid rgba(255,255,255,0.8);
}
.btn-ghost:hover {
  background: rgba(255,255,255,0.8);
  transform: translateY(-2px);
}
.btn.full {
  width: 100%;
}
.lnk {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink);
  font-weight: 600;
  font-size: 14px;
  transition: gap 0.2s ease;
}
.lnk:hover {
  gap: 10px;
}
.lnk .material-symbols-outlined {
  font-size: 18px;
  color: var(--ink);
}

/* Header */
/* ---- .obar - the special-rate offer strip above the header (campaign, 17 Aug 2026) -------
   Static and in normal flow: it scrolls away with the page and the smart-sticky header
   returns ALONE on scroll-up. Never make it sticky - it would stack with the returning
   header and the mobile action bar as a third pinned layer.
   --obar-h IS THE SINGLE SOURCE OF TRUTH for its height, and it is guarded by :has() so the
   whole system self-heals: delete the bar's markup from _parts/header.html at campaign end
   and the variable collapses to 0, taking the header offset with it - no second edit here.
   NO DISMISS (client, 17 Aug 2026): the strip is always on while the campaign runs, so
   there is no stored state and no pre-paint check - the only off-switch is deleting the
   campaign block from _parts/header.html, which the :has() guard makes a one-file edit.
   --obar-gap is the AIR between the strip and the header row (client, 17 Aug 2026 - the
   header started at the strip's very edge and read as glued to it). It works as a
   margin-bottom on the strip, so the whole page - breadcrumbs, hero, everything - moves
   down with it automatically; only the absolute .hdr needs it added to its top, and the
   header JS measures its own rest position (hdr.offsetTop) so no script knows the number. */
:root { --obar-h: 0px; --obar-gap: 0px; }
:root:has(.obar) { --obar-h: 40px; --obar-gap: 14px; }
/* DESKTOP ABSORBS THE GAP INTO THE ROW (client, 17 Aug 2026, after the underline went
   site-wide): with the strip's edge above and the track line below, the 14px air read as
   dead space INSIDE the framed header zone and pushed the logo and nav off centre. On real
   desktop (>=951px, where the nav row lives) the gap goes to 0 and .hdr .wrap grows from
   72px to 86px instead - same total, so the header's bottom edge, the hero, the crumbs and
   every scroll seam stay at exactly the same pixels, while the flex row centres the
   content between the two lines. The row-height half of this lives on .hdr .wrap; the
   pinned rates-bar dock follows it next to the .rsticky rule. Tablets and phones are
   deliberately untouched. */
@media (min-width: 951px) {
  :root:has(.obar) { --obar-gap: 0px; }
}

.obar {
  position: relative;
  z-index: 59;                          /* under .hdr (60), so any overlap during the return
                                           resolves header-first */
  overflow: hidden;                     /* clips the shine sweep */
  margin-bottom: var(--obar-gap);       /* the air before the header row - in flow, so the
                                           rest of the page compensates by itself */
  height: var(--obar-h);
  /* FROSTED GOLD GLASS, not flat gold (client, 17 Aug 2026): the same surface as the
     You Buy / You Sell toggle's selected pill (.seg .ind) - the gold lives in the edge and
     the sheen rather than the fill, and the page shows through it. Flat #feb900 across the
     full width was also the research's one brand worry: the largest gold surface on the
     site. Text stays INK, same as the pill.
     BLUR BUDGET: this is the only live backdrop-filter at rest (the header only frosts once
     it pins, by which point this strip has scrolled away) - so the standing one-blur rule
     on phones still holds. */
  background: linear-gradient(160deg, rgba(254, 185, 0, 0.38) 0%, rgba(254, 185, 0, 0.20) 100%);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  border-bottom: 1px solid rgba(254, 185, 0, 0.55);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.obar .wrap { position: relative; z-index: 1; height: 100%; display: flex; align-items: center; justify-content: center; }
/* The same shine sweep as the offer band (shared keyframes, defined at .offerband::after) -
   client, 17 Aug 2026. Narrower patch because the strip is a thin full-width line and the
   band's 38% would read as a wave rather than a glint. site.js pauses it off screen with
   the same observer that serves the band. */
.obar::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 26%;
  height: 100%;
  background: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.45) 50%, transparent 100%);
  transform: translateX(-140%) skewX(-14deg);
  animation: offer-sweep 7s ease-in-out infinite;
  pointer-events: none;
}
.obar.is-off::after { animation-play-state: paused; }
/* Ink on gold: measured at about 12:1, passes AAA. NEVER white on gold - about 1.7:1. */
.obar-t {
  margin: 0;
  font-size: 14.5px;                    /* bumped from 13.5 for visibility (client, 17 Aug 2026) */
  font-weight: 600;
  color: #000;
  line-height: 1.2;
  display: inline-flex;
  align-items: center;
  /* nowrap is load-bearing: a wrapped second line would outgrow --obar-h and slide the
     header offset out from under the .hdr rule below. ONE copy at every width now (client) -
     "Exchanging above A$4,000? Get a special rate" measures ~305px at the 13px phone size,
     inside a 320px viewport with 16px padding. Re-measure before any copy change ships. */
  white-space: nowrap;
  padding: 0 16px;
}
/* 16px on desktop (client, 17 Aug 2026); the 14.5px base below is the phone-and-tablet
   size, and the <=600 clamp still governs the smallest widths. */
@media (min-width: 951px) {
  .obar-t { font-size: 16px; }
  .obar-t a .ol-svg { width: 15px; height: 15px; }
}
.obar-t a { display: inline-flex; align-items: center; gap: 6px; margin-left: 6px; color: #000; }
/* Underline on the text span only - on the inline-flex anchor it would run under the icon.
   Same rule as .hlink and .cardlink. */
.obar-t .ol-t { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.obar-t a:hover .ol-t { text-decoration-thickness: 2px; }
/* The external-link SVG (client, 17 Aug 2026). Sized a shade under the text's cap height so
   its heavy 2.5 stroke reads as the same weight as Inter 600 next to it. currentColor, so
   it is always exactly the link's ink. */
.obar-t a .ol-svg { width: 14px; height: 14px; flex: none; }
@media (max-width: 600px) {
  :root:has(.obar) { --obar-h: 36px; }
  /* Fluid, not fixed: ONE copy runs at every width (client) and the measured line is 348px
     at 13px - which overflows a 320px viewport by ~28px under the load-bearing nowrap.
     clamp holds 13px from ~382px of viewport down, then shrinks to 11.5px at 320 where the
     line measures ~312px. Shrinking beats clipping; re-measure if the copy changes. */
  .obar-t { font-size: clamp(11.5px, 3.4vw, 13px); }
  .obar-t a .ol-svg { width: 12.5px; height: 12.5px; }
  /* NO air gap on phones (client, 17 Aug 2026) - the 14px that organises the desktop
     header reads as dead space on a small screen, so the strip sits directly on the
     header here, as it did before the gap existed. Every dependent measurement follows
     this variable by itself: .hdr top, the menu panel padding, the loading bar, and the
     JS thresholds, which measure hdr.offsetTop rather than knowing any number. */
  :root:has(.obar) { --obar-gap: 0px; }
}

/* ---- .offerband - the full-bleed landing band for the offer (#offer) ---------------------
   The header strip's link scrolls here; the band ships in _parts/footer.html so the target
   exists on every page. FULL-BLEED gold glass on the client's direction - the surface runs
   edge to edge while every other band's surface stops at its cards, which is exactly the
   "different vibe" doing the separating.
   NO backdrop-filter, deliberately: at this depth there is only the soft page gradient
   behind it, so a blur has nothing sharp to frost and would spend phone GPU for an
   invisible effect - the .findfacts precedent. The glass read comes from the tint, the
   sheen and the gold hairlines, same family as the toggle pill and the header strip. */
.offerband {
  position: relative;
  overflow: hidden;                     /* clips the shine sweep below */
  background: linear-gradient(160deg, rgba(254, 185, 0, 0.30) 0%, rgba(254, 185, 0, 0.14) 100%);
  border-top: 1px solid rgba(254, 185, 0, 0.5);
  border-bottom: 1px solid rgba(254, 185, 0, 0.5);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
  padding: 56px 0 60px;
  text-align: center;
}
/* LANDING ALLOWANCE (client, 17 Aug 2026 - "the header is blocking the section upper
   part"): glides land flush by default (the 22 Jul rule), but that rule assumed .band's
   120px padding; this band has only 56px, thinner than the 86px pinned desktop header,
   which sat on the star. At >=951 - where the nav-click pin keeps the header visible at
   the landing - land 84px down: 2px tucked under the header so a 1px settle miss can
   never show a white sliver between the header underline and the gold. Under 951 there
   is no pin, the header hides after the glide, and flush is exactly right - so no
   allowance. site.js reads this COMPUTED value per target (Lenis ignores CSS scroll
   margins); the native no-Lenis fallbacks respect it natively. One declaration, every
   scroll path. */
@media (min-width: 951px) { .offerband { scroll-margin-top: 84px; } }
/* THE SHINE SWEEP (client, 17 Aug 2026): the same motion language as the calculator's gold
   shine that marks a LIVE rate - a soft highlight crossing the surface, then a long rest.
   Transform-only, so it stays on the compositor; the long tail in the keyframes (sweep for
   ~a third of the cycle, rest for the remainder) is what keeps it calm rather than busy.
   site.js pauses it while the band is off screen, the same observer pattern as the hero
   guilloche - a forever-running animation never lets the GPU idle, and this band spends
   most of every visit off screen. Without JS it simply runs, like the guilloche. */
.offerband::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 38%;
  height: 100%;
  background: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.45) 50%, transparent 100%);
  transform: translateX(-140%) skewX(-14deg);
  animation: offer-sweep 7s ease-in-out infinite;
  pointer-events: none;
}
.offerband.is-off::after { animation-play-state: paused; }
@keyframes offer-sweep {
  0%   { transform: translateX(-140%) skewX(-14deg); }
  38%  { transform: translateX(400%)  skewX(-14deg); }
  100% { transform: translateX(400%)  skewX(-14deg); }
}
@media (prefers-reduced-motion: reduce) { .offerband::after, .obar::after, .orate::after { animation: none; } }

/* ---- .orate - the floating special-rate pill (campaign, 17 Aug 2026) --------------------
   Appears bottom-LEFT once the offer strip has scrolled away, mirroring .gotop's geometry
   on the right - same bottom formula, so it rises above the mobile Call/Directions bar via
   the same --mact-h variable, and the same z-index 54 so the bar and the menu always cover
   it. Created by site.js only when the campaign markup exists; delete the campaign and it
   never mounts.
   VISIBILITY IS PURE CSS, driven by the observer classes the shine sweeps already maintain:
   shown only while the strip is off screen (.obar.is-off) AND the #offer band is also off
   screen (.offerband.is-off) - advertising a section the reader is already looking at is
   noise. No new scroll listener anywhere.
   The shine reuses the shared offer-sweep keyframes and runs ONLY while the pill is shown -
   a paused animation costs nothing while it is hidden.
   TRIED AND CANCELLED (client, 17 Aug 2026): Call and Store pills on this same line, gold
   glass, mobile-only, standing down while the sticky action bar was on duty. Built, tested,
   and the client cancelled after review - do not re-propose. The open/closed status line
   that was to follow them died with the same decision. */
.orate {
  position: fixed;
  left: 14px;
  /* THE LIFT IS ON `translate`, NOT `bottom` (client, 29 Aug 2026 - "it should push the other
     buttons ... smoothly like the mobile"). This reverses the 17 Aug "position never animates"
     call, and it does so WITHOUT reintroducing what that call fixed. The original bug was an
     animated `bottom`: a LAYOUT property, re-evaluated on a variable that was then recomputed
     on every scroll event, so the pill glided on its own timeline and read as "random moves".
     `translate` is a separate compositor property - it does not touch layout, it composes with
     the `transform` used for the slide instead of fighting it, and --mact-h now only changes
     when the bar's .on class flips (a MutationObserver, not a scroll handler), so there is
     nothing left to desynchronise from. `bottom` itself is static again. */
  bottom: calc(14px + env(safe-area-inset-bottom));
  translate: 0 calc(-1 * var(--mact-h, 0px));
  z-index: 54;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 12px 17px;
  border-radius: 999px;
  border: 1px solid rgba(254, 185, 0, 0.55);
  background: linear-gradient(160deg, rgba(254, 185, 0, 0.38) 0%, rgba(254, 185, 0, 0.20) 100%);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  box-shadow: 0 14px 34px -14px rgba(16, 24, 40, 0.4),
              inset 0 1px 0 rgba(255, 255, 255, 0.7);
  color: #000;
  font-size: 13.5px;
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;                     /* clips its shine */
  /* ENTER/EXIT LIKE THE STICKY ACTION BAR (client, 17 Aug 2026, round two - "not
     fading, it should go bottom with the bottom slider, like the sticky bar"): the
     exact .mact recipe - slide below the viewport edge on the same curve, visibility
     gated after the ride down. The extra 14px + safe-area in the calc clears the
     pill's own bottom offset so no sliver of it survives the slide. `bottom` stays OUT
     of the transition list (the --mact-h lift must be instant - the "random moves"
     fix); the slide rides on transform alone. */
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  /* The slide-off must absorb the lift as well as the pill's own bottom offset: `translate`
     is still raising it by --mact-h while it hides, so without this term a hidden pill would
     be parked ~60px ABOVE the viewport edge and stay visible whenever the action bar is up. */
  transform: translateY(calc(120% + 14px + env(safe-area-inset-bottom) + var(--mact-h, 0px)));
  transition: transform .35s cubic-bezier(.22,.9,.3,1), translate .35s cubic-bezier(.22,.9,.3,1),
              opacity .3s ease, visibility 0s linear .35s;
}
.orate .ol-svg { width: 13px; height: 13px; flex: none; }
.orate::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 45%;
  height: 100%;
  background: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%);
  transform: translateX(-140%) skewX(-14deg);
  animation: offer-sweep 7s ease-in-out infinite;
  animation-play-state: paused;
  pointer-events: none;
}
/* SHOW TRIGGER (client, 17 Aug 2026, round two): the pill waits for the calculator's
   own You Buy / You Sell control (#seg) to leave the screen - "not before that" - so it
   never floats next to the calculator it is pointing away from. #seg sits below the
   strip, so the old .obar.is-off condition became redundant and is gone. Still stands
   down while the offer band is on screen (no advertising a scroll already made), and
   rides above the sticky bar via --mact-h in the bottom offset. Both is-off classes
   arrive from the shared observer list in site.js. Enter transition mirrors .mact.on:
   visibility flips immediately on show, delays .35s on hide. */
body:has(#seg.is-off):has(.offerband.is-off) .orate,
/* PAGES WITH NO CALCULATOR (added 29 Aug 2026 with the legal pages, client-reported as "the
   special rate button is missing on the new pages"). The #seg condition above is a WAIT, not
   a requirement: it holds the pill back until the calculator's Buy/Sell control has left the
   screen. A page with no calculator has nothing to wait for - but `:has(#seg.is-off)` can
   never match there either, so the pill mounted and then stayed hidden forever. Guarding on
   the ABSENCE of #seg keeps the original behaviour byte-identical wherever a calculator
   exists, and falls back to the offer-band condition alone where one does not. */
body:not(:has(#seg)):has(.offerband.is-off) .orate {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transform: none;
  transition: transform .35s cubic-bezier(.22,.9,.3,1), translate .35s cubic-bezier(.22,.9,.3,1),
              opacity .3s ease;
}
body:has(#seg.is-off):has(.offerband.is-off) .orate::after,
body:not(:has(#seg)):has(.offerband.is-off) .orate::after { animation-play-state: running; }

/* ---- .m-offer - the offer line inside the mobile menu ------------------------------------
   Campaign furniture, shown ONLY when the menu opens mid-page: at the page top the real
   strip is visible above the panel (see the menu-open state split), so this would duplicate
   it. is-fixed is frozen while the menu is open, so the state cannot flip underneath the
   panel. Same glass family as the strip; the underline sits on the <b>, following the
   text-span rule every arrow link here uses. Panel grammar, not strip grammar: text left,
   icon right, like the rows above it.
   .mobnav-PREFIXED ON PURPOSE: this row lives inside .nav-links, whose generic row rules
   (.nav-links a, specificity 0-1-1) outweigh a bare .m-offer (0-1-0) - the first version
   shipped with display:none silently LOSING and the row visible at rest in plain panel
   styling. Every rule here carries the prefix so this row wins each property it declares,
   and the chevron pseudo the panel rows draw is switched off for this row.
   PLAIN LINE, NOT A GOLD BOX (client, 17 Aug 2026, second round): the gold-glass panel
   version made the row shout inside a calm menu and helped push the panel past one screen.
   Now it is a single quiet line - ink text, the underline and the icon do the marking -
   and white-space:nowrap keeps it to ONE LINE at every phone width; re-measure before any
   copy change here, the same rule as the strip. */
.mobnav .m-offer {
  display: none;
  align-items: center;
  /* CENTRED like the buttons below it, not left like the nav rows (client, 17 Aug 2026,
     round two): this row belongs to the ACTION group - phone button, store location - and
     follows their alignment, not the list's. */
  justify-content: center;
  gap: 7px;
  /* Breathing room above: it sat connected to the Contact row. */
  margin: 12px 0 10px;
  padding: 9px 14px;
  /* The flat version declares no border shorthand, which let a generic panel rule's
     border-bottom hairline show through under the whole line - kill it explicitly. */
  border-bottom: 0;
  color: #000;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}
.mobnav .m-offer b { font-weight: 700; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.mobnav .m-offer .ol-svg { width: 14px; height: 14px; flex: none; }
.mobnav .m-offer::after { content: none; }   /* no panel chevron on this row */
body:has(.hdr.is-fixed) .m-offer { display: flex; }
/* The copy sits above the sweep. */
.offerband .wrap { position: relative; z-index: 1; }
/* The band mark above the heading (client artwork, 17 Aug 2026 - replaced the hand-drawn
   flame the same day): ink like the heading, not gold - a solid gold block this size would
   out-shout the accent system. "Much bigger" than the flame ran, per the client: roughly
   double. */
.ob-mark {
  display: block;
  width: 108px;
  height: 108px;
  margin: 0 auto 18px;
  color: var(--ink);
  /* LOAD-BEARING for the star's jump: SVG clips to its canvas by default, and the star
     already touches the canvas top at rest - the first version of the hop sliced its head
     off at the peak (client caught it). visible lets it leave the canvas mid-air. */
  overflow: visible;
}
@media (max-width: 600px) { .ob-mark { width: 84px; height: 84px; margin-bottom: 14px; } }
/* EXPERIMENT - THE STAR JUMPS (client, 17 Aug 2026, "if I like it, I will keep it"): the
   star is the artwork's FIRST path, so it animates alone with zero markup changes - a
   small hop with a settle bounce roughly every two seconds, resting for most of the cycle
   so it reads as a wink, not a metronome.
   transform-box: fill-box is LOAD-BEARING on SVG children: without it, transform-origin
   resolves against the whole 384-unit canvas and the star swings around the corner instead
   of hopping in place. Percent translate is relative to the star's own box for the same
   reason. Paused while the band is off screen via the same is-off class the shine sweep
   observer already maintains; off entirely under reduced motion.
   Delete this block to end the experiment. */
.ob-mark path:first-of-type {
  transform-box: fill-box;
  /* Pivot on the star's TRUE symmetry centre, which is NOT 50% 50% (client round four -
     "it goes right side and comes again left side, random moves"): a point-up star's top
     tip reaches higher than its feet, so the fill-box centre sits ABOVE the rotation
     centre. Pivoting there made every 72deg turn sway the whole star ~3px sideways, and
     the loop restart shoved it back - the right-then-left jerk. Measured from the path
     (tips at 72deg +-0.7, radii +-0.1%, centre 192,107.8 in a 95.9,7.8 192x184.5 box):
     50.07% 54.17%. With this pivot each 72deg pose is pixel-identical, so the 72->0
     restart is invisible. Do not "fix" this back to 50% 50%. */
  transform-origin: 50% 54.2%;
  animation: ob-star-jump 2.2s ease-in-out infinite;
}
/* ONE CORNER PER JUMP (client round three - "not spinning like crazy"): the star turns
   exactly 72 degrees - one of its five points - across the airborne window, smoothly, and
   holds through the landing bounce. The five-fold symmetry is what makes the loop work:
   the cycle ENDS at 72 and restarts at 0, which are pixel-identical, so each jump appears
   to advance the star one corner, forever - five jumps to a quiet full lap, no snap
   anywhere. */
@keyframes ob-star-jump {
  0%, 58%   { transform: translateY(0) rotate(0deg); }
  70%       { transform: translateY(-14%) rotate(40deg) scale(1.05); }
  82%       { transform: translateY(0) rotate(72deg) scale(1); }
  88%       { transform: translateY(-4%) rotate(72deg); }
  94%, 100% { transform: translateY(0) rotate(72deg); }
}
.offerband.is-off .ob-mark path:first-of-type { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .ob-mark path:first-of-type { animation: none; } }
.offerband h2 { margin-bottom: 12px; }
.offerband p {
  max-width: 62ch;
  margin: 0 auto;
  font-size: 16.5px;                    /* phones keep this size */
  line-height: 1.65;
  color: var(--slate);
}
/* Desktop sizes are the CLIENT'S OWN VALUES (17 Aug 2026), pasted verbatim - same pattern
   as the hand-tuned desktop type scale at the end of this file: the base rule stays the
   mobile size and desktop overrides in a min-width block, never the other way around. */
@media (min-width: 601px) {
  .offerband p {
    font-size: 18.5px;
    line-height: 1.85;
  }
}
.ob-btns {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 26px;
}

.hdr {
  position: absolute;                   /* top of page: in-flow over the hero, flat white, scrolls AWAY with the page */
  top: calc(var(--obar-h) + var(--obar-gap));  /* below the offer strip plus its air gap;
                                           0 + 0 once the campaign block is deleted. site.js
                                           never hardcodes this - it measures hdr.offsetTop,
                                           so the thresholds follow whatever this computes
                                           to. See applyHeader. */
  left: 0;
  right: 0;
  z-index: 60;
  background: transparent;
  border-bottom: none;
  /* NO `top` in this transition list, on purpose. A .25s top animation was tried for the
     scroll-up return and read as the header "sticking" mid-settle (client, 17 Aug 2026).
     The fix is geometric instead, the TMEX-strip mechanic: applyHeader un-fixes at
     y <= bar height - the one scroll position where fixed(top:0) and absolute(top:bar)
     occupy identical pixels - so the swap is invisible and the header then travels down
     with the page as the strip re-enters. Nothing needs animating. */
  transition: transform .35s ease-in-out, background-color .3s ease, backdrop-filter .3s ease, box-shadow .3s ease;
  will-change: transform;
}
.hdr.is-fixed {                         /* scrolled past the bar: pinned + frosted glass (floating) */
  position: fixed;
  top: 0;                               /* pins to the VIEWPORT top - the offer bar's --obar-h
                                           offset applies only to the at-rest absolute state,
                                           and the .rsticky top:72px rule depends on this */
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border-bottom: 1px solid rgba(255,255,255,0.5);
  box-shadow: 0 8px 24px -18px rgba(0, 0, 0, 0.1);
}
.hdr.is-fixed.is-hidden { transform: translateY(-100%); }   /* hidden while scrolling down */
.hdr.noanim { transition: none !important; }                /* suppress the slide during the absolute<->fixed swap */
.hdr .wrap {
  display: flex;
  align-items: center;
  height: 72px;
  gap: 32px;
}
/* 86 = the old 72 row + the 14px gap the desktop no longer draws - see the --obar-gap note.
   The JS never hardcodes this: navH measures offsetHeight live, and hdrRest measures
   offsetTop, so every flip threshold and seam recalibrates itself. */
@media (min-width: 951px) {
  .hdr .wrap { height: 86px; }
}
.brand {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* The <picture> around the logo (PNG desktop / SVG phones) takes no box of its own, so the
   logo lays out exactly as it did when the <img> was the link's direct child. */
/* ...and the <source> must be hidden EXPLICITLY. It computes as display:block here, and with
   the picture's box gone it became a 0px flex item inside .brand, whose 8px gap then pushed
   the logo 8px right (measured: 32px instead of 24px). Hiding it changes nothing about which
   file the browser chooses - <source> selection never depends on layout. */
.brand-pic { display: contents; }
.brand-pic source { display: none; }
.brand-logo {
  height: clamp(22px, 4vw, 36px);
  width: auto;
  display: block;
  object-fit: contain;
}
.brand-logo-footer {
  height: 32px;
  filter: brightness(0) invert(1);
  opacity: 0.9;
}
@media (max-width: 480px) {
  .brand-logo {
    height: 22px;
  }
}
@media (min-width: 481px) and (max-width: 900px) {
  .brand-logo {
    height: 28px;
  }
}
.brand .mk {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gold);
}
.brand .mk .material-symbols-outlined {
  font-size: 18px;
}
.brand .nm {
  font-weight: 800;
  font-size: 20px;
  letter-spacing: -0.04em;
  color: var(--ink);
}
.menu {
  display: flex;
  gap: 28px;
  background: rgba(255,255,255,0.4);
  padding: 8px 20px;
  border-radius: 99px;
  border: 1px solid rgba(255,255,255,0.6);
  backdrop-filter: blur(12px);
}
.menu a {
  font-weight: 600;
  color: var(--slate);
  font-size: 14px;
  transition: color 0.15s ease;
}
.menu a:hover {
  color: var(--gold);
}
.hdr .sp {
  flex: 1;
}
.hdr .cta-d {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(255, 255, 255, 0.35);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.6);
  color: var(--ink);
  font-weight: 700;
  font-size: 14px;
  padding: 10px 20px;
  border-radius: 9999px;
  transition: all 0.2s ease;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
.hdr .cta-d:hover {
  background: rgba(255, 255, 255, 0.6);
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.hdr .cta-d .material-symbols-outlined {
  font-size: 18px;
  color: var(--ink);
}
/* Modern Mobile Nav — fullscreen frosted glass overlay */
.burger {
  display: none;
  width: 44px;
  height: 44px;
  border: 1px solid rgba(255,255,255,0.6);
  border-radius: 50%;
  background: rgba(255,255,255,0.4);
  backdrop-filter: blur(8px);
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--ink);
  transition: all 0.2s;
  z-index: 70;
}
.burger:hover {
  background: rgba(255,255,255,0.8);
}
.burger .material-symbols-outlined {
  transition: transform 0.3s ease;
}
.burger.open .material-symbols-outlined {
  transform: rotate(90deg);
}

/* Glossy frosted glass. The old 0.92 alpha was 92% opaque, so the blur behind it was
   invisible - it just looked like a flat white panel. A translucent gradient lets the page
   actually blur through, which is what makes it read as glass. */
/* NO backdrop-filter here, deliberately - it cost iPhones about two seconds to open.
   This panel is inset:0, so a backdrop-filter makes WebKit snapshot and blur the WHOLE
   composited page behind it. On these pages that is ~12,000px tall, carries 33 OTHER
   live blur layers, and contains a cross-origin Google Maps iframe - which is WebKit's
   worst case, because foreign-origin content has to be composited into the snapshot
   before it can be blurred. The panel also animated transform: scale(), so the snapshot
   was re-derived as it ran. Chromium caches all of this aggressively, which is exactly
   why Android looked fine and only iOS stalled.
   The replacement costs nothing: the surface is opaque enough that there was nothing to
   see through anyway, and a static gold wash carries the brand where the blur used to.
   This is also the standing rule in CLAUDE.md - do not stack live blurs on mobile - and
   the menu is a mobile-only surface, so nothing on desktop loses anything. */
.mobnav {
  position: fixed;
  inset: 0;
  z-index: 65;
  background:
    radial-gradient(120% 80% at 8% 0%, rgba(254,185,0,0.10) 0%, transparent 55%),
    linear-gradient(165deg, rgba(255,255,255,0.80) 0%, rgba(255,255,255,0.64) 100%);
  /* The glass is back, but at a fraction of the old cost: 22px instead of 48px, and no
     saturate() - that was a second full-screen filter pass, and the gold wash above
     supplies the warmth it was adding. Stated explicitly rather than inherited, because
     the earlier .mobnav block sets its own and a later rule that merely omits a property
     does not reset it. The two rules below are what actually make this affordable. */
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
  border: none;                                    /* the earlier 1px border shifted the close button off the burger */
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.9); /* top sheen */
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 24px;
  /* If the menu is ever taller than the screen it scrolls itself, and `contain` stops that
     scroll from chaining through to the locked page behind it. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  visibility: hidden;
  /* The panel-wide scale() went with the blur. Scaling a full-viewport layer repaints
     the whole thing every frame, and it was the reason the backdrop snapshot had to be
     re-derived mid-animation. The motion that actually reads is the per-item stagger
     below, which animates nine small boxes instead of one screen-sized one. */
  transition: opacity .28s cubic-bezier(.16,1,.3,1),
              visibility 0s linear .28s;
}
.mobnav.open {
  opacity: 1;
  visibility: visible;
  transition: opacity .28s cubic-bezier(.16,1,.3,1),
              visibility 0s;
}
/* THE MENU IS A LIST, NOT A STACK OF HEADLINES (rebuilt 9 Aug 2026).
   It carried BOTH a 28px gap and a bottom border on every row, so every hairline
   floated detached halfway between two labels and read as damage rather than
   structure. At 28px/800 the six rows ran about 490px; with the duplicate logo and
   the divider on top of that, the content was taller than a phone screen, and
   `justify-content: center` on an overflowing flex container pushes the top out of
   the scrollable area entirely - which is why the brand appeared jammed under the
   close button with no way to reach it.
   Fixes: gap 0 so each hairline belongs to its own row, 19px/700 so the list is
   ~340px instead of ~490px, left aligned with a chevron so it reads as navigation,
   and `margin: auto` on the child instead of `justify-content: center` - that
   centres identically but degrades to top-aligned scrolling instead of clipping. */
.mobnav { justify-content: flex-start; padding: 84px 22px 28px; }
/* THE REAL HEADER BAR STAYS PUT. This panel used to draw its own logo and its own close
   button. Even pixel-matched to the header's, that is still a second bar arriving over
   the first, which is exactly what read as "a new bar came in". So the page's own header
   is lifted ABOVE the overlay instead: the brand and the button never move, never
   redraw, and the menu simply opens underneath them.
   The background and blur are dropped so the menu's own glass shows through rather than
   stacking two surfaces. applyHeader() already returns early while .mobnav is open, so
   nothing fights this.
   SPLIT BY HEADER STATE (client, 17 Aug 2026). This rule used to force position:fixed and
   top:0 unconditionally, which was right pre-strip and wrong after it: opening the menu at
   the top of the page yanked the header from its slot below the offer strip to the
   viewport top - the logo jumped and the strip vanished behind the panel. Now:
     AT REST - the header is not moved at all. It stays absolute below the strip, the strip
     is lifted over the panel with it, and the panel's padding clears them both, so the
     menu opens with NOTHING shifting. openNav() squares up any few-pixel scroll first and
     the scroll lock holds it there.
     SCROLLED (is-fixed) - exactly the pre-strip behaviour: pinned to the viewport top over
     the panel, transform reset because the smart-sticky rule may have translated it out of
     view (the burger that opened the menu is IN the header, but is-hidden can race a fast
     flick). No strip - it is at the document top, far above. */
/* .closing is included deliberately. Dropping .open the instant the exit begins would
   un-pin the header mid-animation - on a scrolled page it is `is-fixed is-hidden`, so it
   would vanish upward while the menu was still fading. It has to stay put until the exit
   has finished. */
header:has(.mobnav.open, .mobnav.closing) .hdr {
  transform: none;
  z-index: 66;                       /* .mobnav is 65 */
}
/* THE FROST STRIP IS OPEN-ONLY (client bug, 17 Aug 2026). It used to ride on .closing too,
   which mid-page produced a naked frame at every close: the panel fades out, the scrolled
   page shows through a header that still has NO background, and only when the exit timer
   drops .closing does the frost pop back in. Scoped to .open, the moment the close begins
   this rule stops matching and the header's own .3s background/backdrop transitions fade
   the glass back IN while the panel's 280ms exit fades OUT - the two cross-fade and the
   header is never background-less over page content.
   The POSITIONING rule above deliberately keeps .closing - dropping that early is what
   would yank a pinned header mid-exit (see the note above it). Only the surface styling
   releases early. At rest this rule is cosmetic either way: the at-rest header is already
   transparent. */
header:has(.mobnav.open) .hdr {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: none;
  box-shadow: none;
}
header:has(.mobnav.open, .mobnav.closing) .hdr.is-fixed {
  position: fixed;
  top: 0;
}
/* The offer strip rides above the panel with the header, so the menu opens under both.
   Inert once the campaign block is deleted, and inert mid-scroll (the strip sits at the
   document top, off screen - a z-index cannot bring it back). */
body:has(.mobnav.open, .mobnav.closing) .obar { z-index: 66; }
/* The panel's links clear the header wherever it currently sits: 84px was tuned for the
   header at 0..72, so the rest state adds the strip and its air on top. Zero-var
   self-healing: back to 84px the day the campaign block is deleted. */
body:has(.mobnav.open, .mobnav.closing):has(.hdr:not(.is-fixed)) .mobnav {
  padding-top: calc(84px + var(--obar-h) + var(--obar-gap));
}
/* With the real burger on top showing an X, the panel's own close button is dead weight.
   It stays in the markup and visible for browsers without :has() - there the header
   cannot be lifted, so it is the only way out of the menu. */
header:has(.mobnav.open, .mobnav.closing) .mobnav-close { display: none; }

/* ---- WHAT MAKES THE MENU'S GLASS AFFORDABLE ON iOS -----------------------------
   A full-viewport backdrop-filter costs whatever is BEHIND it. These two rules strip
   the backdrop down to flat content for exactly as long as the menu is open, so WebKit
   builds a cheap snapshot instead of an expensive one. Nothing here is visible: it all
   sits under 22px of blur and a 64-80% white veil.

   They cover .closing as well as .open, so the backdrop stays cheap for the whole exit
   animation rather than snapping back to full cost halfway through it.

   1. The Google Maps iframe. Cross-origin content is WebKit's worst backdrop case -
      the foreign document has to be composited before it can be blurred. It is also
      the single most expensive thing on either page. */
body:has(.mobnav.open, .mobnav.closing) iframe { visibility: hidden; }

/* 2. Every OTHER backdrop-filter on the page - 25 of them across these two pages, each
      its own compositing layer that must resolve before the menu's snapshot can be
      taken. Blurring an already-blurred surface changes nothing you can see.

      Stripped across the whole open + closing window, because it is invisible either
      way: these only ever sit behind the panel's own glass.
      ONE EXCEPTION: .hdr. The header rides ABOVE the panel (z66), and since the
      naked-frame fix its frost fades back in DURING .closing - a white sheet without its
      blur reads wrong for exactly the 280ms the exit takes. So the header is exempt here
      and follows rule 3's philosophy instead: its blur returns the instant .closing
      begins, behind a panel still mostly opaque, and the cost lands on the close - which
      was never the slow direction. While OPEN the header's blur is still stripped, by the
      open-only frost rule further up, so the snapshot stays exactly as cheap. */
html:not(.fast) body:has(.mobnav.open, .mobnav.closing) :not(#mobnav):not(#mobnav *):not(.hdr) {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* 3. `filter` - chiefly the hero's two blur(60px) washes at 375x993, which are what made
      the menu slow at the TOP of the page and nowhere else.
      READ THE SELECTOR: .open ONLY, deliberately NOT .closing, and that difference is the
      whole point. Unlike the backdrop-filters above, these blurs are doing visible work -
      they hide the hard rectangular edge where each pseudo-element clips its own gradient
      (see the note on .hero::before). Stripping them through the exit meant the page
      re-emerged from behind the fading panel carrying hard-edged shapes, which then
      snapped soft again when the class cleared. Putting them back the instant .closing
      begins moves that repaint behind a panel that is still fully opaque, where it cannot
      be seen - and the cost lands on the close, which was never the slow direction. */
html:not(.fast) body:has(.mobnav.open) :not(#mobnav):not(#mobnav *),
html:not(.fast) body:has(.mobnav.open) :not(#mobnav):not(#mobnav *)::before,
html:not(.fast) body:has(.mobnav.open) :not(#mobnav):not(#mobnav *)::after {
  filter: none !important;
}

/* ---- ?fast=1 : THE iPHONE MENU-LAG CANDIDATE FIX -------------------------------------
   TEMPORARY A/B SWITCH (client, 10 Aug 2026). Load any page with ?fast=1 to try it; without
   the flag nothing below applies and the page behaves exactly as it does today.

   THE DIAGNOSIS, measured rather than guessed. Tapping the burger fired two costs at the
   same instant:
     1. the two rules directly above matched 963 of this page's 1008 elements, plus both
        pseudo-elements on each, and STRIPPED backdrop-filter from them - a document-wide
        style pass AND a document-wide compositing-layer teardown. 9.7ms on a fast desktop;
        iOS pays several times that for style and far more for the layers.
     2. .mobnav's own backdrop-filter: blur(22px) across the full viewport, fading in from
        opacity 0 - so a blurred snapshot of the entire page has to resolve every frame.
   And 1 exists only to fund 2, while running at the exact moment 2 needs the compositor.
   Main thread blocks, the transition never starts, then it jumps to a late frame. That is
   the "nothing happens, then it suddenly appears".

   THE FIX: on TOUCH ONLY, drop the full-viewport blur. That immediately makes rules 1
   pointless, and the `html:not(.fast)` prefix above switches them off - so both costs go
   together. Desktop is untouched and keeps its glass.

   The visual loss is small by construction: the blur already sits under a white veil at
   0.80 falling to 0.64, so only 20-36% of it was ever visible. The veil is lifted to
   0.95/0.90 here to cover the difference.

   Same blur-off-on-touch split that .ratebox and .rsticky already use, for the same reason:
   the blur is only ever a problem where the compositor is weakest.
   -------------------------------------------------------------------------------------- */
@media (hover: none), (pointer: coarse) {
  html.fast .mobnav {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background:
      radial-gradient(120% 80% at 8% 0%, rgba(254,185,0,0.10) 0%, transparent 55%),
      linear-gradient(165deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.90) 100%);
  }
}

/* --- NAVIGATION PROGRESS BAR ------------------------------------------------------
   ATTACHED TO THE HEADER BY DOM (client, 17 Aug 2026): the bar is a CHILD of .hdr, pinned
   to its bottom edge, so it rides every header movement automatically - the smart-sticky
   slide in and out, the absolute<->fixed swaps, the offer-strip offset, the menu lift.
   It replaced a separately-fixed element whose `top` was computed per state (72px + strip
   vars, a :has() override when pinned, an opacity rule to fake following the hide) - and
   which still visibly detached whenever the header actually moved. Attachment by DOM, not
   by formulas: there is nothing left to drift.
   Being inside .hdr it also inherits the header's z-order in the one moment that matters -
   the menu open/exit, where the header sits at z 66 over the panel's 65 - and it stays on
   screen through the blank period before the next document paints, because the header does. */
.navload {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;                         /* flush under the row, outside its border-bottom */
  height: 3px;
  background: rgba(254,185,0,0.13);
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease;
}
.navload.on { opacity: 1; }
/* The bar hangs 3px BELOW the header's edge, so the hide transform (translateY(-100%) of
   the header's own height) leaves exactly that sliver peeking at the viewport top. Fade it
   with the hidden state rather than retuning the transform everything else is calibrated
   to. An in-flight navigation (.on) stays visible regardless - never abandon a moving
   progress bar. */
.hdr.is-fixed.is-hidden .navload:not(.on) { opacity: 0; }
/* EXPERIMENT - PERMANENT TRACK, NOW SITE-WIDE (client, 17 Aug 2026, "if I like it, I will
   keep it" - extended from phones to desktop the same day): the faint 13% gold track stays
   visible ALWAYS, not only during a page switch - the brighter fill still only sweeps
   while navigating (its span idles at width:0), and because the next document paints its
   own track in the same spot, the line reads as one continuous piece of chrome across page
   switches. Since the bar is attached to the header, it hides and returns WITH the header
   for free, and it is FULL-BLEED by construction - a child of .hdr stretched left:0 to
   right:0, never constrained by the .wrap container.
   Delete this block to end the experiment - everything else is the pre-experiment
   behaviour. */
.navload { opacity: 1; }
.navload span {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(90deg, rgba(254,185,0,0.55), var(--gold));
  box-shadow: 0 0 10px rgba(254,185,0,0.55);
}
/* Fast to just over half, then a decelerating crawl that stalls near the end. That shape
   is the honest one: we cannot know real progress, and the only true completion is the
   document swap, which takes the whole bar with it. */
@keyframes navload-fill {
  0%   { width: 0; }
  30%  { width: 55%; }
  62%  { width: 78%; }
  100% { width: 94%; }
}
.navload.on span { animation: navload-fill 2.4s cubic-bezier(.2,.8,.3,1) forwards; }
@media (prefers-reduced-motion: reduce) {
  .navload.on span { animation: none; width: 55%; }
}

/* One button, two glyphs, swapped on .open - so the header's own control becomes the
   close control with no JS and no second bar. */
.burger .bi-close { display: none; }
.burger.open .bi-menu { display: none; }
.burger.open .bi-close { display: block; }
/* The 90deg spin suited a hamburger rotating into itself. An X that arrives already
   rotated just looks crooked. */
.burger.open .material-symbols-outlined { transform: none; }
.mobnav .nav-links {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: auto;
  width: 100%;
  max-width: 420px;
  text-align: left;
}
.mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.01em;
  padding: 15px 14px;
  border-radius: 14px;
  border-bottom: 1px solid rgba(16,24,40,0.07);
}
/* A single character, so it needs no icon-font subset entry and cannot fail the way
   a missing ligature does. */
.mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer)::after {
  content: "\203A";
  font-size: 21px;
  font-weight: 400;
  line-height: 1;
  color: rgba(16,24,40,0.26);
  transform: translateY(-2px);
}
/* ONE-SCREEN MENU ON PHONES (client, 17 Aug 2026). The offer strip above the panel plus the
   in-menu offer line made the panel taller than a phone viewport, and it degraded to
   scrolling (by design, but not what anyone wants as the everyday state). The client's own
   call: step the LINK text down "just a little bit". 19 -> 17px and 15 -> 12px row padding
   buys ~60px across seven rows; the flattened offer line above buys the rest. Tablets
   (601-950px burger range) keep the 19px rows - they were never short of height. */
@media (max-width: 600px) {
  .mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer) {
    /* ROUND TWO (client, 17 Aug 2026): the first fit pass took these to 17px/10px padding
       and it read as "3 or 4 out of 10" against the big buttons the client liked - the ask
       was "8 or 7 out of 10". 18px with 12px padding is that ratio; the height cost is
       real and the trade is documented below at the panel numbers. */
    font-size: 18px;
    line-height: 1.2;                   /* inherited ~1.5 was 6px of every row */
    padding: 12px 14px;
  }
  .mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer)::after { font-size: 20px; }
  /* The rest of the one-screen budget, measured not guessed (762px of panel against a 660px
     phone viewport): every block below the links carried the roomier layout's margins.
     78px top padding still clears the 72px pinned header; the at-rest variant keeps the
     same constant plus the strip vars so both states shrink together. */
  .mobnav { padding: 76px 22px 14px; } /* 4px clearance over the 72px pinned header */
  body:has(.mobnav.open, .mobnav.closing):has(.hdr:not(.is-fixed)) .mobnav {
    padding-top: calc(76px + var(--obar-h) + var(--obar-gap));
  }
  .mobnav .m-offer { margin: 10px 0 2px; padding: 6px 14px; }
}
/* BOTH WISHES, EACH WHERE IT MATTERS (client, 17 Aug 2026): the big 18px buttons and the
   one-screen fit collide only on SHORT viewports - at 18px the panel runs ~682px. So the
   compact step-down applies by HEIGHT, not to every phone: tall phones (the common case)
   keep the full-size buttons, and only viewports at 700px and under - where 682 would
   scroll - drop to 17px/10px, which measured ~640 and fits with room. */
@media (max-width: 600px) and (max-height: 700px) {
  .mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer) {
    font-size: 17px;
    padding: 10px 14px;
  }
  .mobnav .nav-links a:not(.cta-mob):not(.mobnav-loc):not(.m-offer)::after { font-size: 19px; }
  .mobnav .m-offer { margin-top: 4px; }  /* measured: 10px left the panel 4px over at 660 */
  .mobnav .nav-links .cta-mob { margin-top: 10px; }
  .mobnav .nav-links .mobnav-loc { margin-top: 8px; }
  .mobnav .nav-links .mobnav-follow { margin-top: 8px; }
  .mobnav .nav-links .mobnav-meta { margin-top: 10px; }
}
/* Each item eases up on its own slight delay, so the menu unfolds instead of popping.
   Compressed from .05s steps to .03s: with nine children the old tail put the last item
   at .50s delay + .6s duration = 1.1s before the panel finished settling, which on top
   of an already-slow open read as the menu being sluggish. Now .28s + .42s = .70s. */
.mobnav .nav-links > * {
  opacity: 0;
  transform: translateY(18px);
  /* The single transition declaration for every child of the panel - entry, exit and the
     link hover colours all live here, so no more specific rule can quietly replace it. */
  transition: opacity .3s ease, transform .42s cubic-bezier(.16,1,.3,1),
              color .2s ease, background .2s ease;
}
.mobnav.open .nav-links > * {
  opacity: 1;
  transform: none;
}
/* CLOSING is its own state, not just "not open". Dropping .open would send every item
   back through the entry transition's own timing; this makes the exit deliberate - the
   list sinks 14px and fades as ONE move, slightly quicker than it arrived, which is how
   a dismissal should feel next to an entrance. Nothing is staggered on the way out: a
   reverse stagger reads as the menu hesitating. */
.mobnav.closing .nav-links > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .2s ease, transform .26s cubic-bezier(.4,0,1,1);
  transition-delay: 0s;
}
.mobnav.closing {
  opacity: 0;
  transition: opacity .26s ease .06s, visibility 0s linear .32s;
}
@media (prefers-reduced-motion: reduce) {
  .mobnav.closing, .mobnav.closing .nav-links > * { transition: opacity .15s ease; transform: none; }
}
.mobnav.open .nav-links > *:nth-child(1) { transition-delay: .04s; }
.mobnav.open .nav-links > *:nth-child(2) { transition-delay: .07s; }
.mobnav.open .nav-links > *:nth-child(3) { transition-delay: .10s; }
.mobnav.open .nav-links > *:nth-child(4) { transition-delay: .13s; }
.mobnav.open .nav-links > *:nth-child(5) { transition-delay: .16s; }
.mobnav.open .nav-links > *:nth-child(6) { transition-delay: .19s; }
.mobnav.open .nav-links > *:nth-child(7) { transition-delay: .22s; }
.mobnav.open .nav-links > *:nth-child(8) { transition-delay: .25s; }
.mobnav.open .nav-links > *:nth-child(9) { transition-delay: .28s; }
/* The hours + copyright line is the panel's TENTH child (the offer line made it ten in Aug
   2026) and had no delay, so it popped in first while everything above it was still rising. */
.mobnav.open .nav-links > *:nth-child(10) { transition-delay: .31s; }
@media (prefers-reduced-motion: reduce) {
  .mobnav, .mobnav.open, .mobnav .nav-links > *, .mobnav.open .nav-links > * {
    transform: none;
    transition: opacity .2s ease, visibility 0s;
    transition-delay: 0s;
  }
}
.mobnav a {
  display: block;
  padding: 14px 4px;
  font-weight: 600;
  font-size: 16px;
  border-bottom: 1px solid rgba(0,0,0,0.05);
  color: var(--ink);
}
.mobnav a:last-of-type {
  border-bottom: none;
}
.mobnav a.dir {
  margin-top: 16px;
  background: var(--ink);
  color: #fff;
  border: none;
  border-radius: 9999px;
  text-align: center;
  padding: 14px;
}

/* Hero Section */
.hero {
  padding: 80px 0 64px;
  overflow: hidden;
  position: relative;
}
/* DO NOT try to replace these blurs with a softer gradient. It was tried on 9 Aug 2026
   and it looks wrong, for a reason that is easy to miss: the blur is not only softening
   the gradient, it is softening THE PSEUDO-ELEMENT'S OWN RECTANGULAR EDGES. The circle is
   sized farthest-corner (~509px here) inside a box only ~225px wide, so at the left and
   right edges the gradient is still around alpha 0.095 and the box clips it dead. Blurred,
   that clip is invisible; unblurred it is a hard vertical edge - the "sharp shape".
   A gradient CANNOT fix that on its own, because the box, not the gradient, is what cuts
   it off. The cost is handled instead by stripping these only while the menu is actually
   open - see the :has() rules further up. */
/* ── ?fx=1 : THE GUILLOCHE DEPTH FIELD (experiment, 10 Aug 2026) ───────────────────────────
   Guilloche is the interlocking line-work engraved on banknotes, share certificates and
   passports. It is the visual language of money and security printing, so it reads as
   precision and value without a literal coin anywhere - and no template ships it.

   Built as TWO rosettes at different tooth counts (48 and 33) so the layers can never sync.
   That mismatch is the whole trick: as they counter-rotate they beat against each other and
   the interference reads as depth. Each rosette is ONE ellipse repeated with <use>, the way
   an engraver actually constructs one, which is why the pair costs 4.4 KB. Drawn as
   polylines instead they were 380 KB, which would have defeated the point entirely.

   EVERYTHING ANIMATED HERE IS `transform` OR `opacity`, deliberately. Those are the only two
   properties the compositor can run without touching the main thread - which is the whole
   reason this is affordable where a WebGL scene would not be, on the same phones where a
   22px blur had to be removed earlier today. No filter animation, no backdrop-filter, no
   canvas, no library.

   Very slow on purpose: 150s and 210s per revolution. At that speed you never catch it
   moving, you only notice the hero is alive. Anything faster becomes a screensaver.

   Off by default. Nothing below renders without <html class="fx">. */
.hero-fx { display: none; }
html.fx .hero-fx {
  display: block;
  position: absolute;
  inset: 0;
  /* BLEEDS UP BEHIND THE HEADER (client, 11 Aug 2026). The header bar is already transparent
     at rest - `.hdr` is position:absolute with background:transparent, and only frosts once
     it gains .is-fixed on scroll. So nothing had to change there. The reason the pattern
     stopped short was purely geometric: <header> is 0-height (the bar is out of flow), and
     .hero starts at y=115 because the breadcrumb band sits between them. This box was
     inset:0 of .hero, so it began below all of it.
     -170px clears the 72px bar plus the breadcrumbs with room to spare. The mask below fades
     the top edge, so over-reaching costs nothing and under-reaching would show a hard line. */
  top: -170px;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  /* Fades at every edge so the rosettes dissolve rather than ending on the box. Centred high
     now that the field extends upward. */
  -webkit-mask-image: radial-gradient(125% 85% at 50% 30%, #000 32%, transparent 80%);
          mask-image: radial-gradient(125% 85% at 50% 30%, #000 32%, transparent 80%);
}
html.fx .hero-fx i {
  position: absolute;
  display: block;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  will-change: transform;
}
/* Opacity is the dial to tune. These multiply with the stroke-opacity baked into each SVG
   (0.55 and 0.42), so 0.85 here lands around 0.47 effective gold on near-white. The first
   pass used 0.55/0.4 and was genuinely invisible - present in the DOM, absent to the eye. */
html.fx .hero-fx .fx-a {
  top: -46%; right: -14%;
  width: 105%; aspect-ratio: 1;
  background-image: url('guilloche-a.svg');
  opacity: 0.85;
  animation: fx-spin 150s linear infinite;
}
html.fx .hero-fx .fx-b {
  top: -24%; right: 6%;
  width: 74%; aspect-ratio: 1;
  background-image: url('guilloche-b.svg');
  opacity: 0.6;
  animation: fx-spin 210s linear infinite reverse;
}
@keyframes fx-spin { to { transform: rotate(360deg); } }

/* The light sweep. Reuses the site's existing "gold shine = live" language from the rates
   timestamp, so it reads as part of the same system rather than a new idea. transform only. */
html.fx .hero-fx .fx-sweep {
  top: -30%; left: -60%;
  width: 55%; height: 170%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,0.55) 45%, transparent);
  animation: fx-sweep 14s ease-in-out infinite;
}
@keyframes fx-sweep {
  0%, 62% { transform: translateX(0); opacity: 0; }
  70%     { opacity: 1; }
  100%    { transform: translateX(320%); opacity: 0; }
}

/* PHONES: BIGGER, AND MOVED DOWN-RIGHT BEHIND THE CALCULATOR (client, 11 Aug 2026).
   The desktop hero is two columns - copy left, calculator right - so the rosettes sit top
   right and stay clear of the text. A phone stacks them, which put the pattern squarely
   behind the H1 where it competed with the headline for the same pixels.
   Here it drops to sit behind the CALCULATOR CARD instead. That card is opaque glass, so
   the pattern reads around and under its edges as depth rather than as texture behind text -
   and the headline goes back to sitting on clean background.
   Scaled up (155% / 120% against the desktop 105% / 74%) because a phone crops so much of
   the circle that at desktop size only a thin arc survives, which reads as a stray line
   rather than an engraving. */
@media (max-width: 700px) {
  html.fx .hero-fx {
    top: -130px;
    -webkit-mask-image: radial-gradient(115% 62% at 74% 72%, #000 30%, transparent 78%);
            mask-image: radial-gradient(115% 62% at 74% 72%, #000 30%, transparent 78%);
  }
  /* Anchored from the BOTTOM with a POSITIVE inset, which is what lands them on the card.
     The first pass used negative bottoms (-30% / -12%) and pushed the whole field below the
     calculator - measured 44px of overlap with the card and 581px of rosette hanging off the
     end of the hero. Positive values pull it back up onto the glass. */
  html.fx .hero-fx .fx-a {
    top: auto; bottom: 6%; right: -42%;
    width: 155%;
    opacity: 0.8;
  }
  html.fx .hero-fx .fx-b {
    top: auto; bottom: 16%; right: -16%;
    width: 120%;
    opacity: 0.55;
  }
  /* The sweep is a desktop flourish. On a phone it would cross the calculator's own glass,
     which already has the gold shine on its timestamp - two moving highlights on one card. */
  html.fx .hero-fx .fx-sweep { display: none; }
}

/* Honour the OS setting, and stop the work entirely when the hero is scrolled past - a
   permanently running animation is exactly the battery/thermal problem that rules WebGL out
   here, and it would be hypocritical to then leave this spinning off-screen. */
@media (prefers-reduced-motion: reduce) {
  html.fx .hero-fx i { animation: none; }
}
html.fx .hero-fx.is-off i { animation-play-state: paused; }

.hero::before {
  content:'';
  position: absolute;
  top:-20%; left:-10%;
  width: 60%; height: 60%;
  background: radial-gradient(circle, rgba(254,185,0,0.15) 0%, transparent 60%);
  filter: blur(60px);
  z-index:-1;
}
.hero::after {
  content:'';
  position: absolute;
  bottom:-20%; right:-10%;
  width: 60%; height: 60%;
  background: radial-gradient(circle, rgba(16,185,129,0.1) 0%, transparent 60%);
  filter: blur(60px);
  z-index:-1;
}
/* ---- .hero.hasphoto - a storefront photograph behind the hero (client, 29 Aug 2026) ------
   Being trialled on /contact/. The photograph is TEXTURE, not something to look at: the h1 is
   ink, the sub is slate and the glass card sits on top, so it goes under a heavy blur and a
   near-opaque scrim and all of that has to stay readable. If it ever reads as "a photo with
   text on it" rather than "a warm surface", the scrim is too weak.

   SCOPED TO THE MODIFIER, deliberately. `.hero` already spends BOTH pseudo-elements on its
   gold and green washes, so the photo needs a real element - and those washes sit at
   `z-index: -1` with no stacking context on .hero, which is exactly the arrangement
   background-glow-system-is-fragile records as easy to bury. Nothing here touches the plain
   `.hero`, so every other page's hero is byte-identical to before. */
/* THE WRAPPER, not .hero, is the positioning context: the photograph has to span the
   breadcrumb strip as well as the hero. Inside .hero it stopped dead at the crumb line and
   read as a cut-off band pasted under the header (client, 29 Aug 2026). */
.herowrap {
  position: relative;
  /* A stacking context, so the negative-z child below stays INSIDE the wrapper. Without it a
     z-index:-2 layer escapes to the nearest ancestor context and paints behind the page,
     where it is simply invisible. */
  isolation: isolate;
}
.hero.hasphoto {
  /* NOT isolated. The hero's own gold/green washes are z-index:-1 and must stay in the
     wrapper's context so they paint ABOVE the photograph rather than being trapped in a
     context of their own on top of it. */
  background: transparent;
}
.hero-shot {
  position: absolute;
  inset: 0;
  z-index: -2;                 /* under .hero::before / ::after, which are -1 */
  overflow: hidden;
  pointer-events: none;
  /* THE BOTTOM EDGE IS MASKED, NOT SCRIMMED. Fading the photo out with a white overlay only
     works if the page behind it is white - it is not, it is the soft tinted canvas with the
     six atmospheric glows. A mask removes the layer itself, so whatever the body is showing
     comes through and the join is invisible at any background colour. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgba(0,0,0,0.55) 82%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgba(0,0,0,0.55) 82%, transparent 100%);
}
.hero-shot img {
  position: absolute;
  /* Oversized on purpose. blur() fades a layer's OWN edges to transparent, so an image sized
     to the box shows a pale soft rim inset from the section edges. Bleeding it past the clip
     puts those faded edges outside .hero-shot's overflow. */
  inset: -8%;
  width: 116%;
  height: 116%;
  object-fit: cover;
  /* Softened twice on the client's direction: 26px -> 22px -> 12px. At 12px the storefront is
     recognisable as a building rather than an abstract wash, which is the point of using a
     photograph of the actual shop. */
  filter: blur(12px) saturate(118%);
}
/* TWO scrims, not one, because the two halves of the hero need opposite things. The vertical
   pass keeps the whole band light enough for ink type and fades the photo out into the
   section boundaries top and bottom. The horizontal pass then lifts the LEFT side further,
   which is where the h1 and the slate sub sit - the right half can stay darker because the
   glass card is opaque enough to carry its own contrast. */
.hero-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(100deg,
      rgba(255,255,255,0.72) 0%,
      rgba(255,255,255,0.48) 46%,
      rgba(255,255,255,0.26) 100%),
    linear-gradient(180deg,
      rgba(255,255,255,0.62) 0%,
      rgba(255,255,255,0.30) 45%,
      rgba(255,255,255,0.30) 100%);
  /* The bottom stop no longer climbs back to near-white: the mask on .hero-shot now does the
     fade-out, and scrimming as well would wash the photo pale before it disappeared. */
}

.hgrid {
  display: flex;
  flex-direction: column;
  gap: 32px;
}
.hhead {
  text-align: center;
}
.hhead h1 {
  margin-bottom: 16px;
  letter-spacing: -0.04em;
  /* The find-us H1 carries a street address, so it wraps on a phone. balance evens
     the lines instead of leaving a short stub on the last one. Unsupported browsers
     just get the normal greedy wrap. */
  text-wrap: balance;
}
.hhead .sub {
  font-size: clamp(16px, 1.8vw, 19px);
  color: var(--slate);
  max-width: 520px;
  margin: 0 auto;
}
.hcta {
  display: none;
}
.hmeta {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  justify-content: center;
  color: var(--slate);
  font-size: 14px;
  font-weight: 600;
}
.hmeta .it {
  display: flex;
  align-items: center;
  gap: 6px;
  background: rgba(255,255,255,0.5);
  padding: 6px 14px;
  border-radius: 99px;
  border: 1px solid rgba(255,255,255,0.8);
  backdrop-filter: blur(8px);
  color: inherit;          /* these are <a> jump links now - kill the default blue/underline */
  text-decoration: none;
  transition: background .18s ease, border-color .18s ease, transform .18s ease;
}
a.it:hover {
  background: #ffffff;
  border-color: rgba(0,0,0,0.08);
  transform: translateY(-1px);
}
.stars {
  display: inline-flex;
  color: var(--gold);
}
.stars .material-symbols-outlined {
  font-size: 18px;
  font-variation-settings: 'FILL' 1;
}
/* Partial star (e.g. the 0.8 of a 4.8 rating): dim outline base + gold filled overlay clipped to the fraction */
.stars .sp { position: relative; display: inline-flex; }
.stars .sp .s0 { font-variation-settings: 'FILL' 0; opacity: .4; }
.stars .sp .sf { position: absolute; left: 0; top: 0; overflow: hidden; display: inline-flex; }
.hmeta .sep {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--line);
}
.hmeta .material-symbols-outlined {
  color: var(--slate);
  font-size: 18px;
}
/* ...but the rating stars stay gold - the slate rule above outranks the colour inherited
   from .stars, so this more specific rule wins it back (incl. the dim partial-star base). */
.hmeta .stars .material-symbols-outlined { color: var(--gold); }

/* Calculator Widget */
.calc-area {
  position: relative;
  width: 100%;
  margin-top: -20px;
}
.calc {
  position: relative;
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 20px;
  padding: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.seg {
  position: relative;
  display: flex;
  background: rgba(0, 0, 0, 0.04);
  border-radius: 9999px;
  padding: 4px;
  margin-bottom: 4px;
  border: 1px solid rgba(255,255,255,0.5);
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
/* FROSTED GOLD GLASS selected pill (client 2026-07-16), on BOTH toggles - the hero calculator
   and the rates list share this component, and two identical controls with identical wording
   must not look different. It went white -> solid gold -> this: solid gold read as too heavy,
   so the gold now lives in the EDGE and the sheen rather than the fill, and you can see the
   track through it. The rim + inset highlight are what keep it legible at 20-40% opacity -
   without them a translucent pill would fade back into the track, which is the exact problem
   the gold was brought in to fix. Text on the pill must stay INK (see .seg button.on). */
.seg .ind {
  position: absolute;
  top: 4px;
  left: 4px;
  width: calc(50% - 4px);
  height: calc(100% - 8px);
  background: linear-gradient(160deg, rgba(254, 185, 0, 0.38) 0%, rgba(254, 185, 0, 0.20) 100%);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  border: 1px solid rgba(254, 185, 0, 0.55);
  border-radius: 9999px;
  box-shadow: 0 6px 16px rgba(254, 185, 0, 0.18),
              inset 0 1px 0 rgba(255, 255, 255, 0.7);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.seg.sell .ind {
  transform: translateX(100%);
}
.seg button {
  position: relative;
  z-index: 1;
  flex: 1;
  border: none;
  background: transparent;
  font-family: inherit;
  font-weight: 600;
  font-size: 14px;
  color: var(--muted);
  padding: 11px;
  cursor: pointer;
  transition: color 0.2s;
  border-radius: 9999px;
}
.seg button.on {
  color: var(--ink);
}
/* RATES toggle only - a notch bigger than the calculator's (client: better glanceability
   where people are scanning rates; the calculator's toggle stays 14px).
   `.seg.rseg` (0,2,1), NOT `.rseg` (0,1,1), is deliberate: `.rseg` ties with `.seg button`,
   so the calculator's own `@media (max-width:520px){ .seg button{font-size:13px} }` block -
   which catches this toggle as collateral - would win on source order and silently undo
   this on phones. Media queries add no specificity. */
.seg.rseg button {
  font-size: 15.5px;
}

.calc-panels {
  display: flex;
  flex-direction: column;
  position: relative;
  gap: 8px;
}
/* DIRECTION MARKER between the two amounts. .calc-panels is already position:relative
   and holds exactly two equal-height rows, so top:50% lands on the seam between them
   without having to know either row's height. Small on purpose - it is a hint about
   which way the money runs, not a control. */
.cswap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  width: 29px;
  height: 28px;
  display: grid;
  place-items: center;
  /* 30% of a 29x28 box, so the corners are a ~8.7 x 8.4 ellipse - a soft squircle
     rather than a circle. Client's values, 9 Aug 2026; keep them exact. */
  border-radius: 30%;
  color: var(--gold);
  background: #ffffff;
  /* border: 1px solid rgba(254,185,0,0.45); */  /* gold rim off (client) - the shadow
     alone separates the chip from the row behind it, which is quieter */
  box-shadow: 0 2px 6px rgba(16,24,40,0.10);
  /* Never intercept a tap: the rows underneath focus their input when clicked, and this
     sits directly over the seam where a thumb lands. */
  pointer-events: none;
}
.cswap .material-symbols-outlined {
  font-size: 16px;
  line-height: 1;
  /* Rotated because arrow_downward is not in the 19-glyph subset. Clamped for the same
     reason everything else is: a missing ligature paints its own NAME. */
  width: 16px;
  overflow: hidden;
  transform: rotate(90deg);
}
.crow {
  background: rgba(255,255,255,0.7);
  border-radius: 16px;
  padding: 12px 16px;
  border: 1px solid rgba(255,255,255,0.9);
  transition: all 0.2s ease;
  display: flex;
  flex-direction: column;
  gap: 4px;
  box-shadow: inset 0 2px 4px rgba(255,255,255,0.5), 0 2px 8px rgba(0,0,0,0.02);
}
.crow:focus-within {
  background: #ffffff;
  border-color: rgba(254, 185, 0, 0.55);
  box-shadow: 0 0 0 3px rgba(254, 185, 0, 0.12);
  transform: translateY(-1px);
}
.crow .clabel {
  font-size: 13px;
  color: var(--muted);
  font-weight: 500;
  letter-spacing: 0;
}
.cbody {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 40px;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 26px;
  color: var(--ink);
  letter-spacing: -0.02em;
  white-space: nowrap;
  background: transparent;
  padding: 4px 6px;
  margin-left: -6px;
  border-radius: 10px;
  border: none;
  box-shadow: none;
}
.chip .fl {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(0, 0, 0, 0.08);
}
.selchip {
  position: relative;
  cursor: pointer;
  transition: background-color 0.15s ease;
}
.selchip:hover {
  background: rgba(0, 0, 0, 0.04);
}
.selchip select {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: transparent;
  font-family: inherit;
  font-weight: 700;
  font-size: 16px;
  color: var(--ink);
  cursor: pointer;
  padding-right: 18px;
  outline: none;
}
.selchip .ar {
  color: var(--slate);
  font-size: 22px;
  pointer-events: none;
}
.cval {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0;
  font-size: 26px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.cval .pre {
  color: var(--ink);
  margin-right: 0;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.cval input {
  border: none;
  outline: none;
  background: transparent;
  font-family: inherit;
  font-size: 26px;
  font-weight: 600;
  color: var(--ink);
  text-align: right;
  width: 50px;
  letter-spacing: -0.02em;
  padding: 0 2px 0 0;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Calculator rate & fees box */
.calc-details {
  background: rgba(255,255,255,0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.8);
  border-radius: 16px;
  padding: 4px 0;
  display: flex;
  flex-direction: column;
}
.detail-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  min-height: 40px;
  padding: 8px 16px;
  font-size: 13px;
  color: var(--muted);
  font-weight: 500;
}
.detail-label {
  display: flex;
  align-items: center;
  gap: 8px;
}
.detail-label .material-symbols-outlined {
  font-size: 18px;
}
.check-icon {
  color: var(--gold);
}
.detail-value {
  color: var(--ink);
  font-weight: 600;
  text-align: right;
}
.rate-pair {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
.rate-inv {
  font-size: 11.5px;
  color: var(--muted);
  font-weight: 500;
}
.no-fees {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0px 16px 10px;
  font-size: 12.5px;
  color: var(--slate);
  font-weight: 600;
  text-align: center;
  padding-bottom: 0px;
  padding-top: 10px;
}
.no-fees .material-symbols-outlined {
  font-size: 16px;
}

/* Mobile-only "Call us" under the directions CTA - liquid glass */
.btn-call-glass {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 13px 18px;
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(255,255,255,0.6), rgba(255,255,255,0.28));
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid rgba(255,255,255,0.8);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), inset 0 -1px 0 rgba(255,255,255,0.25), 0 8px 22px -12px rgba(16,24,40,0.28);
  color: var(--ink);
  font-weight: 700;
  font-size: 15px;
  text-decoration: none;
  transition: transform .15s ease, box-shadow .15s ease;
}
.btn-call-glass:active {
  transform: scale(0.985);
}
.btn-call-glass .material-symbols-outlined {
  font-size: 19px;
}
/* GET A TICKET in the calculator (client's pre-registration portal, 8 Oct 2026). A quiet text
   link under the directions button, NOT a third full-width button: on a phone the calculator
   is already the whole first screen, and it sits right beside "Get directions" because it is
   the same intent - "I am going to the store". Gold icon, slate text: gold TEXT on white fails
   contrast. The new-tab mark has an accessible name here (it is aria-hidden on the offer strip,
   where the link stays on the page). */
.calc-ticket {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 12px 6px 2px;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--slate);
  text-decoration: none;
}
.calc-ticket .material-symbols-outlined { font-size: 19px; color: var(--gold-deep); flex: none; }
.calc-ticket .ct-t {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(0, 0, 0, 0.28);
}
.calc-ticket:hover, .calc-ticket:focus-visible { color: var(--ink); }
.calc-ticket:hover .ct-t, .calc-ticket:focus-visible .ct-t { text-decoration-color: currentColor; }
.calc-ticket .ol-svg { width: 12.5px; height: 12.5px; flex: none; }
@media (min-width: 901px) {
  .btn-call-glass { display: none; }
}

/* live / stale rate status inside the calculator */
.rate-lab {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.calc-upd {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--muted);
  font-weight: 500;
  /* anchor for the .rate-tip bubble */
  position: relative;
}
/* LIVE indicator = the same shine that runs on "Rates for buying foreign cash" (.rlead b):
   a narrow gold band sweeping through the text on a loop (client direction, 21 Jul 2026 -
   replaces the status dot). Base colour is this caption's own muted grey, not ink: gold only
   ever PASSES THROUGH the text, matching the brand rule that bans standing gold body text.
   Same @supports guard as .rlead b - without background-clip:text the text would render
   INVISIBLE rather than merely unstyled. Reuses the rleadshine keyframes. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .calc-upd #calc-upd-text {
    background-image: linear-gradient(90deg,
      var(--muted) 0%,
      var(--muted) 46%,
      var(--gold) 50%,
      var(--muted) 54%,
      var(--muted) 100%);
    background-size: 400% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: rleadshine 3.5s linear infinite;
  }
  /* Stale: the shine STOPS rather than pauses - a pause could freeze the gold band
     mid-sweep across the text, and static gold text on white is banned. No shine = not
     live right now; the text gets its solid grey back. */
  .calc-upd.stale #calc-upd-text {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: var(--muted);
    color: var(--muted);
  }
}
@media (prefers-reduced-motion: reduce) {
  /* same unwind as .rlead b: hand the colour back or the text sits transparent */
  .calc-upd #calc-upd-text {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: var(--muted);
    color: var(--muted);
  }
}
/* Stale also dims the WHOLE rate row - "Our current rate", the updated line, both rate
   values - the client's "opacity down" state. Coming back live restores it (and the shine). */
.rate-row { transition: opacity .35s ease; }
.rate-row.stale { opacity: .55; }
.rate-info {
  display: inline-flex;
  align-items: center;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.rate-info .material-symbols-outlined { font-size: 14px; line-height: 1; }
.rate-info:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 50%; }
/* The bubble. Tap toggles it (95%+ of traffic is mobile - hover does not exist there);
   hover shows it only on devices that really hover, so touch taps never double-fire. */
.rate-tip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  z-index: 40;
  display: none;
  width: max-content;
  /* ONE line by design (client): "(AEST)" already names the zone so there is no "Sydney
     time", and the wording fits a 360px phone without wrapping - MEASURE before ever
     re-wording this. */
  white-space: nowrap;
  padding: 9px 13px;
  border-radius: 12px;
  /* GOLDISH-white glass (client, 21 Jul 2026 - his "gold" arrived as "bold" the first time,
     so the text went semibold; both stand now). Warm white falling into a faint brand-gold
     wash, a gold-tinted edge, and the white top sheen that sells it as glass. The gold stays
     a TINT - ink text on top, never gold-on-white text. */
  background: linear-gradient(135deg, rgba(255,255,255,0.93) 0%, rgba(255,243,209,0.8) 55%, rgba(254,185,0,0.18) 135%);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid rgba(254,185,0,0.32);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.98), inset 0 -1px 0 rgba(254,185,0,0.14), 0 10px 26px -12px rgba(16,24,40,0.3);
  font-size: 11.5px;
  /* semibold ink, not slate - the premium read the client asked for on the glass */
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink);
  text-align: left;
  cursor: default;
}
.rate-info.open .rate-tip { display: block; }
@media (hover: hover) { .rate-info:hover .rate-tip { display: block; } }

/* ── Narrow-viewport ladder for the rate row (client, 21 Jul 2026) ──
   MEASURED, not guessed: one line needs 248px of content and the fixed chrome around the
   row is 100px, so the row wraps below a 349px viewport. Real devices live down there
   (older iPhone SE 320, Galaxy Fold cover 280). The fonts step down ONLY once the line
   would actually break - "not early" - and again for the very narrowest screens. Keep the
   two floors in sync with these measurements if any wording or padding changes. */
@media (max-width: 349px) {
  /* .detail-row.rate-row, not .rate-row: the calculator's own mobile queries further down
     (max-width 430/380) also set .detail-row's font-size and sit LATER in the sheet, so a
     single-class selector here silently loses the cascade - measured, not theoretical. */
  .detail-row.rate-row { gap: 8px; font-size: 11px; }
  .detail-row.rate-row .rate-inv { font-size: 10.5px; }
  .detail-row.rate-row .calc-upd { font-size: 10px; }
  .detail-row.rate-row .rate-info .material-symbols-outlined { font-size: 13px; }
  /* the one-line bubble: nudge left and step down so it stays inside the viewport */
  .rate-tip { font-size: 11px; left: -34px; }
}
@media (max-width: 314px) {
  .detail-row.rate-row { gap: 6px; font-size: 10px; }
  .detail-row.rate-row .rate-inv { font-size: 9.5px; }
  .detail-row.rate-row .calc-upd { font-size: 9px; }
}
/* The status dot is GONE FOREVER (client, 21 Jul 2026): the live signal is the shine on the
   "Updated H:MM" text below, and stale dims the whole rate row. Do not re-add a dot. */
#rate-time.stale { color: #b6bbc4; }

/* Anchor landings are FLUSH (client direction): the section top meets the viewport top on
   every scroll path - Lenis uses HEADER_OFFSET 0 in JS, and this explicit 0 documents that
   the native paths (no-Lenis fallback, hash arrivals, back/forward) agree. The sections
   own 120/72px padding is the visual gap. Keep this and HEADER_OFFSET in sync. */
#rates, #reviews, #trust, #how, #why, #location, #faq, #wholesale { scroll-margin-top: 0; }

/* sticky mobile action bar - slides in after the hero, liquid glass */
.mact {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  z-index: 55;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  border-radius: 20px;
  overflow: hidden;
  background: linear-gradient(135deg, rgba(255,255,255,0.7), rgba(255,255,255,0.45));
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  border: 1px solid rgba(255,255,255,0.8);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 12px 32px -12px rgba(16,24,40,0.35);
  visibility: hidden;
  opacity: 0;
  transform: translateY(120%);
  transition: transform .35s cubic-bezier(.22,.9,.3,1), opacity .3s ease, visibility 0s linear .35s;
}
/* ── BACK TO TOP (client, 16 Aug 2026) ───────────────────────────────────────────────────
   Appears once you are a screen deep, bottom-right, and RIDES ABOVE THE ACTION BAR when that
   bar is showing rather than sitting under it or overlapping it.

   `--mact-h` is measured and written by site.js, not guessed: the action bar's height changes
   depending on whether the open/closed status row is present, so a hardcoded offset would be
   right on one page state and wrong on the other. It is 0 whenever the bar is hidden, which
   is also the whole story on desktop where the bar never shows at all.

   The glass is the most extreme on the site, which is the point of it - it floats over
   arbitrary page content rather than over one known photo, so the fill is very low and the
   blur does effectively all of the work. The specular top edge plus the inner bottom shade
   are what stop it reading as a flat translucent circle. */
.gotop {
  position: fixed;
  right: 14px;
  /* Lift on `translate`, not `bottom` - see the matching note on .orate. The two must move
     identically or the pair reads as broken, so keep these declarations in step. */
  bottom: calc(14px + env(safe-area-inset-bottom));
  translate: 0 calc(-1 * var(--mact-h, 0px));
  z-index: 54;                       /* under .mact (55) - it never needs to cover the bar */
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.75);
  background: linear-gradient(158deg, rgba(255,255,255,0.55), rgba(255,255,255,0.28));
  backdrop-filter: blur(24px) saturate(200%) brightness(1.1);
  -webkit-backdrop-filter: blur(24px) saturate(200%) brightness(1.1);
  box-shadow:
    0 14px 34px -14px rgba(16,24,40,0.5),
    inset 0 1px 0.5px rgba(255,255,255,1),
    inset 0 -12px 18px -14px rgba(16,24,40,0.22);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translateY(14px) scale(0.9);
  /* NO `bottom` IN THIS TRANSITION LIST (client bug, 17 Aug 2026 - "the top button does
     random moves when I scroll"). bottom rides var(--mact-h), which is recomputed on EVERY
     scroll event and flips 0 <-> bar-height as the sticky action bar comes and goes - so an
     animated bottom had the button gliding on its own 350ms timeline, desynchronised from
     both the scroll and the bar it is clearing. The Special rate pill was the reference:
     it transitions only opacity and transform, repositions INSTANTLY, and reads as solid.
     Show/hide still animates; position never does. Keep the two lists below identical. */
  transition: opacity .28s ease, transform .28s cubic-bezier(.22,.9,.3,1),
              translate .35s cubic-bezier(.22,.9,.3,1),
              visibility 0s linear .28s;
}
.gotop.on {
  opacity: 1;
  visibility: visible;
  transform: none;
  /* bottom deliberately absent - see the note on the base rule. The lift rides `translate`,
     which is listed here and above; keep the two lists identical. */
  transition: opacity .28s ease, transform .28s cubic-bezier(.22,.9,.3,1),
              translate .35s cubic-bezier(.22,.9,.3,1),
              visibility 0s;
}
.gotop .material-symbols-outlined { font-size: 24px; }
.gotop:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.7), rgba(255,255,255,0.42));
  transform: translateY(-2px);
}
.gotop:active { transform: translateY(0) scale(0.96); }
@media (prefers-reduced-motion: reduce) {
  .gotop { transition: opacity .01s, visibility 0s; }
}

.mact.on {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition: transform .35s cubic-bezier(.22,.9,.3,1), opacity .3s ease;
}
.mact .btn {
  flex: 1;
  justify-content: center;
  padding: 11px 14px;
  font-size: 14.5px;
}
/* THREE BUTTONS SINCE 8 Oct 2026 - "Get a ticket" joined Call and Directions (client's
   pre-registration portal). Equal thirds do not fit a phone: "Directions" with its icon is
   wider than a third of the bar. Sized to their content instead, the two short labels give
   their slack to Directions. The ticket says "Ticket" on phones and "Get a ticket" from
   480px. Directions stays the only dark button - it is still the primary action. */
.mact .btn { flex: 1 1 auto; padding: 11px 10px; white-space: nowrap; }
.mact-ticket .material-symbols-outlined { color: var(--gold-deep); }
.mact-ticket .mt-full { display: none; }
@media (min-width: 480px) {
  .mact-ticket .mt-full { display: inline; }
  .mact-ticket .mt-short { display: none; }
}
/* Measured, not guessed: at 320px the three labels need 263px and the bar offered 262, so
   flex squeezed them by a pixel and two of them wrapped onto a second line. Tighter gaps and
   side padding here leave ~15px of headroom, and nowrap above means a label can never break -
   a real phone's font rendering will not land exactly where the emulator's does. */
@media (max-width: 360px) {
  .mact-btns { gap: 6px; }
  .mact .btn { padding: 10px 4px; font-size: 13.5px; gap: 5px; }
}
/* ── THE ACTION BAR NOW SHOWS ON DESKTOP TOO (client, 29 Aug 2026) ───────────────────────
   It used to be `display: none` above 900px. The rule it defers to is unchanged and is what
   makes this safe: site.js hides the bar whenever a Call + Directions cluster is on screen,
   so on the pages that carry their own CTAs (the homepage store block, find-us, the offer
   band) nothing doubles up - the bar only appears where the page offers no other way to call
   or get directions, which on desktop is exactly the legal and long-form pages.

   IT IS THE MOBILE BAR, JUST WIDER (client, 29 Aug 2026, third revision - and this is the one
   he kept). Two versions were built and rejected first: a centred 520px pill, then a
   full-bleed strip squared off against the viewport edges. The full-bleed version "did not
   look best" - what makes this component read well is the FLOATING glass card: inset from
   every edge, 20px radius, its own shadow. Widening it is fine; grounding it is not.
   So desktop inherits `left/right/bottom`, the radius, the border and the shadow from the
   base rule unchanged, and the ONLY thing this block does is centre the two buttons inside
   the now much wider bar. Do not reintroduce a full-bleed or a squared variant. */
@media (min-width: 901px) {
  .mact-btns {
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    padding: 10px 12px 12px;
  }
}
/* FOOTER CLEARANCE FOLLOWS THE BAR, rather than being reserved permanently (client,
   29 Aug 2026). On a tall desktop screen the offer band's Call + Get directions are on screen
   at the foot of the page, so the bar correctly defers and never appears - and a static
   padding-bottom then left ~130px of dead white space under the footer for a bar that was
   never coming. On a short screen those buttons are scrolled past, the bar does appear, and
   the clearance IS needed.
   `--mact-h` already answers exactly that question: site.js publishes the bar's real height
   (plus a 10px gap) while it is showing and 0 whenever it is hidden, updated from a
   MutationObserver on the class and a ResizeObserver on the box. So the footer reserves space
   only in the state that needs it, on both screen sizes, with no new script. */
@media (min-width: 901px) {
  footer.footer-premium { padding-bottom: var(--mact-h, 0px); }
}
@media (max-width: 900px) {
  /* footer.footer-premium (not just .footer-premium) - the base `padding: 80px 0 0` rule sits
     LATER in this stylesheet and media queries add no specificity, so it would win and zero
     this out. The element+class selector outranks it regardless of order. */
  /* Clearance = the action bar's real height (112px) + its 12px offset + a small buffer, so the
     copyright/legal row is readable even in the worst case where the bar has not slid away. */
  footer.footer-premium { padding-bottom: calc(140px + env(safe-area-inset-bottom)); }
}

.calc .go {
  margin-top: 4px;
}
/* ---- .payrow - the in-store payment line on the calculator card ---------------------------
   Replaced .disc (17 Aug 2026). Sits where small print sat, so it stays small print: muted
   label, quiet chips, no gold - this must not compete with the rate or the buttons above it.
   Text chips ONLY, never scheme logos - see the comment block in the page markup: the marks
   are licensed to accepting merchants, and acceptance is not yet confirmed. */
.payrow {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin-top: 12px;
  padding: 0 8px;
}
.payrow i {
  font-style: normal;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  margin-right: 2px;
}
/* One authoritative .payset lives in the markup (the client swap hook - see the page
   comment); site.js clones it for the mobile marquee and stamps .is-run on the rail.
   At rest and on desktop no clone ever shows: display:contents melts the real set's
   chips straight into .paytrack's centered wrap row, and clones ([aria-hidden]) stay
   display:none. No JS = no .is-run = this same static row at every width - the marquee
   is pure enhancement. */
.payrail { min-width: 0; }
.paytrack {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 8px;
}
.payset { display: contents; }
.payset[aria-hidden] { display: none; }
.pchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  line-height: 1;
  color: var(--slate);
  background: rgba(255,255,255,0.6);
  border: 1px solid rgba(0,0,0,0.08);
  border-radius: 999px;
  padding: 5px 12px 5px 9px;
}
/* Generic Material glyphs only, never scheme marks - the trademark rule above. Brand
   gold by client direction (17 Aug, overriding the first deep-gold cut): the icons are
   accent marks and the accent is #feb900. */
.pchip .material-symbols-outlined {
  font-size: 15px;
  line-height: 1;
  color: var(--gold);
}
/* MOBILE MARQUEE (client, 17 Aug 2026 - "the line is breaking... make it moving
   infinite, stop when anyone interferes, start again when they leave"): under 601px the
   rail becomes a masked viewport and the track a nowrap loop of identical sets driven to
   exactly translateX(-50%). site.js keeps the set count EVEN, so the second half of the
   track is a pixel-identical copy of the first and the loop restart is invisible - the
   same identity trick as the offer star's 72deg. Spacing arithmetic: track gap 0 while
   running; every set carries gap 8 AND margin-right 8, so the seam between sets equals
   the gap inside them - one uniform rhythm, no visible joint (a track-level gap would
   put 3 gaps around 4 sets and -50% would no longer land on a set boundary).
   Transform-only, so it stays on the compositor; paused while pressed or hovered
   (.is-held, site.js) and while off screen (.is-off, the shine-sweep observer list). */
@media (max-width: 600px) {
  .payrow i { margin-right: 0; flex-basis: 100%; text-align: center; }
  .payrail.is-run {
    width: 100%;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 26px, #000 calc(100% - 26px), transparent);
    mask-image: linear-gradient(90deg, transparent, #000 26px, #000 calc(100% - 26px), transparent);
  }
  .payrail.is-run .paytrack {
    flex-wrap: nowrap;
    justify-content: flex-start;
    width: max-content;
    gap: 0;
    animation: pay-marquee 26s linear infinite;
  }
  .payrail.is-run .payset,
  .payrail.is-run .payset[aria-hidden] {
    display: flex;
    align-items: center;
    flex: none;
    gap: 8px;
    margin-right: 8px;
  }
  .payrow.is-off .paytrack,
  .payrow.is-held .paytrack { animation-play-state: paused; }
}
@keyframes pay-marquee { to { transform: translateX(-50%); } }
/* Reduced motion: no marquee at any width - the static centered wrap row IS the design.
   Sits after the mobile block on purpose: same specificity, later source wins. */
@media (prefers-reduced-motion: reduce) {
  .payrail.is-run { overflow: visible; -webkit-mask-image: none; mask-image: none; }
  .payrail.is-run .paytrack { animation: none; flex-wrap: wrap; justify-content: center; width: auto; gap: 6px 8px; }
  .payrail.is-run .payset { display: contents; }
  .payrail.is-run .payset[aria-hidden] { display: none; }
}
/* Footer payment methods row (.fpay, _parts/footer.html) - quiet footer grammar next to
   the ABN/AUSTRAC facts: glyph + word, no chip pills. Same shared placeholder list as
   every other payment surface - change them all together. */
.fpay {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin-top: 14px;
  font-size: 12.5px;
  color: var(--slate);
}
.fpay-t {
  flex-basis: 100%;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.fpay > span:not(.fpay-t) { display: inline-flex; align-items: center; gap: 5px; }
.fpay .material-symbols-outlined { font-size: 14px; color: var(--gold); }
/* About page payment methods section (#payments) - one glass fact-panel, equal method
   cells split by hairlines: information grammar, deliberately unlike the story/photo
   sections around it (client: "a different kind of section, more organized"). Cells go
   3-across on desktop with vertical hairlines, stack with horizontal ones under 601. */
.paysec {
  padding: 34px 26px 14px;
  border-radius: 24px;
  text-align: center;
}
.paysec h2 { margin-bottom: 8px; }
.paysec-lead {
  color: var(--slate);
  margin: 0 auto 24px;
  max-width: 46ch;
}
.paysec-grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.paysec-m {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 20px 10px 24px;
}
.paysec-m + .paysec-m { border-left: 1px solid rgba(0, 0, 0, 0.08); }
/* One method (cash only, client, 8 Oct 2026) spans the whole row instead of sitting in the
   left third of a three-column grid. Two or three methods are untouched, so the hairline
   thirds come back on their own when card payments return. */
.paysec-m:only-child { grid-column: 1 / -1; }
.paysec-m .material-symbols-outlined { font-size: 34px; color: var(--gold); }
.paysec-m b { font-size: 15.5px; font-weight: 700; color: #000; }
@media (max-width: 600px) {
  .paysec { padding: 26px 18px 8px; }
  .paysec-grid { grid-template-columns: 1fr; }
  .paysec-m { flex-direction: row; justify-content: center; gap: 10px; padding: 14px 8px; }
  .paysec-m + .paysec-m { border-left: 0; border-top: 1px solid rgba(0, 0, 0, 0.08); }
  .paysec-m .material-symbols-outlined { font-size: 22px; }
}

.disc {
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  margin-top: 11px;
  line-height: 1.5;
  padding: 0 8px;
}

/* Stats Strip */
.strip {
  border-bottom: 1px solid rgba(0,0,0,0.05);
  background: transparent;
  position: relative;
  z-index: 10;
}
.strip .wrap {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  padding: 40px 24px;
  flex-wrap: wrap;
}
.stat {
  flex: 1;
  min-width: 160px;
}
.stat b {
  font-size: 32px;
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--ink);
  display: block;
  margin-bottom: 2px;
}
.stat span {
  font-size: 13px;
  color: var(--slate);
  font-weight: 600;
}

/* Section Styling */
section.band {
  /* Exposed as a variable purely so a section can take a FRACTION of the standard band
     spacing without hardcoding a number that then has to be kept in sync with the responsive
     override below. See #access / #inside near the store gallery. */
  --band-pad: 120px;
  padding: var(--band-pad) 0;
}
/* HALF SPACING BETWEEN #access AND #inside ONLY (client, 16 Aug 2026).
   The access photo and the store gallery are the same subject seen two ways - the door, then
   the room behind it - so they read as one block, and a full band gap between them separated
   two things that belong together. Every other section boundary keeps the standard spacing.

   Written as `calc(var(--band-pad) / 2)` rather than a flat 60px so it halves whatever the
   band padding currently is: 120px on desktop and 72px below 900px, which a hardcoded number
   would have got wrong on phones. Both IDs are unique to /find-us/, so this cannot reach
   /exchange-rates/. */
#access { padding-bottom: calc(var(--band-pad) / 2); }
#inside { padding-top: calc(var(--band-pad) / 2); }

.band.mist {
  background: transparent;
  position: relative;
}
.band.mist::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(243,244,246,0) 0%, rgba(243,244,246,0.6) 50%, rgba(243,244,246,0) 100%);
  z-index: -1;
}
.head {
  max-width: 660px;
  margin: 0 auto 64px;
  text-align: center;
}
/* A SECOND .head partway down a band, for when one section genuinely covers two subjects.
   Used on /about/, where the six cards under "what we keep behind the counter" were three
   cards of inventory economics followed by three about what the customer can get - two reader
   questions under one heading. Base .head has no top margin because it normally opens a band;
   sitting after a card row it needs one, or it reads as a caption on the row above. */
.head-mid { margin-top: 56px; }
.head p {
  font-size: 18px;
  color: var(--slate);
}

/* Big Statement banner */
.statement {
  text-align: center;
  padding: 128px 0;
  background: transparent;
  position: relative;
}
.statement::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at center, rgba(0,0,0,0.9) 0%, var(--ink) 100%);
  z-index: -1;
}
.statement h2 {
  max-width: 900px;
  margin: 0 auto 24px;
  letter-spacing: -0.04em;
  color: #ffffff;
}
.statement h2 .gold {
  color: var(--gold);
}
.statement p {
  font-size: clamp(17px, 2.2vw, 21px);
  max-width: 600px;
  margin: 0 auto;
  color: #9ca3af;
}

/* Features Grid */
.feats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 24px;
}
/* Best Price Guarantee band (full width under the feature grid) */
.bpg {
  margin-top: 24px;
  display: flex;
  align-items: center;
  gap: 22px;
  flex-wrap: wrap;
  background: var(--ink);
  border-radius: var(--r);
  padding: 26px 32px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.06);
}
.bpg .bpg-ico {
  width: 52px;
  height: 52px;
  border-radius: 14px;
  background: var(--gold);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.bpg .bpg-ico .material-symbols-outlined { font-size: 28px; }
.bpg .bpg-txt { flex: 1; min-width: 220px; }
.bpg .bpg-txt h3 { color: #fff; font-size: 20px; margin-bottom: 5px; }
.bpg .bpg-txt p { color: rgba(255,255,255,0.72); font-size: 14px; line-height: 1.55; }
.bpg .bpg-txt .gold { color: var(--gold); font-weight: 700; }
.bpg .bpg-cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: #fff;
  color: var(--ink);
  border-radius: 999px;
  padding: 12px 20px;
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
  flex: none;
  transition: transform .18s ease;
}
.bpg .bpg-cta:hover { transform: translateY(-1px); }
.bpg .bpg-cta .material-symbols-outlined { font-size: 18px; }
@media (max-width: 640px) {
  .bpg { padding: 22px; }
  .bpg .bpg-cta { width: 100%; justify-content: center; }
}
/* Wholesale brief strip (low on the page - retail funnel stays first) */
.whole {
  display: flex;
  align-items: center;
  gap: 22px;
  flex-wrap: wrap;
  border-radius: var(--r);
  padding: 28px 32px;
}
.whole .w-ico {
  width: 52px; height: 52px; border-radius: 14px;
  background: var(--ink); color: var(--gold);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.whole .w-ico .material-symbols-outlined { font-size: 28px; }
.whole .w-txt { flex: 1; min-width: 240px; }
.whole .w-txt h2 { font-size: 22px; margin-bottom: 5px; }
.whole .w-txt p { color: var(--slate); font-size: 14.5px; line-height: 1.6; }
@media (max-width: 640px) {
  .whole .btn { width: 100%; justify-content: center; }
}
/* Live "Open now / Closed" status (Sydney time) */
.store-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
}
.store-status .sdot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #c2c7cf;
  flex: none;
}
/* updpulse: the shared breathing-dot keyframes. The calculator's own status dot is gone
   (see the tombstone by the shine CSS), but these frames are still used by the store-status
   pill, the calc open/closed bar, the sticky mobile bar and the table timestamp's dot.
   Deleting the dot block once took this definition with it and every one of those dots
   SILENTLY stopped pulsing - undefined keyframes do not error. Keep this next to its users. */
@keyframes updpulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}
/* Dot only: no halo ring, no pulse. The breathing dot is the other half of the generic
   "live" component - keyframes kept because the rates timestamp still uses them. */
.store-status.open .sdot { background: var(--open); }
.store-status .s-main { color: var(--slate); }
.store-status.open .s-main { color: var(--ink); }
.store-status .s-hint { color: var(--muted); font-weight: 500; display: inline-flex; align-items: center; }
.store-status .s-hint::before { content: ""; width: 3px; height: 3px; border-radius: 50%; background: currentColor; flex: none; margin: 0 6px 0 0; }
.calc .store-status { justify-content: center; }
@media (prefers-reduced-motion: reduce) { .store-status.open .sdot { animation: none; } }
/* Bigger location open/closed card (under the hours, above the CTAs) */
.loc-status {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 15px 20px;
  border-radius: 16px;
  background: var(--mist);
  border: 1px solid var(--line);
  margin: 6px 0 22px;
}
/* No green wash on the panel. The state is the dot's job; tinting the whole card was what
   turned a status light into a green component. */
.loc-status.open { border-color: rgba(254,185,0,0.30); }
.loc-status .ls-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #b6bbc4;
  flex: none;
  position: relative;
}
.loc-status.open .ls-dot { background: var(--open); }
/* The lspulse expanding-halo ring is GONE (client, 10 Aug 2026). A dot with a growing ring
   is the most copied "live" indicator on the web and it was half of why this read as
   generic. A steady dot says the same thing and says it more quietly. */
.loc-status .ls-txt { display: flex; flex-direction: column; gap: 1px; }
.loc-status .s-main { font-size: 18px; font-weight: 700; line-height: 1.2; color: var(--slate); }
.loc-status.open .s-main { color: var(--ink); }   /* ink, not green - the dot carries state */
.loc-status .s-hint { font-size: 14px; color: var(--muted); font-weight: 500; }
.loc-status .s-tz { display: block; font-size: 12.5px; color: var(--muted); font-weight: 500; opacity: .8; margin-top: 4px; }
/* Hero trust-line open/closed pill (glassy - inherits the .it glass, green tint when open) */
/* Calculator open/closed bar - glass drawer that rises up from under the calculator on load */
.calc-stack { display: flex; flex-direction: column; }
.calc-stack .calc { position: relative; z-index: 2; }
.calc-openbar {
  align-self: stretch;
  position: relative;
  z-index: 1;
  margin: -12px 12px 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 9px;
  padding: 19px 20px 13px;
  border-radius: 0 0 18px 18px;
  background: rgba(255,255,255,0.5);
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  border: 1px solid rgba(255,255,255,0.65);
  border-top: none;
  box-shadow: 0 18px 30px -20px rgba(16,24,40,0.3);
  font-size: 13.5px;
  font-weight: 600;
  opacity: 0;
  transform: translateY(-28px); /* starts tucked up BEHIND the card, then slides DOWN into place */
  transition: opacity .5s ease, transform .7s cubic-bezier(.2,.9,.3,1.2);
  will-change: transform, opacity;
}
.calc-openbar.cob-in { opacity: 1; transform: translateY(0); }
.calc-openbar .cob-dot { width: 9px; height: 9px; border-radius: 50%; background: #c2c7cf; flex: none; }
.calc-openbar.open .cob-dot { background: var(--open); }
.calc-openbar .s-main { color: var(--slate); }
.calc-openbar.open .s-main { color: var(--ink); }
.calc-openbar .s-hint { color: var(--muted); font-weight: 500; display: inline-flex; align-items: center; }
.calc-openbar .s-hint::before { content: ""; width: 3px; height: 3px; border-radius: 50%; background: currentColor; flex: none; margin: 0 6px; }
@media (prefers-reduced-motion: reduce) { .calc-openbar { transition: opacity .4s ease; transform: none; } .calc-openbar.open .cob-dot { animation: none; } }
/* Sticky mobile action bar - glassy status header + buttons row (one cohesive card) */
.mact-btns { display: flex; gap: 8px; padding: 8px; }
.mact-status { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; text-align: center; gap: 3px 8px; font-size: 12.5px; font-weight: 600; padding: 11px 16px 10px; border-bottom: 1px solid rgba(255,255,255,0.55); }
/* The markup ships this row with `hidden` and the JS removes it once the open/closed state
   resolves - but `display: flex` above is an author rule and beats the UA stylesheet's
   `[hidden] { display: none }`, so the attribute was doing nothing and an EMPTY status row was
   laid out until the script ran. Same trap as .cwlb[hidden] and .rfoot[hidden]. Found while
   measuring the bar's height for the back-to-top offset, which is the only reason it surfaced:
   toggling `hidden` did not change the bar's height. */
.mact-status[hidden] { display: none; }
.mact-status .sdot { width: 7px; height: 7px; border-radius: 50%; background: #c2c7cf; flex: none; }
.mact-status.open .sdot { background: var(--open); }
.mact-status .s-main { color: var(--slate); }
.mact-status.open .s-main { color: var(--ink); }
.mact-status .s-hint { color: var(--muted); font-weight: 500; display: inline-flex; align-items: center; }
.mact-status .s-hint::before { content: ""; width: 3px; height: 3px; border-radius: 50%; background: currentColor; flex: none; margin: 0 6px; }
@media (prefers-reduced-motion: reduce) { .hmeta .store-pill.open .sdot, .mact-status.open .sdot { animation: none; } }

/* Store-closed popup removed per client - its .closed-modal / .cm-* CSS was deleted. */
.feat {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: var(--r);
  padding: 40px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  transition: all 0.3s ease;
}
.feat:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px rgba(0,0,0,0.06);
}
/* Custom brand icons (preview/icons/*.svg) used DIRECTLY - no dark tile behind them.
   Each icon paints its gold shapes first and layers the white details on top, so they read
   correctly on the light cards. Loaded as <img> rather than inlined: every file reuses the
   same .st0/.st1 class names with different fills, which would collide if inlined. */
.feat .ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 24px;
}
.feat .ico img,
.step .snum img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  display: block;
}
.feat h3 {
  font-size: 22px;
  margin-bottom: 12px;
  letter-spacing: -0.02em;
}
.feat p {
  font-size: 16px;
  color: var(--slate);
  line-height: 1.6;
}

/* How it works steps */
.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
.step {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  border-radius: var(--r);
  padding: 36px;
}
.step .snum {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 22px;
}
.step h3 {
  font-size: 21px;
  margin-bottom: 10px;
}
.step p {
  font-size: 16px;
  color: var(--slate);
  line-height: 1.6;
}

/* Rates Table Section */
/* NO backdrop-filter on touch - this is the big one for the iPhone stall.
   This card is ~1150px tall and was a LIVE blur for its whole height, with the sticky
   controls sitting inside it. Every scroll frame WebKit had to re-resolve a
   viewport-sized blurred backdrop *and* re-resolve it again because a child had moved
   within it. That is the "lags, then paints a few seconds later" the client reported: it
   is not a stutter, it is the compositor giving up and catching up.
   0.86 white instead of 0.6-over-blur lands in the same place visually, because what sits
   behind this card is the near-flat #f8fafc page - there is almost nothing back there for
   a blur to reveal. Desktop keeps the real glass below. */
.ratebox {
  background: rgba(255, 255, 255, 0.86);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  border-radius: var(--r);
  /* clip, NOT hidden. Both clip children to the rounded corners, but `hidden` also makes
     this element a scroll container - and a scroll container is what `position: sticky`
     sticks to, so .rsticky below would have been pinned to a box that never scrolls and
     would simply never have stuck. `clip` establishes no scroll container.
     The old comment on .rlist warned that this clipped wide rate values; that is handled
     by the minmax(0,1fr) floor there now, so this is only doing corners. */
  overflow: clip;
}
/* Mouse-driven desktop gets the real glass back. Same split as the sticky bar and the
   mobile menu: the blur is only ever a problem where the compositor is weakest and the
   surface is being scrolled under a finger. */
@media (hover: hover) and (pointer: fine) {
  .ratebox {
    background: rgba(255, 255, 255, 0.6);
    backdrop-filter: blur(24px) saturate(180%);
    -webkit-backdrop-filter: blur(24px) saturate(180%);
  }
}

/* ==========================================================================
   STICKY RATE CONTROLS (client, 10 Aug 2026)
   The toggle, timestamp, search and "Rates for ..." caption ride together while
   the currency cards scroll behind them, so the thing you use to filter the list
   is never scrolled away from the list itself.
   Sticky is scoped by its containing block, which is .ratebox - so it releases by
   itself at the end of the rates card. No scroll maths, no JS.
   ========================================================================== */
.rsticky {
  position: sticky;
  top: 0;
  z-index: 20;              /* .lay-grid .rrow is position:relative/auto and comes LATER in
                               the DOM, so without this the cards would paint over the bar */
  /* AT REST IT IS INVISIBLE. No background, no blur, no rule, no shadow - the card must
     look exactly as it did before this bar existed until a scroll actually pins it.
     Deliberately NO padding either: a sticky element still occupies its space in flow, so
     padding here would push the first card row down and change the resting layout. The
     frosted treatment arrives with .is-stuck, which the script adds only once the bar has
     reached its offset. */
  /* The FILL is permanent on touch (see below) - only the rule and the shadow are gated on
     .is-stuck. That is deliberate insurance: if the stuck flag is ever late, the failure is
     "shadow arrives a beat late", which nobody notices, instead of "the bar is transparent
     and cards are showing through the controls", which is what iOS was doing. */
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid transparent;
  box-shadow: none;
  /* `top` trails the header rather than matching it: the header runs transform .35s, so at
     .42s this settles just after it and the two stop reading as one jumping block.
     The rest fade the frosting in so it arrives rather than snaps. */
  transition: top .42s ease-in-out,
              background .22s ease, box-shadow .22s ease,
              border-color .22s ease, backdrop-filter .22s ease;
}
/* The tripwire the IntersectionObserver watches. 1px so it is a real box the observer can
   see, then pulled back by its own height so it changes nothing in layout. */
.rsticky-sentinel { height: 1px; margin-bottom: -1px; pointer-events: none; }

/* TOUCH KEEPS THE FILL AT ALL TIMES. Against the card's own 0.86 white this is a 0.08
   difference - invisible at rest - but it means a late .is-stuck can never leave the
   controls transparent with cards sliding through them, which is exactly what iPhones
   were showing on a fast flick. Only the rule and the shadow still wait for the flag. */
@media (hover: none), (pointer: coarse) {
  .rsticky { background: rgba(255,255,255,0.94); }
}

/* NO backdrop-filter here. A blurred backdrop on a POSITION:STICKY element is WebKit's
   worst case: it does not reliably re-rasterise the backdrop as the element moves, so on
   iPhone the blur went stale, flickered, or vanished entirely mid-scroll. There is no
   tuning that fixes it - the combination itself is the bug.
   A near-opaque fill does the same job here anyway. The point is that the cards must stop
   being readable as they pass under the controls, and 0.94 white achieves that with no
   compositing work at all. */
.rsticky.is-stuck {
  background: rgba(255,255,255,0.94);
  border-bottom-color: rgba(16,24,40,0.06);
  box-shadow: 0 6px 18px -14px rgba(16,24,40,0.30);
}
/* Mouse-driven desktop only - and even here it is the cheap case, because the bar is one
   short row rather than the four-row stack it becomes on a phone. Touch devices never
   match this, which is what keeps iOS away from it. */
@media (hover: hover) and (pointer: fine) {
  .rsticky.is-stuck {
    background: rgba(255,255,255,0.78);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border-bottom-color: rgba(255,255,255,0.65);
  }
}

/* THE SOFT BOTTOM EDGE (client, 10 Aug 2026 - phones).
   .rtool is `padding: 14px 12px 0`, so the bar's fill stopped 1px under the "Rates as of"
   line and a hairline rule sat right there. Rows passing beneath were sliced clean in half
   by a hard white-to-card edge. This replaces that cut with a tail: ~10px of the SAME solid
   fill (the breathing space under the timestamp), then a fade to nothing over ~22px, so a
   row dissolves as it goes under instead of being cut.

   IT IS A PSEUDO-ELEMENT AT top:100%, AND THAT IS THE WHOLE POINT. It is out of flow, so
   .rsticky's own height does not change. The bar MUST stay one constant height or the early
   release breaks - see the long note under this block about the search that must not
   collapse. Padding or a taller bar would have been the obvious fix and it is exactly the
   one that is not available here.

   pointer-events:none is not optional: this sits over the top ~32px of the first visible
   card, which is a tap target that loads that currency into the calculator.

   Gated on .is-stuck, and faded with opacity rather than switched on, because at rest the
   client wants the card to look precisely as it did before the bar existed - a permanent
   tail would haze the top of the first row. The hairline and the drop shadow are dropped
   here: with the fade doing the work they read as a second, competing edge. */
.rsticky::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 32px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .22s ease;
  background: linear-gradient(to bottom,
    rgba(255,255,255,0.94) 0, rgba(255,255,255,0.94) 10px, rgba(255,255,255,0) 100%);
}
@media (hover: none), (pointer: coarse) {
  .rsticky.is-stuck::after { opacity: 1; }
  .rsticky.is-stuck {
    border-bottom-color: transparent;
    box-shadow: none;
  }
}

/* DESKTOP GETS THE SAME TREATMENT (client, 10 Aug 2026). It had the identical defect,
   measured: 1px of fill below "Rates for buying foreign cash" and then a hard edge, because
   .rlead carries no bottom margin either.

   Taller than the phone's, because the client asked for a little more space here and the
   desktop bar has room: 16px of fill, then the fade over the remaining 28px.

   The fill is 0.78 to match the desktop bar, not the phone's 0.94.

   DELIBERATELY NOT GIVEN ITS OWN backdrop-filter. The bar above already establishes a
   backdrop root and a second one overlapping it is both a real compositing cost and
   poorly-defined territory across engines; at 0.78 over near-white cards the tail's missing
   blur is not perceptible. If that ever needs revisiting, a mask-image fade over a blurred
   layer is the technique - not a second gradient.

   The white 0.65 border stays here: on desktop it is a glass highlight, not the dark
   hairline the phone had, so it never read as the cut. The drop shadow goes, same as touch -
   with a fade below it, a shadow is a second competing edge. */
@media (hover: hover) and (pointer: fine) {
  .rsticky::after {
    height: 44px;
    background: linear-gradient(to bottom,
      rgba(255,255,255,0.78) 0, rgba(255,255,255,0.78) 16px, rgba(255,255,255,0) 100%);
  }
  .rsticky.is-stuck::after { opacity: 1; }
  .rsticky.is-stuck { box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) { .rsticky::after { transition: none; } }

/* THE SEARCH DOES NOT COLLAPSE, and it must not be made to again.
   It used to fold away on phones once the bar pinned. That made .rsticky two different
   heights - 189px open, 131px folded - and the early release (see setStickyRange in the
   page script) needs the wrapper pinned to ONE height to work. Measured in either state
   the other was wrong by 58px: too short and the last row overflowed into the footnote so
   the text collided, too tall and a dead gap opened under the cards. Scrolling up and down
   flipped between the two, which is what read as random shifting.
   The two features are mutually exclusive. The client chose the early release (10 Aug
   2026), so the bar now keeps a constant height and the wrapper measurement is
   trustworthy. Bringing the collapse back breaks the release, not just the tidiness. */
/* The header is not an ancestor of the rates section, so the offset cannot be inherited -
   :has() on the body is what lets the bar react to the header's state. Header pinned and
   visible: drop 72px (the .hdr .wrap row height) so the bar sits under it. Header absent,
   or slid away on a scroll down: go back to the top of the viewport. */
body:has(.hdr.is-fixed:not(.is-hidden)) .rsticky { top: 72px; }
/* The desktop header row is 86px since the gap was absorbed into it (see the --obar-gap
   note near the .obar rules) - the pinned dock follows. AFTER the base rule on purpose:
   same specificity, so source order is what lets this win. */
@media (min-width: 951px) {
  body:has(.hdr.is-fixed:not(.is-hidden)) .rsticky { top: 86px; }
}
@media (prefers-reduced-motion: reduce) { .rsticky { transition: none; } }
/* DESKTOP: ONE line - [search, left] [toggle, CENTRED] ["Rates as of ...", right].
   `minmax(0,1fr) auto minmax(0,1fr)` is what makes the toggle land on the TRUE centre: the
   two side columns are forced equal, so the middle sits dead centre regardless of the search
   and the timestamp being different widths (justify-content or auto margins would only centre
   it in the leftover space). The minmax(0,...) floors let the side tracks shrink below their
   content instead of blowing the card out on a small laptop.
   Mobile re-declares display:flex below and inherits none of this. */
.rtool {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 12px 16px;
  padding: 16px 16px 0;
}
.rseg {
  margin-bottom: 0;
  flex: 0 1 340px;
  min-width: 260px;
}
/* Desktop placement (client). Explicit grid-column, so the DOM order (toggle, timestamp,
   search) is free to stay as it is - and that order IS the mobile order, which is why mobile
   needs no reshuffling at all. */
.rtool .rsearch {
  /* grid-ROW is required, not just the column: with rows left automatic, the placement
     cursor advances past column 1 (the toggle and timestamp are declared before this in the
     DOM), so a column-1 item lands on a NEW row. Pinning all three to row 1 keeps the line. */
  grid-row: 1;
  grid-column: 1;
  justify-self: start;
  width: 100%;       /* fills its track, so it scales with the viewport */
  max-width: 340px;  /* SAME cap as .rtool .rseg (client) - the line reads as organised only
                        if the search and the toggle match; at 420 it was the odd one out */
  min-width: 0;      /* lets it give way first when the row gets tight */
  margin: 0;
}
.rtool .rseg {
  grid-row: 1;
  grid-column: 2;
  justify-self: center;
  width: 340px;      /* flex-basis is ignored on a grid item, so state the width outright */
  margin: 0;
}
.rtool #rate-time {
  grid-row: 1;
  grid-column: 3;
  justify-self: end;
}
/* Between the phone breakpoint and ~1080px there is not enough room for all three on one
   line, and it fails QUIETLY rather than obviously: measured at 1024px the date stamp wraps
   to two lines and the longer "Search a currency to sell back" placeholder truncates (needs
   230px, gets 214px) - buy mode fits, so it only breaks on one side of the toggle. Fall back
   to the two-row arrangement here, which has room to spare. Affects iPad landscape and any
   part-width desktop window. */
@media (min-width: 901px) and (max-width: 1080px) {
  .rtool {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .rtool .rsearch { grid-row: 1; grid-column: 1; }
  .rtool #rate-time { grid-row: 1; grid-column: 2; }
  .rtool .rseg { grid-row: 2; grid-column: 1 / -1; justify-self: center; }
}
/* Block, not inline-flex: the stamp carries the date now, so it is long enough to wrap on a
   phone. As a flex row the dot stranded itself halfway down the two-line block and the
   wrapped line hung off to the left. As normal inline content the dot rides the first line
   and both lines can be centred. */
.rtool .m {
  display: block;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--slate);
  font-weight: 600;
  padding-right: 4px;
}
/* The date + time never splits across lines, so the only break point is after "as of" -
   the wrap lands in the one place that reads correctly, at any width. */
.rtool .m .rt-val { white-space: nowrap; }
/* Gold, not green (client 2026-07-16): this dot means "live", and the brand accent says that
   in our own voice. The GREEN dots elsewhere are left alone on purpose - there green is
   semantic (open for business), not decorative, and gold would throw that meaning away. */
#rate-time::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--gold);
  display: inline-block;
  vertical-align: middle;
  position: relative;
  top: -1px;
  margin-right: 6px;
  animation: updpulse 2s ease-in-out infinite;
}
#rate-time.stale::before {
  background: #c2c7cf;
  animation: none;
}
/* Caption above the list: names the side you are on. Deliberately quieter than a heading but
   darker than the .rexp footnote - it labels the rows, it is not another paragraph. The mode
   words are bold so the eye catches the ONE thing that changes. */
.rlead {
  padding: 14px 20px 0;
  font-size: 15.5px;   /* 13 -> 15.5 (client): this names which side you are on, so it has to
                          land without effort - it was reading as fine print */
  font-weight: 500;
  color: var(--slate);
  text-align: center;
}
.rlead b {
  font-weight: 700;
  color: var(--ink);   /* the mode words are the payload - they get full ink */
}
/* SHINE on the mode words (client: "it should look like it's shining all the time, so users
   can see whether it is buying or selling"). A narrow gold band travels through ink text,
   left to right, on a loop.
   Scoped to `b` - only "buying foreign cash" / "selling it back" - NOT the whole caption:
   gold body text on white is banned by the brand rules and measures ~1.7:1, so the text
   stays ink and gold only ever passes through it. Even mid-sweep the peak gold is a thin
   sliver; the rest of the band is a dark ink-to-gold blend that stays readable.
   Wrapped in @supports because the technique needs transparent text: if background-clip:text
   were unsupported the words would render INVISIBLE rather than merely unstyled. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .rlead b {
    background-image: linear-gradient(90deg,
      var(--ink) 0%,
      var(--ink) 46%,
      var(--gold) 50%,
      var(--ink) 54%,
      var(--ink) 100%);
    /* 400% => the gradient is 4x the text width, so the gold is off-canvas for two thirds of
       the loop: one quick pass, then a pause, instead of a constant strobe. */
    background-size: 400% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: rleadshine 3.5s linear infinite;
  }
}
/* 100% -> 0% is what makes the band travel LEFT to RIGHT (not right to left): at 100% the
   window sits on the gradient's right end, which is solid ink with the gold still off to the
   left; as the position falls the gold enters at the left edge and exits at the right. Both
   ends of the loop show solid ink, so the restart is seamless. */
@keyframes rleadshine {
  0%   { background-position: 100% 0; }
  100% { background-position: 0% 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* a permanently-moving highlight is exactly what this setting exists to stop - and the
     text must be handed its ink back, or it would sit here transparent and invisible */
  .rlead b {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: var(--ink);
    color: var(--ink);
  }
}

/* Footnote under the currency list. It used to sit ABOVE the list, hence the old top-only
   padding; as the closing line it needs the bottom padding instead, and a hairline to set it
   apart from the rows. .rlist's own 12px already provides the gap above. */
.rexp {
  padding: 12px 20px 16px;
  margin-top: 4px;
  border-top: 1px solid rgba(16, 24, 40, 0.06);
  font-size: 13px;
  color: var(--muted);
  font-weight: 500;
  text-align: center;   /* centred at every width (client) - it closes the card, so it reads
                           as a caption under the list rather than a stray left-aligned line */
}
.rlist {
  display: grid;
  /* minmax(0,1fr), NOT 1fr: a 1fr track's automatic minimum is min-content, so one wide row
     (IDR - "1 AUD = Rp11,940.3666" / "A$1,000 -> Rp11,940,367") drags the whole column wider
     than the card and .ratebox's overflow:hidden then CLIPS every rate value. Flooring the
     track at 0 keeps the rows inside the card; the currency NAME ellipsises instead. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 6px;
  padding: 12px;
}
.rrow {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-width: 0; /* let the row shrink inside its grid track instead of forcing it wider */
  text-align: left;
  /* A row is an <a> since 7 Sep 2026, not a <button> - it navigates to the currency's own
     page. These two undo the anchor defaults so the row looks exactly as it always did; the
     rest of this block was written for a button and needs no change. A row for a currency we
     have no page for yet carries no href and still loads the calculator instead. */
  text-decoration: none;
  color: inherit;
  background: rgba(255,255,255,0.5);
  border: 1px solid rgba(255,255,255,0.7);
  border-radius: 16px;
  padding: 14px 16px;
  font-family: inherit;
  cursor: pointer;
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.rrow:hover {
  background: #ffffff;
  transform: translateY(-1px);
  box-shadow: 0 10px 24px -14px rgba(16,24,40,0.25);
}
.rrow:hover .rr-go {
  opacity: 1;
  transform: none;
}
.rrow img.fl {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid rgba(0,0,0,0.08);
  flex: none;
}
/* Row identity: the CODE leads (big + bold), the full currency name is the quiet second line */
.rr-id {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}
.rr-id b {
  font-size: 17px;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: 0.01em;
  line-height: 1.2;
  white-space: nowrap;
}
.rr-id i {
  font-style: normal;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rr-val {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex: none;
}
/* The rate itself is the hero of the row: "1 AUD =" stays small, the value is big and bold
   so a visitor reads the rate at a glance without effort (client feedback). */
.rr-rate {
  display: flex;
  align-items: baseline;
  gap: 5px;
  color: var(--ink);
}
/* 12.5px is a CEILING, not a preference. The client asked for 14.5 (14 Aug 2026); at that
   size the "A$1 = US$0.7007" line wraps on SIX of the 22 cards - every currency with a long
   symbol prefix: USD, NZD, HKD, AED, TWD, SAR - which left four different card heights and a
   ragged grid. Measured on the 171px card the full-bleed board now gives:
     14.5px -> 6 wrapped    14px -> 2    13px -> 1    12.5px -> 0
   So this is as close to the request as the layout allows, and still well up from the
   original 11.5. Raising it again means accepting wrapped rate lines, or shortening the
   label, or dropping to one card per row. */
.rr-rate em {
  font-style: normal;
  font-size: 12.5px;
  color: var(--muted);
  font-weight: 500;
}
.rr-rate .v {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.rr-eq {
  font-size: 14.5px;
  color: var(--muted);
  font-weight: 500;
}
.rr-go {
  font-size: 18px;
  color: var(--muted);
  flex: none;
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity .18s ease, transform .18s ease;
}
.rfoot {
  display: flex;
  justify-content: center;
  padding: 4px 16px 16px;
}
/* .rfoot is display:flex, which outranks the browser's built-in [hidden] rule - so hiding
   the expand control needs this. Driven by RATES_SHOW_ALL in the script. */
.rfoot[hidden] { display: none; }
.rall {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 10px 18px;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  transition: box-shadow .18s ease, transform .18s ease;
}
.rall:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 18px -10px rgba(16,24,40,0.2);
}
.rall .material-symbols-outlined {
  font-size: 18px;
}
.rsearch {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Capped and centred (client): full-width across a 1215px card read as a banner, not a
     search field. The width/max-width pair does both breakpoints in one rule - on desktop the
     cap wins and `auto` margins centre it under the caption; on a phone 100%-24px wins and
     reproduces the old 12px gutters, which a bare `margin:auto` would have thrown away. */
  width: calc(100% - 24px);
  max-width: 420px;
  margin: 12px auto 0;
  padding: 0 16px;
  background: rgba(255,255,255,0.65);
  /* A REAL border (client: "make the search bar more visible"). It was rgba(255,255,255,0.8) -
     white on a white glass panel - so the field had no edge at all and did not read as
     something you could type into. */
  border: 1px solid rgba(16, 24, 40, 0.12);
  border-radius: 999px;
  transition: box-shadow .18s ease, background .18s ease, border-color .18s ease;
}
.rsearch:focus-within {
  background: #ffffff;
  border-color: var(--gold);   /* matches the currency picker's search on focus */
  box-shadow: 0 0 0 3px rgba(254,185,0,0.28);
}
.rsearch .material-symbols-outlined {
  font-size: 20px;
  color: var(--muted);
  flex: none;
}
.rsearch input {
  flex: 1;
  border: none;
  background: transparent;
  font-family: inherit;
  /* 16px IS THE FIX - do not drop it back to 14.5px. iOS Safari force-zooms the page on focus
     for any input under 16px, and the only other "fixes" are viewport hacks (maximum-scale /
     user-scalable=no) that disable pinch-zoom for everyone. The calculator's inputs never had
     this bug purely because they are 23px. */
  font-size: 16px;
  font-weight: 500;
  color: var(--ink);
  padding: 12px 0;
  outline: none;
  min-width: 0;
}
.rsearch input::placeholder {
  color: var(--muted);
  font-weight: 500;
}
/* Clear control - a bare glyph, no background or border (client), showing only once
   something is typed. `align-self: stretch` costs nothing visually but turns a 20px glyph
   into a full-height tap target. */
.rclear {
  flex: none;
  align-self: stretch;
  width: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  padding: 0;
  color: var(--muted);
  cursor: pointer;
  transition: color .18s ease;
}
/* REQUIRED: .rclear is display:flex, which outranks the browser's built-in [hidden] rule -
   the same trap .rfoot had. Without this the control never hides. */
.rclear[hidden] { display: none; }
.rclear:hover { color: var(--ink); }
/* `inherit`, not a fixed colour: .rsearch .material-symbols-outlined already pins the glyph
   to --muted at equal specificity, which would swallow the :hover on the button above. */
.rclear .material-symbols-outlined {
  font-size: 20px;
  color: inherit;
}
.rempty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 28px 16px;
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 500;
}
@media (max-width: 900px) {
  .rlist { grid-template-columns: minmax(0, 1fr); } /* see the desktop rule - must be minmax, not 1fr */
  /* display:flex is REQUIRED here, not redundant: the base rule is now display:grid, and
     flex-direction alone would be ignored, dropping the desktop two-row grid onto phones. */
  .rtool { display: flex; padding: 14px 12px 0; flex-direction: column; align-items: center; gap: 10px; }
  /* 12px (from 12.5) purely for headroom: the widest stamp of the year - "27 June 2026,
     12:34 PM (AEST)" - is 278px at 12.5px against the 286px available here, and the whole
     point of the short label is that it holds ONE line. 12px takes the worst case to 267px.
     Inter is the widest case; the system fallback measures narrower, so it is not a risk. */
  .rtool .m { text-align: center; padding-right: 0; font-size: 12px; }  /* the 4px would bias the centring */
  /* MOBILE IS UNCHANGED and must stay that way - it renders in plain DOM order (toggle,
     timestamp, search), so `order:0` simply undoes the desktop re-arrangement above.
     These selectors are `.rtool X` (0,2,0) to MATCH the desktop rules, not the old bare
     `.rseg` (0,1,0), which would now silently lose to `.rtool .rseg` and drag the desktop
     layout onto phones. Media queries add no specificity.
     The 2px top margin is not arbitrary: .rtool's 10px gap + 2px reproduces the exact 12px
     the search used to get from its own margin when it sat outside .rtool. width:100% (not
     the base calc(100%-24px)) keeps it at 286px, because 100% now resolves against .rtool's
     already-padded content box rather than the card's. */
  .rtool .rseg { width: 100%; max-width: 360px; flex: none; margin: 0; }
  .rtool .rsearch { width: 100%; max-width: 420px; flex: none; margin: 2px 0 0; }
  /* Narrower side padding buys the width the bigger caption needs to stay on ONE line at
     360px: "Rates for buying foreign cash" is ~267px at 15.5px, and 20px sides left only
     270px. 12px sides give 286px. */
  .rlead { padding: 14px 12px 0; }
  /* Centred search text on phones (client). The icon is HIDDEN here - left-anchored, it read
     as off-balance against centred text. With it gone the input is the pill's only child and
     inherits the symmetric 16px padding, so text-align:center alone lands on the true centre.
     The 30px right padding that used to offset the icon had to go WITH it: keeping it would
     push the text 15px the other way. If the icon ever comes back, both must return. */
  /* `>` is REQUIRED: a descendant selector here would also hide the clear button's own glyph,
     which is a .material-symbols-outlined nested inside .rclear. Only the search icon is a
     direct child of .rsearch. */
  .rsearch > .material-symbols-outlined { display: none; }
  .rsearch input { text-align: center; }
  /* The clear button is a flex item, so it shrinks the input from the RIGHT and would drag
     the centred text 17px off the pill's centre while typing. Mirroring its footprint
     (24px button + 10px gap) as left padding cancels that - the same trick the search icon
     needed on desktop. Only while the button is actually showing. */
  .rsearch:has(.rclear:not([hidden])) input { padding-left: 34px; }
  .rexp { padding: 12px 16px 14px; }   /* text-align:center now lives on the base rule */
  .rrow { padding: 12px 13px; gap: 11px; }
  .rrow img.fl { width: 36px; height: 36px; }
  .rr-id b { font-size: 15.5px; }
  .rr-id i { font-size: 12px; }
  .rr-rate .v { font-size: 17px; }   /* still big and glanceable on a phone */
  .rr-eq { font-size: 13.5px; }
  .rr-go { display: none; }
}

/* Reviews section */
.rev .grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
.rcard {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  border-radius: var(--r);
  padding: 32px;
}
.rcard .stars {
  margin-bottom: 16px;
}
.rcard p {
  color: var(--ink);
  font-size: 15px;
  line-height: 1.6;
  margin-bottom: 24px;
  font-weight: 500;
}
.rcard .by {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--slate);
  font-weight: 600;
}
.rcard .av {
  width: 32px;
  height: 32px;
  border-radius: 6px; /* square-ish so a Local Guide star badge in the corner is not clipped */
  overflow: hidden;
  background: var(--mist);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12px;
}
.gbadge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04);
  border-radius: 99px;
  padding: 12px 20px;
  margin-top: 40px;
  font-weight: 700;
  font-size: 14px;
}
.gbadge b {
  font-size: 18px;
}

/* Google trust band (under the hero) */
.trustband .tbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  border-radius: var(--r);
  padding: 20px 28px;
  margin-bottom: 24px;
}
.trustband .tsum { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.trustband .glogo { display: flex; align-items: center; }
.trustband .tnums { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.trustband .tnums b { font-size: 26px; letter-spacing: -0.02em; }
.trustband .tnums .cnt { color: var(--muted); font-size: 13px; font-weight: 500; }
.trustband .tgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.tcard .twho { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.tcard .twho b { font-size: 14px; display: block; color: var(--ink); }
.tcard .tdate { display: block; font-style: normal; color: var(--muted); font-size: 12px; font-weight: 500; margin-top: 2px; }
.tcard .stars { margin-bottom: 12px; }
.tcard p { margin-bottom: 18px; }
.tcard .tsrc { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--slate); font-weight: 600; }
.tcard .tsrc svg { width: 15px; height: 15px; }
/* "Read all reviews" card - balances the grid to a clean 2x3 and gives the see-all path */
.tcard-all { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 7px; text-decoration: none; }
.tcard-all .glogo svg { display: block; margin-bottom: 2px; }
.tcard-all b { font-size: 16px; color: var(--ink); }
.tcard-all p { color: var(--slate); font-size: 13.5px; margin: 0 0 4px; max-width: 260px; }
.tcard-all .all-cta { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--ink); font-size: 13.5px; }
.tcard-all .all-cta .material-symbols-outlined { font-size: 18px; transition: transform .2s ease; }
.tcard-all:hover { border-color: rgba(0,0,0,0.12); }
.tcard-all:hover .all-cta .material-symbols-outlined { transform: translateX(3px); }
/* Reviews reveal wrapper - collapsed with a glass fade + "Read more" on phones only */
.treveal { position: relative; }
.tmore { display: none; }
/* Reviews skeleton (first load, before the live Google reviews arrive). CSS-driven so the
   skeleton is the FIRST paint - the curated cards never flash; they only appear if the API fails. */
.treveal.revload .tgrid > :not(.rskel) { display: none; }
.treveal:not(.revload) .rskel { display: none; }
.rskel { pointer-events: none; }
.rskel .sk { display: block; border-radius: 6px; background: linear-gradient(90deg, rgba(15,23,42,0.06) 25%, rgba(15,23,42,0.11) 37%, rgba(15,23,42,0.06) 63%); background-size: 400% 100%; animation: rsksh 1.4s ease infinite; }
@keyframes rsksh { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) { .rskel .sk { animation: none; } }
.rskel .av.sk { border-radius: 6px; }
.rskel .sk-nm { width: 96px; height: 11px; margin-bottom: 6px; }
.rskel .sk-dt { width: 58px; height: 9px; }
.rskel .sk-st { width: 94px; height: 16px; margin-bottom: 14px; }
.rskel .sk-tx { width: 100%; height: 10px; margin-bottom: 8px; }
.rskel .sk-tx.s2 { width: 94%; }
.rskel .sk-tx.s3 { width: 66%; margin-bottom: 16px; }
.rskel .sk-src { width: 120px; height: 11px; }
@media (max-width: 900px) {
  .trustband .tgrid { grid-template-columns: 1fr; }
  .trustband .tbar { justify-content: center; text-align: center; }
}
@media (max-width: 600px) {
  /* Center the Google summary: logo on top, rating text centered under it (like the button) */
  .trustband .tbar { flex-direction: column; gap: 14px; }
  .trustband .tsum { flex-direction: column; align-items: center; gap: 8px; }
  .trustband .tnums { justify-content: center; }
  /* Centre the review cards on phones (matches the how-it-works / why-us cards).
     .twho and .tsrc are block-level flex rows, so they need justify-content, not text-align. */
  .tcard { text-align: center; }
  .tcard .twho { justify-content: center; }
  .tcard .tsrc { justify-content: center; }
  /* ...but the avatar + name + date stay a GROUP: the name and date keep their desktop
     left-alignment to each other (text-align:center was centring the date under the name,
     which read as broken); only the whole group gets centred, via .twho above. */
  .tcard .twho-n { text-align: left; }
  .rskel .sk { margin-left: auto; margin-right: auto; }
  /* Collapse the list to ~2 reviews behind a glassy fade; "Read more" reveals the rest */
  .treveal:not(.open) .tgrid { max-height: 560px; overflow: hidden; }
  .treveal:not(.open)::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 190px; z-index: 1;
    pointer-events: none;
    background: linear-gradient(to bottom, rgba(248,250,252,0), #f8fafc 80%);
  }
  .tmore {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    position: absolute;
    left: 50%;
    bottom: 6px;
    transform: translateX(-50%);
    z-index: 2;
    /* Absolutely positioned with left:50%, so its shrink-to-fit width is capped at HALF the
       container - on a 360px Android that is too narrow and the label wrapped onto 2 lines.
       nowrap raises its minimum width to the full label so it always stays on one line. */
    white-space: nowrap;
    font-family: inherit;
    font-weight: 700;
    font-size: 13px;
    color: var(--ink);
    padding: 12px 20px;
    border-radius: 99px;
    cursor: pointer;
    background: rgba(255,255,255,0.6);
    backdrop-filter: blur(18px) saturate(170%);
    -webkit-backdrop-filter: blur(18px) saturate(170%);
    border: 1px solid rgba(255,255,255,0.85);
    box-shadow: 0 10px 26px rgba(16,24,40,0.14);
  }
  .tmore .material-symbols-outlined { font-size: 18px; }
  .treveal.open::after, .treveal.open .tmore { display: none; }
}

/* Location & Maps */
.loc {
  display: grid;
  grid-template-columns: 1fr 1.1fr;
  gap: 48px;
  align-items: stretch;
}
.lcta {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 24px;
}
.locinfo h2 {
  margin-bottom: 24px;
}
/* Bumped 15 -> 16.5px (client 2026-07-16): the name, address, phone and access line are the
   details a customer actually has to READ and act on, so they get read-at-a-glance size. */
.r2 {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
  color: var(--slate);
  font-size: 16.5px;
  line-height: 1.55;
  text-decoration: none;
  transition: opacity 0.2s ease;
}
.r2:hover {
  opacity: 0.85;
}
.r2:hover b {
  color: var(--gold-deep);
}
.r2 .material-symbols-outlined {
  color: var(--gold);
  flex: none;
  font-size: 24px;   /* pinned: the icon font would otherwise scale with the 16.5px bump */
}
.r2 b {
  color: var(--ink);
  transition: color 0.2s ease;
}
.hours {
  border: 1px solid rgba(0,0,0,0.1);
  border-radius: var(--r);
  overflow: hidden;
  margin: 24px 0;
  background: rgba(255,255,255,0.4);
  backdrop-filter: blur(12px);
}
.hours div {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 15px 20px;
  font-size: 15.5px;   /* 14 -> 15.5 (client): opening times and the Closed days must be
                          readable at a glance - they decide whether someone walks over */
  border-bottom: 1px solid rgba(0,0,0,0.05);
}
.hours div:last-child {
  border-bottom: none;
}
.hours b {
  color: var(--ink);
  font-weight: 600;
}
.hours .cl {
  color: var(--slate);
  background: rgba(0,0,0,0.02);
}
.map {
  border-radius: var(--r);
  overflow: hidden;
  border: 1px solid rgba(0,0,0,0.1);
  min-height: 460px;
  box-shadow: 0 20px 40px rgba(0,0,0,0.05);
}
.map iframe {
  width: 100%;
  height: 100%;
  min-height: 460px;
  border: 0;
  display: block;
}

/* FAQ Accordion Section */
.faq-accordion {
  max-width: 760px;
  margin: 12px auto 0;
}
.faq-item {
  border-bottom: 1px solid rgba(0,0,0,0.1);
}
.faq-item:first-child {
  border-top: 1px solid rgba(0,0,0,0.1);
}
.faq-trigger {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 24px 8px;
  background: transparent;
  border: none;
  font-family: inherit;
  font-size: 17px;
  font-weight: 700;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease;
}
.faq-trigger:hover {
  color: var(--slate);
}
.faq-icon {
  color: var(--slate);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  font-size: 24px;
}
.faq-item.open .faq-icon {
  transform: rotate(180deg);
}
.faq-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.faq-item.open .faq-content {
  max-height: 250px;
}
.faq-content p {
  padding: 0 8px 24px;
  font-size: 15px;
  color: var(--slate);
  line-height: 1.6;
}

/* Premium light glass footer */
.footer-premium {
  padding: 80px 0 0;
  position: relative;
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(40px) saturate(200%);
  -webkit-backdrop-filter: blur(40px) saturate(200%);
  border-top: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.02);
}
.footer-premium .wrap {
  display: flex;
  flex-direction: column;
  gap: 64px;
}
.footer-premium .grid-top {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1fr 1fr;
  gap: 48px;
}
.footer-premium .brand-col {
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-items: flex-start;
}
.footer-premium .brand-col p {
  font-size: 14px;
  color: var(--slate);
  line-height: 1.7;
  max-width: 280px;
}
/* CONTACT BLOCK in the footer brand column. A resting underline, not the hover-only gold
   rule the link columns use: these sit inside a paragraph, where nothing else marks them
   as tappable, and on a phone the number and the address are the two things people
   actually reach for.
   Hover darkens the TEXT and turns the UNDERLINE gold, rather than turning the text gold.
   Gold body copy on white fails contrast - the standing rule is gold in the edge, ink in
   the type. */
.footer-premium .fcontact a {
  /* These four undo the legacy `footer a` rule further down the file, which makes EVERY
     link in the footer `display: block; padding: 7px 0; font-size: 14px; font-weight: 500`
     - a nav-column tap target. Inside a paragraph that turned each line into its own
     block with 14px of padding, so the address, phone and email sat ~60px apart instead
     of one 22px line-height, and rendered a size larger than the ABN line above them.
     `footer .legal a { display: inline }` exists for the same reason. */
  display: inline;
  padding: 0;
  font-size: inherit;
  font-weight: inherit;
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(16,24,40,0.30);
  text-underline-offset: 3px;
  transition: color .2s ease, text-decoration-color .2s ease;
}
.footer-premium .fcontact a:hover {
  color: var(--ink);
  text-decoration-color: rgba(254,185,0,0.9);
}
.footer-premium h4 {
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink);
  margin-bottom: 20px;
}
.footer-premium .links {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.footer-premium .links a,
.footer-premium .links span {
  font-size: 14px;
  font-weight: 600;
  color: var(--slate);
  transition: color 0.2s;
}
.footer-premium .links a:hover {
  color: var(--gold);
}
.footer-premium .bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 24px 0 32px;
  border-top: 1px solid rgba(0, 0, 0, 0.06);
  font-size: 13px;
  color: var(--slate);
  font-weight: 500;
  flex-wrap: wrap;
  gap: 16px;
}
.footer-premium .legal-links {
  display: flex;
  gap: 20px;
}
.footer-premium .legal-links a {
  font-size: 13px;
  color: var(--slate);
  font-weight: 500;
}
.footer-premium .legal-links a:hover {
  color: var(--gold);
}
footer .cols {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr 1fr;
  gap: 48px;
  padding-bottom: 56px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
footer h4 {
  color: #fff;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 20px;
}
footer a {
  color: #9ca3af;
  font-size: 14px;
  display: block;
  padding: 7px 0;
  font-weight: 500;
  transition: color 0.15s;
}
footer a:hover {
  color: #fff;
}
footer .fb .nm {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
}
footer .fb .mk {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
}
footer .fb .mk .material-symbols-outlined {
  font-size: 18px;
}
footer .fb .nm b {
  color: #fff;
  font-size: 20px;
  letter-spacing: -0.04em;
}
footer .fb .full {
  color: #fff;
  font-weight: 600;
  font-size: 14px;
  margin-bottom: 8px;
}
footer .fb p {
  color: #6b7280;
  font-size: 13px;
  line-height: 1.8;
}
footer .legal {
  display: flex;
  justify-content: space-between;
  padding: 24px 0;
  font-size: 13px;
  color: #6b7280;
  flex-wrap: wrap;
  gap: 12px;
}
footer .legal a {
  display: inline;
}

/* Desktop Styles override */
@media (min-width: 951px) {
  .hero {
    /* min-height:100vh REMOVED (client, 7 Aug 2026). On a rates page the hero was eating
       the whole first screen, so nobody saw a rate without scrolling - which is the one
       thing they came for. The hero now sizes to its content and the rate board starts
       inside the fold. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 104px 0 40px;   /* header is absolute (out of flow): 72px clearance + breathing */
  }
  .hero > .wrap {
    width: 100%;
    align-self: stretch;
  }
  .hgrid {
    display: grid;
    grid-template-columns: 1.1fr 440px;
    grid-template-areas: 
      "head calc" 
      "cta calc" 
      "meta calc" 
      ". calc";
    grid-template-rows: auto auto auto 1fr;
    column-gap: 64px;
    align-items: start;
  }
  .hhead {
    grid-area: head;
    text-align: left;
  }
  .hhead .sub {
    margin: 0;
  }
  .hcta {
    grid-area: cta;
    display: flex;
    gap: 12px;
    margin-top: 36px;
    flex-wrap: wrap;
  }
  .hmeta {
    grid-area: meta;
    justify-content: flex-start;
    margin-top: 36px;
  }
  .calc-area {
    grid-area: calc;
    align-self: center;
    min-height: 480px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }
  .calc-stack {
    width: 100%;
    max-width: 440px;
  }
  .calc {
    position: relative;
    z-index: 2;
    width: 100%;
    max-width: 440px;
  }
}

/* Mobile & Tablet Styles overrides.
   BURGER BREAKPOINT - derived, not chosen. Re-measure whenever the nav changes shape.
     950  original. The menu needed 1021px to fit, so 950-1021 was a dead zone with no
          way to lay out and no fallback: "Exchange rates" split across two lines inside
          its own pill. That is what the client reported.
     1099 after raising it to clear that 1021.
     990  NOW. Find us joined the nav (+79) but the 172px phone button left it, so the
          header only needs 931px: brand 234 + menu 585 + 48 gutters + 64 breathing.
          Holding 1099 would have hidden a menu that fits comfortably for another
          170px of width, and pushed desktop users onto a burger for no reason. */
@media (max-width: 989px) {
  .menu, .hdr .cta-d {
    display: none;
  }
  .burger {
    display: flex;
  }
  /* margin-top:10% REMOVED (8 Aug 2026). A percentage margin resolves against the
     container's WIDTH, so this was silently adding 32.7px at 375px - on top of the
     hero's own padding - and the gap under the breadcrumbs could not be controlled
     from one place. It dates from when the hero carried the header's clearance; the
     breadcrumb bar does that now. .hero padding-top is the only control. */
}
@media (max-width: 900px) {
  section.band, .statement {
    --band-pad: 72px;
    padding: var(--band-pad) 0;
  }
  .feats, .steps, .loc, .faq-accordion, .rev .grid {
    grid-template-columns: 1fr;
  }
  /* Once the how-it-works and why-us cards stack to one column, centre their content.
     The icon wrappers are inline-flex, so text-align also centres the icons themselves. */
  .step, .feat {
    text-align: center;
  }
  .strip .wrap {
    justify-content: space-between;
    gap: 24px;
  }
  .stat b {
    font-size: 26px;
  }
  footer .cols {
    grid-template-columns: 1fr 1fr;
  }
  .cval input {
    width: 100px;
  }
  .cbody {
    gap: 8px;
    min-width: 0;
  }
  .chip, .cval, .selchip {
    min-width: 0;
  }
  td .nm {
    display: none;
  }
  th, td {
    padding: 14px 16px;
    font-size: 14px;
  }
}

@media (max-width: 600px) {
  footer .cols {
    grid-template-columns: 1fr;
    gap: 32px;
  }
}

/* Calculator — progressively shrink to fit small phones (modelled on Money2Travel's
   520/380 converter breakpoints): smaller padding, field height, code + value sizes. */
@media (max-width: 520px) {
  .calc { padding: 14px; gap: 7px; }
  /* Client, 10 Aug 2026: 13px -> 14px and weight 700 down here. The base weight is 600
     and the desktop block only lifts it above 601px, so it has to be restated. */
  .seg button { font-size: 14px; padding: 10px; font-weight: 700; }
  /* the rates toggle keeps its own, larger scale down here - see .seg.rseg button above */
  .seg.rseg button { font-size: 14.5px; }
  .crow { padding: 11px 14px; }
  .crow .clabel { font-size: 13px; }   /* was 12px (client, 10 Aug 2026) */
  .chip { font-size: 23px; }
  .chip .fl { width: 22px; height: 22px; }
  .cval, .cval input { font-size: 23px; }
  .selchip .ar { font-size: 21px; }
  .detail-row { font-size: 12.5px; min-height: 38px; padding: 8px 14px; }
}
/* Section intro paragraphs on phones (client, 10 Aug 2026). Kept OUT of the calculator
   block above so that block stays about the calculator.
   The base rule is `.head p { font-size: 18px }` at line ~1549 and the desktop scale only
   lifts it above 601px, so 17px here just has to sit later in the file than the base -
   same specificity, source order decides.
   600 rather than the 520 originally specified: at 520 this read 17 / 18 / 20 across the
   three bands, stepping UP mid-mobile before the desktop jump. 600 makes it one clean
   step at the same boundary the rest of the phone layout already uses. */
@media (max-width: 600px) {
  .head p { font-size: 17px; }
  /* Weight only. The size is already 14px right across mobile (the 520 block restates the
     same 14px), so the 600 weight in the base rule was the single value making the
     521-600 band disagree with the bands either side of it. */
  .seg button { font-weight: 700; }
}
@media (max-width: 380px) {
  .calc { padding: 12px; }
  .crow { padding: 10px 12px; }
  .chip { font-size: 20px; gap: 6px; }
  .chip .fl { width: 20px; height: 20px; }
  .cval, .cval input { font-size: 20px; }
  .selchip .ar { font-size: 19px; }
  .detail-row { font-size: 12px; padding: 7px 12px; }
  .detail-label .material-symbols-outlined { font-size: 16px; }
}

/* THE PICKER IS THE HOMEPAGE'S, VERBATIM (client decision, 18 Aug 2026 - closes seven rounds
   of iPhone tuning). The client tested the deployed homepage's picker on his iPhone and
   accepted it as it is ("cheap but working good - a little issue, acceptable"), and asked for
   the full website to behave EXACTLY like it, nothing more. Everything from .cpick-backdrop
   down to the end of the <=600px block below is a byte-for-byte copy of the picker CSS in
   Currency Wise Deployment/website/index.html: the backdrop is one element that fades its
   own opacity, the sheet keeps its blur(30) saturate(185) glass on every device, the phone
   sheet slides translateY(100%) -> 0, and there is no blur pad, no separate veil layer, no
   touch-only variant, no pre-painting and no will-change. The engineered variants are
   recorded in the glass-opening-pattern memory so nobody rebuilds them; do NOT re-engineer
   this block without the client asking. If the homepage's picker changes, copy it here
   again as a whole - and re-apply the ONE deliberate deviation, marked on .cpick below
   (the sheet opens opaque and settles to glass after landing; client, 18 Aug 2026; the
   homepage itself is untouched by his instruction). */
/* Currency picker — Revolut-style modal (desktop) / bottom sheet (mobile) */
.cpick-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1000;
  /* Lighter than it was (0.55): .cpick is a CHILD of this, so its backdrop-filter samples this
     veil FIRST. At 0.55 the "glass" just blurred a dark sheet and the page behind never read
     through. 0.35 still separates the dialog from the page while letting the calculator show. */
  background: rgba(17, 19, 24, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.22s ease, visibility 0.22s ease;
}
.cpick-backdrop.open {
  opacity: 1;
  visibility: visible;
}
.cpick {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 420px;
  max-width: calc(100vw - 32px);
  max-height: min(560px, calc(100vh - 80px));
  /* Glass, not solid white (client): translucent + a real backdrop blur, so the page behind
     reads through. The inset highlight is the top-edge sheen that sells it as glass.
     THE ONE DEVIATION FROM THE HOMEPAGE (client, 18 Aug 2026): the sheet arrives OPAQUE and
     settles to glass once it has landed. The backdrop above fades its opacity, and while an
     ancestor is mid-fade a child backdrop-filter cannot render (it samples an isolated,
     empty group), so on the homepage the sheet spends the whole 220ms open as 0.68 white
     with the page text readable through it, and the blur snaps in at the end. Fix chosen
     by the client: keep the fill at 0.96 (reads solid, hides the text) while opening, then
     ease it down to the 0.68 glass over 0.22s AFTER the 0.22s open - by which time the
     fade has finished and the blur is live, so what shows through is already blurred. On
     close the fill snaps back to 0.96 with no delay, so the sheet slides away as a clean
     card instead of going see-through while the dim fades out. Text is never visible
     unblurred in either direction. The glass therefore appears a beat after the sheet,
     not with it - deliberate, and the only shape that cannot misbehave: nothing expects a
     blur while anything is animating. `transition` is a shorthand: BOTH rules carry the
     transform entry, or the entrance dies silently. */
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(30px) saturate(185%);
  -webkit-backdrop-filter: blur(30px) saturate(185%);
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 24px;
  box-shadow: 0 32px 64px -16px rgba(0, 0, 0, 0.32),
              inset 0 1px 0 rgba(255, 255, 255, 0.65);
  overflow: hidden;
  transform: scale(0.97);
  transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1), background-color 0s;
}
.cpick-backdrop.open .cpick {
  transform: scale(1);
  background: rgba(255, 255, 255, 0.68);
  transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.22s ease 0.22s;
}
.cpick-handle {
  display: none;
}
.cpick-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 20px 12px;
}
.cpick-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.02em;
}
.cpick-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 50%;
  background: var(--mist);
  color: var(--ink);
  cursor: pointer;
  flex: none;
  transition: background-color 0.15s ease;
}
.cpick-close:hover {
  background: var(--line);
}
.cpick-search {
  position: relative;
  padding: 0 20px 12px;
}
.cpick-search input {
  width: 100%;
  height: 46px;
  padding: 0 16px 0 46px;
  border-radius: 12px;
  border: 1px solid rgba(16, 24, 40, 0.12);
  background: rgba(255, 255, 255, 0.55);   /* translucent, or it reads as a solid slab on the glass */
  font-family: inherit;
  /* 46px right mirrors the 46px left: the clear button sits in that gutter, so typed text can
     never run underneath it. Symmetric whether the button is showing or not. */
  padding-right: 46px;
  font-size: 16px;   /* 16 minimum or iOS Safari zooms the page on focus - see .rsearch input */
  color: var(--ink);
  outline: none;
  transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.cpick-search input:focus {
  border-color: var(--gold);
  background: #ffffff;
  box-shadow: 0 0 0 3px rgba(254, 185, 0, 0.14);
}
.cpick-search .search-icon {
  position: absolute;
  left: 34px;
  /* Centre on the INPUT, not on .cpick-search. `top:50%` resolved against the container's
     padding box - 58px, because of its 12px padding-bottom - while the input is only 46px,
     so the icon sat exactly half that padding (6px) too low. Spanning the input's own height
     and centring inside it keeps the icon right whatever padding the container carries. */
  top: 0;
  height: 46px;   /* == .cpick-search input's height */
  display: flex;
  align-items: center;
  color: var(--muted);
  font-size: 22px;
  pointer-events: none;
}
/* Clear control - mirrors the rates search bar's: a bare glyph, no background, only once
   something is typed. right:34px mirrors the search icon's left:34px, and it centres on the
   INPUT's 46px height (not the padded wrapper) for the same reason the icon does. */
.cpick-clear {
  position: absolute;
  right: 34px;
  top: 0;
  height: 46px;
  width: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  padding: 0;
  color: var(--muted);
  cursor: pointer;
  transition: color .18s ease;
}
/* REQUIRED: display:flex outranks the browser's built-in [hidden] rule (same trap as .rfoot
   and .rclear) - without this the control never hides. */
.cpick-clear[hidden] { display: none; }
.cpick-clear:hover { color: var(--ink); }
.cpick-clear .material-symbols-outlined { font-size: 20px; color: inherit; }
.cpick-label {
  padding: 4px 24px 8px;
  font-size: 12px;
  font-weight: 600;
  /* --slate for the same reason as .cpick-code: --muted lands at 2.48:1 on the glass, and at
     12px uppercase this is small text, so the 4.5 bar applies - the 3:1 large-text allowance
     does not cover it. */
  color: var(--slate);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.cpick-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 12px 12px;
  -webkit-overflow-scrolling: touch;
  /* Firefox: no ::-webkit-scrollbar there, and scrollbar-color takes a flat colour only -
     no gradient - so it gets the gold without the glass gradient. */
  scrollbar-width: thin;
  scrollbar-color: rgba(254, 185, 0, 0.7) transparent;
}
/* Gold glass scrollbar (client). A thumb is not a real element, so it cannot take a
   backdrop-filter - the "glass" is translucent gold + a vertical falloff + a white top sheen,
   which reads the same against the sheet behind it. */
.cpick-list::-webkit-scrollbar {
  width: 10px;
}
.cpick-list::-webkit-scrollbar-track {
  background: transparent;
}
.cpick-list::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, rgba(254, 185, 0, 0.85) 0%, rgba(254, 185, 0, 0.45) 100%);
  border-radius: 99px;
  /* transparent border + padding-box clip insets the thumb, so a 10px-wide track renders a
     6px pill with breathing room rather than a bar jammed against the edge */
  border: 2px solid transparent;
  background-clip: padding-box;
  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.cpick-list::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, rgba(254, 185, 0, 0.95) 0%, rgba(254, 185, 0, 0.65) 100%);
  background-clip: padding-box;
}
.cpick-item {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 12px;
  /* Transparent border on EVERY row, not just the selected one. Reserving the 1px here is
     what stops selecting a row from nudging its flag and text over by a pixel - with
     box-sizing:border-box a border eats the padding box, so adding it only to .sel would
     make the row twitch on selection. */
  border: 1px solid transparent;
  background: transparent;
  border-radius: 14px;
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.cpick-item:hover {
  background: var(--mist);
}
/* Selected reads exactly like hover (client), with the gold tick as the thing that tells them
   apart. It used to be `rgba(254,185,0,0.10)` - a 10% gold tint that composited to
   rgb(230,224,206) against the glass's rgb(227,228,229), i.e. a 3-point difference nobody
   could see. Sits AFTER :hover deliberately: equal specificity, so a selected row keeps this
   background while hovered rather than flickering between two states. */
.cpick-item.sel {
  background: var(--mist);
  /* Glassy gold rim (client) - deliberately the same vocabulary as the buy/sell toggle's
     pill: a TRANSLUCENT gold edge plus a white top sheen, not a solid gold line. */
  border-color: rgba(254, 185, 0, 0.65);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.cpick-item .fl {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  border: 1px solid rgba(0, 0, 0, 0.08);
}
.cpick-item-details {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  min-width: 0;
}
.cpick-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.cpick-code {
  font-size: 13px;
  /* --slate, not --muted. On the old solid-white sheet this was already only 3.20:1 - under
     the 4.5 bar - and the translucent glass took it to 2.48:1. Opacity is not the lever here:
     even an 85% sheet leaves --muted at ~2.85:1, because the grey itself is too light. Slate
     measures 5.87:1 on the glass. */
  color: var(--slate);
  font-weight: 500;
}
.cpick-item .chk {
  margin-left: auto;
  color: var(--gold);
  font-size: 22px;
  opacity: 0;
}
.cpick-item.sel .chk {
  opacity: 1;
}
/* THE RATE BOARD GOES FULL-BLEED ON PHONES (client, 14 Aug 2026).
   72px of a 375px screen was going on padding before a card even started - 24px each side
   from .wrap, 12px each side from #rate-body. Pulling the board out to the screen edge takes
   the two cards from 145.5px to about 169.5px each, a 16% gain, which is real for the values
   that actually strain: IDR at "Rp1,195,604" and VND at "1,733,459".

   DONE WITH NEGATIVE MARGINS, NOT BY ZEROING .wrap's PADDING. .head (the title) and .bigamt
   (the call-us band) are siblings inside the same .wrap, and they must keep their margin -
   only the board breaks out. Negative margins leave them untouched.

   THE CORNERS AND SIDE BORDER MUST GO WITH IT. A 24px radius and a 1px border sitting hard
   against the viewport edge reads as a broken card, not a deliberate one. ALL FOUR corners
   are squared, not just the sides: once the panel spans the full width every corner is at a
   screen edge, so rounding any of them just curves the glass away from the edge and reverts
   to looking like a card that does not fit. Squared throughout, it reads as a band.
   It stays a distinct object through its fill and its top/bottom edges, not through corners.

   The -24px has to match .wrap's mobile padding exactly. If that padding ever changes, this
   changes with it or the board will sit off-centre.

   #rate-body keeps its own 12px so cards never touch the glass edge, and .rfoot/.rexp keep
   their 16px, so no text runs to the screen edge. */
@media (max-width: 600px) {
  .ratebox {
    margin-left: -24px;
    margin-right: -24px;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }
  /* Base for the "A$100 -> X" line on phones, down from 14.5 (client, 14 Aug 2026). This is
     the size MOST cards keep; fitEqLines() in the page script shrinks only the individual
     lines that still overflow, so a long value like IDR does not drag all 22 down with it.
     Lower base = fewer cards needing a fit = fewer different sizes on screen at once. */
  /* Base size only. The `white-space: nowrap` that makes the fit deterministic lives on
     `.lay-grid .rr-eq` in the 900px block below - (0,2,0) beats a plain `.rr-eq`, so setting
     it here would silently lose. See the note there. */
  .rr-eq { font-size: 13.5px; }
}

@media (max-width: 600px) {
  .cpick-backdrop {
    align-items: flex-end;
  }
  .cpick {
    width: 100%;
    max-width: 100%;
    /* FIXED height, not max-height - this was the bug. As a max-height the sheet sized itself
       to its CONTENT, so filtering to a single match collapsed it to a sliver; being bottom-
       anchored, that sliver ended up behind the keyboard. A fixed height pins the sheet, and
       .cpick-list (flex:1, overflow-y:auto) simply holds fewer rows - the match stays at the
       TOP, above the keyboard, which is where you are already looking. */
    height: 86vh;
    height: 86dvh;   /* dvh where supported: on Android the keyboard shrinks it clear of itself */
    max-height: 86vh;
    max-height: 86dvh;
    border-radius: 24px 24px 0 0;
    transform: translateY(100%);
  }
  .cpick-backdrop.open .cpick {
    transform: translateY(0);
  }
  .cpick-handle {
    display: block;
    width: 40px;
    height: 4px;
    border-radius: 99px;
    background: var(--line);
    margin: 10px auto 2px;
    flex: none;
  }
}

@media (max-width: 900px) {
  .footer-premium .grid-top {
    grid-template-columns: 1fr 1fr;
    gap: 40px;
  }
  .footer-premium .brand-col {
    grid-column: 1 / -1;
  }
  .footer-premium .wrap {
    gap: 40px;
  }
}
@media (max-width: 600px) {
  .footer-premium .grid-top {
    grid-template-columns: 1fr;
    gap: 32px;
    text-align: center;
  }
  .footer-premium {
    padding-top: 56px; /* NOT the padding shorthand - that would wipe the action-bar clearance below */
  }
  .footer-premium .brand-col {
    align-items: center;
  }
  .footer-premium .brand-col p {
    max-width: 100%;
  }
  /* The payment row is a flex container, so the column's text-align:center reaches its
     label but not its flex items - centre the items themselves. */
  .footer-premium .fpay {
    justify-content: center;
  }
  .footer-premium .links {
    align-items: center;
  }
  .footer-premium .bottom {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 12px;
  }
  /* Legal links: smaller type + nowrap per link, so each one stays whole and they wrap as
     tidy centred rows instead of breaking mid-phrase ("In-Store Terms &" / "Conditions"). */
  .footer-premium .legal-links {
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px 14px;
  }
  .footer-premium .legal-links a {
    font-size: 11.5px;
    white-space: nowrap;
  }
}

/* skeleton shimmer */
@keyframes cwShim{0%{background-position:200% 0}100%{background-position:-200% 0}}
.skel{color:transparent!important;-webkit-text-fill-color:transparent!important;caret-color:transparent;display:inline-block;min-width:56px;border-radius:6px;background-color:rgba(120,125,135,.10)!important;background-image:linear-gradient(100deg,rgba(255,255,255,0) 20%,rgba(255,255,255,.75) 50%,rgba(255,255,255,0) 80%)!important;background-size:220% 100%;background-repeat:no-repeat;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.55);animation:cwShim 1.25s ease-in-out infinite}
.skel *{color:transparent!important;-webkit-text-fill-color:transparent!important}
/* The scroll-reveal rules that lived here are REMOVED (client, 8 Aug 2026). Sections are
   simply present - nothing fades or slides up as you scroll. See the matching note where
   the observer used to be, near the header-pin code. */

/* Rate explainer - exchange-rates page only. Type matches .faq-content p exactly
   (15px, --slate, 1.6) so the prose is the homepage's own paragraph style. */
/* --- the pull-quote. One line, set large, doing the work three paragraphs used to.
   Ink, not gold: gold on a light surface measures about 1.7:1. The gold appears as
   the quote mark and the rule underneath, which is decoration and may be gold. --- */
.pullq {
  /* px, NOT ch. `ch` resolves against THIS element's font-size, and the big type is on
     the <p> inside - so 26ch measured 26 x 10px = 260px and wrapped one sentence into a
     six-line column. */
  max-width: 640px;
  margin: 0 auto 44px;
  text-align: center;
  position: relative;
  padding-top: 34px;
}
.pullq::before {
  content: "\201C";                 /* a real left double quote, not a straight " */
  position: absolute;
  top: -14px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 76px;
  line-height: 1;
  color: var(--gold);
  opacity: 0.85;
}
.pullq p {
  font-size: clamp(24px, 3vw, 36px);
  font-weight: 700;
  line-height: 1.22;
  letter-spacing: -0.02em;
  color: var(--ink);
}
@media (max-width: 700px) { .pullq { max-width: 460px; padding-top: 28px; margin-bottom: 34px; } }

/* --- the three answers --- */
.qa { display: grid; gap: 16px; }
@media (min-width: 860px) { .qa { grid-template-columns: repeat(3, 1fr); } }
/* TRANSPORT ICONS on the Find us "Getting here" cards (client, 10 Aug 2026).
   Only those three carry .qa-ico. The rates page reuses .qacard for its rhetorical Q&A
   ("What is the rate Google shows me?") and gets NO icon - a mode of transport is a concrete
   thing worth a glyph, a rhetorical question is not. Because the icon is an extra element
   rather than a pseudo, the rates cards are untouched by this.

   Gold-deep, matching .ls-icon and the menu's location mark. That is a GRAPHIC in gold, not
   body text in gold - the brand rule bans the latter, not the former.

   aria-hidden: the h3 right below already says "By train or metro", so announcing the glyph
   would just repeat it. THE LIGATURE MUST EXIST IN THE SUBSET - the icon font here is a
   46-glyph cut, and a name that is not in it renders as the literal word "train". These three
   were added to NAMES in scripts/subset-material-symbols.py and the font rebuilt. */
.qa-ico {
  font-size: 40px;              /* client's own value, 10 Aug 2026 - was 26 */
  color: var(--gold);           /* client: plain gold everywhere on icons, not gold-deep */
  /* inline-block, not block: the box then hugs the glyph instead of stretching to the card
     width. That also makes "did the ligature resolve?" measurable - a missing one renders the
     literal word and the box blows out to ~170px instead of ~40px. */
  display: inline-block;
  margin-bottom: 10px;
}

.qacard {
  position: relative;
  padding: 26px;
  border-radius: var(--r);
  background: rgba(255,255,255,0.52);
  border: 1px solid rgba(255,255,255,0.75);   /* the fallback edge - see @supports below */
  -webkit-backdrop-filter: blur(16px) saturate(170%);
  backdrop-filter: blur(16px) saturate(170%);
  /* The inset white line along the top is the "light" - it is what separates real glass
     from a flat translucent panel, and it costs nothing. */
  box-shadow: var(--shadow-card), inset 0 1px 0 rgba(255,255,255,0.85);
}

/* GOLD GLAZING. A 1px ring that follows the card's radius: bright gold in the top-left
   and bottom-right corners, fading to a plain white glass rim as it reaches the middle
   of each edge.
   Why a masked pseudo-element and not border-image: border-image IGNORES border-radius,
   so a gradient border drawn that way squares off the corners. Instead this paints a
   gradient-filled box the size of the card and punches its own centre out, leaving only
   the 1px frame. Guarded by @supports because without mask the gradient would render as
   a solid sheet across the whole card - the fallback keeps the plain white rim above. */
@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
  .qacard { border-color: transparent; }
  .qacard::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1px;                    /* = the ring's thickness */
    background: linear-gradient(135deg,
      rgba(254,185,0,0.95)  0%,
      rgba(254,185,0,0.34) 26%,
      rgba(255,255,255,0.85) 50%,
      rgba(254,185,0,0.34) 74%,
      rgba(254,185,0,0.95) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    pointer-events: none;
  }
}

/* --- 3. THE GLAZING, IN MOTION ----------------------------------------------
   The gold travels slowly around the card edge on a 14s loop. A gradient's ANGLE
   is not animatable on its own - the browser has no idea an angle is a number -
   so it is registered as a typed custom property first, which is what makes it
   interpolable. Falls back to the static 135deg glazing wherever @property is
   unsupported, so nothing is lost.
   Affordable despite the page's blur count: this repaints a 1px ring, it does not
   move anything BEHIND a backdrop-filter, so no panel has to re-blur per frame. */
@property --qangle {
  syntax: "<angle>";
  initial-value: 135deg;
  inherits: false;
}
@supports (background: linear-gradient(var(--qangle), red, blue)) {
  .qacard::before {
    background: linear-gradient(var(--qangle),
      rgba(254,185,0,0.95)  0%,
      rgba(254,185,0,0.34) 26%,
      rgba(255,255,255,0.85) 50%,
      rgba(254,185,0,0.34) 74%,
      rgba(254,185,0,0.95) 100%);
    animation: qglaze 14s linear infinite;
  }
  /* Staggered so the three cards are never in step - in sync they read as one
     mechanism blinking, out of step they read as ambient light. */
  .qacard:nth-child(2)::before { animation-delay: -4.6s; }
  .qacard:nth-child(3)::before { animation-delay: -9.2s; }
  @keyframes qglaze { to { --qangle: 495deg; } }   /* 135 + 360, so it loops seamlessly */

  /* --- THE SAME GLAZING ON THE CURRENCY CARDS, but still at rest -------------
     Three large, well-spaced answer cards can carry gold at 0.95 and read as a
     premium accent. Twenty-two dense cards at that alpha read as fairy lights, so
     the resting ring is dialled to 0.42 - a warm edge you register rather than
     look at. Hover takes it to full brightness AND spins it once, which is what
     gives the interaction a payoff instead of just another colour change.
     The ring replaces the card's border rather than sitting inside it, so the
     border goes transparent and the box size is unchanged. */
  .rrow { position: relative; border-color: transparent; }
  .rrow::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1px;
    background: linear-gradient(var(--qangle),
      rgba(254,185,0,0.42)  0%,
      rgba(254,185,0,0.14) 26%,
      rgba(255,255,255,0.80) 50%,
      rgba(254,185,0,0.14) 74%,
      rgba(254,185,0,0.42) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    pointer-events: none;
    transition: opacity .2s ease;
  }
  /* Brighter gold is a SECOND layered ring rather than a repaint of the first:
     animating the gradient's colour stops and its angle at the same time makes the
     browser re-resolve every stop each frame. Fading one static layer over another
     is a compositor job. */
  .rrow::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    /* 2px, against the resting ring's 1px. The hover edge is meant to thicken as well as
       brighten, and because this layer is invisible until hover it costs nothing to make
       it heavier - no animation of the mask, it simply fades in already thick. */
    padding: 2px;
    background: linear-gradient(var(--qangle),
      rgba(254,185,0,0.95)  0%,
      rgba(254,185,0,0.30) 26%,
      rgba(255,255,255,0.85) 50%,
      rgba(254,185,0,0.30) 74%,
      rgba(254,185,0,0.95) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    pointer-events: none;
    opacity: 0;
    transition: opacity .2s ease;
  }
  /* HALF a turn, not a full one. The gradient carries gold at BOTH 0% and 100%, so a
     360deg rotation sweeps gold past every corner TWICE and reads as two spins - which
     is exactly what it looked like. 180deg is one clean pass, and because the gradient
     is symmetric it lands looking identical to where it started: the gold simply swaps
     corners. That is the "spin half and change the corner" effect.
     One iteration, then stop. Both rings turn together so the bright layer stays
     registered with the dim one underneath. */
  @keyframes qhalf { to { --qangle: 315deg; } }    /* 135 + 180 */
  .rrow:hover::before,
  .rrow:hover::after,
  .rrow:focus-visible::before,
  .rrow:focus-visible::after {
    animation: qhalf .95s cubic-bezier(.32,0,.24,1) 1;
  }
  .rrow:hover::after,
  .rrow:focus-visible::after { opacity: 1; }

  /* The same half-turn once as the board arrives on screen. Staggered with the cards so
     the glaze ripples rather than firing as one block. */
  .rr-spin .rrow::before,
  .rr-spin .rrow::after {
    animation: qhalf .95s cubic-bezier(.32,0,.24,1) 1;
    animation-delay: calc(var(--i, 0) * 28ms);
  }
}
@media (prefers-reduced-motion: reduce) {
  .rrow:hover::before, .rrow:hover::after,
  .rrow:focus-visible::before, .rrow:focus-visible::after { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .qacard::before { animation: none; }
}
/* Type sizes here are the client's own, tested in Chrome and handed over 8 Aug 2026.
   Do not "normalise" them back toward the surrounding scale. */
.qacard h3 {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
  margin-bottom: 10px;
}
.qacard p { font-size: 16px; line-height: 1.62; color: var(--slate); }
.qafoot {
  max-width: 62ch;
  margin: 26px auto 0;
  text-align: center;
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--slate);
}

/* --- larger-amount prompt, directly under the board -------------------------
   A whole tel: link rather than a paragraph with a link in it: the entire strip
   is the tap target, which is what makes it work on a phone. */
.bigamt {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 16px;
  padding: 20px 24px;
  border-radius: var(--r);
  background: var(--gold-wash, rgba(254,185,0,0.10));
  border: 1px solid rgba(254,185,0,0.34);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
  color: inherit;
  text-decoration: none;
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.bigamt:hover {
  background: rgba(254,185,0,0.16);
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 10px 24px -14px rgba(254,185,0,0.55);
}
.bigamt-ico { font-size: 26px; color: var(--gold); flex: none; width: 26px; overflow: hidden; }
.bigamt-txt { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
.bigamt-txt b { font-size: 16.5px; font-weight: 700; color: var(--ink); }
.bigamt-txt > span { font-size: 14px; line-height: 1.5; color: var(--slate); }
.bigamt-num {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  font-size: 16px;
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
}
.bigamt-num .material-symbols-outlined { font-size: 19px; width: 19px; overflow: hidden; transition: transform .2s cubic-bezier(.16,1,.3,1); }
.bigamt:hover .bigamt-num .material-symbols-outlined { transform: translateX(3px); }
@media (max-width: 760px) {
  /* The number moves under the copy rather than shrinking it - a phone number squeezed
     to fit is worse than one on its own line. */
  .bigamt { flex-wrap: wrap; gap: 12px 16px; padding: 18px; }
  .bigamt-txt { flex-basis: calc(100% - 42px); }
  .bigamt-num { width: 100%; justify-content: center; padding-top: 4px; }
}

/* --- Pull the board up toward the fold. Scoped to #rates ONLY: the generic .band
   rhythm (120px top, 64px head margin) is right for a section you scroll TO, and
   wrong for the one thing this page exists to show. Removing the hero's 100vh
   bought 156px; this buys another 152. -------------------------------------- */
/* THE FIRST BAND AFTER THE HERO, on any page. The generic .band rhythm (120px top,
   64px head margin) is right for a section you scroll TO and wrong for the one sitting
   directly under the hero. Carried on a class rather than an id, because on find-us the
   lead band is #location and on the rates page it is #rates - and on find-us the hidden
   rate board sits between them, so no sibling selector would reach the right one. */
.band-lead { padding-top: 28px; }
.band-lead .head { margin-bottom: 22px; }
/* The hero no longer carries the header's clearance - the breadcrumb bar above it
   does - so its top padding is just the gap between the crumbs and the H1. */
/* Gap between the breadcrumb links and the H1 (client, 8 Aug 2026 - 22px was too tight
   to separate the crumb trail from the page proper). The crumbs are small, quiet text;
   without real air under them they read as part of the heading block rather than as
   navigation sitting above it. */
.hero { padding-top: 44px; padding-bottom: 24px; }
@media (min-width: 951px) { .hero { padding-top: 60px; padding-bottom: 24px; } }
@media (max-width: 950px) { .band-lead { padding-top: 22px; } }

/* --- THE SEAM UNDER THE CALCULATOR -----------------------------------------
   .hero carried overflow:hidden, which CLIPPED its own two radial glows at the
   hero's bottom edge. With the hero no longer 100vh tall that clip line landed
   right under the calculator and read as the end of a header band.
   The glows are now unclipped and sized in px so they carry on down through the
   rates section - one continuous wash, no section edge. The rate band has no
   background of its own, so there is nothing to paint over them. */
/* Clip sideways, bleed downward. Plain overflow:visible let the glows past the right
   edge and the whole page gained a horizontal scrollbar. Per the overflow spec, `clip`
   on one axis leaves `visible` on the other intact (any other pairing would force the
   visible axis to `auto` and reinstate the clip we are trying to remove). */
.hero { overflow-x: clip; overflow-y: visible; }
/* GEOMETRY RESTORED to the original (client, 8 Aug 2026 - they liked the previous
   hero). These are the homepage's own numbers: 60% x 60% of the hero, offset -20%/
   -10%, blur(60px). The ONLY thing not restored is the clipping - overflow-y stays
   visible above, so the glow fades out naturally instead of being chopped at a hard
   line under the calculator. Same look, no seam.
   The bleed is now about 130px rather than 500px, so the sections below get their own
   glows instead of borrowing the hero's. */
/* SECOND .hero glow block. This file has duplicate blocks for several selectors and a
   later one only sets what it changes, so `filter` has to be removed - or restored - in
   BOTH places or the first block's change does nothing. Same trap as the two .mobnav
   blocks. */
.hero::before {
  top: -20%;
  left: -10%;
  width: 60%;
  height: 60%;
  filter: blur(60px);
}
.hero::after {
  top: auto;
  bottom: -20%;
  right: -10%;
  width: 60%;
  height: 60%;
  filter: blur(60px);
}

/* --- AMBIENT GLOW PER SECTION ------------------------------------------------
   Each section carries its own, rather than one field over everything. Localised
   glows are also better FOR the glass than a flat wash: a panel needs variation
   behind it to read as frosted, and a uniform tint gives it nothing to blur.
   Alphas are kept under the hero's 0.15 so the hero stays the strongest point,
   and they sit toward the edges rather than dead centre behind the cards. */
#rates { position: relative; overflow-x: clip; overflow-y: visible; }
#rates::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 6%;
  left: -12%;
  width: min(700px, 72vw);
  height: 720px;
  background: radial-gradient(circle, rgba(254,185,0,0.10) 0%, transparent 62%);
  filter: blur(80px);
  pointer-events: none;
}
/* The #why-our-rate rules that sat here are gone with the section (client, 14 Aug 2026) - it
   was the "Why our rate is not the rate Google shows you" block on /exchange-rates/. Copy and
   the reasoning are preserved in the why-our-rate-section-removed memory.
   Incidentally this removed ONE of the three off-brand green background glows
   (rgba(16,185,129,0.08) at blur(80px)); two remain, around lines 884 and 3903. */

/* --- FIND US ACCENT ---------------------------------------------------------
   The page is composed rather than coated: a warm glow in the hero, a CALM rate
   board so the glass has something to blur and the numbers stay crisp, one soft
   accent here because this is the conversion section, then the dark footer band.
   Mirrored to the right, opposite the hero's gold, so the eye alternates down
   the page. overflow-x:clip for the same reason as the hero - a bleeding glow
   otherwise gives the whole document a horizontal scrollbar. */
#location { position: relative; overflow-x: clip; overflow-y: visible; }
#location::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -160px;
  right: -8%;
  width: min(780px, 78vw);
  height: 800px;
  background: radial-gradient(circle, rgba(254,185,0,0.13) 0%, transparent 62%);
  filter: blur(90px);
  pointer-events: none;
}

/* --- THE ALTERNATIVE, behind ?bg=all ----------------------------------------
   The all-over wash, for comparison. ONE fixed layer rather than a glow per
   section: fixed paint costs a single buffer no matter how long the page is,
   where per-section filters cost one each and are exactly what the no-stacked-
   blurs rule is about. Delete this block and the switch in the script when a
   decision is made. */
html.bg-all::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60% 50% at 8% 6%,   rgba(254,185,0,0.16) 0%, transparent 60%),
    radial-gradient(55% 45% at 92% 28%, rgba(16,185,129,0.10) 0%, transparent 60%),
    radial-gradient(65% 55% at 10% 62%, rgba(254,185,0,0.11) 0%, transparent 62%),
    radial-gradient(60% 50% at 90% 90%, rgba(99,102,241,0.08) 0%, transparent 60%);
}


/* ==========================================================================
   BREADCRUMB BAR + MENU (client, 7 Aug 2026).
   ========================================================================== */

/* .hdr is position:absolute, so SOMETHING has to hold its 72px of clearance.
   That job moved from the hero to here. If this bar is ever removed, the
   padding has to go back on .hero or the header will sit on the H1. */
/* BARE LINKS, no panel (client, 8 Aug 2026). The first version was a glass pill and
   it read as a second header bar stacked under the real one. What makes this read as
   orientation rather than chrome is air: ~20px under the header, the links, then
   another ~20px before the H1. Nothing else. */
/* CLICKABILITY, not decoration: the hero that follows is position:relative and its ::before
   glow overhangs 20% ABOVE the section, so the hero paints - and hit-tests - over the crumb
   links on every page. Clicks on "Home" silently hit the hero instead (client-reported on the
   USD page, 29 Aug 2026; measured with elementFromPoint - the same overhang exists sitewide).
   Lifting the nav one layer restores every crumb link. */
.crumbs { position: relative; z-index: 2; }
.crumbs { padding-top: 92px; }               /* 72px header + 20px of air */
@media (max-width: 950px) { .crumbs { padding-top: 88px; } }
/* +14 on desktop (client, 17 Aug 2026): absorbing the strip's air gap into the 86px header
   row moved the page FLOW up 14px while the header's bottom edge deliberately stayed put -
   so the crumbs' 20px of air quietly became 6 and the line hugged the header's underline.
   106 = 86px row + the same 20px of air as always. */
@media (min-width: 951px) { .crumbs { padding-top: 106px; } }
.crumbs ol {
  list-style: none;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 9px;
  margin: 0;
  padding: 0;
}
.crumbs li { display: flex; align-items: center; gap: 9px; font-size: 13.5px; font-weight: 600; }
.crumbs li + li::before { content: "/"; color: var(--muted); font-weight: 500; }
.crumbs a { color: var(--muted); transition: color .15s ease; }
.crumbs a:hover { color: var(--gold-deep); }
.crumbs [aria-current="page"] { color: var(--ink); }

/* --- MENU. Each link gets its own pill so the bar reads as navigation instead of
   a row of words, and the page you are on is unmistakable. ------------------ */
.menu { gap: 4px; padding: 6px 8px; flex-wrap: nowrap; }
.menu a {
  padding: 8px 14px;
  border-radius: 99px;
  border: 1px solid transparent;
  /* A nav label must never break mid-phrase. Without this, a header one pixel too
     narrow does not overflow visibly - it silently splits "Exchange rates" across two
     lines inside its own pill, which looks like a bug rather than a layout that needs
     more room. nowrap makes the failure honest, and the breakpoint above stops it
     happening at all. */
  white-space: nowrap;
  transition: color .15s ease, background .18s ease, border-color .18s ease;
}
.hdr .cta-d { white-space: nowrap; }
.menu a:hover { color: var(--ink); background: rgba(255,255,255,0.72); }
/* CURRENT PAGE. Frosted gold - the same recipe as the buy/sell pill, because the
   site should only have one way of saying "this one". Gold lives in the fill at low
   opacity, the rim and the inset sheen; the LABEL stays ink, since gold text on a
   light pill measures about 1.7:1 and is banned by the brand rules. */
.menu a[aria-current="page"] {
  color: var(--ink);
  background: linear-gradient(160deg, rgba(254,185,0,0.30), rgba(254,185,0,0.16));
  border-color: rgba(254,185,0,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.75), 0 4px 12px -6px rgba(254,185,0,0.45);
}
.menu a[aria-current="page"]:hover {
  background: linear-gradient(160deg, rgba(254,185,0,0.36), rgba(254,185,0,0.20));
}
/* The mobile nav now gets the SAME frosted gold pill as the desktop menu rather than
   its own vocabulary. The old treatment was a 34px gold bar absolutely positioned
   6px below the label - on a centred stack with 28px gaps that bar floated in open
   space between two rows and read as a stray mark, not as "you are here". A pill
   also survives the row's hairline, which an underline sat on top of.
   Note this rule must keep owning ::after: it is the same specificity as the chevron
   rule above and later in the file, so the chevron is re-declared here rather than
   left to lose silently. */
.mobnav .nav-links a[aria-current="page"] {
  color: var(--ink);
  background: linear-gradient(160deg, rgba(254,185,0,0.30), rgba(254,185,0,0.16));
  border-bottom-color: transparent;
  box-shadow: inset 0 0 0 1px rgba(254,185,0,0.50), 0 4px 12px -8px rgba(254,185,0,0.5);
}
.mobnav .nav-links a[aria-current="page"]::after {
  content: "\203A";
  font-size: 21px;
  font-weight: 400;
  line-height: 1;
  color: var(--gold);
  transform: translateY(-2px);
}

/* ==========================================================================
   RATE BOARD - card layout. Categories were tried and dropped (client, 7 Aug
   2026); cards keep the design, ordering is the feed's own order.
   ?layout=tiers still returns the full-width row version for comparison.
   ========================================================================== */

/* --- LAYOUT B: card grid --------------------------------------------------
   Same .rrow element, stood upright. Specificity (0,2,0) deliberately beats the
   base .rlist rules AND the max-width:900px override, which are both (0,1,0),
   so no media-query juggling is needed to hold the grid on mobile. */
.lay-grid .rlist {
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 10px;
}
/* CENTRED CARDS (client, 14 Aug 2026). Flag, code, name and both rate lines all sit on the
   card's centre axis instead of a left edge. Four things have to agree or it only half works:
   the card's own align-items, .rr-id (a flex column), .rr-val (a flex column), and .rr-rate
   (a flex ROW, so it needs justify-content, not align-items). text-align on the card catches
   the plain text inside .rr-eq and the name.
   .rr-act is absolutely positioned in the corner and is deliberately left out of this. */
.lay-grid .rrow {
  position: relative;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 12px;
  padding: 16px;
  border-radius: 18px;
}
.lay-grid .rrow img.fl { width: 44px; height: 44px; }
.lay-grid .rr-id { align-items: center; }
.lay-grid .rr-id b { font-size: 18px; }
.lay-grid .rr-val {
  margin-left: 0;            /* the base rule pushes the value to the right of a row */
  align-items: center;
  width: 100%;
}
.lay-grid .rr-rate { flex-wrap: wrap; justify-content: center; }
/* The badge has nowhere to sit at the end of a column, so it moves to the corner. */
.lay-grid .rr-act {
  position: absolute;
  top: 12px;
  right: 12px;
}

/* --- BUY / SELL badge on every card -----------------------------------------
   Resting: just the word, quiet. Hover: it becomes a frosted gold pill and the
   arrow slides out of it. Same gold recipe as the nav pill and the buy/sell
   toggle, because the site should only have one way of saying "this one". */
.rr-act {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 4px 9px;
  border-radius: 99px;
  border: 1px solid transparent;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color .18s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease;
}
.rr-act i { font-style: normal; }
/* The arrow is collapsed by WIDTH, not just opacity. An invisible 15px glyph would
   still hold the badge open, so the pill could not grow on hover and the arrow would
   appear to pop rather than slide out. */
.rr-act .rr-go {
  font-size: 15px;
  width: 0;
  margin-left: 0;
  opacity: 0;
  overflow: hidden;
  transform: none;
  transition: width .22s cubic-bezier(.16,1,.3,1), margin-left .22s cubic-bezier(.16,1,.3,1), opacity .18s ease;
}
/* :focus-visible as well as :hover - the card is a real button and a keyboard user
   has to be able to see the same affordance. */
.rrow:hover .rr-act,
.rrow:focus-visible .rr-act {
  color: var(--ink);          /* ink, never gold: gold on gold is unreadable */
  background: linear-gradient(160deg, rgba(254,185,0,0.34), rgba(254,185,0,0.18));
  border-color: rgba(254,185,0,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.75), 0 4px 12px -6px rgba(254,185,0,0.50);
}
.rrow:hover .rr-act .rr-go,
.rrow:focus-visible .rr-act .rr-go { width: 15px; margin-left: 4px; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .rr-act, .rr-act .rr-go { transition: none; }
}

/* --- 1. RATE CHANGED. A gold ring blooms and fades on the card whose number just
   moved. Animates box-shadow only, so it never fights the card's own background or
   its hover state, and it needs no extra element. ---------------------------- */
@keyframes rrflash {
  0%   { box-shadow: inset 0 0 0 2px rgba(254,185,0,0.90), 0 0 22px -4px rgba(254,185,0,0.55); }
  60%  { box-shadow: inset 0 0 0 2px rgba(254,185,0,0.55), 0 0 18px -6px rgba(254,185,0,0.30); }
  100% { box-shadow: inset 0 0 0 2px rgba(254,185,0,0),    0 0 0 0 rgba(254,185,0,0); }
}
/* .rlist prefix is deliberate, not decoration. The entry-stagger rule below is
   `.rr-run .rr-stagger`, also (0,2,0), and it sits later in the file - so a plain
   `.rrow.rr-flash` lost the tie and a card still carrying the stagger class could
   never flash. Three classes wins it outright, whatever the source order. */
.rlist .rrow.rr-flash { animation: rrflash 1.5s cubic-bezier(.16,1,.3,1); }

/* --- 2. STAGGERED ENTRY. Cards are held down and faded out the moment they are
   armed, then released together when .rr-run lands - so nothing animates until the
   board is actually on screen, and nothing flashes into place before that.
   `both` fill matters: without it the card snaps to visible during its delay. --- */
.rr-stagger { opacity: 0; transform: translateY(14px); }
@keyframes rrin { to { opacity: 1; transform: none; } }
.rr-run .rr-stagger {
  animation: rrin .52s cubic-bezier(.16,1,.3,1) both;
  animation-delay: calc(var(--i, 0) * 28ms);
}
/* --- CARD FLIP on the buy/sell toggle --------------------------------------
   Switching side is literally turning the trade around, so the board turns over.
   perspective lives INSIDE the transform rather than on .rlist: a perspective on the
   parent shares one vanishing point across all 22 cards, so the ones at the edges
   would shear away from the viewer. Per-card perspective means every card turns on
   its own axis, which is what a deck of cards actually does.
   backface-visibility is not needed - there is no second face, the card simply
   arrives edge-on - but transform-style must stay flat or the ring pseudo-elements
   get lifted into 3D with it and hairline-crack along the edges. */
/* Two keyframes, mirrored. Going to SELL the card arrives from one edge, going back to
   BUY it arrives from the other - so the board reads as one two-sided card being turned
   over and turned back, not as two different cards being dealt. */
@keyframes rrflipA {
  0%   { transform: perspective(760px) rotateY(-92deg); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: perspective(760px) rotateY(0deg); opacity: 1; }
}
@keyframes rrflipB {
  0%   { transform: perspective(760px) rotateY(92deg); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: perspective(760px) rotateY(0deg); opacity: 1; }
}
.rlist .rrow.rr-flip-a,
.rlist .rrow.rr-flip-b {
  transform-style: flat;
  animation-duration: .5s;
  animation-timing-function: cubic-bezier(.22,1,.32,1);
  animation-fill-mode: both;
  animation-delay: calc(var(--fi, 0) * 22ms);
}
.rlist .rrow.rr-flip-a { animation-name: rrflipA; }
.rlist .rrow.rr-flip-b { animation-name: rrflipB; }

/* --- DEPTH OF FIELD ON HOVER: REMOVED (client, 10 Aug 2026) -------------------
   Hovering a card used to blur and desaturate every OTHER card, by distance, like a
   lens racking focus. It was pretty in isolation and wrong in use: on a board whose
   entire job is COMPARING rates, putting one currency in focus obscures the very
   numbers the visitor came to read against it. The client's word was that it "blocks
   the other currencies" - the effect was fighting the page's purpose.

   Removing the `filter` here is what actually turns it off; nothing else declares it.
   That also drops 22 cards back off their own compositing layers, since any filter
   value - even blur(0px) - promotes an element.

   The JS that measured the distances is still in the page, short-circuited at the top
   of its IIFE so it costs nothing. Re-enabling means deleting that early return and
   restoring the `filter` line below - do not rebuild it from scratch. The hard-won
   detail: key off a real card being entered, never `.rlist:hover` (true over gaps and
   padding too, which blurred the whole board when the cursor sat between cards), and
   never touch opacity, which `.rr-stagger` owns for the entry reveal.

   The row's own hover treatment - white fill, 1px lift, shadow, gold action pill -
   is untouched and lives up with `.rrow:hover`. */

@media (prefers-reduced-motion: reduce) {
  .rlist .rrow.rr-flash { animation: none; }
  .rlist .rrow.rr-flip-a, .rlist .rrow.rr-flip-b { animation: none; }
  /* the `filter: none` reset that lived here went with the depth-of-field effect above */
  .rr-stagger { opacity: 1; transform: none; }
  .rr-run .rr-stagger { animation: none; }
  .rr-spin .rrow::before, .rr-spin .rrow::after { animation: none; }
}
@media (max-width: 900px) {
  /* Two cards across on a phone. Below ~140px a card cannot hold a rate value on one
     line, so this is the floor, not a preference. */
  .lay-grid .rlist { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  .lay-grid .rrow { padding: 14px; gap: 10px; }
  .lay-grid .rrow img.fl { width: 38px; height: 38px; }
  /* nowrap since 14 Aug 2026, replacing `normal`. This rule used to let the "A$100 -> X" line
     break onto a second line so a long value would fit; the cost was that five currencies
     wrapped and CSS Grid then stretched every card in their row to match, giving a ragged
     board. fitEqLines() in the page script now shrinks only the lines that would overflow,
     which needs them NOT to wrap - forbidden to break, a line overflows instead, and
     scrollWidth vs clientWidth gives the exact shrink ratio in one pass.
     Specificity matters here: this is (0,2,0) and beats a plain `.rr-eq`, so the earlier
     600px block cannot override it - which is why the change belongs in this rule and not
     in a new one. If fitEqLines is ever removed, put `normal` back or values will overflow. */
  .lay-grid .rr-eq { white-space: nowrap; }
}

/* ==========================================================================
   FIND US - rebuilt (client, 7 Aug 2026).
   The old block was a text column beside a map. The research says this is the
   highest-converting content on the site and that the single biggest friction
   point in the business is that the store is UPSTAIRS - people reach the lobby
   and give up. So the section is now built around the route, not the address.
   Order: live status -> how to walk in -> map -> the facts.
   ========================================================================== */
.head p strong { color: var(--ink); font-weight: 700; }

/* --- live status bar. .loc-status.findnow is (0,2,0) so it beats the base
       .loc-status rule regardless of source order. ------------------------- */
.loc-status.findnow {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  padding: 20px 24px;
  border-radius: var(--r);
  background: rgba(255,255,255,0.60);
  border: 1px solid rgba(255,255,255,0.80);
}
/* The rim goes back to brand. Green is now the dot and nothing else. */
.loc-status.findnow.open { border-color: rgba(254,185,0,0.32); }

/* WIDTH CAP (client, 10 Aug 2026). Left to stretch the container this ran 1335px wide with
   the text ending at x=353 and the buttons starting at x=1070 - 717px of dead space, over
   half the bar, which read as unfinished rather than spacious. The gap opened when the hours
   column moved off desktop, and the client chose capping over refilling it: nothing invented
   to occupy space, and no content repeated from the fact card directly below.
   Phones are already narrower than this, so the cap never engages there. */
@media (min-width: 601px) {
  .loc-status.findnow { max-width: 820px; margin-left: auto; margin-right: auto; }
}

/* SUBJECT TILE. The bar used to open with a bare 12px dot and never named what was
   open - on a page full of live rates it could just as easily have read as the
   website. Reading order is now subject -> state -> next change. */
.loc-status.findnow .ls-icon {
  flex: none;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border-radius: 15px;
  color: var(--gold);
  background: linear-gradient(155deg, rgba(254,185,0,0.18), rgba(254,185,0,0.05));
  border: 1px solid rgba(254,185,0,0.30);
  transition: color .25s ease, background .25s ease, border-color .25s ease;
}
.loc-status.findnow .ls-icon img { display: block; width: 26px; height: 26px; }
/* The tile deliberately does NOT turn green when the store opens. The mark inside is
   the client's own gold SVG and cannot be recoloured from CSS, and gold on a green
   wash is a clash. It costs nothing: green still means open on the dot, the badge
   and the bar's rim. The tile's job is identity, the badge's job is state. */

/* flex-grow 0 is deliberate. With grow:1 this column stretched to 597px while
   holding ~230px of text, so it swallowed all the free space and the hours column's
   auto margins had nothing left to centre with - the dead space just moved from the
   right of the bar to the inside of this block. Sized to content, the leftover space
   becomes real margin that .ls-hours can sit in the middle of. min-width forces a
   wrap rather than a squash when the bar gets tight. */
.loc-status.findnow .ls-txt { flex: 0 1 auto; min-width: 240px; gap: 3px; }
.loc-status.findnow .ls-top {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 3px;
}
.loc-status.findnow .ls-sub {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
/* NO PILL (client, 10 Aug 2026). A dot inside a tinted rounded pill is the stock "Open now"
   component every template ships, and with the green fill it was the single most generic
   thing on the page. Stripped back to a dot and a label: the state still reads instantly,
   but as typography rather than as a widget. It keeps its own line, so the label above it -
   which used to share the row and pushed BOTH items off the centre axis on a phone - is
   gone from the markup. */
.loc-status.findnow .ls-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.loc-status.findnow .ls-dot { width: 8px; height: 8px; }
/* NO DOT AT ALL ON THIS BAR, EITHER STATE (client, 10 Aug 2026).

   THE RULE, and it is worth keeping: a dot earns its place where the text is a terse label,
   and does not where the text is a sentence. This bar reads "Our store is open now" - the
   word "open" already carries the meaning, so a green dot beside it repeats it decoratively.
   .calc-openbar and .mact-status still show theirs, because they read "Open now" in two
   words with no room for more. That is a principled split, not an inconsistency.

   It is also the better accessibility answer rather than a worse one: a colour-only signal
   fails for colour-blind users, explicit text never does. And the state is still readable at
   a glance without it, because the headline below swaps entirely between "Closes 5:00 PM"
   and "Opens 9:30 AM Tomorrow".

   Hidden rather than deleted from the markup: it is one empty span, and this has been
   iterated on repeatedly, so keeping the reversal to a single line is worth more than the
   node. --open is still live for the other two components. .ls-badge's 8px gap needs no
   change - a gap needs two visible items. */
.loc-status.findnow .ls-dot { display: none; }
/* SENTENCE, NOT A CAPS LABEL. The text is now "Our store is open now" rather than "OPEN
   NOW", and a full sentence set in spaced uppercase shouts and competes with the time
   headline below it. Sentence case at a quiet weight puts it back in its place as the
   subject line, with the actionable time still carrying the emphasis.
   Colour is deliberately NOT set here: .loc-status.open .s-main handles ink-when-open, and
   it has the same specificity as this rule, so a colour here would win on source order and
   silently kill the open state's emphasis. */
/* THE STATE LEADS (client, 10 Aug 2026). It previously sat at 15.5px/700/ink directly above
   the time at 18px/700/ink - same weight, same colour, a size ratio of 1.16. Two adjacent
   lines that similar establish no hierarchy at all, so neither led and the pair read as a
   paragraph. That is what the client meant by "it looks like just a line".

   Uppercase was considered and rejected: caps work on a 1-3 word label, but this is a
   five-word sentence, and all-caps strips the ascender/descender shapes that let a word be
   recognised at a glance, so long caps strings scan slower. It would also not have fixed
   anything, because both lines would still have weighed the same.

   The fix is contrast, on three axes at once - size (ratio now 1.55), weight and colour. */
.loc-status.findnow .s-main {
  font-size: 21px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.25;
}
/* The time is the headline now: "Opens 9:30 AM tomorrow" is the part a visitor acts
   on, and the pill above it is the label for it. */
/* The time drops back to a support line. It used to be the hero here, on the argument that
   "Opens 9:30 AM tomorrow" is the part a visitor acts on - which is true when the store is
   CLOSED, but the block is read far more often while it is open, when the question being
   answered is simply "can I come now". The client chose the state leading, accepting that
   the closed-case time becomes support text. It stays perfectly legible at 13.5px. */
.loc-status.findnow .s-hint {
  font-size: 15.5px;   /* client's own value, 10 Aug 2026 - was 13.5 */
  font-weight: 500;
  color: var(--muted);
  line-height: 1.35;
}
/* Was a full sentence ("Open and closed times follow Sydney local time.") - three
   greyed lines in a row read as noise. Two words carry the same meaning, and they
   now ride on the END of the time line rather than taking a row of their own, which
   drops the text block from three stacked rows to two. Inline, not flex: the two
   spans differ in size and weight, so normal text flow baselines them correctly and
   lets them wrap as one sentence if the column gets tight. */
.loc-status.findnow .ls-time { display: block; }
.loc-status.findnow .s-hint { display: inline; }
.loc-status.findnow .s-tz { display: inline; margin-top: 0; font-size: 12px; opacity: 0.75; }
.loc-status.findnow .s-tz::before { content: "\00B7"; margin: 0 7px; opacity: 0.55; }

/* THE MIDDLE COLUMN. Desktop was carrying roughly 500px of dead space between the
   status text and the buttons, and the bar only ever named ONE moment - land on a
   Friday evening and "Opens 9:30 AM Monday" still never tells you the store runs
   Mon to Fri. Putting the weekly hours in the gap fixes both at once.
   The auto margins on BOTH sides are what centres it: a flex item with auto margins
   absorbs the free space evenly, so this reads as a real third column instead of a
   block shoved against the buttons. */
.loc-status.findnow .ls-hours {
  flex: 0 1 auto;
  margin-left: auto;
  margin-right: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.loc-status.findnow .ls-hours .ls-sub { margin-bottom: 4px; }
.loc-status.findnow .lsh-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--slate);
}
/* Fixed label width so the two times line up as a column rather than ragging. */
.loc-status.findnow .lsh-row b { min-width: 74px; font-weight: 700; color: var(--ink); }
/* HOURS COME OFF THE BAR ON DESKTOP (client, 9 Aug 2026). The fact-card row a few
   hundred pixels below already carries "OPENING HOURS - Mon to Fri, 9:30 AM - 5:00 PM",
   and on a wide screen both are in view at once, so the bar was repeating itself. The
   phone keeps its copy: there the fact cards are a long way down a stacked page. */
@media (min-width: 601px) {
  .loc-status.findnow .ls-hours { display: none; }
  /* The hours block's auto margins were what pushed the buttons to the right edge. With
     it gone the CTA has to claim that space itself, or the tile, the text and the
     buttons all bunch up on the left with dead space after them. */
  .findnow-cta { margin-left: auto; }
}
.loc-status.findnow .lsh-off,
.loc-status.findnow .lsh-off b { color: var(--muted); font-weight: 600; }

.findnow-cta { display: flex; gap: 10px; flex: none; }

/* --- route + map ---------------------------------------------------------- */
.findwrap { display: grid; gap: 16px; }
@media (min-width: 900px) { .findwrap { grid-template-columns: 1fr 1fr; } }

.route {
  list-style: none;
  margin: 0;
  padding: 28px;
  border-radius: var(--r);
}
.route li { position: relative; display: flex; gap: 16px; padding-bottom: 22px; }
.route li:last-child { padding-bottom: 0; }
/* The text column has to CLAIM the row, not shrink to its sentence. As a plain flex item it
   sized to its longest line, so the photo below it - width:100%, max-width:380px - inherited
   that width and every step ended up a different size: measured 337px on the shortest copy
   against 380px on the longest, and since the frame is ratio-locked that came straight out as
   a different photo HEIGHT per step. The photos should be one size no matter how long the
   step's wording is, so the column takes the full width and the 380px cap is what decides.
   min-width: 0 is required alongside it - a flex item's auto minimum is its content, which
   would otherwise stop it shrinking on narrow screens. */
.route li > div { flex: 1; min-width: 0; }
/* The thread between the steps. left:17px is the centre of the 36px marker minus
   half the 2px rule, so it runs straight through the numbers. */
.route li:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 17px;
  top: 44px;
  bottom: 6px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, rgba(254,185,0,0.40), rgba(254,185,0,0.08));
}
/* Same frosted-gold vocabulary as the buy/sell pill: gold in the edge and the
   sheen, not in the fill. Number stays ink - gold text on light fails contrast. */
.route .rn {
  position: relative;
  z-index: 1;
  width: 36px;
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
  background: linear-gradient(160deg, rgba(254,185,0,0.38), rgba(254,185,0,0.20));
  border: 1px solid rgba(254,185,0,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
}
.route b { display: block; font-size: 16px; font-weight: 700; color: var(--ink); margin-bottom: 3px; }
.route span { display: block; font-size: 14px; color: var(--slate); line-height: 1.5; }

/* --- walking times ------------------------------------------------------- */
.walkfrom {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-bottom: 16px;
}
.walklab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.walklab .material-symbols-outlined { font-size: 19px; width: 19px; overflow: hidden; color: var(--gold); }
.walkchips { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; }
.walkchips li {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 7px 14px;
  border-radius: 99px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--slate);
  background: rgba(255,255,255,0.55);
  border: 1px solid rgba(255,255,255,0.75);
  box-shadow: var(--shadow-card);
}
.walkchips b { font-weight: 700; color: var(--ink); }

/* --- map / street view --------------------------------------------------- */
.mapframe { display: flex; flex-direction: column; padding: 10px; border-radius: var(--r); overflow: clip; }
.mapframe iframe {
  width: 100%;
  flex: 1;
  min-height: 340px;
  border: 0;
  border-radius: 16px;
  display: block;
}
.mapframe iframe[hidden] { display: none; }   /* [hidden] loses to display:block */
.maptabs {
  display: flex;
  gap: 4px;
  padding: 0 2px 10px;
  flex: none;
}
.maptabs button {
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 99px;
  padding: 7px 16px;
  cursor: pointer;
  transition: color .15s ease, background .18s ease, border-color .18s ease;
}
.maptabs button:hover { color: var(--ink); background: rgba(255,255,255,0.6); }
/* Same frosted-gold "this one" signal as the nav and the buy/sell pill. */
.maptabs button.on {
  color: var(--ink);
  background: linear-gradient(160deg, rgba(254,185,0,0.30), rgba(254,185,0,0.16));
  border-color: rgba(254,185,0,0.55);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.75);
}

/* --- route step photos. Only a step carrying data-photo becomes interactive,
       so with no photos yet the route renders exactly as plain text. --------- */
/* The whole row toggles the photo, so the whole row says so - including the step number, which
   is a sibling of the text column. The photo inside sets its own `zoom-in` and wins by being
   deeper in the tree. */
.route li.has-photo { cursor: pointer; }
.route li.has-photo > div { cursor: pointer; }
.route .rstep-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  color: var(--gold-deep);
  cursor: pointer;
}
.route .rstep-toggle .material-symbols-outlined { font-size: 18px; width: 18px; overflow: hidden; transition: transform .2s ease; }
.route li.open .rstep-toggle .material-symbols-outlined { transform: rotate(180deg); }
/* ── THE REVEAL (rebuilt 16 Aug 2026 - see the note in site.js) ──────────────────────────
   Every route photo, single or carousel, sits inside .rstep-reveal and THIS is the thing
   that opens. It animates `grid-template-rows` from 0fr to 1fr, which resolves to the
   child's exact natural height at whatever width the page happens to be.

   IT REPLACED A max-height ANIMATION, and the reason is the whole point: max-height ran
   0 -> 280px against real content of ~253px, so a step hit full height before the animation
   finished. Swapping step A for step B, the two max-heights always summed to 280, but the two
   VISIBLE heights were each capped at 253 - so halfway through, both showed 253 and the
   section was ~27px taller than at either end. That overshoot and snap-back is what the
   client saw as the section shaking. No fixed max-height can fix it, because the frame is
   ratio-locked and its true height changes with viewport width.

   Requires `min-height: 0` on the grid child - without it the child's automatic minimum size
   keeps the row open and nothing ever collapses. */
.route .rstep-reveal {
  display: grid;
  grid-template-rows: 0fr;
  margin-top: 0;
  opacity: 0;
  /* ⚠ overflow: hidden BELONGS ON THIS ELEMENT, the one that actually collapses.
     It was originally only on the child, which was wrong in a way that was invisible: the
     wrapper's grid row shrinks to 0, but `.rstep-shots` has `aspect-ratio: 3/2` on a definite
     width, so its own height stays 253px regardless. `overflow: hidden` on the CHILD clips
     the child's contents, not the child's own overflow out of the parent - so a closed step
     left a full-size photo, its arrows, its expand button and the image's `title` tooltip
     sitting in the layout, hidden ONLY by `opacity: 0` and therefore still hoverable and
     clickable. The client hit exactly that: phantom controls and a tooltip over a step whose
     photo was not even open. Clipping here is what makes a closed step genuinely absent. */
  overflow: hidden;
  transition: grid-template-rows .28s cubic-bezier(.16,1,.3,1),
              margin-top .28s cubic-bezier(.16,1,.3,1),
              opacity .2s ease;
}
.route li.open .rstep-reveal { grid-template-rows: 1fr; margin-top: 10px; opacity: 1; }
/* Belt to that braces: even if something ever escapes the clip, a closed step must not be
   interactive. Cheap, and it also stops the title tooltip firing on a hidden image. */
.route li:not(.open) .rstep-reveal { pointer-events: none; }
/* min-height: 0 is still required on the child - a grid item's automatic minimum size is its
   content, which would otherwise stop the row collapsing at all. */
.route .rstep-reveal > * { min-height: 0; overflow: hidden; }

.route .rstep-img {
  display: block;
  width: 100%;
  max-width: 380px;
  border-radius: 14px;
  border: 1px solid rgba(16,24,40,0.08);
  /* A STATIC cap, no longer animated - it only exists so a future single photo that is not
     3:2 cannot run away vertically. At the 380px cap a 3:2 photo is 253px, well under it. */
  max-height: 280px;
}

/* ── MULTI-PHOTO ROUTE STEPS (client, 15 Aug 2026) ──────────────────────────────────────
   Only built when data-photo carries 2+ pipe-separated paths. A single photo still renders
   as a plain .rstep-img above and never gets any of this.

   THE FRAME IS RATIO-LOCKED, unlike the single-photo case which keeps its natural ratio.
   With several photos the box has to be ONE height or every advance would jog the accordion
   and the page below it. 3:2 matches the ratio the client is cropping route photos to, and
   object-fit: cover absorbs anything slightly off. The collapse animation still runs on
   max-height so it behaves exactly like the single-photo step. */
.route .rstep-shots {
  position: relative;
  display: block;
  width: 100%;
  max-width: 380px;
  aspect-ratio: 3 / 2;
  border-radius: 14px;
  border: 1px solid rgba(16,24,40,0.08);
  overflow: hidden;
  /* No max-height and no transition here any more - .rstep-reveal above owns the open/close
     animation now, and leaving a competing max-height on this element is what caused the
     bounce it was rebuilt to remove. */
}

/* Slides stack and cross-fade. Opacity only - the compositor runs it without touching the
   main thread, which is the same reason the guilloche and the calculator swap animate what
   they animate. A sliding track would have been a transform per frame plus a drag model. */
.route .rstep-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* Duration is set per step by site.js from data-dwell, because the fade has to fit inside
     the interval - at a 1s dwell the old flat .45s left the photo mid-dissolve for nearly
     half its life, which reads as permanently out of focus. The .45s here is the fallback
     for a frame the script has not written the property onto. */
  transition: opacity var(--rstep-fade, .45s) ease;
}
.route .rstep-slide.on { opacity: 1; }

/* The whole frame opens the gallery, so say so. The arrows and the expand button sit above
   this and set their own cursor. */
.route .rstep-shots { cursor: zoom-in; }

/* EXPAND, bottom-right - the corner every video player uses, which is the client's reference.
   Hidden until hover on a mouse; permanently visible on touch, where there is no hover to
   reveal it with. Deliberately opposite the dots (bottom-centre) and clear of the arrows
   (mid-height), so nothing on the photo overlaps anything else. */
.route .rstep-expand {
  position: absolute;
  right: 8px;
  bottom: 8px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  /* Squircle rather than a circle, so it reads as a different KIND of control from the two
     round arrows either side of it - one navigates, this one changes mode. */
  border-radius: 11px;
  cursor: zoom-in;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .18s ease, transform .18s ease, background .18s ease, box-shadow .18s ease;
}
.route .rstep-expand .material-symbols-outlined { font-size: 18px; }
.route .rstep-shots:hover .rstep-expand,
.route .rstep-expand:focus-visible { opacity: 1; transform: none; }
.route .rstep-expand:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.62), rgba(255,255,255,0.34));
  transform: scale(1.06);
  box-shadow:
    0 14px 30px -12px rgba(16,24,40,0.6),
    inset 0 1px 0.5px rgba(255,255,255,1),
    inset 0 -10px 16px -12px rgba(16,24,40,0.22);
}
.route .rstep-expand:active { transform: scale(0.97); }

/* ── THE GLASS CONTROLS (client, 16 Aug 2026) ────────────────────────────────────────────
   The brief was the iOS look: the control should BLEND the photo behind it rather than sit
   on top of it as a flat white disc, which is what a plain rgba background did.

   FOUR THINGS TOGETHER MAKE IT READ AS GLASS, and dropping any one of them flattens it:
     1. a LOW-OPACITY, slightly angled gradient fill - a flat fill reads as plastic, the
        gradient is what suggests a curved surface catching light;
     2. a heavy `backdrop-filter` with a SATURATION lift - the saturation is what makes the
        photo's colour bloom through the glass instead of going grey, and it is the single
        biggest difference from the old blur(10px);
     3. a `brightness` lift, which is doing real work here: the glyph is ink, and lifting the
        backdrop is what guarantees it stays readable over a DARK photo (the lift interior,
        the corridor) without hardcoding a colour per image;
     4. an inset highlight on the TOP edge plus a soft inset shade at the bottom - the
        specular rim. This is the detail that reads as "Apple" and it is one line.
   The outer shadow is what lifts it off the photo; without it the disc looks printed on.

   Shared by the step arrows, the route expand button and the access photo's expand button so
   the three can never drift apart. **`.sw-zoom` has to be listed here explicitly** - it sits
   outside `.route`, so a `.route`-scoped selector does not reach it, which is exactly how the
   first version shipped an unstyled, unpositioned button on the access photo. */
.route .rstep-arw,
.route .rstep-expand,
.sw-zoom {
  border: 1px solid rgba(255,255,255,0.62);
  background: linear-gradient(158deg, rgba(255,255,255,0.46), rgba(255,255,255,0.22));
  backdrop-filter: blur(20px) saturate(190%) brightness(1.12);
  -webkit-backdrop-filter: blur(20px) saturate(190%) brightness(1.12);
  box-shadow:
    0 10px 26px -12px rgba(16,24,40,0.55),
    inset 0 1px 0.5px rgba(255,255,255,0.95),
    inset 0 -10px 16px -12px rgba(16,24,40,0.22);
  color: var(--ink);
  cursor: pointer;
  padding: 0;
}

/* Sized for a thumb (36px) and pulled to the edges. */
.route .rstep-arw {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.route .rstep-arw.prev { left: 8px; }
.route .rstep-arw.next { right: 8px; }
/* Hover brightens the glass and lifts it very slightly. translateY(-50%) has to be repeated
   or the scale would drop the button back to the top of the frame. */
.route .rstep-arw:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.62), rgba(255,255,255,0.34));
  transform: translateY(-50%) scale(1.06);
  box-shadow:
    0 14px 30px -12px rgba(16,24,40,0.6),
    inset 0 1px 0.5px rgba(255,255,255,1),
    inset 0 -10px 16px -12px rgba(16,24,40,0.22);
}
.route .rstep-arw:active { transform: translateY(-50%) scale(0.97); }
.route .rstep-arw .material-symbols-outlined { font-size: 20px; }
/* No arrow_back or chevron in the 46-glyph subset, so the left one is arrow_forward turned
   around - the same trick .cswap uses for its down arrow. */
.route .rstep-arw.prev .material-symbols-outlined { transform: rotate(180deg); }

/* Dots sit in a dark glass pill rather than straight on the photo. White dots on a pale
   shot - a bright lobby, an overexposed street - disappear entirely, and which photo you
   are on is the one thing they exist to say. The pill makes that independent of the image. */
.route .rstep-dots {
  position: absolute;
  left: 50%; bottom: 8px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 8px;
  border-radius: 99px;
  background: rgba(16,24,40,0.38);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}
.route .rstep-dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: rgba(255,255,255,0.5);
  transition: background .2s ease, width .2s ease;
}
.route .rstep-dot.on { background: #fff; width: 14px; border-radius: 99px; }

/* Touch keeps the arrows but loses the blur - same split as every other glass surface here;
   a backdrop-filter over a photo is exactly the case WebKit handles worst. */
@media (hover: none), (pointer: coarse) {
  /* THE GLASS SURVIVES ON TOUCH HERE, which is a deliberate exception to the standing rule
     that backdrop-filter comes off on coarse pointers - and the exception is about AREA.
     That rule was written after the mobile menu lagged on the client's iPhone, and the
     measured cause there was a FULL-VIEWPORT blur(22px) compositing the entire page. These
     are three ~36px discs: about 3,000px of blurred area against roughly 340,000px for a
     phone-sized menu overlay, two orders of magnitude apart. Stripping the blur here bought
     nothing measurable and cost exactly the effect the client asked for, on the device he
     asked for it on.
     KEEP THE BLUR RADIUS LOWER THAN DESKTOP: cost scales with radius as well as area, and
     14px is visually indistinguishable at this size.
     If a phone ever does struggle with these, the fix is `backdrop-filter: none` here - not
     re-flattening the desktop too. */
  .route .rstep-arw,
  .route .rstep-expand,
  .sw-zoom {
    backdrop-filter: blur(14px) saturate(190%) brightness(1.12);
    -webkit-backdrop-filter: blur(14px) saturate(190%) brightness(1.12);
  }
  /* NOTE: the "always visible on touch" rule for .sw-zoom is NOT here - it lives beside the
     .sw-zoom base rules further down the file. Putting it here would lose: this block sits
     ABOVE `.sw-zoom { opacity: 0 }` in the source, and at equal specificity the later rule
     wins. It was written here first and silently did nothing. */
  .route .rstep-dots {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(16,24,40,0.5);
  }
  /* No hover to reveal it with, so it is simply always there. */
  .route .rstep-expand { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .route .rstep-slide, .route .rstep-reveal { transition: none; }
}

/* ── THE ROUTE PHOTO GALLERY (client, 16 Aug 2026) ──────────────────────────────────────
   Full-screen overlay holding every route photo, opened by the expand button or by clicking
   a photo. Built by site.js on first open, so none of this applies until then.

   NO backdrop-filter anywhere in here, deliberately. A full-viewport blur is the single most
   expensive thing WebKit can be asked for and it is what makes the mobile menu lag on the
   client's iPhone (see the mobile-engine notes). An opaque ground costs nothing and, over a
   photo gallery, is the better look anyway - the page behind should be gone, not softened. */
.cwlb {
  position: fixed;
  inset: 0;
  /* Above the header (66) and the mobile menu (65), which are the only fixed things here. */
  z-index: 300;
  display: flex;
  flex-direction: column;
  background: rgba(8, 11, 18, 0.97);
  opacity: 0;
  transition: opacity .2s ease;
}
.cwlb.open { opacity: 1; }

/* ⚠ THIS RULE IS LOAD-BEARING. WITHOUT IT THE WHOLE PAGE STOPS RESPONDING TO CLICKS.
   `display: flex` above is an AUTHOR rule, and author rules beat the UA stylesheet's built-in
   `[hidden] { display: none }` regardless of specificity. So setting `lb.hidden = true` in JS
   sets the attribute and changes nothing: the overlay stays laid out at full viewport size,
   z-index 300, opacity 0 - invisible, and swallowing every click on the page underneath.
   Reported as "after I close a photo I cannot click anything". Confirmed with
   `document.elementFromPoint(centre)` returning `.cwlb-img` after close.
   Exactly the same trap as `.rfoot[hidden]` on the homepage; any element given a display
   value by a class AND hidden via the attribute needs its own [hidden] rule.
   Verify with computed display or elementFromPoint - NOT with `el.hidden`, which reports the
   property and will happily say `true` while the element is still on screen. */
.cwlb[hidden] { display: none; }

/* Second line of defence: during the 200ms fade-out the class is gone but the element is
   still displayed, and if that hide timer is ever delayed (a throttled background tab) this
   is what stops it capturing clicks in the meantime. `.open` is added synchronously on open,
   so there is no frame where the gallery is visible but inert. */
.cwlb:not(.open) { pointer-events: none; }

/* The gallery's controls get the SAME glass, tuned for a dark ground instead of a photo:
   the fill is a low-opacity white over near-black, so the brightness lift is dropped (it
   would grey the overlay) and the glyph is white rather than ink. Same gradient + specular
   rim + shadow, so the two sets read as one family. */
.cwlb-x,
.cwlb-arw {
  border: 1px solid rgba(255,255,255,0.30);
  background: linear-gradient(158deg, rgba(255,255,255,0.18), rgba(255,255,255,0.07));
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
  box-shadow:
    0 12px 30px -14px rgba(0,0,0,0.75),
    inset 0 1px 0.5px rgba(255,255,255,0.5),
    inset 0 -10px 16px -12px rgba(0,0,0,0.3);
  color: #fff;
  cursor: pointer;
  padding: 0;
}

.cwlb-x {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  right: 14px;
  z-index: 2;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  transition: background .18s ease, transform .18s ease;
}
.cwlb-x:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.3), rgba(255,255,255,0.14));
  transform: scale(1.06);
}
.cwlb-x:active { transform: scale(0.97); }
.cwlb-x .material-symbols-outlined { font-size: 22px; }

/* The title line above the image. Padded clear of the close button on BOTH sides so the
   centring stays true - padding only on the right would centre the text in a box that is
   itself off-centre, which reads as a mistake rather than a layout. */
.cwlb-head {
  flex: none;
  padding: max(16px, env(safe-area-inset-top)) 68px 0;
  text-align: center;
  color: rgba(255,255,255,0.95);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.4;
}
.cwlb-title {
  display: block;
  max-width: 900px;
  margin: 0 auto;
}

/* min-height: 0 is load-bearing on a flex child that holds an image: without it the stage
   refuses to shrink below the image's intrinsic height and the bar gets pushed off-screen
   on short windows. */
.cwlb-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: grid;
  place-items: center;
  padding: 12px 5vw;
}
.cwlb-img {
  max-width: min(90vw, 900px);
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 14px;
  box-shadow: 0 30px 80px -30px rgba(0,0,0,0.8);
  /* The peek zoom writes `transform` here directly. Its transition is set per-gesture in JS -
     none while pinching so it tracks the fingers exactly, and a spring only on release. */
  transform-origin: center center;
}

/* `touch-action: none` is what lets the pinch work at all: without it the browser claims the
   two-finger gesture for its own page zoom and our touchmove never sees it. Safe to disable
   here because the page behind is already scroll-locked while the gallery is open, and the
   thumbnail strip sits OUTSIDE the stage so it keeps its own horizontal scrolling. */
.cwlb-stage { touch-action: none; }

.cwlb-arw {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  transition: background .18s ease, transform .18s ease;
}
.cwlb-arw:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.3), rgba(255,255,255,0.14));
  transform: translateY(-50%) scale(1.06);
}
.cwlb-arw:active { transform: translateY(-50%) scale(0.97); }
.cwlb-arw.prev { left: max(8px, 1vw); }
.cwlb-arw.next { right: max(8px, 1vw); }
.cwlb-arw .material-symbols-outlined { font-size: 24px; }
/* Same rotation trick as the step arrows - the icon subset carries no arrow_back. */
.cwlb-arw.prev .material-symbols-outlined { transform: rotate(180deg); }

.cwlb-bar {
  flex: none;
  padding: 0 5vw max(14px, env(safe-area-inset-bottom));
}
/* Caption + counter, CENTRED under the image (client, 16 Aug 2026). */
.cwlb-cap {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 10px;
  color: rgba(255,255,255,0.82);
  font-size: 14.5px;
  font-weight: 500;
}
/* "STEP N" on its own centred line directly above the caption (client, 16 Aug 2026).
   The step number is the only gold here. It is a label, not body text, so it does not hit
   the no-gold-text rule - and on this near-black ground it clears contrast comfortably,
   which is exactly the case gold fails on white. */
.cwlb-step {
  display: block;
  text-align: center;
  margin-bottom: 4px;
  color: var(--gold);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: .07em;
  text-transform: uppercase;
}
.cwlb-txt { min-width: 0; }
.cwlb-count {
  flex: none;
  color: rgba(255,255,255,0.5);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.cwlb-thumbs {
  display: flex;
  /* `safe center` centres the strip while it fits and falls back to flex-start the moment it
     overflows. Plain `center` is the trap here: an overflowing centred flex row pushes its
     first items into NEGATIVE scroll space that no scrollbar can reach, so photo 1 becomes
     unreachable as soon as the route grows past the screen width. */
  justify-content: safe center;
  gap: 8px;
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.cwlb-thumbs::-webkit-scrollbar { display: none; }

/* One photo in the set: nothing to page to, so the arrows, the strip and the counter are all
   chrome that does nothing. This is what lets the single access photo reuse the same viewer. */
.cwlb-solo .cwlb-arw,
.cwlb-solo .cwlb-thumbs,
.cwlb-solo .cwlb-count { display: none; }
.cwlb-thumb {
  flex: none;
  width: 76px;
  height: 51px;          /* 3:2, same as every route photo */
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  overflow: hidden;
  background: none;
  cursor: pointer;
  opacity: .45;
  transition: opacity .18s ease, border-color .18s ease;
}
.cwlb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cwlb-thumb:hover { opacity: .8; }
.cwlb-thumb.on { opacity: 1; border-color: var(--gold); }

@media (max-width: 600px) {
  /* 68px of side padding is a fifth of a 375px screen, so the title gets its own row clear
     of the close button instead of squeezing between two of them. */
  .cwlb-head { padding: max(14px, env(safe-area-inset-top)) 60px 0 14px; text-align: left; font-size: 14px; }
  .cwlb-title { margin: 0; }
  .cwlb-stage { padding: 10px 4vw; }
  .cwlb-img { max-width: 92vw; }
  /* Arrows shrink but stay. Swipe is the primary gesture on a phone, but the arrows are the
     only visible sign that there is anything to swipe TO. */
  .cwlb-arw { width: 36px; height: 36px; }
  .cwlb-arw .material-symbols-outlined { font-size: 20px; }
  .cwlb-bar { padding-left: 4vw; padding-right: 4vw; }
  /* The caption wraps rather than ellipsising - it is the one line telling you what you are
     looking at, so losing its end defeats the point. Centred, same as desktop. */
  .cwlb-cap { font-size: 14px; gap: 10px; flex-wrap: wrap; line-height: 1.35; }
  .cwlb-thumb { width: 60px; height: 40px; }
}
@media (prefers-reduced-motion: reduce) {
  .cwlb, .cwlb-thumbs { transition: none; scroll-behavior: auto; }
}

/* --- fact cards. Translucent white + hairline, deliberately NOT .glass-panel:
       this section already runs three live backdrop-filters and the standing
       rule is not to stack blurs on mobile. Same look, none of the cost. ---- */
.findfacts {
  display: grid;
  gap: 12px;
  margin-top: 16px;
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
}
.ffact {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 18px;
  border-radius: 18px;
  background: rgba(255,255,255,0.55);
  border: 1px solid rgba(255,255,255,0.75);
  box-shadow: var(--shadow-card);
  color: inherit;
  text-decoration: none;
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
a.ffact:hover {
  background: #ffffff;
  transform: translateY(-2px);
  box-shadow: 0 12px 28px -16px rgba(16,24,40,0.28);
}
/* width + overflow are a SAFETY NET, not styling. The icon font is a subset; an icon
   whose ligature is missing renders as its literal name and blows the card apart (the
   "schedule" glyph measured 192px and squeezed the text column to 64px). Clamping the
   box means a future missing ligature degrades to a clipped glyph instead of a broken
   layout. Costs nothing when the ligature is present - the glyph is exactly 24px. */
.ffact > .material-symbols-outlined {
  font-size: 24px;
  color: var(--gold);
  flex: none;
  width: 24px;
  overflow: hidden;
}
/* The text column must be allowed to shrink below its min-content width, or a long
   unbroken value pushes the card wider than its grid track. */
.ffact > div { flex: 1; min-width: 0; }
.ffact i {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}
.ffact b { display: block; font-size: 15.5px; font-weight: 700; color: var(--ink); line-height: 1.3; margin-bottom: 2px; }
.ffact > div > span { display: block; font-size: 13px; color: var(--slate); line-height: 1.45; }

/* THE CENTRED STACK, AND THE WRAP ZONE IT NOW COVERS (client, 10 Aug 2026).

   THE BUG THIS FIXES: between 601px and the width where the one-line layout genuinely fits,
   .findnow-cta wrapped onto a row of its own and its `margin-left: auto` then absorbed that
   entire row, slamming the buttons flush right. Measured: 1px from the right edge with up to
   407px of gap on the left. The bar looked broken, which is what the client caught while
   resizing the window.

   WHY 820: bar width runs at about viewport minus 63, and the one-line layout needs roughly
   740px of bar, so anything under about 803px of viewport was at risk. 820 is the round
   number above that, and it is also where the max-width cap stops binding - so the cap and
   this breakpoint change hands at the same place instead of fighting over a middle band.

   WHY A STACK RATHER THAN JUST CENTRING THE BUTTONS: text left with buttons centred
   underneath reads as unresolved. If it has to wrap, it should commit.

   The original reason still stands lower down: side by side, the tile forced the text into a
   narrow column and the three blocks all started at different x positions - tile at the left
   edge, text 60px in, hours centred. Stacking gives one axis for everything and hands the
   full width back to the time line, which is what lets the 320px case keep its 18px type. */
@media (max-width: 820px) {
  .loc-status.findnow {
    gap: 12px;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .loc-status.findnow .ls-icon { margin: 0 auto; }
  .loc-status.findnow .ls-top { justify-content: center; }
  /* flex: none is load-bearing. The bar is a COLUMN here, so flex-basis resolves against
     height, not width - the old `flex: 1 1 160px` handed this block a 160px height basis for
     70px of content and pushed the bar from 288px to 367px. The desktop 240px min-width goes
     for the same reason: it is a horizontal constraint that means nothing once stacked. */
  .loc-status.findnow .ls-txt {
    flex: none;
    min-width: 0;
    width: 100%;
    align-items: center;
    text-align: center;
  }
  /* BOTH margins auto, not just the left. In a COLUMN flex container a lone `margin-left:
     auto` pushes on the CROSS axis and outranks align-items:center, which parks the buttons
     against the right edge - precisely the bug this block exists to kill. The base rule sets
     only margin-left, so this has to reset the pair. */
  .loc-status.findnow .findnow-cta { margin-left: auto; margin-right: auto; }
}

@media (max-width: 600px) {
  .loc-status.findnow { padding: 18px; }
  /* The merged "time - Sydney time" line fits on one row on desktop but wraps at
     375px, and a sentence that rags across two lines looks accidental. On phones the
     two go back to being two deliberate rows, without the separator. Same height as
     the wrap, but it reads as a layout instead of an overflow. */
  .loc-status.findnow .s-tz { display: block; margin-top: 1px; }
  .loc-status.findnow .s-tz::before { content: none; }
  .findnow-cta { width: 100%; }
  /* nowrap + tighter side padding: at 375px the two buttons share ~139px each, and
     the default 28px side padding made the primary label break across two lines,
     which then stretched BOTH buttons to 78px tall. */
  .findnow-cta .btn { flex: 1; padding-left: 18px; padding-right: 18px; white-space: nowrap; }
  /* The hours drop to their own row under the status. The centring auto-margins have
     to go or the row sits off-centre, and the label plus the weekend line would add
     ~55px of height to a bar that is already the tallest thing above the fold - the
     weekend is implied anyway, since the status itself reads "Opens 9:30 AM Monday"
     all weekend. One line survives, with a rule to separate it. */
  .loc-status.findnow .ls-hours {
    margin: 0;
    width: 100%;
    padding-top: 12px;
    border-top: 1px solid rgba(16,24,40,0.08);
  }
  .loc-status.findnow .ls-hours .ls-sub,
  .loc-status.findnow .lsh-off { display: none; }
  /* Centred on phones: it is a single full-width line under a centred divider, and
     left-aligning it under a text block that starts 60px in read as misaligned.
     The 74px label floor has to go with it - a fixed-width label inside a centred
     row shifts the whole line off centre by the slack it reserves. */
  .loc-status.findnow .lsh-row { justify-content: center; }
  .loc-status.findnow .lsh-row b { min-width: 0; }
  .route { padding: 22px 20px; }
  .mapframe iframe { min-height: 260px; }
}

/* The tile used to be hidden here and the time dropped to 16.5px, because side by
   side the tile ate 60px and "Opens 9:30 AM Wednesday" (223px at 18px) had only 174px
   to live in. Stacking removed that constraint - the time now spans the full column -
   so both workarounds are gone and 320px keeps the brand mark and the full type. */
@media (max-width: 360px) {
  .loc-status.findnow { padding: 16px; }
  /* The state line steps down here or it wraps. Measured at 320px: the content box is 240px
     and "Our store is closed now" needs 238px at 21px - it went to two lines, and a
     five-word sentence ragging across two lines at hero size looks accidental.
     The CLOSED wording is the constraint, not the open one: "closed" is two characters
     longer than "open", so it is the string that has to be measured whenever this changes.
     19px brings it to about 215px, leaving ~25px of slack. Same step-down pattern the rates
     timestamp uses below 349/314px. */
  .loc-status.findnow .s-main { font-size: 19px; }
}

@media (max-width: 720px) {
  /* Six chips wrapped to four rows and cost 205px for secondary information. One
     scrolling row costs about 80px and is the pattern people already know from every
     filter bar on a phone. The bleed to the gutter edge is what signals scrollability
     - a strip that stops neatly inside the padding reads as a finished list. */
  .walkfrom { display: block; }
  .walklab { margin-bottom: 10px; }
  .walkchips {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    margin-inline: calc(var(--gutter, 20px) * -1);
    padding-inline: var(--gutter, 20px);
    scroll-padding-inline: var(--gutter, 20px);
  }
  .walkchips::-webkit-scrollbar { display: none; }
  .walkchips li { flex: none; }
}

/* ==========================================================================
   FOOTER - premium pass (client, 7 Aug 2026).
   The footer was already glass; what it lacked was a REASON to reach it and any
   sense of being alive. It now opens with a live band and the links respond.
   Everything here is site-wide, so it stays light - no new fonts, no new
   requests, and exactly one extra backdrop-filter.
   ========================================================================== */

/* Gloss on the top edge. A flat white 1px border read as a seam; a gold gradient
   that fades at both ends reads as a finished edge. Pure paint, no layout cost. */
.footer-premium { border-top-color: transparent; }
.footer-premium::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg,
    rgba(254,185,0,0) 0%, rgba(254,185,0,0.55) 28%, rgba(254,185,0,0.55) 72%, rgba(254,185,0,0) 100%);
}

/* --- the band. The one DARK surface on the page. -------------------------
   After a full page of white glass, another white panel here would disappear.
   Ink makes the last thing you see the one thing you can act on, and gold is
   legal as text on ink (12.15:1) which it never is on white. */
.fcta {
  position: relative;
  isolation: isolate;              /* keeps the glow layers under the text, not the page */
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 28px;
  flex-wrap: wrap;
  padding: 44px 48px;
  border-radius: var(--r);
  margin-bottom: 8px;
  background: linear-gradient(135deg, #161b22 0%, #0c0f14 100%);
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 24px 60px -30px rgba(12,15,20,0.55);
}
/* THE PHOTO SLOT. Two soft gold washes standing in for a blurred store photograph.
   When the photography lands, this one background becomes
   url(...) center/cover + a filter: blur(28px) - the layer, the clipping and the
   text contrast above it are already solved. */
.fcta::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: -1;
  background:
    radial-gradient(38% 60% at 12% 18%, rgba(254,185,0,0.30) 0%, transparent 62%),
    radial-gradient(42% 70% at 88% 96%, rgba(254,185,0,0.16) 0%, transparent 60%);
  filter: blur(28px);
}
/* A single sheen across the top edge. This is what reads as "glossy" rather than
   "flat dark rectangle", and it costs one gradient. */
.fcta::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  z-index: -1;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.28) 22%, rgba(254,185,0,0.5) 50%, rgba(255,255,255,0.28) 78%, transparent);
}
.fcta-txt { display: flex; flex-direction: column; gap: 8px; flex: 1 1 300px; }
.fcta-txt > b {
  font-size: clamp(22px, 2.4vw, 30px);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.025em;
  color: #ffffff;
}
.fcta-txt > span { font-size: 15px; line-height: 1.55; color: rgba(255,255,255,0.62); max-width: 46ch; }
.fcta-go { flex: none; padding-inline: 26px; }
.fcta-go .material-symbols-outlined { font-size: 20px; transition: transform .2s cubic-bezier(.16,1,.3,1); }
.fcta-go:hover .material-symbols-outlined { transform: translateX(3px); }

/* --- links: a gold rule that grows from the left instead of a colour flip. --- */
.footer-premium .links a {
  position: relative;
  display: inline-block;
  width: fit-content;
}
.footer-premium .links a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -3px;
  height: 2px;
  width: 100%;
  border-radius: 2px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .22s cubic-bezier(.16,1,.3,1);
}
.footer-premium .links a:hover::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .footer-premium .links a::after { transition: none; }
}

@media (max-width: 720px) {
  /* Mobile: stack it and let the button own a full row. Centring the text here is
     deliberate - a left-aligned two-line heading above a full-width button reads as
     two unrelated things on a narrow screen. */
  .fcta { padding: 32px 24px; gap: 22px; text-align: center; }
  .fcta-txt { align-items: center; flex-basis: 100%; }
  .fcta-txt > span { max-width: 34ch; }
  .fcta-go { width: 100%; }
}

/* ==========================================================================
   PHOTO BLOCKS (find-us, and any page that needs them later).
   width/height are on every <img> so the browser reserves the box before the
   file arrives - without them a lazy image collapses to zero and the page
   jumps as each one lands, which is the whole of a CLS score on a photo page.
   ========================================================================== */
/* 3:2, CAPPED AT 1200px AND CENTRED (client, 16 Aug 2026 - was 16:9 full-bleed).
   Changed so the client can reuse the store photos he already exports rather than shooting a
   separate wide counter frame: every master he produces is 3:2 with the branded footer, so a
   16:9 slot was centre-cropping 15% off the top and bottom of anything he reused.

   THE WIDTH CAP IS THE OTHER HALF OF THE CHANGE and is not cosmetic. 3:2 across the full
   1384px container would be 923px TALL - taller than most laptop viewports, so the photo
   alone would fill the screen. Capping at 1200 gives 1200x800, which is also exactly the
   dimensions of the masters, so the numbers stay easy to reason about: supply 2400x1600 for a
   crisp 2x, and an existing 1200x800 file still works at 1x.
   Keep `object-fit: cover` - it is doing nothing while the source is 3:2, and it is the
   safety net if a photo ever arrives slightly off. */
.shotwide { margin: 0 0 24px; }

/* The frame that carries the overlay controls. It has to shrink-wrap the photo, not the
   figure: the image is capped at 1200px and centred, so a full-width positioning context
   would park the buttons against the page edge instead of the picture's corners. */
.sw-frame {
  position: relative;
  width: 100%;
  max-width: 1200px;
  margin-inline: auto;
  cursor: zoom-in;
}
/* The expand button takes its glass from the shared rule above and its LAYOUT from here.
   Written out in full rather than inherited from `.route .rstep-expand`, which cannot reach
   outside the route section. Slightly larger than the route's 32px because the frame it sits
   on is up to 1200px wide rather than 380px. */
.sw-zoom {
  position: absolute;
  right: 12px;
  bottom: 12px;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  cursor: zoom-in;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .18s ease, transform .18s ease, background .18s ease, box-shadow .18s ease;
}
.sw-zoom .material-symbols-outlined { font-size: 21px; }
.sw-frame:hover .sw-zoom, .sw-zoom:focus-visible { opacity: 1; transform: none; }
.sw-zoom:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.62), rgba(255,255,255,0.34));
  transform: scale(1.06);
}
.sw-zoom:active { transform: scale(0.97); }
/* Must come AFTER the opacity:0 above - equal specificity, so source order decides. */
@media (hover: none), (pointer: coarse) {
  .sw-zoom { opacity: 1; transform: none; }
}

/* GET DIRECTIONS - bottom left, PURE glass (client, 16 Aug 2026).
   The brief was explicitly "no white background, full pure glass, blend the image" - so the
   fill is much weaker than the round controls (0.18 against 0.46) and the blur does almost
   all the work. That only reads at this size because the pill is wide enough for the blur to
   show the photo's colour through it; the same fill on a 32px circle would look like a smudge,
   which is why the two are tuned separately rather than sharing one class.
   The glyph and label are WHITE with a soft shadow rather than ink: a nearly-clear pill takes
   its brightness from whatever is behind it, and this photo runs from a dark doorframe to a
   near-white wall, so no fixed dark colour survives the whole width. */
.sw-dir {
  position: absolute;
  left: 12px;
  bottom: 12px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 14px 9px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.42);
  background: linear-gradient(158deg, rgba(255,255,255,0.24), rgba(255,255,255,0.10));
  backdrop-filter: blur(18px) saturate(190%);
  -webkit-backdrop-filter: blur(18px) saturate(190%);
  box-shadow:
    0 10px 26px -12px rgba(0,0,0,0.6),
    inset 0 1px 0.5px rgba(255,255,255,0.75),
    inset 0 -10px 16px -12px rgba(0,0,0,0.28);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  text-shadow: 0 1px 6px rgba(0,0,0,0.45);
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.sw-dir .material-symbols-outlined { font-size: 19px; }
.sw-dir:hover {
  background: linear-gradient(158deg, rgba(255,255,255,0.36), rgba(255,255,255,0.18));
  transform: translateY(-2px);
  box-shadow: 0 16px 32px -14px rgba(0,0,0,0.65), inset 0 1px 0.5px rgba(255,255,255,0.9);
}
.sw-dir:active { transform: translateY(0); }

@media (max-width: 520px) {
  /* Both controls tuck in and the label shrinks - at 327px of photo the two must not meet in
     the middle. Measured: they stay 100px+ apart at 320px of viewport. */
  .sw-dir { left: 8px; bottom: 8px; padding: 7px 11px 7px 9px; font-size: 13px; gap: 5px; }
  .sw-dir .material-symbols-outlined { font-size: 17px; }
  .sw-frame .sw-zoom { right: 8px; bottom: 8px; width: 32px; height: 32px; }
}
.shotwide img {
  display: block;
  width: 100%;
  max-width: 1200px;
  margin-inline: auto;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--r);
  border: 1px solid rgba(255,255,255,0.75);
  box-shadow: var(--shadow-card);
  background: var(--mist);
}
.shotwide figcaption {
  margin-top: 10px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
}

/* ── THE STORE GALLERY (client, 16 Aug 2026) ─────────────────────────────────────────────
   Replaced `.shotgrid`, which was three fixed slots. This takes any number of photos and
   every tile opens the full-screen viewer, so "Inside the store" grows by adding entries to
   the pipe lists in the markup rather than by writing more figures.

   TWO COLUMNS ON PHONES, THREE EVERYWHERE ELSE (client, 16 Aug 2026). With six photos that
   is deliberately a complete rectangle at both ends - 2x3 on a phone, 3x2 on a desktop - with
   no orphan tile on the last row, which is the thing that makes a small gallery look
   unfinished. An earlier version added a 4-column tier above 1000px; it was removed because
   six photos across four columns leaves a row of four and a row of two.
   At 375px a tile is ~160px: a browsing thumbnail rather than a showcase image, which is the
   right call - it shows six photos in the space one used to take, and anyone who wants to
   actually look at one taps it.
   **If the photo count changes, re-check this.** The column counts are chosen for six. */
.storegal {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}
@media (min-width: 600px) { .storegal { grid-template-columns: repeat(3, 1fr); gap: 16px; } }

.sg-tile {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  padding: 0;
  /* A smaller radius than the old three-up. 20px on a 160px tile eats the corners of the
     photo; it has to scale with the tile, not stay at the value a 450px card wanted. */
  border-radius: 12px;
  border: 1px solid rgba(255,255,255,0.75);
  overflow: hidden;
  background: var(--mist);
  box-shadow: var(--shadow-card);
  cursor: zoom-in;
  transition: transform .22s cubic-bezier(.16,1,.3,1), box-shadow .22s ease;
}
@media (min-width: 600px) { .sg-tile { border-radius: 16px; } }
.sg-tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.sg-tile:hover { transform: translateY(-3px); box-shadow: 0 18px 40px -20px rgba(16,24,40,0.5); }
.sg-tile:active { transform: translateY(-1px); }

/* Same glass as the route's expand button, at tile scale - one visual language for "this
   opens bigger". Hover only on a mouse; on touch it is always faintly there. */
.sg-zoom {
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  font-size: 15px;
  border-radius: 9px;
  border: 1px solid rgba(255,255,255,0.62);
  background: linear-gradient(158deg, rgba(255,255,255,0.46), rgba(255,255,255,0.22));
  backdrop-filter: blur(14px) saturate(190%) brightness(1.12);
  -webkit-backdrop-filter: blur(14px) saturate(190%) brightness(1.12);
  box-shadow: 0 8px 20px -10px rgba(16,24,40,0.55), inset 0 1px 0.5px rgba(255,255,255,0.95);
  color: var(--ink);
  opacity: 0;
  transform: translateY(3px);
  transition: opacity .18s ease, transform .18s ease;
  pointer-events: none;
}
.sg-tile:hover .sg-zoom, .sg-tile:focus-visible .sg-zoom { opacity: 1; transform: none; }
@media (hover: none), (pointer: coarse) { .sg-zoom { opacity: .85; transform: none; } }

/* 3:2 TOO, so the WHOLE SITE is one ratio (client, 16 Aug 2026 - was 4:3).
   4:3 is the better interior ratio in the abstract - more ceiling-to-counter in frame - and
   it is what a phone shoots natively. It still loses on the argument that matters here: every
   master the client produces is 3:2 with a branded footer baked in at a fixed position, so a
   4:3 slot would centre-crop 12.5% off each SIDE of anything reused, and would need its own
   crop, its own export preset and its own footer placement. One ratio across the site means
   one preset and no per-slot thinking, which is worth more than the marginal framing gain.
   At 451px wide these render 451x301, so the existing 1200x800 export is already 2.7x -
   comfortably enough here without a bigger file. */
@media (hover: hover) and (pointer: fine) {
}


/* ==========================================================================
   CURRENCY PICKER TRIGGER (client, 8 Aug 2026).
   At rest this was a transparent chip identical to the fixed AUD one beside it -
   the only clue you could press it was a small chevron, so people read it as a
   label and never found out they could change currency.
   It now carries the SAME gold glazing as the currency cards: gold in the
   top-left and bottom-right corners, fading to a light rim through the middle.
   No grey border, and no pulsing ring - the attention comes from the glaze
   taking one half-turn every 20 seconds, which is the same gesture the cards
   make on hover.
   .chip.selchip is (0,2,0) so it beats both .chip and .selchip whatever the order.
   ========================================================================== */
/* RESERVE THE BORDER ON THE PLAIN CHIP TOO. It is transparent and invisible, and it is
   load-bearing: the picker chip carries a 1px border, so without this it stands 42px
   against the plain AUD chip's 40px. The two swap rows on every buy/sell toggle, which
   made "You pay" grow 2px and "You receive" shrink 2px on every switch.
   The overall card height never changed - the +2 and -2 cancel - which is exactly why
   this reads as a twitch rather than an obvious layout break.
   Same trap the currency picker rows already document: reserve the border everywhere,
   never add it only to the active state. */
.chip { border: 1px solid transparent; }

.chip.selchip {
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.8);   /* fallback rim; the ring replaces it below */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 2px rgba(16, 24, 40, 0.05);
  padding: 4px 10px 4px 6px;
  transition: background .18s ease, box-shadow .18s ease, transform .12s ease;
}
/* Gold fill dialled back to 20% -> 10% (client, 8 Aug 2026 - the previous 26/13 read
   as a solid gold chip rather than a tint). Low enough that the white surface still
   shows through, strong enough to register as a state change. */
.chip.selchip:hover,
.chip.selchip:focus-visible {
  background: linear-gradient(160deg, rgba(254, 185, 0, 0.20), rgba(254, 185, 0, 0.10));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 5px 14px -6px rgba(254, 185, 0, 0.40);
}
.chip.selchip:active { transform: translateY(1px); }
/* The chevron is the part that says "opens". */
.selchip .ar { color: var(--gold); transition: transform .2s cubic-bezier(.16, 1, .3, 1); }
.chip.selchip:hover .ar { transform: translateY(2px); }

@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)) {
  .chip.selchip { border-color: transparent; }

  /* Same masked-gradient ring as .rrow and .qacard. border-image cannot do this - it
     ignores border-radius and would square the corners off - so the ring is a gradient
     box with its own centre punched out. */
  .chip.selchip::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1px;
    background: linear-gradient(var(--qangle),
      rgba(254,185,0,0.90)  0%,
      rgba(254,185,0,0.28) 26%,
      rgba(255,255,255,0.85) 50%,
      rgba(254,185,0,0.28) 74%,
      rgba(254,185,0,0.90) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    pointer-events: none;
    /* ONE HALF-TURN EVERY 20 SECONDS. The sweep occupies only the first 6% of the
       cycle - about 1.2s of movement, then 18.8s at rest. A gradient that turned
       continuously would be decoration you stop seeing; a rare, brief move is what
       actually catches an eye that has settled elsewhere on the page.
       135deg and 315deg look identical because the gradient carries gold at both
       ends, so the loop restarting is invisible - no jump to hide. */
    animation: chipglaze 20s linear infinite;
  }
  @keyframes chipglaze {
    0%   { --qangle: 135deg; animation-timing-function: cubic-bezier(.32, 0, .24, 1); }
    6%   { --qangle: 315deg; }
    100% { --qangle: 315deg; }
  }

  /* HOVER GETS ITS OWN HALF-TURN, on a SECOND ring layer rather than by retiming the
     first. Putting the hover animation on ::before would replace the 20s cycle, and
     un-hovering would restart that cycle from 0 - firing a second, unasked-for sweep
     every time the pointer left. Two layers keep the ambient rhythm and the response
     to a human completely independent.
     Same trick as the currency cards: the bright ring is always present and simply
     fades in, so nothing has to re-resolve five gradient stops mid-animation. */
  .chip.selchip::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 1.5px;                 /* thicker than the 1px resting ring, but this chip
                                       is small - 2px reads as chunky at a 10px radius */
    background: linear-gradient(var(--qangle),
      rgba(254,185,0,1)     0%,
      rgba(254,185,0,0.34) 26%,
      rgba(255,255,255,0.9) 50%,
      rgba(254,185,0,0.34) 74%,
      rgba(254,185,0,1)   100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    pointer-events: none;
    opacity: 0;
    transition: opacity .2s ease;
  }
  .chip.selchip:hover::after,
  .chip.selchip:focus-visible::after {
    opacity: 1;
    animation: qhalf .95s cubic-bezier(.32, 0, .24, 1) 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .chip.selchip::before, .chip.selchip::after { animation: none; }
  .chip.selchip, .chip.selchip::after, .selchip .ar { transition: none; }
}

/* ==========================================================================
   DESKTOP TYPE SCALE (client, new.css, 10 Aug 2026)
   ==========================================================================
   28 type values the client sized by hand on a desktop screen. MOBILE KEEPS THE
   SIZES IT HAS TODAY - that was the explicit instruction, and it is why these
   are a min-width block instead of edits to the base rules.

   Doing it the other way round would have been wrong twice over:
   1. Only 4 of these 28 declarations have any mobile override at all, so raising
      the base value would have silently raised 24 of them on phones too.
   2. Three of them (.seg button, .crow .clabel, .detail-row) already step DOWN
      inside max-width: 520px / 380px blocks for the small-phone calculator. A
      max-width pin added after those blocks would have out-ordered them at equal
      specificity and wiped the step-down out. Scoping upward cannot reach them.

   601px is the boundary because 600px is where this file already switches the
   footer, the status bar and the legal links to their phone layouts. Nothing in
   this list is overridden in the 720/900/950/989 blocks, so there is no conflict
   anywhere above 601. Placed last in the file so it wins on source order at equal
   specificity - keep it last.
   ========================================================================== */
@media (min-width: 601px) {
  .menu a { font-size: 15px; }
  .seg button { font-size: 16px; font-weight: 700; }
  .crow .clabel { font-size: 15px; }
  .detail-row { font-size: 14px; }
  .rate-inv { font-size: 12.5px; }
  .calc-upd { font-size: 12px; }
  /* The "A$1 =" label on the board cards. Desktop only, and that split is load-bearing: the
     base is 12.5px because on a 171px PHONE card anything larger wraps the rate line (14.5
     wrapped six of the 22). A desktop card is 264px and has the room, so it gets the client's
     preferred 14.5 while phones keep the size that fits. */
  .rr-rate em { font-size: 14.5px; }

  .head p { font-size: 20px; }
  .crumbs li { font-size: 14.5px; }

  .faq-trigger { font-size: 20px; }
  .faq-content p { font-size: 17px; }
  .qacard h3 { font-size: 20px; }
  .qacard p { font-size: 17px; }

  .loc-status.findnow .ls-sub { font-size: 12.5px; }
  .loc-status.findnow .s-tz { font-size: 14px; }
  .route b { font-size: 18px; }
  .route span { font-size: 16px; }
  .walkchips li { font-size: 14.5px; }
  .maptabs button { font-size: 15.5px; }
  .ffact i { font-size: 13.5px; }
  .ffact b { font-size: 17.5px; }
  .ffact > div > span { font-size: 17px; }
  .shotwide figcaption { font-size: 18px; }

  .footer-premium .brand-col p { font-size: 16px; }
  .footer-premium h4 { font-size: 14px; }
  .footer-premium .links a,
  .footer-premium .links span { font-size: 17px; }
  .footer-premium .bottom { font-size: 14px; }
  .footer-premium .legal-links a { font-size: 14px; }
}

/* ==========================================================================================
   ABOUT PAGE (/about/) - rewritten 17 Aug 2026
   The first version of this block invented .abrow / .abtext / .abshots / .abq and a
   `hero-plain` hero. That was the mistake: a page-specific layout language, when the site has
   one block system used everywhere. All of it is gone. About now runs on the site's own
   components - .hgrid, .findfacts, .qa/.qacard, .storegal, .shotwide, .trustband and the real
   .faq-item accordion - so what remains here is only what those components genuinely need on
   this page. If a future page needs more than a handful of rules, that is the signal it has
   drifted off the system, not that the system needs extending.
   ======================================================================================= */

/* ---- fixed-column variants of the existing .qa grid --------------------------------------
   .qa is `display:grid; gap:16px` with no column rule, so it stacks by default and these only
   add columns once there is room. Two variants because the page uses a three-up row and a
   two-up row, each matching its own card count so nothing is ever orphaned. */
@media (min-width: 760px) {
  .qa-3 { grid-template-columns: repeat(3, 1fr); }
  /* .qa-2 was removed once when the section using it was cut from /about/, and that silently
     broke /wholesale/ and /contact/, which both use two-card rows - they stacked instead of
     sitting side by side. These modifiers belong to the SHARED .qa component, so do not delete
     one because the page you happen to be editing stopped using it. */
  .qa-2 { grid-template-columns: repeat(2, 1fr); }
}
/* Two .qa grids in a row were butting together at EXACTLY 0px, so six cards read as one
   block with an unexplained seam through the middle. Matching .qa's own 16px internal gap
   makes the two rows read as a single continuous 3 x 2 grid, which is what they are. */
.qa + .qa { margin-top: 16px; }

/* ---- fixed 3-up fact grid ----------------------------------------------------------------
   Base .findfacts is `repeat(auto-fit, minmax(232px, 1fr))`, which fits FIVE across the
   1432px container. Five suits the four and five-card rows on find-us; with six cards it
   wraps 5 + 1 and strands the last one on its own row. Six wants 3 x 2.
   If a card is added or removed here, re-check this - auto-fit will not do it for you. */
@media (min-width: 600px) and (max-width: 950px) {
  .ff-3 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 951px) {
  .ff-3 { grid-template-columns: repeat(3, 1fr); }
}


/* ---- .story - the editorial narrative block ---------------------------------------------
   Prose beside one photograph, alternating sides. Added on the client's direction (17 Aug
   2026) for the About page's origin story, and deliberately built as a SITE component rather
   than an about-only one: the same shape is what the wholesale page, the currency pages and
   any guide will want. Use it wherever an argument needs to be read in order - a .qa card
   grid gives every idea equal weight, which is right for scannable points and wrong for a
   story.
   Sizing lesson from this page: the photo column is the NARROW one. An earlier attempt made
   the images the wider track, which left the prose ending half a screen above the bottom of
   the picture. One image per block, not a stack, keeps the two columns close in height. */
.story { display: grid; gap: 24px; align-items: start; }
.story + .story { margin-top: 40px; }

.story-text h3 {
  font-size: 21px;
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin-bottom: 10px;
}
.story-text p {
  font-size: 16.5px;
  line-height: 1.68;
  color: var(--slate);
  margin-bottom: 14px;
}
.story-text p:last-child { margin-bottom: 0; }
/* Links in body prose carry a resting underline. Inside a paragraph there is nothing else to
   signal a link, and "wholesale" is a link EVERY time it appears (client rule), so it has to
   read as one without relying on colour alone.
   COLOUR AND WEIGHT ARE DELIBERATELY INHERITED (client, 17 Aug 2026). An earlier version set
   `color: var(--ink)` and `font-weight: 600`, which made the word darker AND bolder than the
   slate paragraph around it - three signals for one link, and with "wholesale" appearing twice
   in the same block it read as shouting. A thin gold rule under otherwise ordinary body text
   is enough: the link is discoverable without breaking the line it sits in. Do not add colour
   or weight back. */
.story-text a {
  text-decoration: underline;
  text-decoration-color: var(--gold);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.story-text a:hover { text-decoration-color: var(--ink); }

.story-shot { margin: 0; }
.story-shot img {
  width: 100%;
  /* height:auto is load-bearing - see the herophoto note. The <img> carries height="800",
     and a presentational height attribute beats aspect-ratio when CSS sets width alone. */
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 20px;
  border: 1px solid rgba(0,0,0,0.06);
}
.story-shot figcaption {
  font-size: 13px;
  color: var(--slate);
  padding-top: 9px;
}

@media (min-width: 900px) {
  .story { grid-template-columns: 1.25fr 0.75fr; gap: 44px; }
  /* Photo to the left on the flipped block. Done with `order` so the DOM keeps text first -
     that is the reading order for a screen reader and the stacking order on a phone, and
     only the visual columns swap. */
  .story-flip .story-text { order: 2; }
  .story-flip .story-shot { order: 1; }
  /* THE COLUMN WIDTHS MUST BE REVERSED TOO, and forgetting this is the whole trap.
     `order` moves an element into a different grid TRACK, and the tracks keep their own
     widths - so flipping alone handed the photo the wide 1.25fr column and squeezed the
     prose into 0.75fr. Measured: text 500px, photo 833px, the exact inversion of the intent.
     Reversing the template keeps the photo on the narrow track whichever side it sits. */
  .story-flip { grid-template-columns: 0.75fr 1.25fr; }
}

/* ---- .story photo blend (client, 17 Aug 2026) -------------------------------------------
   The photo's INNER edge - the one facing the text - fades out so the image and the prose
   read as one composition instead of a picture parked in a box beside a paragraph.

   DONE WITH A MASK, NOT A WHITE GRADIENT OVERLAY, and that matters here. An overlay has to
   match the colour behind it exactly; these blocks sit on `.band.mist`, whose background is
   itself a vertical gradient, so a flat white overlay would show as a pale rectangle over the
   darker part of the band. A mask fades the image to real transparency, so whatever is behind
   shows through and it stays correct if the band's background ever changes.

   DESKTOP ONLY. Below 900px the layout stacks and the photo is full width - fading one side
   of a full-width image reads as a rendering fault, not a design. The border and full radius
   come back at that size, which is why they live on the base rule and are overridden here.

   The BORDER IS REMOVED at this size on purpose: a 1px outline drawn around an edge that is
   fading to transparent stays fully opaque and hangs in the air as a floating hairline.
   The radius is kept only on the OUTER corners - the inner ones sit inside the faded zone
   where no radius is visible anyway. */
@media (min-width: 900px) {
  .story-shot img {
    border: 0;
    /* outer corners keep the brand radius; inner edge is square because it dissolves */
    border-radius: 0 20px 20px 0;
    -webkit-mask-image: linear-gradient(to left, #000 58%, transparent 100%);
            mask-image: linear-gradient(to left, #000 58%, transparent 100%);
  }
  /* Flipped block: photo is on the LEFT, so the fade and the radius both mirror. */
  .story-flip .story-shot img {
    border-radius: 20px 0 0 20px;
    -webkit-mask-image: linear-gradient(to right, #000 58%, transparent 100%);
            mask-image: linear-gradient(to right, #000 58%, transparent 100%);
  }
  /* Caption follows the SOLID edge of the photo. Left-aligned under a right-hand image would
     sit under the part that has faded away and read as unattached to anything. */
  .story-shot figcaption { text-align: right; }
  .story-flip .story-shot figcaption { text-align: left; }
}

/* ---- .herocard - the hero slot on pages with no calculator --------------------------------
   Sits in grid-area: calc, the 440px column the rate calculator occupies elsewhere. Used by
   /wholesale/ and /contact/, both on the client's instruction that wholesale is a different
   kind of page.
   THIS IS NOT A LICENCE TO DROP THE CALCULATOR. On the retail pages the calculator IS the
   page's action and it stays. These two are different because their action is different: a
   bureau buying a bundle rings a dealing desk (every wholesale operator researched does
   exactly this), and a contact page's most valuable element is the phone number. A rate widget
   in the top slot of either would be answering a question the visitor did not ask.
   It is deliberately built as ONE component shared by both rather than two page-specific ones. */
.herocard { margin: 0; }
.herocard-in {
  padding: 22px 20px;
  border-radius: 24px;
}
.hc-eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate);
  margin-bottom: 14px;
}
/* Each row is a whole tap target, not just the text. On a phone this card is the primary
   action on the page and a 12px-tall link would be the wrong thing to aim at. */
.hc-line {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 11px 10px;
  margin: 0 -10px;
  border-radius: 14px;
  color: inherit;
  transition: background 0.15s ease;
}
a.hc-line:hover { background: rgba(0,0,0,0.04); }
.hc-line + .hc-line { border-top: 1px solid rgba(0,0,0,0.07); }
.hc-line > .material-symbols-outlined {
  font-size: 21px;
  color: var(--gold);
  flex: none;
}
.hc-line i {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--slate);
  margin-bottom: 2px;
}
.hc-line b {
  display: block;
  font-size: 16px;
  line-height: 1.32;
  color: var(--ink);
  /* The email address is the longest string in here and must not push the card wider than
     its 440px track. */
  overflow-wrap: anywhere;
}
/* The hours row is not a link - it is information. It keeps the row rhythm but must not
   suggest it is tappable. */
.hc-static { cursor: default; }
.hc-note {
  font-size: 13px;
  line-height: 1.55;
  color: var(--slate);
  padding: 13px 0 2px;
  margin: 0;
}
@media (min-width: 951px) {
  .herocard { grid-area: calc; align-self: start; }
}

/* ---- .hlink - inline "next step" link under a hero paragraph, PHONE ONLY -----------------
   Added for /contact/ (client, 17 Aug 2026). On a phone the hero stacks as heading, sub,
   contact card, buttons - so the "How to find us" button sat below a full card and was easy
   to scroll past, even though it is the second thing most visitors want after the number.
   This puts it immediately under the sentence it follows from.
   Hidden from 951px because .hcta already carries the same destination as a proper button at
   that size; two links to one page a few centimetres apart is clutter, not emphasis. */
.hlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 18px;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
  /* A hero sits directly on the page background with nothing else to hit, so the row is
     padded out to a comfortable target rather than being a bare line of text. */
  padding: 6px 0;
}
.hlink .material-symbols-outlined { font-size: 20px; color: var(--gold); }
/* THE UNDERLINE IS ON THE TEXT SPAN, NOT THE ANCHOR. Putting text-decoration on the flex
   anchor drags the rule under the icon too, which reads as a rendering fault rather than as
   a link. Same fix already applied to .mobnav-loc in the header - see the note there. */
.hl-t {
  text-decoration: underline;
  text-decoration-color: var(--gold);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.hlink:hover .hl-t { text-decoration-color: var(--ink); }
@media (min-width: 951px) { .hlink { display: none; } }

/* The map pane inside a .story column. .story-shot's own rules target `img`, so the frame
   needs its height set here - without it the flex column collapses to the iframe's 340px
   minimum and sits shorter than the prose beside it. */
.story-shot .mapframe { padding: 10px; }
@media (min-width: 900px) {
  .story-shot .mapframe iframe { min-height: 360px; }
}

/* MEASURE CAP on .story prose. Measured on /contact/: the 833px column was running 115
   CHARACTERS PER LINE. Comfortable reading measure is 45-75 - past about 80 the eye starts
   losing its place on the return sweep, which is a readability fault before it is an
   aesthetic one. It also had a second effect: the copy collapsed into four long lines, so a
   189px text block sat beside a 427px map and left a 238px hole.
   Capping the measure fixes both at once - the same words wrap into more lines, so the column
   gets taller as it gets easier to read. Deliberately in `ch` rather than px so it tracks the
   font size instead of needing a new number every time the type scale moves.
   The right-hand gap this leaves inside the column is intentional editorial whitespace, not a
   layout fault. Do NOT "fix" it by widening the text back. */
.story-text { max-width: 64ch; }

/* ==========================================================================================
   LEGAL DOCUMENT PAGES (29 Aug 2026)
   privacy-policy.html, in-store-terms-and-conditions.html and
   best-price-guarantee-terms-and-conditions.html.

   These do NOT reuse .story. The first build did, and it was wrong twice over: .story becomes
   a 1.25fr/0.75fr grid above 900px for the photo that sits beside editorial prose, and
   .story-text caps at 64ch - between them a legal page used barely half the container and
   read as a broken layout with a dead right-hand side. Widening .story to fill the container
   then swung too far the other way, at ~220 characters a line.

   The structure here is the one proven on the TMEX legal pages: a STICKY CONTENTS RAIL beside
   the document. That is what resolves it - the rail occupies the left, the prose keeps a
   readable measure on the right, and there is no dead space, because the space is now doing a
   job. Numbered, anchored sections make every clause deep-linkable, which is the other thing
   a legal page needs and prose blocks cannot give.

   ICONS: only `menu`, `info` and `verified_user` are used. The icon font is a 46-glyph
   SUBSET and a name that is not in it renders as the literal word - `list`,
   `event_available` and `shield_person` (the TMEX choices) are all absent. Re-subsetting is
   scripts/subset-material-symbols.py; do not type a new ligature without doing it.
   ======================================================================================= */
.legal-head { padding: 40px 0 26px; border-bottom: 1px solid var(--line); }
.legal-head h1 { font-size: 44px; line-height: 1.08; letter-spacing: -0.02em; }
.legal-updated {
  display: flex; align-items: center; gap: 7px;
  margin-top: 12px; font-size: 13.5px; color: var(--muted);
}
.legal-updated .material-symbols-outlined { font-size: 18px; color: var(--gold); }
.legal-intro { margin-top: 12px; font-size: 17px; line-height: 1.6; color: var(--slate); max-width: 68ch; }

/* "In short" - a plain-English summary above the document. It carries its own note saying the
   full text is what applies, which is not decoration: a summary that could be mistaken for
   the terms themselves is a liability, and the note is what stops that reading. */
.legal-sumwrap { padding-top: 26px; }
.legal-sum {
  border: 1px solid var(--line);
  border-left: 4px solid var(--gold);
  background: var(--mist);
  border-radius: var(--rs);
  padding: 22px 26px;
}
.legal-sum h2 { font-size: 19px; letter-spacing: -0.01em; display: flex; align-items: center; gap: 9px; }
.legal-sum h2 .material-symbols-outlined { font-size: 21px; color: var(--gold); }
.legal-sum-note { font-size: 13px; color: var(--muted); margin-top: 5px; }
.legal-sum ul { margin: 15px 0 0; padding-left: 0; list-style: none; }
.legal-sum li {
  position: relative; padding-left: 20px;
  font-size: 15.5px; line-height: 1.62; color: var(--slate); margin-bottom: 9px;
}
.legal-sum li:last-child { margin-bottom: 0; }
.legal-sum li::before {
  content: ""; position: absolute; left: 3px; top: 10px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--gold);
}

.legal-main { padding-top: 30px; padding-bottom: 26px; }
/* One column on phones - a sticky rail above a document is just a wall to scroll past. */
.legal-grid { display: grid; grid-template-columns: 1fr; gap: 26px; align-items: start; }
@media (min-width: 900px) {
  .legal-grid { grid-template-columns: 264px minmax(0, 1fr); gap: 56px; }
  /* 96px clears the header at its resting height. */
  .legal-nav { position: sticky; top: 96px; }
}

.legal-toc { border: 1px solid var(--line); border-radius: var(--rs); background: var(--paper); overflow: hidden; }
.legal-toc summary {
  display: flex; align-items: center; gap: 8px;
  padding: 13px 16px;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink); cursor: pointer; background: var(--mist); list-style: none;
}
.legal-toc summary::-webkit-details-marker { display: none; }
.legal-toc summary .material-symbols-outlined { font-size: 19px; color: var(--gold); }
/* The list scrolls inside the rail rather than the rail growing past the viewport - a 14-item
   contents that runs off the bottom of a sticky element can never be reached. */
.legal-toc ol { list-style: none; margin: 0; padding: 8px 0; max-height: calc(100vh - 200px); overflow-y: auto; }
.legal-toc li a {
  display: flex; gap: 10px; padding: 7px 16px;
  font-size: 14px; color: var(--slate); text-decoration: none; line-height: 1.4;
  transition: color 0.18s ease, background-color 0.18s ease;
}
.legal-toc li a:hover { color: var(--ink); background: var(--mist); }
.legal-toc .ln-n { flex-shrink: 0; font-variant-numeric: tabular-nums; font-weight: 650; color: var(--muted); }
.legal-toc li a.on { color: var(--ink); font-weight: 650; }
.legal-toc li a.on .ln-n { color: var(--gold); }

/* 92ch, not the 64ch of editorial prose and not the full column either. The rail plus this
   measure lands the content near the container edge at 1440 without the very long lines that
   a full-width legal page produces. */
.legal-body { max-width: 92ch; }
.legal-sec { padding-bottom: 28px; scroll-margin-top: 112px; }
.legal-sec:last-child { padding-bottom: 0; }
.legal-sec h2 { font-size: 23px; letter-spacing: -0.01em; line-height: 1.25; margin-bottom: 11px; display: flex; gap: 10px; }
.legal-sec .ls-n { color: var(--gold); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.legal-sec p { font-size: 16.5px; line-height: 1.72; color: var(--slate); margin-bottom: 12px; }
.legal-sec p:last-child { margin-bottom: 0; }
.legal-sec b, .legal-sec strong { color: var(--ink); font-weight: 650; }

.legal-list { margin: 0 0 12px; padding-left: 0; list-style: none; }
.legal-list:last-child { margin-bottom: 0; }
.legal-list li {
  position: relative; padding-left: 20px;
  font-size: 16.5px; line-height: 1.72; color: var(--slate); margin-bottom: 7px;
}
.legal-list li:last-child { margin-bottom: 0; }
.legal-list li::before {
  content: ""; position: absolute; left: 3px; top: 12px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--gold);
}

.callout {
  display: flex; gap: 12px;
  border: 1px solid var(--line); border-radius: var(--rs);
  background: var(--mist); padding: 16px 18px; margin: 6px 0 4px;
}
.callout .material-symbols-outlined { font-size: 22px; color: var(--gold); flex-shrink: 0; }
.callout b { display: block; margin-bottom: 4px; color: var(--ink); }
.legal-sec .callout p { margin-bottom: 0; font-size: 15.5px; line-height: 1.62; }

@media (max-width: 600px) {
  .legal-head { padding: 30px 0 22px; }
  .legal-head h1 { font-size: 32px; }
  .legal-sum { padding: 18px 18px; }
  .legal-sec h2 { font-size: 20px; }
}

/* ---- .story-map - prose + facts beside a map that fills the height -----------------------
   Used by /contact/. .story on its own top-aligns both columns, which is right when a photo
   sits beside prose but wrong beside a map: a map has no natural height, so it left a 113px
   hole under the text.
   Stretching solves it structurally rather than by tuning numbers. .mapframe is a flex column
   whose iframe is flex:1, so the map simply grows to whatever height the column beside it
   ends up being - the gap cannot return when the copy is edited later. */
.story-map { align-items: stretch; }
.story-col { display: flex; flex-direction: column; gap: 22px; }
/* The facts sit inside the left column, so they must NOT inherit .story-text's 64ch measure
   cap - that is for reading prose, and these are scannable cards that want the full column. */
.story-col .findfacts { margin-top: 0; }
@media (min-width: 900px) {
  .ff-2 { grid-template-columns: repeat(2, 1fr); }
  /* The map has no intrinsic height here, so its floor comes from the column beside it.
     This only stops it collapsing if that column is ever very short. */
  .story-map .mapframe { height: 100%; }
  .story-map .mapframe iframe { min-height: 300px; }
}

/* ==========================================================================================
   THE SHAPE LADDER (17 Aug 2026)
   Added because every content section on /wholesale/ and /contact/ was the same shape: a
   centred heading over a grid of equal .qacard boxes. Individually fine, three times a page it
   reads as one note held down, and it throws away information - a sequence, a two-sided answer
   and a scannable list all looked identical.
   These are NOT a move away from the glass language. .qacard stays exactly as it is and stays
   the right answer for things you CHOOSE BETWEEN. These are the lighter rungs beneath it, for
   content that is a list, a comparison or an order. The precedent for translucent-without-blur
   is already in this file: .findfacts is deliberately not .glass-panel for the same reason.
   Pick by what the content IS:
     .plainrow   things that are all true at once, scanned to find yourself
     .splitlist  a longer scannable set, two columns, hairline separated
     .flow       an ordered sequence where the order carries meaning
     .contrast   two sides of one line (already existed)
     .qacard     options you pick between (already existed)
   ======================================================================================== */

/* ---- .plainrow - borderless text blocks. The lightest rung. -------------------------------
   No box, no blur, no shadow. For a set where the reader is looking for THEMSELVES in a list
   rather than choosing between products - putting those in boxes implies a choice that is not
   being offered.
   THE ICON IS OPTIONAL and there is a reason it is left off on /wholesale/. The Material
   Symbols subset is 21 glyphs and none of them honestly reads "financial institution" or
   "cash-heavy business"; the nearest are travel and shop glyphs that would misdescribe the
   audience. Adding glyphs to the subset for decoration is not worth the font weight, and a
   wrong icon is worse than none. Where an honest glyph exists, add it and it will lay out. */
.plainrow { display: grid; gap: 30px 44px; }
@media (min-width: 760px) { .plainrow { grid-template-columns: repeat(3, 1fr); } }
.plainrow-item { display: flex; gap: 16px; align-items: flex-start; }
.plainrow-item > .material-symbols-outlined {
  font-size: 26px;
  color: var(--gold);
  flex: none;
  margin-top: 2px;   /* icon boxes sit high against text */
}
.plainrow-item h3 { font-size: 17.5px; line-height: 1.3; letter-spacing: -0.01em; margin-bottom: 7px; }
.plainrow-item p { font-size: 15.5px; line-height: 1.62; color: var(--slate); margin: 0; }

/* ---- .splitlist - two columns of hairline-separated rows ----------------------------------
   For six or more short claims. Six boxes is the most over-carded shape there is; six rows
   separated by a rule reads in about a third of the vertical space and lets the eye run down
   the bold line only.
   NO ROW MARKER, and that is a brand rule, not a style preference: a tick is not a mark this
   brand uses. The hairline and the bold lead-in are what make it read as a list. If a marker
   is ever wanted here it must be a glyph that describes the CONTENT, never a tick.
   It was briefly named .checklist, which was wrong the moment the ticks came out. The top border on every item is deliberate - at two columns it draws a
   continuous rule across the head of both, which is what ties the two columns together. */
.splitlist { display: grid; gap: 0 48px; }
@media (min-width: 820px) { .splitlist { grid-template-columns: 1fr 1fr; } }
.splitlist-item {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  padding: 19px 0;
  border-top: 1px solid var(--line);
}
.splitlist-item > .material-symbols-outlined {
  font-size: 20px;
  color: var(--gold);
  flex: none;
  margin-top: 3px;
}
.splitlist-item b { display: block; font-weight: 700; font-size: 16px; color: var(--ink); margin-bottom: 4px; }
.splitlist-item p { font-size: 15px; line-height: 1.6; color: var(--slate); margin: 0; }

/* ---- .flow - a numbered sequence ----------------------------------------------------------
   For steps where the ORDER is the information. A card grid states the same two things without
   saying one comes first, which is the whole point of a process.
   The number is a gold disc with black text - gold as a BACKGROUND, never as body text on
   white, which is the standing brand rule.
   The connector rule only exists in the stacked layout, where it runs down the left gutter
   between two discs. Across a row it would have to cross the headings, so it is not drawn. */
.flow { display: grid; gap: 30px; counter-reset: flow; }
@media (min-width: 760px) { .flow { grid-template-columns: 1fr 1fr; gap: 48px; } }
.flow-step { counter-increment: flow; position: relative; padding-left: 60px; }
.flow-step::before {
  content: counter(flow);
  position: absolute;
  left: 0;
  top: -2px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--gold);
  color: #000;
  font-weight: 700;
  font-size: 17px;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 759px) {
  .flow-step:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 19px;              /* centre of the 40px disc, so it never meets the text column */
    top: 44px;
    bottom: -30px;
    width: 1px;
    background: var(--line);
  }
}
.flow-step h3 { font-size: 18.5px; line-height: 1.3; letter-spacing: -0.01em; margin-bottom: 8px; }
.flow-step p { font-size: 15.5px; line-height: 1.62; color: var(--slate); margin: 0; }

/* ---- .qa-icon - the big glyph at the head of a routing card ------------------------------
   Client, 17 Aug 2026. The three cards on /contact/ are the ONE place on these pages where an
   icon earns its keep: they are mutually exclusive routes, so the glyph lets someone find
   their row before reading a word. Contrast with .plainrow, where the icon is deliberately
   absent because no honest glyph exists for "financial institution".
   `inventory_2` was added to the font subset for this - see scripts/subset-material-symbols.py.
   display:inline-block IS LOAD-BEARING, not a style choice. A ligature the font does not carry
   renders as the literal word, silently, and the only way to catch it is to measure the box:
   ~30px resolved, ~170px failed. With display:block the box fills the card and the measurement
   says nothing. align-self keeps it hugging the glyph inside the :has() flex column below. */
.qacard .qa-icon {
  display: inline-block;
  align-self: flex-start;
  font-size: 32px;
  line-height: 1;
  color: var(--gold);
  margin-bottom: 16px;
}

/* ---- .cardlink - the action line at the foot of a card -----------------------------------
   Client, 17 Aug 2026, for the routing cards on /contact/. A card that describes a route but
   does not offer it makes the reader go and find the destination for themselves; this is the
   "and here it is" line.
   THE UNDERLINE IS ON THE TEXT SPAN, NOT THE ANCHOR. text-decoration on a flex anchor drags
   the rule under the arrow too, which reads as a rendering fault rather than as a link. Same
   trap already documented on .hlink and .mobnav-loc - do not move it onto the anchor.
   The rule is INK at 35%, not gold: gold is the accent and a row of gold underlines competes
   with the actual buttons. The arrow keeps the gold, which is enough brand for one line. */
.cardlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  font-size: 15.5px;
  font-weight: 600;
  color: var(--ink);
}
.cardlink .material-symbols-outlined { font-size: 19px; color: var(--gold); flex: none; }
.cardlink .cl-t {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: rgba(0,0,0,0.35);
}
.cardlink:hover .cl-t { text-decoration-color: var(--ink); }

/* Bottom-align the action line so three cards of different copy lengths still present their
   links on one row, instead of stepping down the row wherever each paragraph happens to end.
   SCOPED WITH :has() ON PURPOSE. .qacard is used thirty-odd times across the site and must not
   become a flex column everywhere - only the cards that actually carry a link change.
   align-self is required: a flex column stretches its children, which would pull the
   inline-flex anchor across the full card width and make the whole row a click target.
   A browser without :has() gets the link where the copy ends. That is the previous behaviour,
   not a broken one, so no fallback is needed. */
.qacard:has(.cardlink) { display: flex; flex-direction: column; }
.qacard:has(.cardlink) .cardlink { margin-top: auto; padding-top: 16px; align-self: flex-start; }

/* INLINE LINKS IN BODY COPY - the phone number and the email address inside a card were
   rendering with text-decoration:none in the same slate as the sentence around them, so
   nothing marked them as tappable at all. Measured, not guessed: computed decoration was
   "none" and colour rgb(75,85,99), identical to the paragraph.
   A thin ink rule is the least that reads as a link without turning a paragraph into a row
   of gold.
   WEIGHT STAYS NORMAL (client, 17 Aug 2026). This briefly carried font-weight:600 and the
   bolded phone number, email address and inline "wholesale" pulled harder than the sentence
   they sat in - a paragraph with three bold fragments reads as three emphases, not as three
   links. The rule and the ink colour are enough to mark a link; the bold was doing a second
   job nobody asked for. Do not put it back. */
.qacard p a,
.splitlist p a,
.plainrow-item p a,
.flow-step p a,
.feat p a,
.contrast-col li a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(0,0,0,0.35);
}
.qacard p a:hover,
.splitlist p a:hover,
.plainrow-item p a:hover,
.flow-step p a:hover,
.feat p a:hover,
.contrast-col li a:hover { text-decoration-color: var(--ink); }

/* ---- .mapwide - the map across the whole container, the facts underneath -----------------
   /contact/ (client, 17 Aug 2026), replacing the prose-beside-map split above. The prose it
   displaced was route directions, and the walk-in route is /find-us/'s job - so this keeps
   the two things this page owes a reader, the map and the checkable facts, and drops the
   duplication.
   HEIGHT IS CAPPED, NOT AN ASPECT RATIO. The site's photo slots are all 3:2, but 3:2 across
   the 1384px container is 923px tall - taller than most laptop viewports, so the facts under
   it would never be on screen with it. A clamp keeps the map big without eating the fold.
   The street pane's framing is COMPUTED here rather than hand-tuned (data-fov-v in the
   markup): this pane runs about 2.7 wide on a desktop and under 1 on a phone, and the
   two-URL data-src/data-src-wide pair cannot span that. See streetUrl() in site.js. */
.mapwide .mapframe { height: clamp(380px, 40vw, 580px); }
/* Base .mapframe iframe carries min-height:340px because it is normally a flex:1 child with
   no container height to fill. Here the container HAS a height, so that floor would fight the
   clamp at the small end and overflow the frame. */
.mapwide .mapframe iframe { min-height: 0; }
/* The one paragraph that survived the route prose. FULL WIDTH on the client's direction
   (17 Aug 2026) - it was capped at a 68ch reading measure, which left the text filling only
   the left half under a full-width map and read as a mistake rather than as a measure.
   It works here because the paragraph is four sentences: across the container it sets as two
   or three lines, so the long measure never accumulates. Do NOT reuse this for longer prose -
   .story-text keeps its 64ch cap precisely because that block runs to real length. */
.mapwide-lead {
  margin: 24px 0 0;
  font-size: 16.5px;
  line-height: 1.68;
  color: var(--slate);
}

/* ---- .contrast - two-column before/after ------------------------------------------------
   Built for /wholesale/ (client, 17 Aug 2026) as a deliberately DIFFERENT treatment from the
   .story blocks on /about/. The content there is a narrative; this one is a comparison, and a
   two-column contrast SHOWS the trade-off where prose only describes it. A B2B reader gets the
   delta in about five seconds rather than inferring it from two paragraphs.
   Reusable: any page weighing one option against another should use this rather than inventing
   a third layout.
   The two columns are deliberately NOT symmetrical in weight - the cost side is plain and the
   outcome side carries the gold, so the eye finishes on the thing being sold. */
.contrast { display: grid; gap: 16px; }
@media (min-width: 820px) { .contrast { grid-template-columns: 1fr 1fr; gap: 24px; } }

.contrast-col {
  padding: 26px 24px 20px;
  border-radius: 22px;
  background: rgba(255,255,255,0.72);
  border: 1px solid rgba(0,0,0,0.07);
}
/* The outcome column. A tint and a gold hairline, not a heavier box - this is emphasis, not a
   different component. */
.contrast-out {
  background: rgba(254,185,0,0.06);
  border-color: rgba(254,185,0,0.42);
}
.contrast-col h3 {
  font-size: 19px;
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin-bottom: 6px;
}
.contrast-col ul { list-style: none; margin: 0; padding: 0; }
.contrast-col li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 13px 0;
  font-size: 15.5px;
  line-height: 1.55;
  color: var(--slate);
  border-top: 1px solid rgba(0,0,0,0.06);
}
.contrast-col .material-symbols-outlined {
  font-size: 20px;
  flex: none;
  /* 3px nudges the glyph onto the first line's baseline - icon boxes sit high against text. */
  margin-top: 3px;
}
/* Kept for any list that still marks its rows, but NOTE: the rows on /contact/ and /wholesale/
   deliberately carry no icon at all now. Ticks are not a mark this brand uses, and a cross
   beside "identification is required" reads as forbidden when all it means is "not over the
   phone". If a row ever needs a marker again it must be MUTED, never red - gold is the only
   accent here. */
.contrast-col .material-symbols-outlined { color: var(--muted, #9ca3af); }
.contrast-out .material-symbols-outlined { color: var(--gold); }

/* THE CONTEXT ICON GOES ON THE COLUMN HEADING, not on every row. It names what the column is
   ABOUT - a phone, a counter - so the reader can tell the two apart at a glance without
   reading either list. One glyph per column is information; the same glyph repeated down six
   rows is decoration. Only use a glyph that honestly describes the column; leave it off
   otherwise, which is why /wholesale/'s cost-versus-buy contrast has none. */
.contrast-col h3 { display: flex; align-items: center; gap: 10px; }
.contrast-col h3 .material-symbols-outlined { font-size: 22px; flex: none; color: var(--ink); }
.contrast-out h3 .material-symbols-outlined { color: var(--gold); }

/* ---- .calcnote - the buy-intent hand-off on the sell hub (client, 29 Aug 2026) -------------
   Sits at the TOP of the calculator card, above the You Buy / You Sell toggle, and appears
   only while the calculator is on the BUY side. Inert everywhere else: no other page carries
   the markup, and without the .on class it occupies zero height.

   THE REVEAL IS grid-template-rows 0fr -> 1fr, not max-height. Same rule as the route steps:
   a max-height has to be guessed, and a guess that is too small clips the text at one font
   size and bounces the card at another. 0fr -> 1fr animates to the content's real height, so
   there is no number to get wrong. The inner element owns the overflow - without it the text
   spills out of the collapsed row instead of being clipped.
   It appears above the control that triggers it, so the toggle moves down as it opens; the
   transition is what makes that read as a message sliding in rather than the card jumping. */
.calcnote-reveal {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .28s cubic-bezier(.22,.9,.3,1),
              opacity .22s ease, margin-bottom .28s cubic-bezier(.22,.9,.3,1);
  opacity: 0;
  margin-bottom: 0;
}
.calcnote-reveal.on { grid-template-rows: 1fr; opacity: 1; margin-bottom: 10px; }
.calcnote-in { overflow: hidden; min-height: 0; }
.calcnote-body {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(254, 185, 0, 0.42);
  background: linear-gradient(160deg, rgba(254,185,0,0.20), rgba(254,185,0,0.10));
  color: var(--ink);
  font-size: 13.5px;
  font-weight: 550;
  line-height: 1.45;
}
.calcnote-body .material-symbols-outlined { font-size: 18px; flex: none; margin-top: 1px; }
/* The underline is on the anchor itself here, not a child span: this link is plain text with
   no icon inside it, so the .hlink/.ol-t trap (a decoration running under an icon) does not
   apply. */
.calcnote-body a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
.calcnote-body a:hover { text-decoration-thickness: 2px; }
@media (prefers-reduced-motion: reduce) {
  .calcnote-reveal { transition: opacity .01s; }
}

/* ---- .foot-feedback - the feedback / complaints link under the footer's open hours ---------
   Client, 29 Aug 2026. Right-aligned under the last footer column. A QUIET OUTLINE button, not
   a solid one: it is a trust signal rather than a call to action and must not out-shout the
   "Check exchange rates" CTA in the band above. It goes in _parts/footer.html, never inline on
   a page - shared-shell furniture that ships on one page only is a bug this project has had
   three times (see shared-shell-must-be-inside-markers). */
.foot-feedback {
  /* BLOCK-level flex with a fit-content width, not inline-flex. `margin-left: auto` is what
     does the right-aligning, and auto margins are ignored on an inline-level box - the first
     version was inline-flex and only LOOKED aligned because the label happened to be exactly
     as wide as the column. Measure the margin, not the edges. */
  display: flex;
  width: fit-content;
  align-items: center;
  gap: 7px;
  margin-top: 16px;
  margin-left: auto;                 /* the right-align */
  padding: 9px 14px;
  border-radius: 999px;
  border: 1px solid rgba(16, 24, 40, 0.14);
  background: rgba(255, 255, 255, 0.55);
  color: var(--slate);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  /* One line. The open-hours column is narrower than this label, so without nowrap it breaks
     as "Feedback or a / complaint", which reads as a mistake. Right-aligned + nowrap makes it
     grow LEFTWARDS into the gap beside the column rather than pushing the grid wider - the
     footer's last column has empty space on that side and nothing to collide with. */
  white-space: nowrap;
  transition: border-color .2s ease, background .2s ease, color .2s ease;
}
.foot-feedback .material-symbols-outlined { font-size: 17px; }
.foot-feedback:hover {
  border-color: rgba(254, 185, 0, 0.6);
  background: rgba(254, 185, 0, 0.12);
  color: var(--ink);
}
/* The column is not a flex container on phones, where the footer collapses to one column and
   auto margins have nothing to push against - so align it explicitly there instead. */
@media (max-width: 900px) {
  .foot-feedback { margin-left: 0; }
}

/* ---- .paysec-m as a LINK, plus a hint line (complaints page, 29 Aug 2026) -----------------
   The /about/ payment cells are inert divs holding a single label. The same panel on
   /complaints-and-feedback/ carries three real actions, so the cells are anchors and each one
   gets a one-line hint. Both additions are scoped to the new class or the element type, so the
   about page's panel is byte-identical to before. */
a.paysec-m { text-decoration: none; color: inherit; border-radius: 16px; transition: background .2s ease; }
a.paysec-m:hover { background: rgba(254, 185, 0, 0.10); }
a.paysec-m:hover b { color: var(--ink); }
.paysec-hint {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  max-width: 24ch;
  margin-top: -2px;
}
@media (max-width: 600px) {
  /* The cells go row-wise on phones, so the hint would be squeezed beside the label. Stack the
     label and hint together and let the icon sit alongside the pair. */
  a.paysec-m { flex-wrap: wrap; text-align: left; justify-content: flex-start; padding-left: 14px; }
  .paysec-hint { max-width: none; flex: 1 1 100%; padding-left: 32px; margin-top: -6px; }
}

/* ---- .paysec-m as a LINK, plus a hint line (complaints page, 29 Aug 2026) -----------------
   The /about/ payment cells are inert divs holding one label. The same panel on
   /complaints-and-feedback/ carries three real actions, so the cells are anchors and each gets
   a one-line hint. Both additions are scoped to the anchor form or the new class, so the about
   page's panel computes byte-identical to before. */
a.paysec-m { text-decoration: none; color: inherit; border-radius: 16px; transition: background .2s ease; }
a.paysec-m:hover { background: rgba(254, 185, 0, 0.10); }
.paysec-hint {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  max-width: 26ch;
  margin-top: -2px;
}
@media (max-width: 600px) {
  /* Cells go row-wise on phones, where a hint squeezed beside the label would wrap to shreds.
     Let the icon and label share the first line and drop the hint onto its own, indented to
     clear the icon. */
  a.paysec-m { flex-wrap: wrap; justify-content: flex-start; text-align: left; padding-left: 16px; }
  .paysec-hint { flex: 1 1 100%; max-width: none; padding-left: 32px; margin-top: -4px; }
}

/* ---- .futility - the footer's payment + feedback line (client, 29 Aug 2026) ---------------
   One full-width row under the columns: in-store payment methods on the left, the feedback and
   complaints button hard right, sharing a baseline. Both items previously sat inside columns of
   different heights, so they could never line up no matter how they were nudged - taking them
   out of the grid is the whole fix. */
.footer-premium .futility {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px 28px;
  flex-wrap: wrap;
}
.footer-premium .futility .fpay { margin: 0; }
.footer-premium .futility .foot-feedback { margin-top: 0; margin-left: 0; }
@media (max-width: 600px) {
  /* Stacked, with the button UNDER the methods (client). Centred to match the rest of the
     footer, which switches to text-align:center at this width. */
  .footer-premium .futility { flex-direction: column; align-items: center; gap: 18px; }
}

/* ---- .futility - the footer's payment + feedback line (client, 29 Aug 2026) ---------------
   One full-width row under the columns: in-store payment methods left, the feedback and
   complaints button hard right, sharing a baseline. Both items previously sat inside columns of
   different heights, so they could never line up however they were nudged - taking them out of
   the grid is the whole fix. */
.footer-premium .futility {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px 28px;
  flex-wrap: wrap;
}
.footer-premium .futility .fpay { margin: 0; }
.footer-premium .futility .foot-feedback { margin-top: 0; margin-left: 0; }
@media (max-width: 600px) {
  /* Stacked, button UNDER the methods (client). Centred, to match the rest of the footer,
     which switches to text-align:center at this width. */
  .footer-premium .futility { flex-direction: column; align-items: center; gap: 18px; }
}

/* The visible action label inside each complaints-panel cell. Underlined so the cell reads as
   clickable WITHOUT hover - on touch there is no hover state to discover, which is where an
   unlabelled clickable card fails. Gold on hover to match every other link on the site. */
.paysec-do {
  margin-top: 4px;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
a.paysec-m:hover .paysec-do { text-decoration-thickness: 2px; }
@media (max-width: 600px) {
  .paysec-do { flex: 1 1 100%; padding-left: 32px; margin-top: 2px; }
}

/* ---- .ff-4 - a four-up findfacts row (opening-hours page, 29 Aug 2026) --------------------
   The base .findfacts is auto-fit minmax(232px), which fits FIVE across the 1432px container.
   The holiday closure grids hold EIGHT cards each, so auto-fit laid them out 5 + 3 with a
   ragged second row. Four columns gives a clean 4 + 4 for both years.
   THIS IS TUNED TO A COUNT, exactly like .ff-3 is tuned to six. If the number of weekday
   closures in a year changes - and it does, because a holiday moving on or off a weekend
   changes it - re-check this. auto-fit will not do it for you. */
@media (min-width: 600px) and (max-width: 950px) {
  .ff-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 951px) {
  .ff-4 { grid-template-columns: repeat(4, 1fr); }
}

/* ---- .holref - the weekend public-holiday reference panel (29 Aug 2026) -------------------
   The holidays that fall on a Saturday or Sunday change nothing: the counter is shut every
   weekend regardless. They are listed for completeness and are DELIBERATELY SUBORDINATE to the
   weekday closures above - small type, three quiet columns in one panel, no cards. Cards here
   would compete with the eight dates that actually matter.
   It replaced a .story/.story-text block, which caps at 64ch and left-aligns: on a desktop that
   rendered as a narrow column of text against an empty right half. Same fault, same fix, as the
   legal pages. */
.holref { padding: 26px 24px 20px; border-radius: 22px; }
.holref-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px 28px;
}
.holref-r {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 2px;
  border-bottom: 1px solid rgba(16, 24, 40, 0.07);
}
.holref-r b { font-size: 14px; font-weight: 650; color: var(--ink); }
.holref-r span { font-size: 13px; color: var(--muted); white-space: nowrap; }
.holref-note {
  margin: 18px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 78ch;
}
@media (max-width: 950px) { .holref-grid { grid-template-columns: repeat(2, 1fr); gap: 2px 22px; } }
@media (max-width: 600px) {
  .holref { padding: 20px 18px 16px; }
  .holref-grid { grid-template-columns: 1fr; }
  .holref-note { text-align: left; }
}

/* ---- footer payment row: centre it on phones -----------------------------------------------
   .fpay is a wrapping flex row and .fpay-t takes flex-basis:100%, so both the label and the
   method chips lay out from the START of the row. Everything else in the footer centres below
   600px, which left "IN-STORE PAYMENT" hard left while the chips under it looked centred - the
   misalignment reported 29 Aug 2026. Centring the flex line AND the full-width label fixes both
   halves; without the text-align the label alone stays left. */
@media (max-width: 600px) {
  .footer-premium .futility .fpay { justify-content: center; }
  .footer-premium .futility .fpay-t { text-align: center; }
}

/* ---- .hourscard - the opening-hours block (29 Aug 2026, rev 2) ----------------------------
   Built for /opening-hours-and-public-holidays/, where the hours ARE the page. /find-us/'s
   .loc-status bar was tried here first and rejected by the client: it is a wide one-line
   component whose weekly hours are hidden above 600px, so forcing them back left the buttons
   wrapping under the text with a hole beside them. Do not reuse it here.

   Rev 2 is the same information given real shape: a status HEADER that bleeds to the card
   edges and changes colour with the state, a week where the open days read as data and the
   closed ones as quiet chips, and today marked with a gold rail rather than only a tint.
   Capped at 620px - a seven-row list across the 1432px container is a table of contents, not
   a shopfront sign. */
.hourscard {
  max-width: 620px;
  margin: 0 auto;
  padding: 0;                    /* the header bleeds to the edges; rows carry their own */
  overflow: hidden;              /* so the header's tint clips to the radius */
  border-radius: 26px;
}

/* --- status header --- */
.hstatus {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px 24px;
  background: linear-gradient(135deg, rgba(16, 24, 40, 0.045), rgba(16, 24, 40, 0.015));
  border-bottom: 1px solid rgba(16, 24, 40, 0.08);
  transition: background .3s ease;
}
/* The whole header greens when the store is open, not just the dot - the state is the first
   thing anyone came here for, and a 9px dot is a small thing to hang it on. */
.hstatus.open { background: linear-gradient(135deg, rgba(15, 138, 95, 0.12), rgba(15, 138, 95, 0.035)); }
.hstatus[hidden] { display: none; }   /* [hidden] loses to display:flex - same trap as .mact-status */
.hstatus .hdot {
  width: 10px; height: 10px; border-radius: 50%;
  background: #c2c7cf; flex: none;
  box-shadow: 0 0 0 4px rgba(16, 24, 40, 0.05);
}
.hstatus.open .hdot { background: var(--open); box-shadow: 0 0 0 4px rgba(15, 138, 95, 0.16); }
.hstatus .htxt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hstatus .s-main { font-size: 17px; font-weight: 700; color: var(--slate); letter-spacing: -0.01em; }
.hstatus.open .s-main { color: var(--ink); }
.hstatus .s-hint { font-size: 13px; color: var(--muted); }
.hstatus .htz {
  margin-left: auto; flex: none;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--muted);
  padding: 5px 10px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid rgba(16, 24, 40, 0.07);
}

/* --- the week --- */
.hweek { padding: 8px 14px 6px; }
.hrow {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 12px;
  border-radius: 12px;
}
.hrow + .hrow::before {
  content: ""; position: absolute; left: 12px; right: 12px; top: 0;
  height: 1px; background: rgba(16, 24, 40, 0.06);
}
.hrow b {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 138px; font-size: 15px; font-weight: 650; color: var(--ink);
}
.hrow b .material-symbols-outlined { font-size: 18px; color: var(--gold); }
.hval { font-size: 15px; color: var(--slate); }
/* Closed reads as a quiet chip, not as data - it is the absence of hours, and it should not
   sit at the same weight as a real opening time. */
.hclosed {
  font-size: 13px; font-weight: 600; color: var(--muted);
  padding: 4px 11px; border-radius: 999px;
  background: rgba(16, 24, 40, 0.045);
}
.hrow-off b { color: var(--muted); font-weight: 600; }
/* Public holidays are a rule, not a day of the week - separated, and the only row with an icon. */
.hrow-hol { margin-top: 8px; }
.hrow-hol::before { display: none; }
.hrow-hol { border-top: 1px solid rgba(16, 24, 40, 0.09); border-radius: 0 0 12px 12px; padding-top: 16px; }

/* Today: a gold rail as well as the tint. The tint alone was legible but flat, and the rail is
   what makes the row read as "you are here" at a glance. */
.hrow-today {
  background: linear-gradient(90deg, rgba(254, 185, 0, 0.20), rgba(254, 185, 0, 0.07));
}
.hrow-today::after {
  content: ""; position: absolute; left: 0; top: 6px; bottom: 6px;
  width: 3px; border-radius: 3px; background: var(--gold);
}
.hrow-today b { color: #000; font-weight: 700; }
.hrow-today .hval { color: var(--ink); font-weight: 600; }
.htag {
  margin-left: auto; flex: none;
  font-size: 10px; font-weight: 800; font-style: normal;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: #000; background: var(--gold);
  padding: 4px 9px; border-radius: 999px;
}

/* NOT .hcta: that class is the HERO CTA row and is on 11 pages. Naming this one .hcta made
   the mobile rule below stack every hero's buttons site-wide - caught by checking the new
   class names against existing ones rather than by looking at this page. */
.hours-cta { margin: 4px 0 0; padding: 16px 22px 22px; justify-content: center; }

@media (max-width: 600px) {
  .hourscard { border-radius: 22px; }
  .hstatus { padding: 16px 16px; gap: 10px; flex-wrap: wrap; }
  .hstatus .s-main { font-size: 16px; }
  .hstatus .htz { display: none; }      /* the hint line already says Sydney; two labels crowd it */
  .hweek { padding: 6px 8px 4px; }
  .hrow { padding: 13px 10px; gap: 10px; }
  .hrow b { min-width: 108px; font-size: 14.5px; }
  .hval { font-size: 14px; }
  /* The TODAY pill and a full time string will not share a 375px row with the day name, and
     the pill is the more useful of the two on a phone - so the time wraps to its own line
     rather than the pill being dropped. */
  .hrow-today { flex-wrap: wrap; row-gap: 4px; }
  .hrow-today .hval { order: 3; flex: 1 1 100%; padding-left: 0; }
  .hours-cta { flex-direction: column; padding: 14px 14px 18px; }
  .hours-cta .btn { width: 100%; justify-content: center; }
}

/* The footer brand logo is a link now, like the header's. inline-flex so the anchor hugs the
   image instead of stretching to the column width and giving the logo a wide dead click area
   beside it. */
.fbrand { display: inline-flex; align-items: center; text-decoration: none; }

/* ---- currency pages: /exchange-rates/<slug>/ (29 Aug 2026) --------------------------------
   Structure follows the reference site the client chose; these are the only new components it
   needed. Everything else on the page is an existing block (.qa, .faq-accordion, the hero
   calculator, the trust band). */

/* The reference panel: an intro, two fact tables side by side, and a tip line. It is the part
   of a currency page that is genuinely about the CURRENCY rather than about us, and the part a
   search engine or an assistant can quote. */
/* Full container width (client, 29 Aug 2026 - the 1000px cap read as a narrow leftover
   between full-bleed sections). The lead+photo grid spans the wrap; the two fact tables run
   as two full halves under it. */
.curinfo { margin: 0 auto; }
.curinfo-lead {
  font-size: 17.5px; line-height: 1.68; color: var(--slate);
  max-width: 74ch; margin: 0 auto 26px; text-align: center;
}
.curtables { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }
.curtable {
  padding: 26px 28px 20px;
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.62);
  border: 1px solid rgba(16, 24, 40, 0.07);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.curtable h3 {
  display: flex; align-items: center; gap: 9px;
  font-size: 16px; margin-bottom: 14px;
}
.curtable h3 .material-symbols-outlined { font-size: 20px; color: var(--gold); }
.curtable dl { margin: 0; }
/* Each row is a <div> wrapping dt+dd. A bare dt/dd pair cannot be laid out as a row without it
   - dl is not a grid container in any useful way across browsers. */
.curtable dl > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 9px 0;
}
.curtable dl > div + div { border-top: 1px solid rgba(16, 24, 40, 0.06); }
.curtable dl > div { padding: 11px 0; }
.curtable dt {
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); flex: none;
}
.curtable dd { margin: 0; font-size: 15px; font-weight: 600; color: var(--ink); text-align: right; }
.curinfo-tip {
  display: flex; align-items: flex-start; gap: 9px;
  margin: 20px 0 0; padding: 14px 16px;
  border-radius: 14px;
  border: 1px solid rgba(254, 185, 0, 0.42);
  background: linear-gradient(160deg, rgba(254,185,0,0.16), rgba(254,185,0,0.07));
  font-size: 14px; line-height: 1.55; color: var(--ink);
}
.curinfo-tip .material-symbols-outlined { font-size: 19px; flex: none; margin-top: 1px; }

/* "Need a different currency?" - four cards, the same grammar as a rate row so the link reads
   as going somewhere rather than as decoration. */
.curgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.curcard {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-radius: 16px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid rgba(16, 24, 40, 0.08);
  text-decoration: none;
  transition: border-color .2s ease, background .2s ease, transform .2s ease;
}
.curcard:hover { border-color: rgba(254,185,0,0.6); background: rgba(254,185,0,0.10); transform: translateY(-1px); }
.curcard .fl { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; flex: none; }
.curcard > span:not(.material-symbols-outlined) { display: flex; flex-direction: column; min-width: 0; }
.curcard b { font-size: 14.5px; font-weight: 650; color: var(--ink); }
.curcard i { font-style: normal; font-size: 12.5px; color: var(--muted); }
.curcard .material-symbols-outlined { margin-left: auto; font-size: 18px; color: var(--muted); flex: none; }
.curcard:hover .material-symbols-outlined { color: var(--ink); }

@media (max-width: 950px) {
  .curtables { grid-template-columns: 1fr; }
  .curgrid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .curinfo-lead { font-size: 16px; text-align: left; }
  .curtable { padding: 18px 16px; }
  .curtable dd { font-size: 13.5px; }
  .curgrid { grid-template-columns: 1fr; }
}

/* ---- .curhero - the currency-page hero (29 Aug 2026, hand-built rev) ----------------------
   The reference site's composition: a country-specific backdrop with ONE large rounded panel
   floating on it - headline and rating left, live calculator right - rendered in our language:
   the panel is glass, not solid navy, and the backdrop is the currency's own FLAG scaled huge
   and blurred into a colour atmosphere. The flag is the honest choice: the repo holds no
   licensed destination photography, the flag is unmistakably country-specific, and the same
   device works for all 22 currencies with zero new assets. A licensed city photo can replace
   the <img> later with no other change.
   isolation:isolate keeps the z-index:-2 backdrop INSIDE the section - without it a negative-z
   child escapes to the page root and vanishes behind the canvas (the background-glow trap). */
.hero.curhero { isolation: isolate; padding-top: 46px; padding-bottom: 56px; }
.curhero-bg {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  pointer-events: none;
  /* Fade the atmosphere out at the bottom so the section blends into the page body instead of
     ending on a hard line - a mask removes the layer itself, so whatever the body shows comes
     through (the same reasoning as the contact hero's mask). */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 100%);
}
/* v2 (client feedback, 29 Aug 2026): the backdrop is a VISIBLE landmark photograph, unblurred
   - "like Travelex". The glass panel carries the text contrast, so the image only gets a
   whisper of veil to sit on our light canvas, and the bottom fade into the page stays. */
.curhero-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(108%);
}
.curhero-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(248,250,252,0.22), rgba(248,250,252,0.04) 42%, rgba(248,250,252,0.30));
}
.curpanel {
  border-radius: 32px;
  padding: 44px 46px 40px;
  /* A firmer fill than the stock glass: this panel sits on saturated national colour and holds
     the H1 and the rating, so legibility outranks transparency here. */
  background: linear-gradient(160deg, rgba(255,255,255,0.86), rgba(255,255,255,0.72));
  box-shadow: 0 24px 60px -24px rgba(16,24,40,0.28), inset 0 1px 0 rgba(255,255,255,0.95);
}
.curflag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(16,24,40,0.10);
  background: rgba(255,255,255,0.7);
  font-size: 13px;
  font-weight: 750;
  letter-spacing: 0.04em;
  color: var(--ink);
}
.curflag img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.curflag i { font-style: normal; font-weight: 600; letter-spacing: 0; color: var(--muted); }
@media (max-width: 950px) {
  .curpanel { padding: 28px 24px 26px; border-radius: 26px; }
}
@media (max-width: 600px) {
  .hero.curhero { padding-top: 30px; padding-bottom: 36px; }
  .curpanel { padding: 22px 16px 20px; border-radius: 22px; }
  .curhero-bg img { filter: blur(38px) saturate(130%); }
}


/* ---- currency hero v2 additions ------------------------------------------------------------
   The breadcrumb line sits ON the landmark image, so it gets a glass chip - readable on any
   photograph without dictating what the photograph is. Scoped to the modifier: every other
   page's crumbs are untouched. */
.crumbs-onimg ol {
  display: inline-flex;
  padding: 8px 16px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.72);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow: 0 10px 26px -14px rgba(16, 24, 40, 0.35);
}

/* Key-info: lead text beside the banknotes figure, tables below. Single column until the
   figure exists - :has() collapses the grid while the slot is empty, so the missing photo
   never leaves a hole. */
.curinfo-grid { display: grid; gap: 24px; align-items: center; margin-bottom: 26px; }
.curinfo-grid .curinfo-lead { margin: 0; text-align: left; max-width: none; }
.curinfo-photo { margin: 0; }
.curinfo-photo[hidden] { display: none; }
.curinfo-photo img {
  width: 100%;
  height: auto;               /* load-bearing: a bare height attribute would win otherwise */
  aspect-ratio: 3 / 2;        /* the site-wide one-ratio rule */
  object-fit: cover;
  border-radius: 20px;
  box-shadow: 0 18px 44px -20px rgba(16, 24, 40, 0.35);
}
@media (min-width: 951px) {
  /* stretch, not center: the photo fills the row the TEXT defines, so the two columns are
     always exactly level regardless of viewport width - "fill the gap" (client). The 3:2
     source centre-crops a little; it is a banknote texture, nothing is lost. */
  .curinfo-grid:has(.curinfo-photo:not([hidden])) { grid-template-columns: 1.05fr 0.95fr; gap: 32px; align-items: stretch; }
  .curinfo-grid:has(.curinfo-photo:not([hidden])) .curinfo-photo { display: flex; }
  .curinfo-grid:has(.curinfo-photo:not([hidden])) .curinfo-photo img { height: 100%; aspect-ratio: auto; }
}
@media (max-width: 600px) {
  .crumbs-onimg ol { padding: 7px 13px; }
}

/* Centred call under the other-currencies cards (client, 29 Aug 2026). */
/* 36px, not 22 - at 22 the button visually touched the card row above it (client). */
.curgrid-cta { display: flex; justify-content: center; margin-top: 36px; }

/* ---- currency hero: the image starts at the header's underline, not behind it -------------
   (client, 29 Aug 2026, red-line screenshot). The crumbs' own padding-top exists to clear the
   fixed header - inside the image wrapper that padding made the PHOTO run up behind the white
   header. The wrapper now carries the clearance as margin (old crumb padding minus the 20px
   air), so the photo's top edge sits exactly on the header's bottom line and the header stays
   the same white bar as every other page. */
.curimgwrap { margin-top: 72px; }
@media (max-width: 950px) { .curimgwrap { margin-top: 68px; } }
@media (min-width: 951px) { .curimgwrap { margin-top: 86px; } }
.curimgwrap .crumbs { padding-top: 20px; }

/* ---- .placestrip - the auto-playing filmstrip of destinations (29 Aug 2026) ---------------
   The reference is a filmstrip carousel: every slide is a narrow sliver, the active one grows
   wide with its name over a bottom scrim. Autoplay advances it; hovering anywhere on the strip
   holds it; clicking a sliver or an arrow selects; reduced-motion gets no autoplay (site.js).
   flex-grow is the whole animation - width would fight the flex layout, grow IS the layout. */
.placestrip { position: relative; }
.ps-track { display: flex; gap: 12px; height: 440px; }
.ps-item {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: 20px;
  overflow: hidden;
  cursor: pointer;
  background: #101828;
  transition: flex-grow .55s cubic-bezier(.22,.9,.3,1);
}
.ps-item img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
.ps-item.is-on { flex-grow: 6.5; }
.ps-cap {
  position: absolute; inset: auto 0 0 0;
  display: flex; flex-direction: column; gap: 2px;
  padding: 42px 20px 16px;
  text-align: left;
  background: linear-gradient(180deg, transparent, rgba(6, 10, 20, 0.72));
  opacity: 0;
  transition: opacity .35s ease .15s;
  pointer-events: none;
}
.ps-item.is-on .ps-cap { opacity: 1; }
.ps-cap b { color: #fff; font-size: 19px; font-weight: 750; letter-spacing: -0.01em; white-space: nowrap; }
.ps-cap i { color: rgba(255,255,255,0.82); font-style: normal; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ps-nav { display: flex; justify-content: center; gap: 10px; margin-top: 18px; }
.ps-nav button {
  width: 42px; height: 42px;
  display: grid; place-items: center;
  padding: 0; border-radius: 12px; cursor: pointer;
  border: 1px solid rgba(16, 24, 40, 0.12);
  background: rgba(255, 255, 255, 0.7);
  color: var(--ink);
  transition: border-color .2s ease, background .2s ease;
}
.ps-nav button:hover { border-color: rgba(254,185,0,0.6); background: rgba(254,185,0,0.14); }
.ps-nav .material-symbols-outlined { font-size: 20px; }
/* the subset has no arrow_back - mirror arrow_forward, the same trick the homepage uses */
.ps-prev .material-symbols-outlined { transform: scaleX(-1); }
@media (max-width: 950px) { .ps-track { height: 360px; } .ps-item.is-on { flex-grow: 5; } }
@media (max-width: 600px) {
  .ps-track { height: 300px; gap: 8px; }
  .ps-item { border-radius: 14px; }
  /* six slivers on a phone leaves each ~24px - readable enough as stripes, and the tap
     target is the full sliver height. The cap wraps instead of one line. */
  .ps-cap { padding: 30px 12px 11px; }
  .ps-cap b { font-size: 16px; white-space: normal; }
  .ps-cap i { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ps-item, .ps-cap { transition: none; }
}

/* ---- .convsheet - the live at-a-glance conversion table (currency pages, 29 Aug 2026) -----
   Two glass columns, one per direction, big tabular figures. The values are LIVE (site.js,
   from the calculator's own exported store) - the markup carries baked crawler values that
   the script overwrites, so what a reader sees always equals what the calculator says. */
.convsheet { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.cs-col { padding: 26px 28px 18px; border-radius: 22px; }
.cs-col h3 { display: flex; align-items: center; gap: 9px; font-size: 16.5px; }
.cs-col h3 .material-symbols-outlined { font-size: 20px; color: var(--gold); }
.cs-sub { font-size: 13.5px; color: var(--muted); margin: 4px 0 14px; }
.cs-row {
  display: flex; align-items: baseline; gap: 12px;
  padding: 12px 2px;
}
.cs-row + .cs-row { border-top: 1px solid rgba(16, 24, 40, 0.06); }
.cs-row > span:first-child { font-size: 15px; color: var(--slate); min-width: 86px; }
.cs-row .material-symbols-outlined { font-size: 16px; color: var(--muted); align-self: center; }
.cs-row b { margin-left: auto; font-size: 18px; font-weight: 750; color: var(--ink); letter-spacing: -0.01em; }
.cs-note { margin: 16px auto 0; max-width: 84ch; text-align: center; font-size: 13px; line-height: 1.6; color: var(--muted); }

/* ---- .costgrid - what things cost, as typographic tiles -----------------------------------
   Amount-first on purpose: the price IS the information, and the icon subset has no coffee or
   museum glyphs anyway. The A$ line is live from the same store. */
.costgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.cost {
  display: flex; flex-direction: column; gap: 5px;
  padding: 22px 24px 19px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid rgba(16, 24, 40, 0.07);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
/* Hierarchy flipped (client, 29 Aug 2026): the ITEM is the headline, the price is the data
   under it. The first version led with a 26px price and the label read as a caption. */
.cost span { font-size: 18.5px; font-weight: 750; letter-spacing: -0.015em; color: var(--ink); line-height: 1.25; }
.cost b { font-size: 15.5px; font-weight: 650; color: var(--slate); }
.cost i { font-style: normal; font-size: 12.5px; color: var(--muted); }
@media (max-width: 950px) {
  .convsheet { grid-template-columns: 1fr; gap: 16px; }
  .costgrid { grid-template-columns: repeat(2, 1fr); gap: 14px; }
}
@media (max-width: 600px) {
  .cs-col { padding: 20px 16px 14px; }
  .cs-row b { font-size: 16.5px; }
  .costgrid { grid-template-columns: 1fr; }
  .cost span { font-size: 17px; }
}

/* Big glyph on each cost tile (client, 29 Aug 2026). The five new ligatures - local_cafe,
   lunch_dining, restaurant, museum, directions_subway - exist only from icon-font stamp
   8bc7db82 onward; on the old font they render as the literal words. inline-block is what
   makes success MEASURABLE: a resolved glyph box is ~34px, a failed one ~200px of text. */
.cost > .material-symbols-outlined {
  display: inline-block;
  font-size: 34px;
  color: var(--gold);
  margin-bottom: 2px;
}


/* ---- convert v2: per-direction live rate chip + the direction facts ------------------------
   The chip is the direction's own unit rate, written live by the same site.js tick as the
   amount rows; the facts absorbed the old both-directions section, which said the same things
   two screens later with no numbers attached. */
/* Header row: title left, the live unit-rate chip RIGHT on the same line (client screenshot,
   29 Aug 2026), with clear air below before the sub line. On phones the chip drops to its own
   line, centred. */
.cs-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px 14px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.cs-head h3 { margin: 0; }
.cs-head .cs-rate { margin: 0; }
.cs-rate {
  display: inline-flex;
  margin: 6px 0 2px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(254, 185, 0, 0.5);
  background: linear-gradient(160deg, rgba(254,185,0,0.18), rgba(254,185,0,0.08));
  font-size: 14px;
  font-weight: 750;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.cs-facts {
  margin: 14px 0 2px;
  padding: 14px 2px 0;
  border-top: 1px solid rgba(16, 24, 40, 0.07);
  list-style: none;
}
.cs-facts li {
  position: relative;
  padding: 5px 0 5px 20px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--slate);
}
.cs-facts li::before {
  content: "";
  position: absolute; left: 4px; top: 13px;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--gold);
}

/* ---- .tipsplit - one featured tip beside a ledger of small ones ---------------------------
   Deliberately a different shape from the qa cards and findfacts rows around it (client:
   "differently organized"). The hero card carries the rule that actually changes behaviour;
   the rows carry the reminders. */
.tipsplit { display: grid; grid-template-columns: 1.1fr 1fr; gap: 22px; align-items: stretch; }
.tip-hero {
  padding: 30px 32px;
  border-radius: 24px;
  background: linear-gradient(160deg, rgba(254,185,0,0.20), rgba(254,185,0,0.06)),
              linear-gradient(160deg, rgba(255,255,255,0.85), rgba(255,255,255,0.65));
  border: 1px solid rgba(254, 185, 0, 0.4);
}
.tip-hero > .material-symbols-outlined { font-size: 38px; color: var(--gold); display: inline-block; margin-bottom: 10px; }
.tip-hero h3 { font-size: 21px; letter-spacing: -0.015em; margin-bottom: 10px; }
.tip-hero p { font-size: 15px; line-height: 1.65; color: var(--slate); }
.tip-rows { display: flex; flex-direction: column; gap: 12px; }
.tip-row {
  flex: 1;
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid rgba(16, 24, 40, 0.07);
}
.tip-row > .material-symbols-outlined { font-size: 24px; color: var(--gold); flex: none; }
.tip-row b { display: block; font-size: 15px; font-weight: 700; color: var(--ink); margin-bottom: 2px; }
.tip-row span { font-size: 13.5px; line-height: 1.5; color: var(--slate); }

/* ---- why-us stat leads --------------------------------------------------------------------- */
.wu-stat {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 32px; font-weight: 800; letter-spacing: -0.02em;
  color: var(--ink);
  margin-bottom: 8px;
}
.wu-stat .material-symbols-outlined { font-size: 26px; color: var(--gold); }

@media (max-width: 950px) {
  .tipsplit { grid-template-columns: 1fr; }
  .tip-hero { padding: 24px 22px; }
}


/* The why-us rating stat: FILLED star (the FILL axis is kept in the subset for exactly this),
   and the number is the live .js-rating span - same source as every rating on the site. */
.wu-stat .material-symbols-outlined { font-variation-settings: 'FILL' 1; }

@media (max-width: 600px) {
  /* chip centred on its own line under the column title (client) */
  .cs-head { flex-direction: column; align-items: flex-start; }
  .cs-head .cs-rate { align-self: center; }
  /* cost tiles and the why-us cards read centred on phones (client) */
  .cost { align-items: center; text-align: center; }
  #why-us .qacard { text-align: center; }
  #why-us .wu-stat { margin-left: auto; margin-right: auto; }
}

/* The key-info lead is a DIV of paragraphs (not one <p>) so it can run long enough to sit
   level with the 3:2 photo beside it - the client rejected a separate depth section below
   the tables in favour of a fuller lead (29 Aug 2026). */
.curinfo-lead p + p { margin-top: 13px; }

/* Key-info refinements (client, 29 Aug 2026). The section h2 holds ONE LINE on desktop -
   scoped to this section, never the base heading rules (the client's desktop type scale
   lives in its own block and must not grow mobile sizes). The CURRENCY table row carries
   the same flag chip as the hero, in a table-sized cut. */
@media (min-width: 951px) {
  /* EVERY currency-page section title holds one line on desktop (client, 29 Aug 2026) -
     the .head h2 centering measure was wrapping them. Scoped to the currency-page section
     ids (the costs band by its .curcosts class - its id names the region, #us-costs, #eu-costs),
     never the base heading rules; mobile wraps naturally. #trust is the shared band
     with its own layout and is deliberately not in this list. If a future currency's title
     overflows at 951px, shorten the TITLE, not this rule. */
  #how .head h2, #convert .head h2, #about-currency .head h2, .curcosts .head h2,
  #tips .head h2, #places .head h2, #why-us .head h2, #faq .head h2,
  #other-currencies .head h2 { max-width: none; white-space: nowrap; }
  /* The .head box is a 660px centred measure - and a CENTRED line WIDER than its box is
     clamped to the left edge and overflows right, so a one-line title sat visibly
     off-centre (measured +59px: half the overflow). The title's box must span the wrap;
     the sub-paragraph keeps the 660px reading measure itself. */
  #how .head, #convert .head, #about-currency .head, .curcosts .head,
  #tips .head, #places .head, #why-us .head, #faq .head,
  #other-currencies .head { max-width: none; }
  #how .head p, #convert .head p, #about-currency .head p, .curcosts .head p,
  #tips .head p, #places .head p, #why-us .head p, #faq .head p,
  #other-currencies .head p { max-width: 660px; margin-left: auto; margin-right: auto; }
}
.curflag-sm { font-size: 12.5px; padding: 4px 11px; gap: 7px; margin-bottom: 0; }
.curflag-sm img { width: 18px; height: 18px; }

/* ---- currency-page calculator at slim widths (client screenshots, 29 Aug 2026) ------------
   THE PANEL TAX: on currency pages the calculator sits inside .curpanel, whose side padding
   plus border makes the calculator ~30px narrower than the same calculator on every other
   page. The global rate-row step-downs key on VIEWPORT width (349px / 314px), so between
   ~350-383px the currency page's calculator was already tier-1 narrow while still wearing
   full-size type - "A$1 = US$0.70" wrapped, "Updated 1:48 AM" wrapped. These mirrors fire
   the SAME tiers 34px earlier, scoped to .curpanel so no other page changes. At 348px and
   below the global tiers take over (which is exactly the width the client observed it
   "becoming normal again"). */
@media (max-width: 600px) {
  .curpanel { padding: 22px 12px 20px; }   /* was 16px sides - 8px back to the calculator */
}
@media (max-width: 383px) {
  .curpanel .detail-row.rate-row { gap: 8px; font-size: 11px; }
  .curpanel .detail-row.rate-row .rate-inv { font-size: 10.5px; }
  .curpanel .detail-row.rate-row .calc-upd { font-size: 10px; }
  .curpanel .detail-row.rate-row .rate-info .material-symbols-outlined { font-size: 13px; }
  .curpanel .rate-tip { font-size: 11px; left: -34px; }
}
@media (max-width: 348px) {
  .curpanel .detail-row.rate-row { gap: 6px; font-size: 10px; }
  .curpanel .detail-row.rate-row .rate-inv { font-size: 9.5px; }
  .curpanel .detail-row.rate-row .calc-upd { font-size: 9px; }
}
/* The directions CTA on slim phones: shorter words, smaller type, one line. The "to store"
   span exists in the SHARED calc markup on every page (byte-identical); only the currency
   page hides it, and only when the panel makes the button genuinely tight. */
/* The label is ONE flex item (.dirlbl): the button is a flex row with a gap for the icon,
   and a bare text node + span rendered as TWO items - the icon gap appeared INSIDE the
   label ("Get directions  to store", the client's double-space screenshot). Never split a
   flex button's label across siblings.
   Two-stage sizing (client): stage 1 only shrinks the type so the FULL words hold one line
   down to 351px; stage 2 drops " to store" from 350px down. */
@media (max-width: 383px) {
  .curpanel .btn.go { font-size: 14px; }
}
@media (max-width: 350px) {
  .curpanel .btn.go .dirlong { display: none; }
}


/* ---- currency-page calculator, slim-width round 2 (client screenshots, 29 Aug 2026) ------- */
@media (max-width: 600px) {
  /* the open/closed bar breathed too much around its one line - trim the tuck padding */
  .curpanel .calc-openbar { padding: 15px 14px 10px; }
}
@media (max-width: 374px) {
  /* one line for "Closed - Opens 9:30 AM Today" on slim phones */
  .curpanel .calc-openbar { font-size: 12px; gap: 3px 7px; }
  .curpanel .calc-openbar .s-hint::before { margin: 0 4px; }
}
@media (max-width: 350px) {
  /* "No commission, no hidden fees" held one line to ~346px and then wrapped */
  .curpanel .no-fees { font-size: 11.5px; gap: 5px; }
  .curpanel .no-fees .material-symbols-outlined { font-size: 15px; }
}
@media (max-width: 320px) {
  .curpanel .no-fees { font-size: 11px; }
}

/* The footer band's "Exchange <currency> now" glides to #calculator, which is the hero
   panel itself - the first thing on the page - so the honest landing is the page top, the view
   the visitor first arrived on (desktop: calculator in the right column; phones: the headline
   with the calculator directly under it). The margin is the desktop header clearance plus the
   hero's own top padding, i.e. the panel's document offset: the glide resolves to exactly 0.
   On phones the header is 72px, so this overshoots by a few px - Lenis (and native scrolling)
   clamp at 0, so it still lands at the top. Not the section rule (scroll-margin 0), which is
   for bands whose own padding hides the pinned header. */
.curpanel { scroll-margin-top: calc(84px + var(--obar-h) + var(--obar-gap) + 46px); }

/* ===========================================================================
   CURRENCY DIRECTORY (.cdir) - the grid of links to every currency page.
   Added 7 Sep 2026.

   WHY IT EXISTS: the 22 pages under /exchange-rates/<slug>/ had NO inbound
   links except the four hardcoded in the footer, so 18 of them were orphaned -
   built, and reachable by nobody. This grid is the door.

   It is PLAIN STATIC <a> markup on purpose. A JS-built list would leave them
   orphaned all over again, which is the whole problem being fixed.

   The rate rows above it are deliberately UNTOUCHED: a .rrow is a <button>
   that loads the calculator, and an <a> cannot legally nest inside a button.

   Modelled on .rrow (copy an existing component) but smaller - this is a
   directory, not a rate. auto-fill, never a fixed column count: the currency
   list comes from the feed and CHANGES, so a grid tuned to exactly 22 would
   break the day one is added or dropped.
   =========================================================================== */
/* FLEX, NOT GRID, for one reason: the last row.
   22 currencies over 5 columns leaves 2 tiles on the final row, and CSS Grid packs them hard
   against the left with three empty cells trailing off to the right, which reads as a broken
   layout rather than a finished one (client, 7 Sep 2026). justify-content:center fixes it and
   costs nothing anywhere else: a FULL row's tiles add up to exactly 100% of the container, so
   centering moves nothing and the left and right edges still line up with the heading above.
   Only a short final row is affected, which is the whole point.
   The column count is fixed per breakpoint because the basis has to divide the row exactly;
   that is still independent of how MANY currencies there are, which is what matters - the
   feed adds and drops them and no count is baked in anywhere. */
.cdir {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}
.cdir-i {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  flex: 0 1 calc((100% - 40px) / 5);   /* 5 per row: 5 tiles + 4 x 10px gap = 100% */
  padding: 12px 14px;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.7);
  text-decoration: none;
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.cdir-i:hover {
  background: #ffffff;
  transform: translateY(-1px);
  box-shadow: 0 10px 24px -14px rgba(16, 24, 40, 0.25);
}
.cdir-i img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex: none; }
/* flex:1 matters. Without it this block sizes to its CONTENT, not to the space left in
   the tile, and the two longest names (Indonesian Rupiah, Vietnamese Dong) spilled past
   the tile edge at 2 columns on a 375px phone instead of wrapping. */
.cdir-t { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
.cdir-t b { font-size: 14.5px; font-weight: 700; color: var(--ink); letter-spacing: -0.01em; }
.cdir-t i {
  font-style: normal;
  font-size: 12.5px;
  color: var(--slate);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The chevron stays VISIBLE at rest, only brightening on hover. On touch there is no hover
   state to reveal that a tile is clickable - the same reason .paysec-do carries a visible
   action label. A hover-only affordance would read as a static fact on every phone. */
.cdir-i .material-symbols-outlined {
  margin-left: auto;
  font-size: 19px;
  color: var(--gold);
  opacity: .45;
  transition: opacity .18s ease, transform .18s ease;
}
.cdir-i:hover .material-symbols-outlined,
.cdir-i:focus-visible .material-symbols-outlined { opacity: 1; transform: translateX(2px); }
@media (max-width: 1199px) { .cdir-i { flex-basis: calc((100% - 30px) / 4); } }   /* 4 per row */
@media (max-width: 939px)  { .cdir-i { flex-basis: calc((100% - 20px) / 3); } }   /* 3 per row */
@media (max-width: 600px) {
  /* Two columns on a phone leaves ~80px for the name once the flag, the gap and the chevron
     are taken out, so the type steps down here. The longest names we hold (Indonesian Rupiah,
     Vietnamese Dong, South African Rand) wrap to two lines at this size, which is fine - every
     tile in a grid row is the same height anyway. overflow-wrap is the safety net: the
     currency list comes from the feed and changes, so a longer name than any we hold today
     must still be impossible to push past the tile edge. */
  .cdir { gap: 8px; }
  .cdir-i { flex-basis: calc((100% - 8px) / 2); }   /* 2 per row */
  .cdir-i { padding: 11px 11px; gap: 9px; }
  .cdir-i img { width: 28px; height: 28px; }
  .cdir-t b { font-size: 13px; line-height: 1.25; overflow-wrap: anywhere; }
  .cdir-t i { font-size: 11.5px; }
  .cdir-i .material-symbols-outlined { font-size: 16px; }
}

/* =============================================================================================
   THE HOMEPAGE (moved into the full website, 10 Oct 2026)
   ============================================================================================= */
/* The only page with NO breadcrumb bar, so its hero carries the header clearance the crumbs
   carry everywhere else (.crumbs padding-top 92 / 88 / 106). These are the deployed homepage's
   own numbers: 80px on top below 951px (the 72px header + 8px of air) and, from 951px, the
   whole first screen - strip + hero = 100vh, content centred - with 126px on top (the 86px
   desktop header row + 40px of air). The other pages dropped the full-height hero on 7 Aug
   2026 so a rate shows above the fold on a rates page; the homepage keeps it, as it always had.
   .hero is already a centred flex column from 951px, so only the height and padding change. */
.hero.hero-home { padding-top: 80px; padding-bottom: 64px; }
/* ...and below 951px the deployed homepage also dropped the hero content by 10% of the width
   (34px on a 390px phone) so the H1 does not sit hard under the header. The crumbs make that
   gap on every other page, which is why the shared stylesheet never needed it. */
@media (max-width: 950px) {
  .hero-home .hgrid { margin-top: 10%; }
}
@media (min-width: 951px) {
  .hero.hero-home {
    min-height: calc(100vh - var(--obar-h) - var(--obar-gap));
    padding: 126px 0 40px;
  }
}
/* A centred "more" line under a section's panel (the rate board, the FAQ). The link itself is
   the shared .cardlink; this only centres it and gives it air from the panel above. */
.hmore { display: flex; justify-content: center; margin-top: 28px; }
/* Visit us: the "How to find our counter" line under the Store location / Call us buttons. */
.locinfo .cardlink { margin-top: 20px; }
/* "A better rate, in person, in the CBD" broke as "...in the / CBD" on desktop - one word alone
   on its own line. It holds one line from 951px (758px of title at 48px, 903px of room at the
   narrowest), the same treatment the currency pages give their section titles: the .head box
   spans the wrap, because a centred line wider than its 660px box is clamped to the left edge
   and sits off-centre. Scoped through .hero-home because /wholesale/ has a #why of its own. */
@media (min-width: 951px) {
  .hero-home ~ #why .head { max-width: none; }
  .hero-home ~ #why .head h2 { white-space: nowrap; }
}
