/* Component styles
   =================

   Built on the token vocabulary in tokens.css. No colour, size, radius or
   spacing value below is a literal — every one is a var(--token). See
   tokens.css's header comment for how those tokens were read off the Figma
   file NU1oF2CrE9RggFQxkqLZ35.

   Source frame for the entity-page components (hero, card, chip, data-table,
   table-scroll, gallery, gallery-credit, empty-state, badge, rank-banner,
   faq, map-placeholder): "Entity Pages", node 10:651, read via
   get_screenshot + get_design_context per
   .superpowers/sdd/2026-08-29-front-end/design-procedure.md Steps A and B.

   site-header/site-nav/site-footer/site-wordmark/skip-link were rewritten
   for Task 3 from the actual Footer frame (node 2:1797), read the same
   way. It is a four-column footer -- brand blurb, "Directory",
   "Community", "Legal" -- on a dark fill with a brand-coloured top rule.
   "Community" renders nothing on this build and never will: it links to
   "Write a Review" / "Suggest a Facility" / "Conservation News", and rule
   11 puts reviews permanently out of reach. Per the brief, a control with
   no backend is worse than no control, so it is omitted outright rather
   than rendered with only a heading. "Legal" was omitted for the same
   reason until 2026-09-06 and is now drawn, widened and renamed to
   "General": five prose pages exist behind it, `/about/` joins them, and
   "Cookie Settings" -- the one link with still nothing behind it -- stays
   dropped. See `render.FOOTER_GENERAL` for that reasoning. The column is
   guarded on a configured contact address, so a build without one is back
   to the two-column footer this grid was written for, which is why
   .footer-grid takes its column count from auto-fit rather than naming a
   number. "Directory" (the frame's "All Zoos" / "All
   Aquariums" / etc.) is driven by the same `nav_links` context list as
   the header nav -- see render.py's `_shell()` -- which is empty until
   Tasks 9-11 land the pages it will point at, so today's build renders
   that column empty too rather than linking anywhere that 404s.

   Two judgement calls worth naming:

   - The Figma frame highlights the #1 ranking pill with a gold fill
     (rgba(253,234,137,.2) / rgba(253,234,137,.4) border) that has no
     equivalent in tokens.css and appears nowhere else in the frame. Rather
     than add a one-off gold token for a single pill, .rank-banner's first
     child is emphasised using --colour-brand and --colour-surface-alt,
     which are already in the vocabulary and read as "this one is
     different" without introducing a colour nothing else uses.
   - The frame draws FAQ items as "Details - FAQ Item" / "Slot -> Summary"
     layers — Figma's own vocabulary for exactly the native <details> and
     <summary> elements. .faq styles those elements directly, which gives
     the accordion for free with no JavaScript.
*/

/* ---- Page container ------------------------------------------------------
   design.md 2.7 gives four content widths and says which one a template uses
   is meaningful: Home, Category Hub and Country Hub are marketing (1128);
   City Hub, Entity, Index, Article Guide and About are app (1360). Rather
   than thread a width through every shell rule, base.html stamps one class
   on <body> and that class re-points --container -- which .site-header-inner
   (4.1: "Container matches the page's container width"), .footer-inner,
   .page and .hero-split all read. tokens.css defaults --container to the app
   width, so .page-app is the no-op default and only marketing overrides it.
   No page claims marketing, prose or article today: 2.7 puts Home on
   marketing (1128), but that leaves 156px of gutter a side at 1440 and reads
   too narrow on a real monitor, so Home was moved back to app by decision --
   see _shell(). The class stays because the mechanism is right and the other
   two marketing templates are still to come. */
.page-marketing {
  --container: var(--container-marketing);
}

/* 2.7 puts the best-of listicle on `container-article`. That is not a taste
   call about this one page -- it is what makes the prose fit its own column.
   At the 1360 app width the ranked card's content column runs ~990px, so the
   65ch measure on .ranked-prose stops the text less than two thirds across
   and leaves the rest of the card empty. At 1024 the column lands at roughly
   65ch and the two coincide. */
.page-article {
  --container: var(--container-article);
}

/* ---- Reading column: the six prose pages ------------------------------- */

/* design.md 2.7 gives `container-prose` (768) to "Article body, About
   mission/values", but the four content widths govern the PAGE SHELL -- the
   header, the crumb bar and the footer all read `--container` from the body
   class, and a policy page whose header is 768 wide while every other page's
   is 1360 would announce itself as a different site.

   So the shell stays app-width and this narrows only the content column.

   It exists because the two caps disagreed: `.page` ran to --container
   (1360) while `.prose` capped at --measure (65ch, ~550px) and sat flush
   left inside it. Headings and the section rules spanned the full 1360 while
   the text ran out a third of the way across, which reads as a layout bug
   rather than a measure -- and on these six pages the text IS the page, so
   there was nothing in the remaining 800px to justify it.

   The column is the measure plus the container's own padding, so the rule
   above a heading, the heading, and the paragraph under it share one left
   edge and one width. Nothing is centred as text -- the ragged right edge
   stays where a reader's eye expects it; it is the COLUMN that is centred in
   the viewport. Centring the text itself would cost the consistent left edge
   that makes a document scannable. */
/* `.page.prose-page`, not `.prose-page`: base.html loads site.css AFTER
   components.css, and site.css's own `.page { max-width: var(--container) }`
   is the same 0-1-0 specificity, so a single class here loses on load order
   and silently does nothing. Measured before this was raised: the column
   still came out 1360px wide. */
.page.prose-page {
  /* 1.5x the bare measure, at the owner's instruction (2026-09-07): the
     measure-width column read as too thin against the 1360 shell around it.

     Derived from --measure rather than snapped to one of design.md 2.7's
     four widths on purpose. Those four are SHELL widths -- the header, the
     crumb bar and the footer all read --container from the body class -- and
     this is a reading width, which is what --measure is for. Snapping to
     container-prose (768) would have been the nearest token but only widens
     by a third; container-article (1024) overshoots by half again.

     The trade is real and taken knowingly: ~97ch is past the 45-75ch that
     reads most comfortably, so lines are long by typographic convention.
     Against that, the page no longer looks like a column that failed to fill
     its shell, which is the complaint this answers. */
  max-width: calc((var(--measure) + 2 * var(--container-pad)) * 1.5);
}

/* `.prose` already caps at --measure, which is now the column width, so the
   cap is a no-op here and paragraphs simply fill it. Stated rather than left
   implicit: a future change to either value must keep them in step, or the
   text starts falling short of its own headings again. */
.page.prose-page .prose {
  max-width: 100%;
}

/* Balance the ragged edge rather than letting a single word hang alone on
   the last line. Progressive: browsers without it wrap as before, and the
   layout does not depend on it. */
.page.prose-page h1,
.page.prose-page h2,
.page.prose-page .lede,
.page.prose-page .prose {
  text-wrap: pretty;
}

/* ---- Site chrome: skip link, header, nav, footer ----------------------- */

/* Off-screen until focused. A site-wide nav ahead of every page's content
   is exactly what traps a keyboard user without one -- see base.html,
   where this is the first focusable element on the page and targets
   #main. */
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 10;
  padding: var(--space-2) var(--space-4);
  background: var(--colour-brand);
  color: var(--colour-surface);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* Visually hidden, still announced. design.md 10 names this class outright
   -- `<span class="sr-only">Rated 4.6 out of 5 from 12,450 reviews</span>` --
   and asks for the numeric value of a rating to be readable even where only
   stars are drawn. The header uses it for the same reason at a smaller scale:
   a bare "163" beside a country name could be anything, so the unit travels
   with it for anyone not seeing the column heading.

   `clip-path` does the hiding; the legacy `clip` stays for the browsers that
   never learned the newer property. Not `display: none` and not
   `visibility: hidden`, both of which remove the text from the accessibility
   tree as well as from the page, which is the opposite of the point. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The wordmark. design.md 4.1 specifies TEXT -- "ZOOAQUA, display bold
   24/32/-1.2" -- and this build draws an image instead, because the brand it
   sets is a lockup: a custom glyph beside the name, in two weights. Type
   properties cannot express that, so 4.1 records the deviation rather than
   this rule pretending to implement it.

   What survives from 4.1 is the SIZE, at double the 24px the frame drew. The
   mark stands 48px tall in both contexts (4.3 draws the footer's at the same
   size, so one rule still serves both), which is --text-3xl -- reached for as
   a token rather than typed as 48px, per the components.css rule that a
   literal never appears here.
   `width: auto` keeps the 523x118 aspect, so the lockup cannot be squashed by
   a narrower header.

   Colour is no longer set here at all: it is baked into the two files. The
   header sits on --colour-surface and takes logo.png; the footer sits on
   --colour-ink and takes logo-white.png. `display: block` on the image
   removes the inline-baseline gap that would otherwise pad the header by a
   few stray pixels below the mark. */
.site-wordmark {
  display: inline-flex;
  text-decoration: none;
}

.site-wordmark img {
  display: block;
  height: var(--text-3xl);      /* 48px, design.md 4.1 doubled */
  width: auto;
}

/* --- The header bar. design.md 4.1 -------------------------------------
   "Height 80px + 1px bottom border. rgba(254,254,254,0.95) +
   backdrop-blur(2px)."

   There is deliberately NO top rule here. The 6px --colour-brand rule is the
   FOOTER's (4.3), and it is the one place the design puts a brand band; a
   second one at the top of every page would compete with it, and appears
   nowhere in the file.

   9's note -- "Header: static, not sticky, in the Figma. If you make it
   sticky, keep the blur and the 1px rule" -- is honoured by leaving it
   static. `position: relative` is here for the dropdown panels, which are
   positioned against this box, not for stickiness.

   The 0.95 paper fill and the 2px blur have no tokens: 2.3 names the opaque
   --paper, and 2.6 lists this separately as a "glass effect" rather than as
   one of the three shadows. Both values are read straight from 4.1 and are
   commented rather than tokenised, because a one-off glass recipe used by
   exactly one surface is not a vocabulary item. */
.site-header {
  position: relative;
  z-index: 40;
  background: rgba(254, 254, 254, 0.95);   /* 4.1's glass fill over --paper */
  backdrop-filter: blur(2px);              /* 4.1 */
  border-bottom: 1px solid var(--colour-rule);
}

.site-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;   /* 4.1: logo, nav, actions */
  gap: var(--sp-24);
  max-width: var(--container);
  min-height: 5rem;                 /* 4.1's 80px bar; no token is this tall */
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

/* --- Primary nav. design.md 4.1 ----------------------------------------
   "Nav -- 32px gap ... `ui` token, --navy-900. [It] has a 16px chevron and
   4px gap -- it is a dropdown."

   `.site-nav a` and `.site-nav button` share one shape so the row reads as a
   single set of controls whether or not an item owns a menu. The button only
   exists once nav.js has upgraded the link: base.html ships four plain
   links, because a <button> in a template is what
   test_no_template_contains_a_form_or_a_bare_button forbids -- rightly, since
   with no script behind it a trigger opens nothing. */
.site-nav ul {
  display: flex;
  align-items: center;
  gap: var(--sp-32);                /* 4.1: 32px */
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav a,
.site-nav button {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);                 /* 4.1: 4px to the chevron */
  padding: 0;
  border: 0;
  background: transparent;
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

/* 5.1's undesigned state for a ghost control: "ghost text -- --blue-500".
   --colour-brand-dark rather than --colour-brand because 10's fix 4 says the
   plain blue is too close to the contrast line for small bold text, and every
   one of these labels is `ui`-sized and bold. */
.site-nav a:hover,
.site-nav a:focus-visible,
.site-nav button:hover {
  color: var(--colour-brand-dark);
}

/* The open marker. 4.1 draws none -- the frame has no open state at all -- so
   this is specified here and flagged (undesigned). A 2px --colour-ink rule
   under the trigger is the quietest thing the palette can do to tie a trigger
   to the panel below it, and it introduces no colour. */
.site-nav button::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--sp-4) * -1.5);
  height: 2px;
  background: var(--colour-ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 140ms ease;
}

.site-nav button[aria-expanded="true"]::after {
  transform: scaleX(1);
}

.nav-chevron {
  width: var(--sp-16);              /* 4.1: 16px chevron */
  height: var(--sp-16);
  flex: 0 0 auto;
  transition: transform 140ms ease;
}

[aria-expanded="true"] > .nav-chevron {
  transform: rotate(180deg);
}

/* --- Header actions. design.md 4.1 and 5.1 -----------------------------
   4.1: "Actions -- 16px gap: a 40px circular icon-only search button (22px
   glyph), then a `Sign In` pill button (primary)."

   Sign In is NOT built. It has no backend, and
   test_no_dead_controls_anywhere_in_the_templates bans the string outright --
   which is CLAUDE.md's rule that where the design draws a control the data
   cannot support, the design loses. So the row is: the icon search button 4.1
   specifies, then About as 5.1's `ghost`, then Add a listing as 5.1's
   `secondary`. No primary pill survives, because the only thing 4.1 makes
   primary is the control that cannot ship. */
.header-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-16);                /* 4.1: 16px */
}

/* 5.1 `icon`: no fill, no border, 22px glyph, 40x40, radius full. 10 confirms
   the size is deliberate -- "the 40px icon button is fine" is listed under
   target size. 40px is 2.5rem and has no spacing token. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.5rem;                    /* 5.1: 40x40 */
  height: 2.5rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  color: var(--colour-ink);
  cursor: pointer;
}

.icon-button svg {
  width: 1.375rem;                  /* 5.1: 22px glyph */
  height: 1.375rem;
}

.icon-button:hover {
  color: var(--colour-brand-dark);
}

/* design.md 9 collapses the nav below lg; above it the hamburger would be a
   second way to reach links already on screen. */
.nav-drawer-toggle {
  display: none;
}

/* 5.1 `ghost`: no fill, --navy-900, `ui`, no border, no padding. */
.button-ghost {
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
  white-space: nowrap;
}

.button-ghost:hover {
  color: var(--colour-brand-dark);
}

/* 5.1 `secondary`: --white fill, --navy-900 `ui`, 1px --border, 16/8, pill.
   The fill is --colour-surface rather than a pure-white token, which is what
   every other card-like surface in this file sits on. */
.button-secondary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-8) var(--sp-16);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-pill);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
  white-space: nowrap;
}

/* 5.1 states (undesigned): "secondary/outline background -- --tint". */
.button-secondary:hover {
  background: var(--colour-surface-alt);
}

.button-secondary svg {
  width: var(--sp-12);
  height: var(--sp-12);
}

/* --- The search band. design.md 4.1 and 5.14 ---------------------------
   4.1 puts an icon-only button in the bar, not a field. The field it opens is
   5.14's: "White, 2px --border, --shadow-sm, no radius, max 512px. 22px
   [glyph] in a 16px-padded slot, body-lg input with placeholder."

   Two departures from 5.14, both deliberate:
     - no --colour-brand submit button. 5.14 draws one, but this control is a
       live combobox over search-index.json (static/search.js) and there is no
       results page to submit to. A button that posts nowhere is exactly what
       the no-dead-controls test forbids.
     - the placeholder is not the label. 10's fix 3 says "never use
       placeholder text as the only label", so nav.js builds a
       visually-hidden <label> and the placeholder only repeats it.

   The band itself is undesigned: it takes the header's own surface and 1px
   rule so it reads as part of the bar rather than a new kind of surface.
   `.search` and its children are styled by the existing search rules further
   down this file -- this only sets the band and the width cap 5.14 gives. */
.search-band {
  background: var(--colour-surface);
  border-bottom: 1px solid var(--colour-rule);
}

.search-band-inner {
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-24) var(--container-pad);
}

.search-band .search {
  max-width: 32rem;                 /* 5.14: max 512px */
}

/* --- The dropdown panel  (undesigned -- flag for design review) --------
   4.1 says in as many words that "the menu is not designed" and asks the
   build to "specify: on hover/focus, a panel of ... entry points". This is
   that specification, and every value in it comes from 2: 2.5 puts directory
   surfaces at --radius-none, 2.6 puts every border at 1px --colour-rule, and
   --shadow-md is the file's heaviest, which is what a surface floating over
   the page wants. */
.nav-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 45;
  background: var(--colour-surface);
  border-bottom: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  box-shadow: var(--shadow-md);
  animation: nav-panel-drop 140ms ease both;
}

@keyframes nav-panel-drop {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* Bottom padding is --sp-24 rather than the --sp-40 the panel used before it
   had a footer: the hub link below adds its own --sp-16 above the rule, and
   40 + 16 read as two unrelated blocks rather than one panel. */
.nav-panel-inner {
  display: grid;
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-32) var(--container-pad) var(--sp-24);
}

/* Even columns. The count is not fixed: a category whose city hubs are too
   few to be worth a column (render.HEADER_CITY_COLUMN_MIN) renders two, and
   the grid re-proportions rather than leaving a gap where a column used to
   be. nav.js stamps the count it actually drew. */
.nav-panel-inner[data-cols="2"] { grid-template-columns: 1fr 1fr; }
.nav-panel-inner[data-cols="3"] { grid-template-columns: 1fr 1fr 1fr; }

.nav-col {
  padding: 0 var(--sp-32);
}

/* Cap the COLUMN, not each row: the heading rule, the rows and the feature
   card then all stop at one edge, and a column that drops out cannot stretch
   its neighbours into a measure too wide to read across. */
.nav-col > * {
  max-width: 26rem;
}

.nav-col + .nav-col {
  border-left: 1px solid var(--colour-rule);
}

.nav-col:first-child { padding-left: 0; }
.nav-col:last-child { padding-right: 0; }

/* `label-caps` -- 2.2's uppercase 12px role, the one the design already uses
   for ALSO KNOWN AS and TABLE OF CONTENTS. */
.nav-col h2,
.nav-sub-trigger {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--label-caps-size);
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  text-transform: uppercase;
  font-weight: 700;
  color: var(--colour-muted);
}

.nav-col h2 {
  margin-bottom: var(--sp-12);
  padding-bottom: var(--sp-8);
  border-bottom: 1px solid var(--colour-rule);
}

.nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A menu row: `body` --navy-900. 5.1's undesigned hover for a bordered
   surface is --tint, and that is the only state a row needs.

   It was `body-sm` (14px), matching the role 4.3 gives the footer's link
   columns, and moved up one role to `body` (16px) at the owner's request
   (2026-09-08). The footer comparison was the wrong one: a footer column is
   read once on the way off the page, where a menu row is a target somebody
   aims a pointer at repeatedly, and at the density these columns run to 14px
   read cramped.

   A ROLE change, not a nudged pixel value -- both sizes are in 2.2, so the
   panel stays inside the type scale rather than beside it. Costs about 40px
   of panel height. */
.nav-row,
.nav-geo {
  display: grid;
  align-items: center;
  column-gap: var(--sp-12);
  padding: var(--sp-4) var(--sp-8);
  margin: 0 calc(var(--sp-8) * -1);
  font-family: var(--font-body);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-ink);
  text-decoration: none;
}

.nav-row { grid-template-columns: 1fr auto; }
.nav-geo { grid-template-columns: 1fr auto 3rem; }

.nav-row:hover,
.nav-geo:hover {
  background: var(--colour-surface-alt);
}

.nav-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Counts are `body-sm`: one role below the name they sit beside, so the
   number stays secondary to the place without becoming hard to read. They
   were `caption` (12px) when the rows were `body-sm`, and both moved up one
   role together -- what carries the hierarchy is the GAP between the two
   roles, not either size on its own. Comfortably clear of 10's 12px floor
   either way. Tabular figures so a column of numbers lines up rather than
   jittering by digit width. */
.nav-count {
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  font-variant-numeric: tabular-nums;
  color: var(--colour-muted);
  text-align: right;
  min-width: 2.5em;
}

/* The tally  (undesigned -- flag for design review).
   These lists are ordered by venue count and re-order themselves as inventory
   grows, so each row carries the number it was ranked by plus a measure of
   that place's share of the leader. Track --colour-rule, fill --colour-ink:
   the ranking becomes visible without introducing a colour 2.3 does not have.
   It is aria-hidden in the markup, because the numeral beside it says the
   same thing and nothing may depend on seeing it. */
.nav-bar {
  height: 3px;
  background: var(--colour-rule);
}

.nav-bar i {
  display: block;
  height: 100%;
  width: var(--share, 0%);
  background: var(--colour-ink);
}

/* 5.15: "optional right-aligned `View all ...` ghost link". */
.nav-more {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  margin-top: var(--sp-16);
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
}

.nav-more:hover {
  color: var(--colour-brand-dark);
}

/* The panel's route to the category page itself -- see `buildPanel` in
   nav.js for why the panel is the only route there is. Constrained to the
   container so its rule and its label line up with the columns above it,
   rather than running the full bleed of a panel whose contents do not. */
.nav-hub {
  display: block;
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-16) var(--container-pad) var(--sp-24);
  border-top: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
}

.nav-hub:hover {
  color: var(--colour-brand-dark);
}

/* --- Responsive. design.md 9 -------------------------------------------
   "Header nav | < 1024 | collapse to a hamburger + drawer; keep logo, search
   icon, Sign In." lg is 1024, so the drawer owns everything below it and the
   full bar owns 1024 up. Sign In is not built (see .header-actions), so the
   collapsed bar keeps the logo, the search icon and the hamburger.

   The nav is hidden rather than removed: the drawer nav.js builds is a copy
   of the same links, and a phone that never runs the script still has the
   four links in the DOM. That is also why the hamburger is revealed by
   nav.js and not by this rule -- a hamburger CSS alone can show is one that
   opens nothing. */
@media (max-width: 63.99rem) {
  .site-header .site-nav,
  .header-actions .button-ghost,
  .header-actions .button-secondary {
    display: none;
  }

  .site-header-inner {
    gap: var(--sp-12);
    min-height: 4rem;
  }

  /* 9 keeps the logo in the collapsed bar. At 48px the lockup is 213px wide
     and the bar cannot also hold the search icon and a hamburger inside a
     360px viewport. 24px is 4.1's own original size -- the doubling to 48 was
     an explicit decision about the DESKTOP bar, so undoing it here restores
     the documented value rather than inventing a third one. Scoped to the
     header: 4.3 puts the footer's mark at 48px and the footer has the width
     to keep it. */
  .site-header .site-wordmark img {
    height: var(--text-xl);
  }

  .header-actions {
    gap: var(--sp-8);
  }

  .nav-drawer-toggle {
    display: inline-flex;
  }

  .search-band-inner {
    padding: var(--sp-16) var(--container-pad);
  }
}

/* --- The drawer  (undesigned -- flag for design review) ----------------
   9 names the pattern -- "a hamburger + drawer" -- and nothing else, so the
   anatomy is specified here: the same surface, rules and type roles the panel
   uses, with the country lists as accordions rather than a wall of rows to
   scroll past on the way to the next section. */
.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  /* --colour-ink at 42%. Written as a colour-mix on the token rather than a
     second rgba() literal, so the scrim still tracks the ink colour if that
     ever moves. */
  background: color-mix(in srgb, var(--colour-ink) 42%, transparent);
}

.nav-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 55;
  display: flex;
  flex-direction: column;
  width: min(92vw, 25rem);
  background: var(--colour-surface);
  border-left: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-md);
  overflow-y: auto;
}

.nav-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-16);
  padding: var(--sp-16) var(--sp-24);
  border-bottom: 1px solid var(--colour-rule);
}

.nav-drawer-head .site-wordmark img {
  height: var(--text-xl);
}

.nav-drawer-body {
  padding: 0 var(--sp-24) var(--sp-40);
}

.nav-acc + .nav-acc,
.nav-acc + .nav-drawer-link,
.nav-drawer-link + .nav-drawer-link {
  border-top: 1px solid var(--colour-rule);
}

/* `heading-4` for the drawer's top level -- the same role the featured card's
   title takes, and the largest thing in this component. */
.nav-acc-trigger,
.nav-drawer-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--sp-16) 0;
  border: 0;
  background: transparent;
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  font-weight: 700;
  color: var(--colour-ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.nav-acc-trigger:hover,
.nav-drawer-link:hover {
  color: var(--colour-brand-dark);
}

.nav-acc-panel {
  padding: 0 0 var(--sp-16);
}

.nav-sub-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--sp-12) 0;
  border: 0;
  border-top: 1px solid var(--colour-rule);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.nav-sub-trigger .nav-chevron {
  width: var(--sp-12);
  height: var(--sp-12);
}

.nav-sub-panel {
  padding-bottom: var(--sp-12);
}

.nav-drawer-foot {
  display: grid;
  justify-items: start;
  gap: var(--sp-12);
  margin-top: var(--sp-24);
  padding-top: var(--sp-24);
  border-top: 1px solid var(--colour-rule);
}

/* Everything nav.js hides, it hides with the `hidden` attribute, so the state
   lives in the DOM rather than in a class only CSS knows about. `hidden` is a
   presentational default that loses to `display: flex` and `display: grid` on
   specificity, which is why it is restated here for the elements that carry
   one. */
[hidden] {
  display: none !important;
}

/* --- Focus. design.md 10 -----------------------------------------------
   "Focus-visible ring on every interactive element -- 2px --blue-500, 2px
   offset. There is no focus treatment anywhere in the Figma."
   One rule, one colour, no exceptions -- including inside the drawer, which
   is a dialog and therefore the one place a missing ring strands somebody
   completely. --colour-brand, not --colour-brand-dark: a focus ring is a 2px
   band against a surface, not small text, so fix 4 does not apply. */
.site-header :focus-visible,
.nav-drawer :focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* The frame's top-6px brand-coloured rule, read directly: border-t-6 on
   #4362ef. 6px has no token of its own; --space-1 (4px) is the nearest
   step in the file's 4px rhythm and reads as the same "thin accent bar"
   at a glance. */
.site-footer {
  background: var(--colour-ink);
  color: var(--colour-on-dark-body);
  border-top: var(--space-1) solid var(--colour-brand);
  padding: var(--space-7) var(--space-5) var(--space-5);
}

.footer-inner {
  max-width: var(--container);
  margin: 0 auto;
}

.footer-grid {
  display: grid;
  grid-template-columns: minmax(14rem, 1.4fr) repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-6);
}

/* This grid mixes one fixed-minimum explicit track with an auto-fit repeat
   group. auto-fit's own track-count algorithm is what keeps .card-grid and
   .gallery reflowing safely with no breakpoint at all -- but that algorithm
   is guaranteed to place at least one repeated track once real content
   needs it, even if the container is too narrow to hold it alongside the
   14rem explicit track: 14rem + the gap + one 9rem track is 400px of hard
   minimum, and every page's footer renders on every viewport including
   phones under 448px wide. Reflow alone cannot fix a forced-minimum
   overflow like this, so this is the one deliberate breakpoint in the
   stylesheet, set with headroom above that 448px threshold. */
@media (max-width: 30rem) {
  .footer-grid {
    grid-template-columns: 1fr;
  }
}

.footer-brand p {
  margin: var(--space-3) 0 0;
  max-width: 24rem;
  font-size: var(--text-sm);
  color: var(--colour-on-dark-muted);
}

.site-footer .site-wordmark {
  color: var(--colour-surface);
  font-size: var(--text-xl);
}

.site-footer a {
  color: var(--colour-on-dark-body);
}

.site-footer :is(h2, h3) {
  font-size: var(--text-sm);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--colour-on-dark-faint);
  margin: 0 0 var(--space-3);
}

.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.footer-bottom {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--colour-on-dark-rule);
}

.site-footer .footer-bottom .attribution {
  color: var(--colour-on-dark-faint);
}

.site-footer .footer-bottom .attribution a {
  color: var(--colour-on-dark-body);
}

/* ---- Breadcrumbs -------------------------------------------------------- */

/* design.md 4.2: "A full-width bar, --paper background, 1px bottom border,
   12px vertical padding, container-width inner."

   Tinted rather than --paper. The doc's own value is near-white, which reads
   as a seam against a white page and vanishes entirely against the tinted
   hero band it sits above on a place hub -- the bar has to be its own strip or
   it is not a bar. --colour-surface-alt is the design's tint and the same fill
   the hero bands use, so nothing new enters the palette. */
.crumb-bar {
  background: var(--colour-surface-alt);
  border-bottom: 1px solid var(--colour-rule);
}

.crumb-bar > .crumbs {
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-12) var(--container-pad);
}

.crumb-home {
  flex: none;
  width: 0.875rem;  /* 14px, per 4.2 */
  height: 0.875rem;
}

/* 4.2: the current page is display BOLD and --navy-900, not a link. It is the
   one crumb a reader needs to find to know where they are. */
.crumbs [aria-current="page"] {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--colour-ink);
}

.crumbs {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  font-size: var(--text-xs);
  color: var(--colour-muted);
}

.crumbs li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.crumbs li + li::before {
  content: "\203A";
  color: var(--colour-muted);
}

.crumbs a {
  color: var(--colour-muted);
  text-decoration: none;
}

.crumbs a:hover,
.crumbs a:focus-visible {
  color: var(--colour-brand);
  text-decoration: underline;
}

/* ---- Hero ---------------------------------------------------------------
   Page title + rating/rank meta row, drawn on the frame as "Section - Hero
   Header & Rankings": h1 at --text-3xl, bottom rule, generous padding. */

.hero {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--colour-rule);
}

.hero h1 {
  margin: 0;
  font-size: var(--text-3xl);
  line-height: 1;
  color: var(--colour-ink);
}

/* ---- Cards ---------------------------------------------------------------
   Bordered panel used for the sidebar address block and the "Nearby"
   lateral-linking cards. */

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  /* .card is applied directly to the <a> that wraps a place-hub venue/child
     card (see place.html): the whole card is one link, not a link nested
     inside a heading, so it needs its own colour/underline reset the way a
     <div class="card"> never did. Harmless on the <div class="card"> uses
     elsewhere (best_list.html): a block element has no visible underline to
     remove and its own child elements set their own colours. */
  color: inherit;
  text-decoration: none;
}

.card img {
  max-width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-md);
}

/* A collection of venue/place cards is a list of links, not a div soup:
   `<ul class="card-grid">` with `<li>` wrapping each `.card` announces its
   item count and gives assistive tech list navigation, the same reasoning
   `.chip-row` below already applies to the facility tags. list-style/margin/
   padding are reset here for the same reason they are on `.chip-row` --
   the grid, not the browser's default list box model, controls the layout. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.card-grid .card {
  flex-direction: row;
  align-items: center;
}

/* ---- Chips ---------------------------------------------------------------
   Facilities row: small bordered tags, --radius-sm, --text-sm. */

.chip-row {
  display: flex;
  flex-wrap: wrap;
  /* 2.4 stack gaps: "8 (chip rows)". */
  gap: var(--sp-8);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Facility tags are usually a short word ("Parking"), but crawled source
   text is inconsistent: some venues supply a full sentence for "dining" or
   "parking" instead of a tag (a 508-character example exists in the live
   export). white-space stays default (wrap) rather than nowrap, and
   max-width/overflow-wrap keep a long chip inside its row instead of
   pushing the page sideways -- it just grows tall instead of wide. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  max-width: 100%;
  /* design.md 5.3 attribute chip: --tint fill, 1px border, radius-xs, 8/2
     padding, caption. The previous 8/12 padding at --text-sm was read off a
     single frame before the doc landed; at that size three tags will not sit
     on one line inside a quarter-row card, which 5.6 draws them doing.
     Vertical is --sp-4, not the doc's 2px: 2.4's ladder starts at 4. */
  padding: var(--sp-4) var(--sp-8);
  /* An explicit paper fill, not the doc's --tint. On the entity page the chips
     sit inside a tinted band, so a --colour-surface-alt chip was tint on tint
     and all but vanished -- the same failure the admission table hit, and for
     the same reason. The 1px border carries the shape on a white card, where
     the tint was barely a shade anyway. Deviation from 5.3, at the owner's
     instruction (2026-09-03). */
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-xs);
  font-family: var(--font-body);
  /* 5.3 specifies `caption` (12px). Raised to body-sm (14px), also at the
     owner's instruction: these hold crawled facility text -- often a phrase
     and sometimes a whole sentence -- rather than the one-word tags the frame
     draws, and 12px was too small to read a sentence in. */
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
  overflow-wrap: anywhere;
}

/* The venue's own wording, below the chips. Body type rather than chip type:
   these are sentences, and the whole reason they are not chips is that a tag
   box is the wrong container for a sentence. */
.venue-notes {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  margin: var(--sp-8) 0 0;
  /* Dot points, not a bare stack. These are the venue's own sentences sitting
     among the description's prose, and a bullet is what separates a list of
     statements from a run of paragraphs. */
  padding-left: var(--sp-24);
  list-style: disc;
  max-width: var(--measure);
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* ---- Tables ----------------------------------------------------------------
   Opening-hours and admission tables: bordered box, --colour-surface-alt
   header row, cells on the --space-4 / --space-3 rhythm. */

.data-table {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--colour-rule);
  /* An explicit paper fill, not transparency. On the entity page the table
     sits inside a tinted band, so an unstyled row inherits that tint -- which
     made the striping below draw tint on tint and vanish entirely. The
     alternation only reads if the base row is paper. */
  background: var(--colour-surface);
  font-size: var(--text-sm);
}

.data-table caption {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  color: var(--colour-muted);
}

.data-table th,
.data-table td {
  text-align: left;
  /* 16/20, read off the admission frame. 2.4's ladder has no 20, so this is
     --sp-16 vertical and --sp-20 horizontal rather than one scale step. */
  padding: var(--sp-16) var(--sp-20);
  border-bottom: 1px solid var(--colour-rule);
}

/* 2.1's rule: display bold for anything structural, body for anything read as
   prose. A column header is structural; the values under it are not. */
.data-table thead th {
  background: var(--colour-surface-alt);
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--colour-ink);
}

.data-table tbody td {
  font-family: var(--font-body);
  color: var(--colour-muted);
}

/* Zebra striping. design.md specifies no table anywhere -- 12.6 lists the
   gaps and tables are simply absent -- so this is built from the admission
   frame using the two surface tokens the rest of the system already
   alternates between (.band / .band-alt do the same thing at page scale).
   It is a banding aid, not information: 10 does not allow colour to be the
   only carrier of meaning, and the 1px rules stay under every row so the
   grouping survives with the fill ignored. */
.data-table tbody tr:nth-child(even) {
  background: var(--colour-surface-alt);
}

/* The frame prints the price column in the action blue rather than in body
   grey -- it is the value a visitor came to the table for. Applied by class
   rather than by :nth-child so a table with a different column order cannot
   silently colour the wrong one. */
.data-table .cell-figure {
  color: var(--colour-link);
}

.data-table tbody tr:last-child th,
.data-table tbody tr:last-child td {
  border-bottom: 0;
}

/* A wide table (the admission table especially, on a phone) must scroll
   inside its own box rather than pushing the whole page sideways. Wrap the
   <table> in <div class="table-scroll">. */
.table-scroll {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ---- Gallery ---------------------------------------------------------------
   Three square images in a row on the frame; reflows to fewer columns on
   narrow viewports instead of a fixed breakpoint. */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--space-3);
}

.gallery-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.gallery-figure img {
  display: block;
  width: 100%;
  max-width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--colour-surface-alt);
  border-radius: var(--radius-sm);
}

/* A CC-BY-SA image's credit is a licence condition, not decoration: it must
   stay visible with no hover or toggle involved. Small and muted, but
   always in the document flow. */
.gallery-credit {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--colour-muted);
}

.gallery-credit a {
  color: inherit;
}

/* ---- Empty state -----------------------------------------------------------
   Drawn on the frame as "Section - Exhibitions/Events (Empty State
   Pattern)": dashed border so it reads as "nothing here yet", not a broken
   panel, centred icon + heading + one sentence, capped measure. */

/* At 25%-43% data fill, a typical venue page shows several of these in a
   row -- up to six stacked on a data-poor page. The padding scales down on
   narrow viewports (rather than staying at the 1440px reading of
   var(--space-6) on every screen) so six of them read as a compact, distinct
   list of "nothing here yet" notices instead of one long run of identical
   boxes; each box's heading and copy already differ per section, which
   does the rest of the work of making the run feel deliberate. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: clamp(var(--space-4), 6vw, var(--space-6));
  text-align: center;
  background: var(--colour-surface);
  border: 1px dashed var(--colour-rule);
  border-radius: var(--radius-md);
}

.empty-state h2,
.empty-state h3 {
  margin: 0;
  font-size: var(--text-lg);
  color: var(--colour-ink);
}

.empty-state p {
  margin: 0;
  max-width: 24rem;
  font-size: var(--text-sm);
  color: var(--colour-muted);
}

/* ---- Badges and rankings ---------------------------------------------- */

.badge {
  display: inline-block;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--colour-ink);
}

.rank-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Most pills are short ("1,178 venues", "AZA"), but a best-list link's
   label includes the place name verbatim (up to ~48 characters seen in the
   real build, e.g. "Best aquariums in People's Republic of China") -- long
   enough to crowd a 375px viewport. white-space stays default so a long
   pill wraps onto a second line instead of overflowing. */
.rank-banner > * {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 100%;
  padding: var(--space-1) var(--space-3);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-lg);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--colour-ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.rank-banner > :first-child {
  border-color: var(--colour-brand);
  color: var(--colour-brand);
}

/* ---- FAQ ----------------------------------------------------------------
   The frame's "Details - FAQ Item" / "Slot -> Summary" layers are Figma's
   name for exactly the native <details>/<summary> elements, so .faq styles
   those directly: an accordion with no script, that still reads as a full
   question-and-answer list with JavaScript off. */

.faq {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.faq details {
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-sm);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--colour-ink);
  cursor: pointer;
  list-style: none;
}

.faq summary::-webkit-details-marker {
  display: none;
}

.faq summary::after {
  content: "+";
  color: var(--colour-muted);
  font-weight: 400;
}

.faq details[open] summary::after {
  content: "\2212";
}

.faq details p {
  margin: 0;
  padding: 0 var(--space-5) var(--space-4);
  color: var(--colour-muted);
}

/* ---- Map card --------------------------------------------------------------
   6.7 3e's map card. 12's "map view itself" is undesigned, so the anatomy is
   built from the tokens the rest of the entity page uses.

   A fixed height rather than an aspect ratio: the card is replaced in place
   by Leaflet, and a container that resizes as its contents arrive shifts
   every section below it. */
.map-card {
  position: relative;
  height: 20rem;   /* 320px */
  margin-top: var(--sp-16);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-sm);
  background: var(--colour-surface-alt);
  overflow: hidden;
}

/* Both states are present in the markup and hidden by default; map.js swaps a
   class on the card. Nothing is injected, so a reader with no JavaScript sees
   an empty card and the address and link outside it -- never a spinner that
   spins for ever. */
.map-state {
  display: none;
  position: absolute;
  inset: 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-8);
  padding: var(--sp-16);
  text-align: center;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.map-state p {
  margin: 0;
}

.map-card.is-loading .map-state-loading,
.map-card.is-failed .map-state-failed {
  display: flex;
}

/* Leaflet paints into this. It is added by map.js, so it never exists for a
   reader without JavaScript. */
.map-card .map-frame {
  position: absolute;
  inset: 0;
}

/* Leaflet stamps .map-frame with .leaflet-container, whose grey background is
   opaque and sits later in document order than the state layers. Without this
   the failure message renders UNDERNEATH it -- the exact silent grey rectangle
   the failure state exists to replace. */
.map-card.is-failed .map-frame {
  display: none;
}

.map-credit,
.map-open {
  margin: var(--sp-8) 0 0;
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

.map-credit a,
.map-open a {
  color: inherit;
}

/* ---- Section rhythm and copy ------------------------------------------- */

.section {
  display: flex;
  flex-direction: column;
  /* 5.15: "24px gap to content" -- the same figure .band-inner uses, so a
     section reads the same whether it sits in a band or in the page column. */
  gap: var(--sp-24);
  /* Padding on BOTH sides of the rule. It used to be top-only, which meant a
     section's last row sat flush against the next section's border while the
     next section's heading had 32px of air -- the rule looked stuck to the
     content above it rather than set between the two. Symmetric is what makes
     it read as a separator.

     Tightens on narrow screens: ticket prices are 25% filled and opening
     hours 43%, so a typical venue page shows several empty states in a row
     (six on a data-poor page), and this rhythm is what keeps that run from
     reading as one long scroll of identical padding. */
  padding-top: clamp(var(--sp-24), 4vw, var(--sp-40));
  padding-bottom: clamp(var(--sp-24), 4vw, var(--sp-40));
  border-top: 1px solid var(--colour-rule);
}

/* The last section on a page already has the footer beneath it; a second
   40px band of air between the two reads as a gap rather than a rhythm. */
.section:last-child {
  padding-bottom: 0;
}

.section-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  color: var(--colour-ink);
}

/* design.md 5.15: a section header is "heading-3 (or heading-1 on wide
   templates)", an optional subtitle, and a 24px gap to the content. The
   plain .section-title above is the heading-3 default every template has
   been using; .section-title-lg is the heading-1 option, opted into per
   template rather than switched on by page width, because "wide" is the one
   word 5.15 leaves undefined and a silent 36px heading on eight untouched
   templates is not a change worth guessing at. Home takes it. */
.section-title-lg {
  font-size: var(--heading-1-size);
  font-weight: 700;
  line-height: var(--heading-1-leading);
  letter-spacing: var(--heading-1-tracking);
}

/* 5.15's subtitle, in --ink and sitting tight under the title rather than at
   .section's own 16px rhythm.

   Size is `body` (16), not the `body-sm` (14) or `lede` (21) that 5.15 offers
   -- neither matches. Measured off the Home frame against the 36px heading in
   the same image, the subtitle comes out at 16-17px, so 5.15's pair looks
   like it was written from a different section. `body` is the nearer of the
   two candidates and the standard prose size; if the frame turns out to be
   `body-lg` (17) this is a one-token change. */
.section-sub {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-muted);
}

.section-header {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}

/* The Category Hub centres this one header over its country grid. Everywhere
   else a section header is left-aligned against its content, so this is a
   modifier rather than a change to the component. */
.section-header-center {
  align-items: center;
  text-align: center;
}

.section-header-center .section-sub {
  max-width: 44rem;
}

/* design.md 5.15: a section header may carry an "optional right-aligned
   `View all ...` ghost link". The heading and its subtitle stack on the left,
   the link sits opposite them.

   `align-items: end` rather than baseline: the link belongs on the subtitle's
   line, not the heading's, and the left column is two lines of different
   sizes, so a baseline would tie it to the first of them and leave it floating
   beside the 36px heading. Wrapping is what happens on a narrow screen -- the
   link drops under the header rather than squeezing the title. */
.section-header-row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: var(--sp-12) var(--sp-24);
}

.section-header-row .section-header {
  flex: 1 1 20rem;
}

/* 5.1's `ghost` button: no fill, no border, `ui` type (14 / 20 / -0.35,
   display bold), a 6-8px gap to its icon. The doc colours ghost text
   --navy-900, but every frame that draws THIS link draws it in the action
   colour, and 2.3 makes --blue-500 "the text colour of inline links" -- so it
   is a link that reads as a link, not a heading that happens to be clickable.
   Underline is left to hover: at 14px bold on white the blue is already the
   affordance, and an underlined item beside a 36px heading reads as noise. */
.view-all {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  flex: none;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.4286;
  letter-spacing: -0.025em;
  color: var(--colour-link);
  text-decoration: none;
  white-space: nowrap;
}

.view-all:hover,
.view-all:focus-visible {
  text-decoration: underline;
}

/* The arrow is decoration, not content: the link already reads "View all
   countries". Drawn as a pseudo-element so it cannot be selected, copied into
   a quote, or announced as a word. */
.view-all::after {
  content: "\2192";
  font-weight: 400;
}

.lede {
  margin: 0;
  font-size: var(--text-lg);
  color: var(--colour-muted);
}

.note {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--colour-muted);
}

.attribution {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--colour-muted);
}

.attribution a {
  color: var(--colour-link);
}

/* ---- A-Z index -------------------------------------------------------------
   Source frame: "Index", node 12:2640, read the same way as every other
   component. The frame draws a "Browse Directory" hero followed by five
   browse-by sections (category, country, city, animal, feature) this
   pipeline has no data to fill -- see render.py's `_index_page()` docstring
   for the full reasoning. What carries over: the frame's rounded pill-tag
   idiom for the letter jump nav (`.chip-row`/`.chip`, reused as-is, no new
   rule needed), and the page's real job -- a flat list of every venue, not
   another card browse surface. At 1,178+ entries `.card-grid`'s bordered,
   padded boxes would make this the heaviest page on the site for a list
   whose entries are only ever a name and a link. `.index-list` is a CSS
   multi-column list instead: `column-width` reflows exactly the way
   `.card-grid`'s `auto-fill` does -- narrower viewports simply lose columns
   -- with none of `.card-grid`'s per-item markup or paint cost. */

.letter-group + .letter-group {
  margin-top: var(--space-6);
}

.index-list {
  column-width: 16rem;
  column-gap: var(--space-6);
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
}

.index-list li {
  break-inside: avoid;
  padding: var(--space-1) 0;
}

.index-list a {
  color: var(--colour-link);
  text-decoration: none;
}

.index-list a:hover,
.index-list a:focus-visible {
  text-decoration: underline;
}

/* ---- Prose ------------------------------------------------------------
   Task 11 (Guides, About). Neither frame's own body copy survives intact --
   see render.py's `_about_page()` and `_guide_page()` docstrings for why --
   but both frames set their paragraph text the same way the rest of the
   file's body copy already reads: 16px against #555 (--text-base /
   --colour-muted, get_variable_defs returns no tokens for either frame, the
   same as every other screen in this file -- see tokens.css's header). The
   capped width reuses --measure, the same reading-length cap the file
   already defines for prose; line-height has no directly-read value (a
   fixed-width Figma frame carries no reflow), so 1.6 is a judgement call for
   comfortable multi-paragraph reading, the same kind of call --measure
   itself already was. */

/* On the role-named scale from design.md 2.2 rather than the older size-only
   --text-* one, per CLAUDE.md's migration note. Same rendered size (--text-base
   and --body-size are both 1rem); what changes is the leading, from a
   hard-coded 1.6 to the 1.5 the doc specifies, so prose and headings sit on one
   rhythm instead of two. */
.prose {
  max-width: var(--measure);
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-muted);
}

.prose + .prose {
  margin-top: var(--space-4);
}

/* ---- Hero, split ---------------------------------------------------------
   design.md 6.1 section 1 (Figma node 2:245), built to the tokens in 2
   rather than to a reading of the frame by eye. Anatomy the doc specifies,
   and what implements each line:

     --tint bg, 96px padding                 .band-alt + .hero-split padding
     12-col grid, 48px gutter                grid-template-columns / gap
     left cols 1-6, image cols 7-12          grid-column on the two children
     24px hero stack gap (2.4)               .hero-split > div gap
     eyebrow badge, dot variant (5.2)        .hero-eyebrow
     display-1 H1, 60/60/-1.5 (2.2)          .hero-split h1
     lede subtitle, max 576 (2.2)            .hero-split .lede
     radius-lg card, --shadow-md, 389px      .hero-figure
     H1 40/44 under 1024, 32/36 under 768,
       image stacks below content (9)        the two media queries below

   Two pieces of the doc's anatomy are not drawn, and both are refusals
   rather than omissions:

   - The search field (5.14) between the lede and the photo. Search is an
     explicit non-goal of the spec and has no backend; a control that
     discards input reads as a broken site, and a test enforces that no
     template contains one. The slot is not left empty -- the count pills
     take it, which is the same substitution `_covered_venues()` makes for
     the frame's star ratings: keep the shape design.md draws, fill it with
     something this data can stand behind.
   - The glass caption label naming the facility, bottom-left of the image
     card. The photograph is design stock, not a picture of any venue in the
     directory, so a venue name under it would assert something rule 18
     forbids guessing at. See `_home_page()`.

   Both are standing exceptions recorded in CLAUDE.md's Frontend section,
   not work outstanding. */

/* design.md 5.1's `secondary` button as a link: white, 1px border, pill, `ui`
   type. Used for the "show all" under a truncated grid, centred beneath it. */
.pill-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  align-self: center;
  padding: var(--sp-12) var(--sp-24);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--colour-ink);
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.pill-link:hover,
.pill-link:focus-visible {
  border-color: var(--colour-ink);
}

.pill-link:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.pill-link::after {
  content: "\2192";
  font-weight: 400;
}

/* A pill that follows a left-aligned list rather than a full-width listing.
   `.pill-link` centres itself, which is right under a listing and wrong at
   the foot of a list, where it floated alone mid-column. */
.pill-link-start {
  align-self: flex-start;
}

@media (prefers-reduced-motion: reduce) {
  .pill-link {
    transition: none;
  }
}

/* ---- Place hub: filters beside the listing (design.md 6.4 s3 / 6.5) ------
   The frame's two-column body: a filter rail on the left, the listing on the
   right. The rail collapses above the listing below the grid's breakpoint
   rather than beside it, because a 280px column and a card row do not both
   fit on a phone.

   What the rail can and cannot do is the whole story here. Category and
   Location are counts and child hubs this build already has, so they are real
   -- Location's rows are links to those hubs, which is navigation rather than
   filtering and is the honest version of what the frame draws. Features and
   Rating have no data behind them at all: no feature tags in the export, and
   no ratings anywhere by rule 11 and 12.2. Those two are placeholder-boxed.
   None of the four is a checkbox, because a checkbox that filters nothing is
   a dead control. */
.place-layout {
  display: grid;
  grid-template-columns: 17.5rem minmax(0, 1fr);
  gap: var(--sp-32);
  align-items: start;
}

@media (max-width: 63.99em) {
  .place-layout {
    grid-template-columns: minmax(0, 1fr);
  }
}

.filters {
  display: flex;
  flex-direction: column;
  gap: var(--sp-24);
  padding: var(--sp-20);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
}

.filters > h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-ink);
}

.filter-group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}

/* 2.2 label-caps: 12 / 16 / +0.6, uppercase -- the doc's role for exactly this
   kind of small all-caps group heading. Descendant rather than child, because
   a collapsible group nests its heading inside a <summary>. */
.filter-group h3 {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--colour-muted);
}

/* A collapsible filter group. Native <details>, the same no-JavaScript
   accordion `.faq` already uses -- 8 wants no script for this and a rail that
   only opens with JS is a rail that does not open.

   Kept `open` in the markup: the rows are navigation, and a crawler reads them
   either way but a reader should not have to hunt for them. Collapsing is
   there for the long lists -- Europe's Location group runs to fifty. */
.filter-group-toggle > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-8);
  cursor: pointer;
  list-style: none;
}

.filter-group-toggle > summary::-webkit-details-marker {
  display: none;
}

.filter-group-toggle > summary::after {
  content: "\002B";
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  color: var(--colour-muted);
}

.filter-group-toggle[open] > summary::after {
  content: "\2212";
}

.filter-group-toggle > summary:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* <details> is a block, so the group's own flex gap does not reach its open
   contents; the list spaces itself off the summary instead. */
.filter-group-toggle[open] > .filter-list {
  margin-top: var(--sp-8);
}

.filter-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-12);
  padding: var(--sp-4) 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
  text-decoration: none;
}

/* A linked row carries an arrow so the two kinds are told apart before the
   pointer reaches them -- half this rail navigates and half is a count, and
   an underline on hover only reveals that after the fact. */
a.filter-row .filter-count::after {
  content: " \203A";
  color: var(--colour-muted);
}

a.filter-row:hover,
a.filter-row:focus-visible {
  color: var(--colour-brand);
}

a.filter-row:hover .filter-count,
a.filter-row:focus-visible .filter-count,
a.filter-row:hover .filter-count::after,
a.filter-row:focus-visible .filter-count::after {
  color: var(--colour-brand);
}

a.filter-row:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.filter-count {
  flex: none;
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  color: var(--colour-muted);
}

/* The coverage line, under the Features rail and under the lead of every
   facility page. Smaller and quieter than a filter row because it qualifies
   the group rather than belonging to it -- --caption-size and --colour-muted
   are the caption role in tokens.css. --sp-8 rather than the legacy
   --space-2 of the same 8px, because tokens.css says new work reads --sp-*. */
.filter-note {
  margin: var(--sp-8) 0 0;
  font-size: var(--caption-size);
  color: var(--colour-muted);
}

/* The frame's swatch beside each Category row. Decorative: the row's own words
   are its name. */
.filter-swatch {
  flex: none;
  width: var(--sp-12);
  height: var(--sp-12);
  border-radius: var(--radius-xs);
}

.filter-swatch-zoo { background: var(--gradient-zoo); }
.filter-swatch-aquarium { background: var(--gradient-aquarium); }
.filter-swatch-other { background: var(--colour-ink); }

.filter-label {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  min-width: 0;
}

/* `.mock-check` -- a drawn checkbox -- stood here. It dressed the two filter
   groups that had nothing to check: Features, which became a real set of
   links on 2026-09-10, and Rating, which the owner removed on 2026-09-19
   because there are no ratings on this site and none are coming. With both
   gone no template draws a checkbox, real or mock, so the rule goes too. */

/* ---- Place hub: the listing toolbar --------------------------------------
   "Showing all N locations" on the left, view toggle and sort on the right.
   The count is real; neither control is, so they sit in a placeholder box. */
/* The listing beside the rail: toolbar, cards, then the "show more" link,
   which centres under them (.pill-link is align-self: center, and only a flex
   parent honours that). */
.listing-column {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  min-width: 0;
}

.list-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-16);
  padding-bottom: var(--sp-16);
  border-bottom: 1px solid var(--colour-rule);
}

.list-count {
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  color: var(--colour-ink);
}

/* 5.12's segmented List/Map control and its "Sort by: Top rated" label had
   their rules here -- `.mock-segmented`, `.mock-segment`, `.mock-segment-on`
   and `.mock-sort`. Both controls were placeholders and both were removed on
   2026-09-19: there is no map on this site, and the sort named a rating that
   rule 11 with 12.2 keeps off it entirely.

   `.list-toolbar` above stays. It still lays out the venue count, and keeps
   its `space-between` so the count sits where it always has. */

/* ---- Place hub: the listing card (design.md 5.6, horizontal) -------------
   The frame's own orientation for this template: a fixed image panel on the
   left, content on the right. The vertical `.venue-card` on the home page is
   the same anatomy stood up.

   The rating row, the OPEN NOW pill, the "#1 RATED" ribbon and the "0.5km
   from center" line are all drawn on the frame and none is here. They are not
   placeholder-boxed either: a tag around a whole invented section is honest,
   but a fake number sitting inside an otherwise real card, beside a real
   venue's real name, is a different thing entirely. They are simply absent
   until there is something true to put there. */
.listing-cards {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  list-style: none;
  margin: 0;
  padding: 0;
}

.listing-card {
  display: grid;
  grid-template-columns: 14rem minmax(0, 1fr);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  color: inherit;
  text-decoration: none;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}

@media (max-width: 47.99em) {
  .listing-card {
    grid-template-columns: minmax(0, 1fr);
  }
}

.listing-card:hover {
  border-color: var(--colour-ink);
  box-shadow: var(--shadow-xs);
}

.listing-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.listing-card:hover h3,
.listing-card:focus-visible h3 {
  text-decoration: underline;
}

/* The venue's lead photograph, over --tint with 5.6's muted icon for the
   venues that have none. A stock photograph over a named venue would claim to
   be a picture of it, which is why the empty state stays rather than
   borrowing one. `position: relative` is the ribbon's containing block, and
   now the photograph's too. */
.listing-photo {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Stretches to the card, whose height the CONTENT sets. A fixed aspect here
     made every card as tall as a 4:3 panel 224px wide and left a hole under
     the short ones. 5.6 says card heights vary with content on purpose. */
  min-height: 9rem;
  background: var(--colour-surface-alt);
  border-right: 1px solid var(--colour-rule);
  color: var(--colour-muted);
  /* Clips what `object-fit: cover` crops off the photograph. */
  overflow: hidden;
}

@media (max-width: 47.99em) {
  .listing-photo {
    min-height: 7rem;
    border-right: 0;
    border-bottom: 1px solid var(--colour-rule);
  }
}

.listing-photo svg {
  display: block;
  width: var(--sp-32);
  height: var(--sp-32);
  opacity: 0.55;
}

/* Absolute, not a flex item, and that is the whole point. This panel has no
   fixed aspect ratio on purpose -- the comment above records why -- so the
   card's height comes from its body column. An in-flow image would put a
   Commons photograph's own intrinsic height into that calculation and every
   card would go back to being the shape of its picture, which is exactly what
   the fixed aspect ratio was removed to stop. Taking it out of flow lets it
   fill whatever height the body sets, at any card height, with no floor of
   its own beyond the panel's `min-height`. `cover` rather than `contain`
   because Commons photographs arrive at every aspect ratio and a letterboxed
   panel reads as a broken one; the stored derivative is 600px, wider than the
   14rem column, so cover crops rather than upscales. */
.listing-photo img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The frame's corner ribbon, carrying the one thing this build knows for
   certain about a venue: which kind it is. */
.listing-ribbon {
  position: absolute;
  top: var(--sp-12);
  left: var(--sp-12);
  padding: var(--sp-4) var(--sp-8);
  background: var(--colour-surface);
  border-radius: var(--radius-xs);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--colour-ink);
}

.listing-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  padding: var(--sp-20);
}

.listing-card h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-ink);
}

.listing-where {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

.listing-where svg {
  flex: none;
  width: 0.875rem;
  height: 0.875rem;
}

.listing-card .chip-row {
  padding-top: var(--sp-4);
}

@media (prefers-reduced-motion: reduce) {
  .listing-card {
    transition: none;
  }
}

/* ---- Best-of listicle (design.md 6.6) ------------------------------------
   The frame's hero: eyebrow, display-1 headline, and a "How we rank" callout.
   48/40 padding per 6.6 s2. */
.best-hero {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--sp-24);
  padding: var(--sp-48) 0 var(--sp-40);
  border-bottom: 0;
}

.best-hero h1 {
  max-width: 48rem;
  font-family: var(--font-display);
  font-size: var(--display-1-size);
  font-weight: 700;
  line-height: var(--display-1-leading);
  letter-spacing: var(--display-1-tracking);
  color: var(--colour-ink);
}

@media (max-width: 63.99em) {
  .best-hero h1 {
    font-size: 2.5rem;
    line-height: 1.1;
  }
}

@media (max-width: 47.99em) {
  .best-hero h1 {
    font-size: 2rem;
    line-height: 1.125;
  }
}

/* 6.6 s2's callout: --tint, 1px border, 16px padding, a 40px circular white
   icon and a bold-lead paragraph.

   The box spans the full container, same as the .jumpto card directly below
   it and the .ranked-item cards below that. It used to stop at
   --container-prose, which left its right edge short of every other edge on
   the page -- and unlike the .methodology note, which is centred and so reads
   as a deliberately narrow column, this one is left-aligned, so the short
   edge read as a mistake. The reading measure moves onto the paragraph, which
   is what actually needs capping. */
/* .best-hero is a column flex with `align-items: start`, so its children size
   to their content rather than stretching. The wrapper has to opt back in for
   the box to reach the container edge. */
.howto-box {
  align-self: stretch;
}

.howto {
  display: flex;
  align-items: start;
  gap: var(--sp-16);
  padding: var(--sp-16);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
}

/* Tagged as a placeholder (see best_list.html), so it takes the same dashed
   red edge every other tagged region has. Applied from the wrapper rather
   than by adding `.placeholder-body` to the box, because that class carries an
   aria-hidden requirement this one region deliberately does not meet. */
.placeholder > .howto {
  outline: 1px dashed var(--colour-placeholder);
  outline-offset: var(--sp-8);
}

.howto-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--sp-40);
  height: var(--sp-40);
  border-radius: var(--radius-full);
  background: var(--colour-surface);
  color: var(--colour-brand);
}

.howto-icon svg {
  display: block;
  width: var(--sp-20);
  height: var(--sp-20);
}

.howto p {
  max-width: var(--container-prose);
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.howto strong {
  color: var(--colour-ink);
}

/* 5.16's jump-link block, built from Figma node 5:73 ("Table of Contents
   (Jump Links)") rather than by eye. Every value below is that node's, with
   its four raw hexes mapped onto the tokens they already equal: #e9e9e9 is
   --colour-rule, #f7f8fd is --colour-surface-alt, #1d2a62 is --colour-ink and
   #555 is --colour-muted.

   The node pads 25px, which per design.md 0 is the 24px step with a 1px
   border counted in. Real anchors, so it works with no script and survives a
   reload. */
.jumpto {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  padding: var(--sp-24);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-xs);
}

/* The node's heading row: an 18px icon box, 8px gap, and the `ui` type role
   (14 / 20 / -0.35, display bold). */
.jumpto > h2 {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--ui-size);
  font-weight: 700;
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  color: var(--colour-ink);
}

/* Outer box and leaf are sized separately, as the node has them: an 18px
   container holding a 14.625 x 11.625 glyph. */
.jumpto-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.125rem;   /* 18px */
  height: 1.125rem;
}

.jumpto-icon svg {
  display: block;
  width: 14.625px;
  height: 11.625px;
}

/* Three columns, 24px apart across and 12px down -- the node's own grid. */
.jumpto > ol {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-12) var(--sp-24);
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (max-width: 63.99em) {
  .jumpto > ol {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 47.99em) {
  .jumpto > ol {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The label is --colour-muted body-sm on the node, not link blue: the number
   badge beside it is what marks the row, and a column of blue would fight the
   ranked list below. The underline arrives on hover. */
.jumpto a {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
  text-decoration: none;
}

.jumpto a:hover,
.jumpto a:focus-visible {
  color: var(--colour-ink);
  text-decoration: underline;
}

.jumpto a:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* A 20px circle of --tint carrying a 12px bold navy numeral, centred. */
.jumpto-num {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--sp-20);
  height: var(--sp-20);
  border-radius: var(--radius-full);
  background: var(--colour-surface-alt);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  color: var(--colour-ink);
}

/* ---- Best-of listicle: the ranked article (design.md 5.7) ----------------
   White, 1px border, --shadow-xs, 24px padding, 32px gap, no radius, with the
   number badge overlapping its top-left corner.

   The badge is a POSITION, not a place. This list is alphabetical -- see the
   .howto callout the hero carries and the methodology note at the foot -- and
   the frame's gold "#1" treatment is deliberately not reproduced, because it
   dramatises a ranking this build does not have. */
.ranked-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-32);
  list-style: none;
  margin: var(--sp-16) 0 0;
  padding: 0;
}

.ranked-item {
  position: relative;
  display: grid;
  grid-template-columns: 16rem minmax(0, 1fr);
  gap: var(--sp-32);
  padding: var(--sp-24);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-xs);
  /* The badge hangs outside the top-left corner. */
  margin-left: var(--sp-16);
}

@media (max-width: 63.99em) {
  .ranked-item {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-16);
  }
}

/* 5.5: a circle, --blue-500, 3px --paper border, --shadow-xs, hung at
   top:-16px left:-16px of the article. */
.rank-badge {
  position: absolute;
  top: calc(var(--sp-16) * -1);
  left: calc(var(--sp-16) * -1);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-48);
  height: var(--sp-48);
  border-radius: var(--radius-full);
  background: var(--colour-brand);
  border: 3px solid var(--colour-surface);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--colour-surface);
}

.ranked-figure {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
  color: var(--colour-muted);
  /* Holds the 4:3 panel once a real photograph is inside it. A Commons
     photograph's intrinsic height is whatever the photographer's camera
     produced, and `.ranked-item` is a grid whose row height is its tallest
     cell -- so without this the panel becomes the shape of its own picture
     and drags the card's height with it. Same fix, same reason, as
     `.venue-card-photo`. */
  overflow: hidden;
}

.ranked-figure svg {
  display: block;
  width: var(--sp-32);
  height: var(--sp-32);
  opacity: 0.55;
}

/* `object-fit: cover` rather than `contain`, as on `.venue-card-photo img`:
   Commons photographs arrive at every aspect ratio and a letterboxed panel
   reads as a broken one. The stored derivative is 600px, wider than the 16rem
   column this panel occupies, so cover crops rather than upscales. */
.ranked-figure img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  object-fit: cover;
}

.ranked-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
}

/* 2.2 heading-2: 30 / 36 / -0.75, display bold -- 5.7's ranked-item title. */
.ranked-body h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-2-size);
  font-weight: 700;
  line-height: var(--heading-2-leading);
  letter-spacing: var(--heading-2-tracking);
}

.ranked-body h3 a {
  color: var(--colour-ink);
  text-decoration: none;
}

.ranked-body h3 a:hover,
.ranked-body h3 a:focus-visible {
  text-decoration: underline;
}

/* 5.7's body copy: `body` (16 / 24), unclamped -- the doc calls this
   editorial and says explicitly not to truncate it. On a measure, because it
   is prose and the card is wide. */
.ranked-prose {
  margin: 0;
  max-width: var(--measure);
  font-family: var(--font-body);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-muted);
}

/* 5.7 closes each article with a 1px rule and 16px of padding above its
   actions. */
.ranked-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-16);
  margin-top: auto;
  padding-top: var(--sp-16);
  border-top: 1px solid var(--colour-rule);
}

/* 6.6 s5's closing note, centred under the list. */
.methodology {
  align-items: center;
  text-align: center;
}

.methodology p {
  max-width: var(--container-prose);
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* ---- Place hub: regions beside major hubs (design.md 6.4 s4) ------------
   Two lists side by side: the place's own child regions, and the cities under
   it with the most listings. Both are real -- the place tree already holds
   them -- and both are 7.5's "vertical" linking, which is how a country hub
   pushes equity down to the long tail. */
.split-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--sp-48);
  align-items: start;
}

.split-column {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
}

/* 2.2 heading-3, with the frame's small leading icon. */
.split-column > h2 {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-3-size);
  font-weight: 700;
  line-height: var(--heading-3-leading);
  letter-spacing: var(--heading-3-tracking);
  color: var(--colour-ink);
}

.split-column > h2 svg {
  flex: none;
  width: var(--sp-20);
  height: var(--sp-20);
  color: var(--colour-brand);
}

.region-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: var(--sp-12);
  list-style: none;
  margin: 0;
  padding: 0;
}

.region-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-12);
  height: 100%;
  padding: var(--sp-12) var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.region-card:hover,
.region-card:focus-visible {
  border-color: var(--colour-ink);
}

.region-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* The frame's "{Type} in Nearby Cities" cards: a bold place name over a muted
   "Top N ..." line, two across. Distinct from .region-card, which is a single
   row with its count pushed to the right -- this one stacks. */
.nearby-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: var(--sp-12);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nearby-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  height: 100%;
  padding: var(--sp-12) var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.nearby-card:hover,
.nearby-card:focus-visible {
  border-color: var(--colour-ink);
}

.nearby-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.nearby-card span {
  font-family: var(--font-body);
  font-size: var(--caption-size);
  font-weight: 400;
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

@media (prefers-reduced-motion: reduce) {
  .nearby-card {
    transition: none;
  }
}

.hub-rows {
  display: flex;
  flex-direction: column;
  /* Matches .region-grid's gap: the two sit side by side in .split-columns
     and a 4px difference between them reads as one column being cramped. */
  gap: var(--sp-12);
  list-style: none;
  margin: 0;
  padding: 0;
}

.hub-row {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  padding: var(--sp-12) var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  color: inherit;
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.hub-row:hover,
.hub-row:focus-visible {
  border-color: var(--colour-ink);
}

.hub-row:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* The frame's round icon badge on each city row. */
.hub-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--sp-32);
  height: var(--sp-32);
  border-radius: var(--radius-full);
  background: var(--colour-surface-alt);
  color: var(--colour-brand);
}

.hub-icon svg {
  display: block;
  width: var(--sp-16);
  height: var(--sp-16);
}

.hub-text {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
}

.hub-count {
  font-family: var(--font-body);
  font-size: var(--caption-size);
  font-weight: 400;
  color: var(--colour-muted);
}

.hub-row::after {
  content: "\203A";
  flex: none;
  font-size: var(--text-lg);
  color: var(--colour-muted);
}

/* ---- Place hub: the best-of promo (design.md 6.4, frame 2:1078 sibling) --
   The frame's ranked-list banner. Unlike the Category Hub's, this one points
   at a page that exists: `model.best_lists()` generates a ranked list per
   country and per type above its threshold, which is exactly what this links
   to. The frame's "OFFICIAL RANKING 2024" eyebrow and its "millions of
   reviews" copy do not survive -- there is no ranking authority here and no
   reviews at all -- so the eyebrow states what the list is and the copy says
   how it is ordered. */
.promo {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: var(--sp-32);
  align-items: center;
  padding: var(--sp-32);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-xs);
}

.promo-copy {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--sp-12);
}

.promo-copy h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-2-size);
  font-weight: 700;
  line-height: var(--heading-2-leading);
  letter-spacing: var(--heading-2-tracking);
  color: var(--colour-ink);
}

.promo-copy p {
  margin: 0;
  max-width: var(--measure);
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* 5.1's `primary`: --blue-500 fill, white `ui` text, pill. */
.promo-button {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-12) var(--sp-24);
  background: var(--colour-brand);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--colour-surface);
  text-decoration: none;
}

.promo-button::after {
  content: "\2192";
  font-weight: 400;
}

.promo-button:focus-visible {
  outline: 2px solid var(--colour-ink);
  outline-offset: 2px;
}

.promo-figure {
  margin: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
}

.promo-figure img {
  display: block;
  width: 100%;
  height: 12rem;
  object-fit: cover;
}

/* ---- Place hub: sibling places (design.md 6.4, "Explore X & Nearby") -----
   7.5's lateral linking: the places alongside this one under the same parent.
   The frame prefixes each with a country code, which this build has on a
   country place and nowhere else, so the badge renders only where it is
   true. */
.sibling-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: var(--sp-12);
  list-style: none;
  margin: 0;
  padding: 0;
}

.sibling-card {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  height: 100%;
  padding: var(--sp-12) var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  color: var(--colour-ink);
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.sibling-card:hover,
.sibling-card:focus-visible {
  border-color: var(--colour-ink);
}

.sibling-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.sibling-code {
  flex: none;
  padding: var(--sp-4) var(--sp-8);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-xs);
  font-size: var(--label-size);
  letter-spacing: 0.06em;
  color: var(--colour-muted);
}

@media (prefers-reduced-motion: reduce) {
  .region-card,
  .hub-row,
  .sibling-card {
    transition: none;
  }
}

/* ---- Hero, photo (design.md 6.4 s2) --------------------------------------
   The Country Hub's hero: a full-bleed photograph of the place, veiled to
   near-white so navy type sits on it at full contrast, with the copy in the
   usual container on top.

   The photograph is an <img> rather than a `background-image`, for the same
   reasons 11.5 gives for every other image on this site: it can be lazy or
   eager per position, it can carry intrinsic dimensions, and it is not a
   dynamic value smuggled into a style attribute. It is absolutely positioned
   and covers, so it contributes no height -- the copy sets that.

   The veil is a sibling element rather than a ::before so it sits in a
   predictable place in the stacking order whether or not there is a
   photograph behind it. Where there is none, the band falls back to
   --colour-surface-alt like any other tinted band, and the veil over nothing
   is invisible. */
.hero-photo {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.hero-photo-img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-photo-veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--colour-hero-veil);
  backdrop-filter: blur(1px);
}

/* 6.4 s2: "80/96 padding", H1 capped at 768 and the lede at 672. A one-column
   hero -- the photograph is the second column's job here. */
.hero-country {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--sp-24);
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-80) var(--container-pad) var(--sp-96);
  border-bottom: 0;
}

.hero-country h1 {
  max-width: 48rem;  /* 768px */
  font-family: var(--font-display);
  font-size: var(--display-1-size);
  font-weight: 700;
  line-height: var(--display-1-leading);
  letter-spacing: var(--display-1-tracking);
  color: var(--colour-ink);
}

.hero-country .lede {
  max-width: 42rem;  /* 672px */
  font-family: var(--font-body);
  font-size: var(--lede-size);
  line-height: var(--lede-leading);
}

/* 9, under 1024 / under 768: the same H1 steps the home hero takes. */
@media (max-width: 63.99em) {
  .hero-country {
    padding-top: var(--sp-64);
    padding-bottom: var(--sp-64);
  }

  .hero-country h1 {
    font-size: 2.5rem;
    line-height: 1.1;
  }
}

@media (max-width: 47.99em) {
  .hero-country {
    padding-top: var(--sp-48);
    padding-bottom: var(--sp-48);
  }

  .hero-country h1 {
    font-size: 2rem;
    line-height: 1.125;
  }
}

/* ---- Article split (design.md 6.3 s6) ------------------------------------
   The Category Hub's editorial block: two prose paragraphs over a photograph.

   It was drawn beside the photograph, which worked while the copy was filler
   at the frame's length. Once real editorial landed -- ~390 words per hub --
   the column ran far past a vertically centred figure, so the picture floated
   in the middle of a wall of text with white space above it. Owner's call
   (2026-09-08) was to set the prose in TWO COLUMNS and put the picture full
   width underneath, which is what this now does.

   `.article-split` is therefore a plain vertical stack; the two-up grid moved
   inside `.article-copy`, where the paragraphs are the tracks. Same auto-fit
   idiom as before, so it still collapses to one column on a narrow screen
   without a media query -- and a hub that ever carries one paragraph, or
   three, still lays out sensibly rather than assuming exactly two. */
.article-split {
  display: grid;
  gap: var(--sp-32);
}

/* MULTI-COLUMN, not a two-track grid. A grid lays its items out across the
   row first: with two paragraphs that read left-then-right, which was fine,
   but the owner's rewrite broke the piece into six headed sections and a grid
   would have ordered them 1-2 / 3-4 / 5-6 across the band -- the reader
   zig-zagging instead of reading down. `columns` flows the whole piece down
   the first column and continues at the top of the second, which is what a
   two-column article actually means.

   The heading spans both columns. Every section is a block that may not be
   split, so a subheading can never sit at the foot of one column with its
   paragraph beginning in the next. */
.article-copy {
  columns: 2;
  column-gap: var(--sp-48);
}

.article-copy h2 {
  column-span: all;
  margin-bottom: var(--sp-24);
}

.article-block {
  break-inside: avoid;
}

.article-block + .article-block {
  margin-top: var(--sp-24);
}

/* 2.2 body-sm, bold and in ink rather than muted: these are the owner's
   section headers and their whole job is to be findable when the eye is
   scanning the band, so they take the weight and the darker colour while
   staying on the body face at body size. Not a --heading-* role -- those are
   the display face, and a display subhead every 80 words would fight the h2
   above it for the section's one heading slot. */
.article-block h3 {
  margin: 0 0 var(--sp-8);
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  font-weight: 700;
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
}

.article-block p + p {
  margin-top: var(--sp-16);
}

/* Single column below the breakpoint the rest of this file stacks at. */
@media (max-width: 47.99em) {
  .article-copy {
    columns: 1;
  }
}

/* 2.2 heading-3: 24 / 32 / -0.6, display bold. */
.article-copy h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-3-size);
  font-weight: 700;
  line-height: var(--heading-3-leading);
  letter-spacing: var(--heading-3-tracking);
  color: var(--colour-ink);
}

/* 2.2 body-sm. No --measure cap in the two-column layout: the TRACK is the
   measure now (~640px at the 1360 container, ~91ch), and capping the text at
   65ch inside a wider track is what left slack at the end of both columns
   instead of the band reading as full. The cap comes back below, where one
   column spans the whole container and nothing else is holding the line
   length down. */
.article-copy p {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

@media (max-width: 47.99em) {
  .article-copy p {
    max-width: var(--measure);
  }
}

.article-figure {
  margin: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}

/* Full width under the prose now, so the old 15rem would letterbox a 3:2
   photograph to nearly 5:1 across the container. Clamped instead: it grows
   with the viewport and stops before the picture starts competing with the
   text it is illustrating. */
.article-figure img {
  display: block;
  width: 100%;
  height: clamp(12rem, 24vw, 20rem);
  object-fit: cover;
}

/* ---- Placeholders --------------------------------------------------------
   Sections design.md draws that this build cannot fill yet, rendered so the
   page can be reviewed at full height, and tagged so nobody mistakes the
   filler for data.

   Every placeholder is `aria-hidden` and contains no `<input>`, `<form>` or
   `<button>`. That is the line this component walks: the project's rule is
   that a control with no backend is worse than no control, and a mock search
   field that could be clicked, focused and submitted would still be a dead
   control however it was labelled. What is drawn instead is a picture of a
   control -- spans, styled to look right, reachable by nobody. A sighted
   reviewer sees the layout; a keyboard or screen-reader user is not offered
   something that does not work.

   The red is --colour-placeholder, which is not in design.md's palette on
   purpose. See tokens.css. */
.placeholder {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
}

/* The tag itself: red, uppercase, and bordered so it reads as an annotation
   stuck onto the page rather than a badge belonging to the design. */
/* A placeholder whose contents belong on one line -- the listing toolbar's
   view toggle and sort sit beside each other, not stacked. */
.placeholder-inline {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-12);
}

.placeholder-tag {
  display: inline-flex;
  align-self: start;
  align-items: center;
  gap: var(--sp-4);
  margin: 0;
  padding: var(--sp-4) var(--sp-8);
  background: var(--colour-surface);
  border: 1px dashed var(--colour-placeholder);
  border-radius: var(--radius-xs);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-placeholder);
}

/* A dashed rule around whatever the tag is annotating, so the extent of the
   fake region is obvious at a glance and not just its top-left corner. */
.placeholder-body {
  outline: 1px dashed var(--colour-placeholder);
  outline-offset: var(--sp-8);
}

/* ---- Placeholder: rankings banner (design.md 6.3 s4) ---------------------
   The frame's full-width gradient promo. Reuses --gradient-aquarium: it is
   the design's own blue-to-violet wash and the banner is drawn in the same
   family. */
.mock-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-24);
  padding: var(--sp-40);
  background: var(--gradient-aquarium);
  color: var(--colour-surface);
}

.mock-banner-copy {
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
  max-width: 34rem;
}

.mock-banner h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-1-size);
  font-weight: 700;
  line-height: var(--heading-1-leading);
  letter-spacing: var(--heading-1-tracking);
  color: inherit;
}

.mock-banner p {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-on-dark-body);
}

/* 5.1's `primary` button, drawn on a dark fill: pill, white, `ui` type. */
.mock-button {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: var(--sp-12) var(--sp-24);
  background: var(--colour-surface);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--colour-brand);
}

/* The eyebrow badge on the banner, 5.2's `glass` variant. */
.mock-eyebrow {
  display: inline-flex;
  align-self: start;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-4) var(--sp-12);
  background: var(--colour-on-dark-badge);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colour-surface);
}

/* ---- The hero photo's caption chip (design.md 6.1 s1) --------------------
   5.2's `glass` variant, bottom-left of the hero image card. This was filler
   while the photograph was stock -- naming a venue under a picture of no
   venue asserts something untrue. The Home hero now shows a venue that is in
   the directory, so the chip carries that venue's name and links to it. */
.hero-caption {
  position: absolute;
  left: var(--sp-16);
  bottom: var(--sp-16);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-8) var(--sp-12);
  background: var(--colour-on-dark-badge);
  backdrop-filter: blur(6px);
  border-radius: var(--radius-xs);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  color: var(--colour-surface);
}

.hero-caption svg {
  display: block;
  width: 0.875rem;
  height: 0.875rem;
}

/* The chip became a link when the photograph became a real venue's. It keeps
   the badge's own colour rather than the link palette -- it sits on top of a
   photograph, where --colour-link carries no contrast guarantee -- so the
   underline on hover and focus is what says it is clickable. */
.hero-caption {
  text-decoration: none;
}

.hero-caption:hover,
.hero-caption:focus-visible {
  text-decoration: underline;
}

/* ---- Bands ---------------------------------------------------------------
   The home page is a stack of full-bleed bands that alternate white and
   --tint down the page. The colour change IS the separator: there is no rule
   between two bands, because a hairline plus a fill change reads as two
   separators doing one job. .section's `border-top` is what every other
   template uses instead, and those templates are unchanged.

   A band is full width and its contents stop at --container, which is the
   same shape .site-header and .site-footer already have. It has to be a real
   element: the two full-bleed idioms available to a child of `.page` are
   both worse -- `width: 100vw` counts the scrollbar this page always has, so
   the band overflows by the scrollbar's width and puts a horizontal
   scrollbar on every screen; a `0 0 0 100vmax` spread shadow lays out
   correctly but rasterises over the hero's own photograph in Chrome, which
   measures right and screenshots blank. So home.html overrides base.html's
   `body` block and owns its own layout -- see the note there.

   Which band is tinted is set per section in the markup rather than by
   :nth-of-type, so a section that renders conditionally cannot silently
   invert the whole page below it. */
.band {
  background: var(--colour-surface);
}

.band-alt {
  background: var(--colour-surface-alt);
}

.band-inner {
  display: flex;
  flex-direction: column;
  /* 5.15: "24px gap to content". */
  gap: var(--sp-24);
  max-width: var(--container);
  margin: 0 auto;
  /* 2.4 section vertical padding. The doc gives 96 for the Home hero and the
     content bands are drawn on the same rhythm. */
  padding: var(--sp-96) var(--container-pad);
}

/* 96px of band above and below is a third of a phone screen. Steps down with
   the hero's own padding, on the same 9 breakpoints. Not in the doc, which
   stops at the hero -- a judgement call, flagged per CLAUDE.md. */
@media (max-width: 63.99em) {
  .band-inner {
    padding-top: var(--sp-64);
    padding-bottom: var(--sp-64);
  }
}

@media (max-width: 47.99em) {
  .band-inner {
    padding-top: var(--sp-48);
    padding-bottom: var(--sp-48);
  }
}

.hero-split {
  display: grid;
  /* 2.7: "Home hero: 12 columns, 48px gutter, content 1/7, image 7/13". */
  grid-template-columns: repeat(12, 1fr);
  gap: var(--sp-48);
  align-items: center;
  max-width: var(--container);
  margin: 0 auto;
  /* 2.4 "Section vertical padding: 96 (Home hero)"; 2.7 "Inner horizontal
     padding inside every container: 16px". Also cancels `.hero`'s page-title
     rule -- on this template the band is the separator, and a rule under it
     would draw the same line twice. */
  padding: var(--sp-96) var(--container-pad);
  border-bottom: 0;
}

.hero-split > div {
  grid-column: 1 / 7;
  display: flex;
  flex-direction: column;
  /* 2.4 stack gaps: "24 (hero stack)". */
  gap: var(--sp-24);
  /* Without this the eyebrow pill stretches the full column width: a flex
     column stretches its children by default, and the pill's own
     inline-flex only governs what is inside it. */
  align-items: start;
}

.hero-figure {
  grid-column: 7 / 13;
  margin: 0;
  /* The frame captions this photograph, bottom-left inside the card. That
     caption is `.hero-caption`, and it positions against this box. */
  position: relative;
  /* 2.5: marketing surfaces are rounded -- the Home hero image card is the
     one place --radius-lg (20px) is used. 2.6 gives it --shadow-md, the only
     use of that shadow in the system. */
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}

/* 6.1: the card is 389px tall, which is a crop of whatever photograph it
   holds rather than that photograph's own aspect. `cover` is what makes the
   fixed height a crop instead of a squash; the intrinsic width/height stay
   on the <img> so the box is still reserved before the file arrives
   (11.5, CLS). */
.hero-figure img {
  display: block;
  width: 100%;
  height: 389px;
  object-fit: cover;
}

/* 2.2 display-1: 60 / 60 / -1.5, display bold. No measure of its own: the
   doc describes a "4-line H1" for its own wording, and what produces that
   stack is the six-column track this sits in, not a cap on the heading. A
   different headline wraps to a different line count in the same column,
   which is the behaviour the grid is there to give. */
.hero-split h1 {
  font-family: var(--font-display);
  font-size: var(--display-1-size);
  font-weight: 700;
  line-height: var(--display-1-leading);
  letter-spacing: var(--display-1-tracking);
  color: var(--colour-ink);
}

/* 2.2 lede: 21 / 31.5, body regular. 6.1: "lede subtitle (max 576)". */
.hero-split .lede {
  max-width: 36rem; /* 576px */
  font-family: var(--font-body);
  font-size: var(--lede-size);
  line-height: var(--lede-leading);
}

/* 5.2 eyebrow badge, `dot` variant: pill, 12/4 padding, `label` uppercase
   --navy-900, white bg, 1px border, --shadow-xs, plus an 8px --green-400
   dot. Marked up as a paragraph rather than a heading -- it labels nothing
   and outranks nothing, it is the hero's own kicker. */
.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  margin: 0;
  padding: var(--sp-4) var(--sp-12);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--colour-ink);
}

/* Decorative, so it is a pseudo-element: an empty <span> for it would be one
   more thing a screen reader walks past for no reason. 2.5 puts circles on
   --radius-full, not --radius-pill. */
.hero-eyebrow::before {
  content: "";
  flex: none;
  width: var(--sp-8);
  height: var(--sp-8);
  border-radius: var(--radius-full);
  background: var(--colour-accent);
}

/* The count pills standing in for the search field. They are the eyebrow
   badge's dot-less sibling -- 5.2's white-on-tint pill treatment and the
   `label` type role -- rather than .rank-banner's default fill, which is
   --colour-surface-alt and therefore invisible against the band. */
.hero-split .rank-banner {
  gap: var(--sp-8);
}

.hero-split .rank-banner > * {
  padding: var(--sp-4) var(--sp-12);
  background: var(--colour-surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-display);
  font-size: var(--label-size);
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
}

/* 9, under 1024: "image card stacks below content; H1 to 40/44". The grid
   collapses to one column, so both children span it. */
@media (max-width: 63.99em) {
  .hero-split {
    grid-template-columns: 1fr;
    gap: var(--sp-32);
    padding-top: var(--sp-64);
    padding-bottom: var(--sp-64);
  }

  .hero-split > div,
  .hero-figure {
    grid-column: 1 / -1;
  }

  .hero-split h1 {
    font-size: 2.5rem; /* 40px */
    line-height: 1.1;  /* 44 / 40 */
  }

  /* Not in 9, which stops at the H1 and the stacking. A stacked card still
     389px tall is most of a phone screen before the fold, so it steps down
     with the type. A judgement call, flagged per CLAUDE.md. */
  .hero-figure img {
    height: 20rem; /* 320px */
  }
}

/* 9, under 768: "H1 to 32/36". */
@media (max-width: 47.99em) {
  .hero-split {
    padding-top: var(--sp-48);
    padding-bottom: var(--sp-48);
  }

  .hero-split h1 {
    font-size: 2rem;    /* 32px */
    line-height: 1.125; /* 36 / 32 */
  }

  .hero-figure img {
    height: 15rem; /* 240px */
  }
}

/* ---- Category tiles ------------------------------------------------------
   design.md 6.1 section 2 (Figma node 2:281): a section header over three
   flat-filled cards, each carrying a circular icon badge, a title and one
   line of copy. The fills are the frame's own, sampled into --gradient-zoo
   and --gradient-aquarium.

   Two things about this row are not the design, and both are the data
   rather than a shortcut:

   - The frame draws THREE cards -- "Marine Aquariums", "Safari Parks",
     "Insectariums". This build has two categories that resolve to a page,
     /zoos/ and /aquariums/, and no insectarium category exists at all. A
     third card would link nowhere and the two names are narrower than what
     they would point at: /aquariums/ is every public aquarium, not only the
     marine ones, and /zoos/ is every zoo, of which safari parks are a
     subset. So the treatment is the design and the names stay the data's.
   - The frame's cards carry no count. This one does, on 3's "counts are
     always live and always visible" -- it is a true number and the row is a
     directory, not an advert.

   Each card is the <a> itself rather than a panel containing one, so the
   whole rectangle is the target. */

.cat-tiles {
  display: grid;
  /* auto-FIT: the two real categories fill the row between them rather than
     holding a third track open for a card that does not exist. The frame
     draws three across, so each of ours is wider than a frame card -- that
     is the deliberate trade, a row that looks finished over one that keeps
     the frame's card proportions and a visible hole.
     min() so the last remaining track can collapse below 16rem on a narrow
     phone instead of overflowing -- same reason as .hero-split. */
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--sp-32);
  list-style: none;
  margin: 0;
  padding: 0;
}

.cat-tile {
  display: flex;
  flex-direction: column;
  /* The frame's cards are deliberately taller than their content and stack
     it against the bottom edge, which is what gives the row its poster-like
     weight. 17rem is measured off the frame; 2.4's ladder has no step
     between 16rem and 20rem, so it is written as a length and flagged. */
  justify-content: flex-end;
  gap: var(--sp-20);
  min-height: 17rem;
  padding: var(--sp-24);
  /* 2.5: "Directory content is square; marketing content is rounded." These
     sit on a marketing page but they are the directory's own front door and
     the frame draws them with sharp corners, so: square. */
  border-radius: var(--radius-none);
  color: var(--colour-surface);
  text-decoration: none;
}

.cat-tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  /* 48px, measured off the frame against the 36px section heading in the
     same image. 5.1's `icon` button is 40 -- this is not that control, and
     the frame draws the badge larger. */
  width: var(--sp-48);
  height: var(--sp-48);
  border-radius: var(--radius-full);
  background: var(--colour-on-dark-badge);
}

/* The glyphs are invented. The frame draws an icon in this badge on all
   three cards but the file carries no icon set, and neither 5 nor the
   appendix names one -- so these are two plain shapes that say "zoo" and
   "aquarium" without pretending to be a system. They are decorative: the
   card's own title is the accessible name, and the <span> around each is
   aria-hidden. */
.cat-tile-icon svg {
  display: block;
  width: var(--sp-24);
  height: var(--sp-24);
}

.cat-tile-text {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}

/* 2.2 heading-3: 24 / 32 / -0.6, display bold. */
.cat-tile h3,
.cat-tile-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-3-size);
  font-weight: 700;
  line-height: var(--heading-3-leading);
  letter-spacing: var(--heading-3-tracking);
  color: inherit;
}

/* The placeholder card's description line, at 2.2 body-sm like the real
   card's would be. */
.venue-card-blurb {
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* 2.2 body-sm: 14 / 20, body regular. */
.cat-tile p {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-on-dark-body);
}

/* 2.2 label, on the count: display bold, so a number reads as a number
   rather than as a third line of prose. */
.cat-tile-count {
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  color: var(--colour-on-dark-muted);
}

/* 8: hover raises a shadow, and nothing scales -- a transform on a card in
   a grid reflows its neighbours. 10 requires a focus-visible ring on every
   interactive element, which the Figma has nowhere in it. */
.cat-tile {
  transition: box-shadow 150ms ease-out;
}

.cat-tile:hover {
  box-shadow: var(--shadow-md);
}

.cat-tile:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.cat-tile:hover h3,
.cat-tile:focus-visible h3 {
  text-decoration: underline;
}

@media (prefers-reduced-motion: reduce) {
  .cat-tile,
  .venue-card {
    transition: none;
  }
}

.cat-tile-zoo { background: var(--gradient-zoo); }
.cat-tile-aquarium { background: var(--gradient-aquarium); }

/* A tile carrying its category's photograph, framed by the gradient: the
   tile's padding is the frame. Owner's request (2026-09-17); design.md draws
   these tiles without photographs, so the layout below is a judgement call.
   Narrow: the photograph sits above the copy. Wide enough for both (the
   container query, measured on the <li> since a tile cannot query itself):
   copy on the left, photograph filling the right half. 34rem is roughly where
   each half still clears the copy's longest line. */
.cat-tiles > li {
  container-type: inline-size;
}

.cat-tile-photo {
  display: block;
  order: -1;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-none);
}

.cat-tile-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@container (min-width: 34rem) {
  /* Three children -- icon badge, copy, photograph -- so the areas are
     named: auto-placement would put the copy in the right-hand column. */
  .cat-tile-has-photo {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr auto;
    grid-template-areas:
      "icon photo"
      "text photo";
    column-gap: var(--sp-24);
    row-gap: var(--sp-20);
  }

  .cat-tile-has-photo .cat-tile-icon {
    grid-area: icon;
    align-self: end;
  }

  .cat-tile-has-photo .cat-tile-text {
    grid-area: text;
  }

  .cat-tile-has-photo .cat-tile-photo {
    grid-area: photo;
    aspect-ratio: auto;
  }
}

/* The frame draws three guide cards in green, blue and RED. The first two are
   the category gradients above; the third is not red here, because
   --colour-placeholder already owns red in this build and a colour that means
   two things means neither.

   Flat --colour-ink rather than --gradient-tile, which was the first choice:
   that wash runs pale at the top and the card's copy sits high enough in it to
   land on 2:1 white-on-near-white. 10 targets WCAG AA and the same white on
   this navy measures 13.46:1, which the doc states directly. */
.cat-tile-neutral { background: var(--colour-ink); }

/* A guide card is a shorter panel than a category tile -- no icon above the
   copy, so it does not need the height that leaves room for one. */
.cat-tile-short {
  min-height: 12rem;
}

/* ---- Venue cards ---------------------------------------------------------
   design.md 6.1 section 3 draws four entity cards across the row. The card
   itself is 5.6, rotated: the frame's City Hub card puts its image panel on
   the left of a full-width row, and the home row stands the same parts up --
   image, then title, then meta, then the tag strip along the bottom.

   Three of 5.6's parts are not here, and the reasons differ:

   - The star rating and review count. There are none to show: rule 11
     forbids storing third-party review text and 12.2 says first-party
     reviews start at zero, so the row would be four identical blanks or
     four invented numbers.
   - The frame's corner ribbons -- TOP 10, SAFARI, CLOSING SOON. 12.3 says
     outright that no schema field backs CLOSING SOON, and the other two are
     a ranking and a kind this row does not sort by.
   - The two-line description. The text exists, but it is CC BY-SA prose that
     the venue page carries an attribution line for; an excerpt on a card
     with no room for that line is a licence problem, not a design one.

   What replaces the description is 5.6's own bottom row, the feature tags.
   The frame fills them with species and exhibit names; this row fills them
   with which details are actually on file, which is the one thing the
   section's premise makes interesting and is checked per venue rather than
   assumed. See `_coverage_facets()`.

   The image panel stays even though no venue photograph exists yet -- that
   is 5.6's own empty-image state, verbatim: "keep the panel, fill --tint,
   centre a 32px muted icon. Never collapse the panel -- it breaks the row
   rhythm." A placeholder claims nothing; a stock photograph over a named
   venue would claim to be a picture of it. */

.venue-cards {
  display: grid;
  /* auto-FIT: 6.1 s3 is one row of four and the renderer now sends exactly
     four (HOME_COVERED_VENUES), so the four fill the row between them rather
     than leaving auto-fill's fifth track open at the container width. */
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--sp-24);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* This block's rows always hold 1-3 guides (one hub, or "More guides"),
   never the four `.venue-cards` is tuned for -- the owner's call was "one
   card, same size", left-aligned, not a feature that stretches to fill the
   row. `.venue-cards`' own `1fr` max is what stretches a lone item: with
   only one occupied track, auto-fit gives that track all the row's free
   space. Capping the max at a fixed width instead of `1fr` stops that, and
   `auto-fill` (rather than `auto-fit`) keeps the extra unoccupied tracks
   from being collapsed into that same free space. design.md has no card
   width for this grid; --card-track (20rem/320px, tokens.css) is a
   judgement call per CLAUDE.md -- the nearest round figure to the ~314px a
   card renders at in the four-across home row (`.venue-cards` at the
   1328px container width), so a single guide card reads as "the same size
   a card would be in a full row" rather than a new, smaller size of its
   own. */
.guide-cards {
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), var(--card-track)));
}

/* The home page's guide band always holds exactly three (HOME_GUIDES), in the
   slot 6.1 s7 draws as three quotes spanning the container -- so here the
   cards fill the row the way `.venue-cards` does, instead of leaving a fourth
   track empty on the right. */
.guide-cards-fill {
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

.venue-card {
  display: flex;
  flex-direction: column;
  /* A placeholder card is a <span>, not an <a>; both need to fill their grid
     cell and stack their parts the same way. */
  height: 100%;
  height: 100%;
  /* 5.6: white, 1px border, no radius. 2.5 puts directory surfaces on
     --radius-none, and this is the directory unit. */
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  color: inherit;
  text-decoration: none;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}

/* 5.6 hover (undesigned in the frame, specified by the doc): border to
   --navy-900 plus --shadow-xs. 10: a focus ring on the card, not on the
   text inside it. */
.venue-card:hover {
  border-color: var(--colour-ink);
  box-shadow: var(--shadow-xs);
}

.venue-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.venue-card:hover h3,
.venue-card:focus-visible h3 {
  text-decoration: underline;
}

/* The card photo panel. Fixed aspect so every card in the row starts its
   text at the same height whatever the name length -- and so a card holding
   a real photograph and a card holding the empty-state icon are the same
   size, which is what lets the two mix in one row. The flex centring is for
   the icon; the photograph overrides it below. */
.venue-card-photo {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 3 / 2;
  background: var(--colour-surface-alt);
  border-bottom: 1px solid var(--colour-rule);
  color: var(--colour-muted);
  /* Both of these are what make `aspect-ratio` actually hold once a real
     photograph is inside. The panel is a flex item in the card's column, so
     its automatic minimum size is its content -- and a Commons photograph's
     intrinsic height is whatever the photographer's camera produced.
     Measured before this: four cards in one row came out 235, 210, 209 and
     213px tall, each one the shape of its own picture rather than the shape
     the design draws. `overflow: hidden` drops that automatic minimum to
     zero (and clips what `object-fit: cover` crops); `flex: none` stops the
     panel absorbing the slack when a two-line venue name makes its
     neighbours taller. */
  flex: none;
  overflow: hidden;
}

.venue-card-photo svg {
  display: block;
  width: var(--sp-32);
  height: var(--sp-32);
  opacity: 0.55;
}

/* A venue's lead photograph fills the panel. `object-fit: cover` rather than
   `contain` because Commons photographs arrive at every aspect ratio and a
   letterboxed card reads as a broken one; the 600px derivative is wider than
   any column this row uses, so cover crops rather than upscales. `max-width`
   is the overflow cap `test_images_never_overflow_their_container` asks of
   every image this site sizes itself. */
.venue-card-photo img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  object-fit: cover;
}

/* 2.4 card interior: "20 (entity card)". */
.venue-card-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--sp-8);
  padding: var(--sp-20);
}

/* 2.2 heading-4: 18 / 28 / -0.45, display bold. `.venue-card-title` is the
   same treatment for a placeholder card, which uses a <span> rather than an
   <h3> so filler text never enters the document outline. */
.venue-card h3,
.venue-card-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-ink);
}

/* 5.6's meta row, reduced to the part this build has: a 14px pin and the
   city. 2.2 caption: 12 / 16, body regular. */
.venue-card-where {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

.venue-card-where svg {
  flex: none;
  width: 0.875rem;  /* 14px, per 5.6's meta row */
  height: 0.875rem;
}

/* 5.6 row 4: "feature tags, 8px gap, bottom-aligned". */
.venue-card .chip-row {
  margin-top: auto;
  padding-top: var(--sp-8);
}

/* ---- Photo tiles ---------------------------------------------------------
   The frame's country grid and its two best-of rows are the same component:
   a landscape rectangle with a caption over the bottom edge.

   The important property is that the photograph is OPTIONAL. The frame draws
   it both ways itself -- eight of its own tiles have no image and fall back
   to a pale-to-navy wash -- so a tile with nothing on file is a designed
   state rather than a gap. `.tile` carries that wash as its own background;
   `.tile img`, when there is one, covers it. Photo coverage is partial by
   nature (twelve of the top twenty-four countries have one), so the two
   states sit side by side in the same row on purpose.

   `.tile-label` is readable in both states without a second rule: --scrim
   darkens the bottom of a photograph, and the fallback wash is already dark
   at the bottom where the caption sits. */

.tile-grid {
  display: grid;
  /* 18rem, not the 13rem this was before the container went to 1360: at that
     width a 13rem minimum fits six tracks and the frame draws four. min() so
     the last track can still collapse below 18rem on a narrow phone instead
     of overflowing. */
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* design.md 2.7 gives the Category Hub's country grid as "15 tiles, 202x152
   each, 5 per row" -- a denser row than the home page's four. 15rem lands on
   five tracks at the 1360 container and steps down cleanly below it. */
.tile-grid-dense {
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
}

.tile {
  position: relative;
  display: block;
  aspect-ratio: 262 / 191;
  overflow: hidden;
  /* 2.5: directory surfaces are square, and the frame draws this grid with
     sharp corners. */
  border-radius: var(--radius-none);
  background: var(--gradient-tile);
  color: var(--colour-surface);
  text-decoration: none;
}

.tile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Sits above the photograph, below the caption. Drawn as an element rather
   than a ::after so it is present in exactly the same place whether or not
   the tile has an image behind it. */
.tile-scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}

.tile-label {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
}

/* 2.2 heading-4: 18 / 28 / -0.45, display bold -- the tile's own name. */
.tile-label strong {
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: var(--heading-4-tracking);
}

/* The count, with the frame's small glyph ahead of it. 2.2 caption. */
.tile-count {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
}

.tile-count svg {
  flex: none;
  width: 0.875rem;  /* 14px */
  height: 0.875rem;
}

.tile-label span {
  font-size: var(--text-sm);
  color: var(--colour-on-dark-body);
}

.tile:hover .tile-label strong,
.tile:focus-visible .tile-label strong {
  text-decoration: underline;
}

/* ---- Section head --------------------------------------------------------
   The frame pairs several section titles with a right-aligned "View all ..."
   link on the same baseline. Below the breakpoint the link drops under the
   heading rather than squeezing beside it. */

.section-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-5);
  align-items: baseline;
  justify-content: space-between;
}

.section-head .section-title {
  margin: 0;
}

/* ---- 6.7 Entity page: header slab, 8/4 split, info card ---------------- */

/* 4.2 puts the breadcrumb bar on --paper. Only the entity page opts in: on a
   place page the bar sits under a photographic hero, and the site-wide tint is
   what keeps it from reading as part of the picture. */
.crumb-bar-paper {
  background: var(--colour-surface);
}

/* The bookmark control 6.7 draws at the right of this bar had its rules here
   until 2026-09-18. The owner ruled the feature out -- it needs accounts, and
   there are none -- so the markup went from venue.html and these went with it
   rather than being left behind styling nothing. */

/* 6.7 section 2. --paper, 32/32 padding, a bottom border, and the page's tint
   picking up below it. */
.entity-hero {
  background: var(--colour-surface);
  border-bottom: 1px solid var(--colour-rule);
}

.entity-hero-inner {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-32) var(--container-pad);
}

/* display-2, the one place in the system that uses it. */
.entity-h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2rem, 4.5vw, var(--display-2-size));
  font-weight: 700;
  line-height: var(--display-2-leading);
  letter-spacing: var(--display-2-tracking);
  color: var(--colour-ink);
}

.entity-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-16);
}

.entity-meta-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-12);
}

/* Three rules stood here and all three are dead as of 2026-09-18, when the
   owner removed reviews from the site:

   - `.entity-meta-rule`, the frame's vertical divider between the rating and
     the ranking badges. With the rating gone there is nothing on its left,
     so venue.html no longer draws it.
   - `.entity-review-cta-face`, the type treatment for the invitation the
     divider used to sit beside.
   - `.rank-badge-face`, which shared that declaration. Already unused before
     this change -- no template has ever carried the class -- and it is only
     visible as dead now that the selector it hid behind is going.

*/
/* 5.11's ranking badges. Named `entity-rank-badge` rather than `rank-badge`
   because that class is already the gold circular #1 marker on listing cards
   and reusing it would restyle every one of them.

   Given the same treatment as .accred-badge deliberately -- tint fill, 1px
   rule, `label` type. The gold variant reads as a prize, and what this badge
   claims is completeness of our listing, not quality of the venue. The visual
   weight has to match the claim. */
.rank-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
}

.entity-rank-badge {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-4) var(--sp-8);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  color: var(--colour-ink);
}

/* 6.7's instruction to surface accreditation next to the ranking badges. The
   default 5.11 badge treatment -- tint fill, 1px rule, `label` type -- because
   this is a factual membership, not the gold #1 variant. */
.accred-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
}

.accred-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-8);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--label-size);
  font-weight: 700;
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  color: var(--colour-ink);
}

.accred-badge svg {
  flex: none;
  width: 0.875rem;  /* 14px */
  height: 0.875rem;
}

/* 6.7 section 3: main 871 / aside 419 with a 32px gap, which is 8/4 of the
   1360 app container once its padding is taken off. Expressed as fr so the
   ratio survives the responsive steps rather than pinning two pixel widths. */
.entity-split {
  display: grid;
  grid-template-columns: minmax(0, 871fr) minmax(0, 419fr);
  gap: var(--sp-32);
  align-items: start;
}

.entity-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The first section in the column has the band's own padding above it, so it
   must not add a rule and a second helping of space on top of that. */
.entity-main > .section:first-child {
  padding-top: 0;
  border-top: 0;
}

.entity-aside {
  position: sticky;
  top: var(--sp-24);
  min-width: 0;
}

/* 5.9: --paper, 1px border, --shadow-xs, 20px padding, no radius. */
.info-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-none);
  box-shadow: var(--shadow-xs);
  padding: var(--sp-20);
}

.info-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  margin: 0;
}

/* 20px icon with a 2px top offset, then a 12px gap. */
.info-item {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-12);
}

.info-icon {
  flex: none;
  width: 1.25rem;   /* 20px */
  height: 1.25rem;
  margin-top: 2px;
  color: var(--colour-brand);
}

.info-body {
  min-width: 0;
}

.info-body dt {
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
}

.info-body dd {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.info-body address {
  font-style: normal;
}

.info-body a {
  color: var(--colour-muted);
}

/* 5.9: caption, bold, --blue-500, with a 12px arrow. */
.info-map-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--caption-size);
  font-weight: 700;
  line-height: var(--caption-leading);
  color: var(--colour-link);
  text-decoration: none;
}

.info-map-link:hover,
.info-map-link:focus-visible {
  text-decoration: underline;
}

.info-map-link svg {
  flex: none;
  width: 0.75rem;   /* 12px */
  height: 0.75rem;
}

/* 5.9's rule above ALSO KNOWN AS: 1px --border, 16px padding. */
.info-aka {
  margin-top: var(--sp-16);
  padding-top: var(--sp-16);
  border-top: 1px solid var(--colour-rule);
}

.info-aka-label {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--label-caps-size);
  font-weight: 700;
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  text-transform: uppercase;
  color: var(--colour-ink);
}

.info-aka-value {
  margin: var(--sp-4) 0 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  font-style: italic;
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* 6.7 3a's sub-topic: an H3 under the description's single H2. */
.subtopic-title {
  margin: var(--sp-16) 0 var(--sp-8);
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-ink);
}

/* 9: the aside drops under the main column before it gets too narrow to hold
   an address, and stops being sticky when it is no longer beside anything. */
@media (max-width: 60rem) {
  .entity-split {
    grid-template-columns: minmax(0, 1fr);
  }

  .entity-aside {
    position: static;
  }
}

/* 6.7 gives the entity's main column 871px, and the frame runs the description
   the full width of it. `.prose` otherwise stops at the 65ch --measure, which
   is right for the article and listicle templates but leaves a third of this
   column empty -- the same mismatch 2.7 warns about between a measure and a
   container that were chosen independently. */
.entity-main .prose {
  max-width: none;
}

/* The description is one section but reads as several: an H2, its body, then
   H3 sub-topics with their own paragraphs. 5.15's flat 24px gap spaces all of
   those identically, which detaches each sub-heading from the paragraph it
   introduces. Zero the gap and let the elements carry their own rhythm. */
.entity-main .description {
  gap: 0;
}

.entity-main .description > .prose + .prose,
.entity-main .description > .section-title + .prose {
  margin-top: var(--sp-12);
}

.entity-main .description > .subtopic-title {
  margin: var(--sp-24) 0 var(--sp-8);
}

/* The content bands elsewhere carry the 96px home-hero rhythm from 2.4. The
   entity page does not: 6.7 sets the split straight under the header slab, so
   this band opens on 32px like the slab's own padding rather than on a gap
   deep enough to look like a missing section. */
.entity-band > .band-inner {
  padding-top: var(--sp-32);
}

/* The admission frame heads its table with a small ticket glyph before the
   words. Applied per section rather than to .section-title globally: only
   the tabular sections carry one in the frame, and a heading with a slot for
   an icon that nothing fills would just be inconsistent spacing. */
.section-title-icon {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
}

/* The frame's icon box: 20x20, holding a leaf at its own natural size. Both
   are preserved -- the box is fixed here, each leaf carries its own width and
   height attributes in the markup, and they differ (the ticket is 17.92 x
   14.58 where the clock and the facilities glyph are square). Nothing is
   stretched to a shared size. */
.section-icon {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.25rem;   /* 20px */
  height: 1.25rem;
  overflow: hidden;
  /* The exported strokes are #4362EF. They are set to currentColor in the
     markup so the value comes from the token rather than from a hex repeated
     in three places. */
  color: var(--colour-brand);
}

.section-icon svg {
  display: block;
}

/* ---- Animals carousel ----------------------------------------------------
   A scroll-snap strip rather than a scripted carousel: this site ships no
   JavaScript, so the only honest "carousel" is one the browser drives. Scroll
   snapping gives the slide-to-slide feel; the region is focusable so a
   keyboard can drive it too. design.md draws no carousel anywhere -- 12.6
   lists what is undesigned and this is on the far side of that line -- so the
   anatomy below is built from the tokens the rest of the entity page uses. */
.carousel {
  display: flex;
  gap: var(--sp-16);
  margin: 0;
  padding: 0 0 var(--sp-12);
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

/* The anchor name is per-strip, not on `.carousel`. Two elements sharing one
   `anchor-name` resolve to a single anchor, so with the gallery and the
   animals strip both on the entity page the gallery's arrows would track the
   animals strip several sections further down. */
.carousel-animals {
  anchor-name: --carousel-animals;
}

.carousel-gallery {
  anchor-name: --carousel-gallery;
}

.carousel-animals::scroll-button(*) {
  position-anchor: --carousel-animals;
}

.carousel-gallery::scroll-button(*) {
  position-anchor: --carousel-gallery;
}

/* The arrows are CSS `::scroll-button()` pseudo-elements, not scripted
   controls. The browser generates real buttons, wires the scrolling, exposes
   them to assistive tech with its own accessible names, and disables each one
   when the strip cannot move further in that direction -- none of which this
   stylesheet has to implement or can get wrong. It is also the only way to
   add them here without shipping the site's first JavaScript file: 11.1 bans
   client-side CONTENT, not interaction, but there is no script anywhere on
   this build and a carousel is a poor reason to start.

   Anchored to the strip rather than positioned in the section, so they track
   it rather than a guessed offset. */
.carousel::scroll-button(*) {
  position: fixed;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-32);
  height: var(--sp-32);
  padding: 0;
  background: var(--colour-surface);
  /* Brand blue rather than --colour-rule. The arrows are the only control on
     these strips, and a hairline grey border made them read as disabled
     dividers rather than as something to press. */
  border: 1px solid var(--colour-brand);
  color: var(--colour-ink);
  font-family: var(--font-display);
  font-size: var(--body-size);
  line-height: 1;
  cursor: pointer;
}

/* Hover fills rather than merely darkening the edge: with a blue border at
   rest, a blue-on-blue border change is close to invisible. */
.carousel::scroll-button(*):hover {
  background: var(--colour-brand);
  color: var(--colour-surface);
}

.carousel::scroll-button(*):focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* The UA disables the button at each end of the travel. Shown rather than
   hidden: a control that vanishes mid-interaction moves everything beside it. */
.carousel::scroll-button(*):disabled {
  opacity: 0.35;
  cursor: default;
}

.carousel::scroll-button(left) {
  content: "‹";
  /* Placed with explicit anchor() insets rather than a position-area. A
     `top right` area sits OUTSIDE the strip's right edge: the wide layout hid
     that in the 32px gap to the aside, and the narrow one pushed the button
     off the side of the viewport. Insets keep both within the strip's own
     right edge at every width. */
  bottom: calc(anchor(top) + var(--sp-8));
  right: calc(anchor(right) + var(--sp-40));
}

.carousel::scroll-button(right) {
  content: "›";
  bottom: calc(anchor(top) + var(--sp-8));
  right: anchor(right);
}

/* The scrollbar is hidden ONLY where the arrows that replace it exist. In a
   browser without `::scroll-button()` the bar is the sole visible affordance,
   and hiding it there would leave a strip with no sign it scrolls at all. */
@supports selector(::scroll-button(right)) {
  .carousel {
    scrollbar-width: none;
  }

  .carousel::-webkit-scrollbar {
    display: none;
  }
}

.carousel:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: var(--sp-4);
}

.carousel-slide {
  flex: none;
  width: 13.75rem;   /* 220px */
  scroll-snap-align: start;
}

/* The frame puts three photographs across the main column. Sized so three sit
   full-width with a sliver of the fourth showing, because a strip that ends
   flush on the last visible slide gives no sign there is more to scroll to --
   the arrows say it, but only once you have looked for them. */
.carousel-gallery .carousel-slide {
  width: 16rem;   /* 256px */
}

/* Square, as the frame draws them, and as .gallery-figure did before this was
   a strip. The animals strip uses 3:2 because those are stock photographs
   requested in that orientation; these are whatever Commons holds, so a square
   crop treats a portrait and a landscape shot alike. */
.carousel-gallery .carousel-figure img {
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-sm);
}

/* The credit moves out of the flow and behind an (i) in the image's corner.
   It is STILL a licence condition -- 585 of these are CC-BY-SA-3.0 and 523
   CC-BY-SA-4.0 -- so it is hidden with opacity and visibility, never
   `display: none`, which would strip it from the accessibility tree. A screen
   reader still announces it, the page source still carries it, and it appears
   for hover, keyboard focus and tap alike. Owner's decision, 2026-09-04. */
.gallery-shot {
  position: relative;
  display: block;
}

.gallery-info {
  position: absolute;
  right: var(--sp-8);
  bottom: var(--sp-8);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-24);
  height: var(--sp-24);
  border-radius: var(--radius-full);
  background: var(--colour-ink);
  color: var(--colour-surface);
  font-family: var(--font-display);
  font-size: var(--caption-size);
  font-style: italic;
  font-weight: 700;
  line-height: 1;
  cursor: help;
}

.gallery-info:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.carousel-gallery .carousel-figure figcaption {
  position: absolute;
  right: var(--sp-8);
  bottom: var(--sp-32);
  left: var(--sp-8);
  padding: var(--sp-8);
  border-radius: var(--radius-sm);
  background: var(--colour-ink);
  color: var(--colour-surface);
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  /* Not `display: none` -- see the note above. */
  opacity: 0;
  visibility: hidden;
}

.carousel-gallery .carousel-figure figcaption a {
  color: inherit;
}

/* Triggered by the (i) itself, not the whole image -- an image-wide hover
   put a panel over the photograph whenever the cursor crossed it.

   The credit stays open while the pointer is on the panel too, and while
   anything inside it has focus. It carries the photographer's link, and a
   tooltip that closed as you moved towards that link would make it
   unclickable. `.gallery-credit` sits immediately after `.gallery-info` in
   the markup, so an adjacent-sibling selector reaches it with no wrapper
   state involved. */
.gallery-info:hover + .gallery-credit,
.gallery-info:focus + .gallery-credit,
.gallery-credit:hover,
.gallery-credit:focus-within {
  opacity: 1;
  visibility: visible;
}

.carousel-figure {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  margin: 0;
}

.carousel-figure img,
.carousel-blank {
  display: block;
  width: 100%;
  /* 3:2, the orientation the photographs are requested in. Fixed by ratio so
     a slide with no picture is exactly the height of one that has it and the
     strip does not step up and down. */
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
}

.carousel-figure figcaption {
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
}

.carousel-figure figcaption .original {
  color: var(--colour-muted);
}

@media (prefers-reduced-motion: reduce) {
  .carousel {
    scroll-behavior: auto;
  }
}

/* The grid this replaced. Kept for `.gallery-credit`, which the strip still
   uses, and because nothing else should silently inherit the strip's rules. */

/* ---- 6.2 Directory index -------------------------------------------------
   Anatomy read from node 12:2663 for the category tiles; the rest is built
   from the frame screenshot on the tokens the other hub pages already use,
   since design.md gives 6.2 node references rather than per-section anatomy. */

.dir-cat-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--sp-16);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 12:2667: tint fill, 1px rule, 12px gap, 21px padding, equal widths. */
.dir-cat-tile {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  height: 100%;
  padding: var(--sp-20);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--ui-size);
  font-weight: 700;
  line-height: var(--ui-leading);
  letter-spacing: var(--ui-tracking);
  color: var(--colour-ink);
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.dir-cat-tile:hover,
.dir-cat-tile:focus-visible {
  border-color: var(--colour-ink);
}

/* 12:2668: a 40px white disc with its own 1px rule, holding a 20px glyph. */
.dir-cat-tile-icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--sp-40);
  height: var(--sp-40);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-full);
  color: var(--colour-ink);
}

.dir-cat-tile-icon svg {
  display: block;
}

/* The country grid: two columns of continent groups on a wide viewport,
   collapsing to one. Not a fixed two-column split -- the continents are
   uneven and a hard split leaves one side long and the other empty. */
.dir-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--sp-24) var(--sp-40);
}

.dir-group-title {
  margin: 0 0 var(--sp-8);
  padding-bottom: var(--sp-8);
  border-bottom: 1px solid var(--colour-rule);
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  font-weight: 700;
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-ink);
}

.dir-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

.dir-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-12);
  padding: var(--sp-4) 0;
  font-family: var(--font-body);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
  text-decoration: none;
}

.dir-row:hover span:first-child,
.dir-row:focus-visible span:first-child {
  text-decoration: underline;
}

/* The frame sets the count small and faint: it qualifies the link, it is not
   the link. */
.dir-count {
  flex: none;
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted-80);
}

.city-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr));
  gap: var(--sp-16);
  list-style: none;
  margin: 0;
  padding: 0;
}

.city-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  height: 100%;
  padding: var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  text-align: center;
  text-decoration: none;
  transition: border-color 150ms ease-out;
}

.city-tile:hover,
.city-tile:focus-visible {
  border-color: var(--colour-ink);
}

.city-tile-name {
  font-family: var(--font-display);
  font-size: var(--body-sm-size);
  font-weight: 700;
  line-height: var(--body-sm-leading);
  color: var(--colour-ink);
}

.city-tile-count {
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

@media (prefers-reduced-motion: reduce) {
  .dir-cat-tile,
  .city-tile {
    transition: none;
  }
}

/* ---- 6.9 About ------------------------------------------------------------
   Anatomy from node 14:3492 for the methodology columns; the stats block is
   the frame's four 329x128 tiles rebuilt on the spacing scale. */

.stat-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
}

/* The frame divides the tiles with a single hairline rather than gaps, so the
   strip reads as one object. `:not(:first-child)` rather than a right border
   on every tile, which would double up at the wrap. */
.stat-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  padding: var(--sp-24) var(--sp-16);
  text-align: center;
}

.stat-tile:not(:first-child) {
  border-left: 1px solid var(--colour-rule);
}

.stat-value {
  font-family: var(--font-display);
  font-size: var(--heading-2-size);
  font-weight: 700;
  line-height: var(--heading-2-leading);
  letter-spacing: var(--heading-2-tracking);
  color: var(--colour-ink);
}

/* 2.2's label-caps: the frame sets these as small uppercase captions. */
.stat-label {
  font-family: var(--font-body);
  font-size: var(--label-caps-size);
  font-weight: 700;
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  text-transform: uppercase;
  color: var(--colour-muted);
}

/* 14:3495 / 14:3514: a 48px disc filled with the icon's own colour at 10%,
   holding a 24px glyph. */
.about-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-48);
  height: var(--sp-48);
  margin-bottom: var(--sp-8);
  border-radius: var(--radius-full);
}

.about-icon svg {
  display: block;
}

.about-icon-brand {
  background: var(--colour-brand-wash);
  color: var(--colour-brand);
}

.about-icon-open {
  background: var(--colour-open-wash);
  color: var(--colour-open);
}

/* These two were one `.about-methodology` two-column split on a tinted band.
   The About page is a pillar page now -- one column, read straight down -- so
   they are two ordinary stacked sections and the split is gone. What they keep
   is their own internal rhythm and the frame's 17px body. */
#methodology,
#sourcing {
  display: flex;
  flex-direction: column;
  gap: var(--sp-12);
}

#methodology .section-title,
#sourcing .section-title {
  margin: 0;
}

/* The frame set the methodology body at 17px against 16 everywhere else. Side
   by side in their own tinted band that was a deliberate contrast between the
   two halves. Stacked in one column it is just two sizes of the same thing,
   and the middle of the page reads as though it were quoting something. Body
   copy is body copy: the size comes from `.prose` now, for every section. */

.about-emphasis {
  font-weight: 700;
  color: var(--colour-ink);
}

/* 6.9 section 8: the "Get in touch" cards that close the page.

   The frame drew three across; two are drawn, because only two have a page
   behind them -- see about.html's comment on why the press card is omitted
   rather than tagged. `auto-fit` with a min track rather than a fixed
   `repeat(2, 1fr)`: the count is a function of what the build can link to, and
   a hard-coded two would misdraw the day that changes.

   Not `.card-grid`. That grid's cards are `flex-direction: row` (an image
   beside a title) and its tracks are sized for the directory's venue cards.
   These stack an icon, a heading, a line of copy and a link. */
.contact-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.contact-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-8);
  padding: var(--space-4);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-md);
}

.contact-card-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--sp-48);
  height: var(--sp-48);
  border-radius: var(--radius-full);
  background: var(--colour-brand-wash);
  color: var(--colour-brand);
}

.contact-card-icon svg {
  display: block;
}

.contact-card-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-5-size);
  line-height: var(--heading-5-leading);
  color: var(--colour-ink);
}

.contact-card-text {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-muted);
}

/* Pushed to the card's baseline so the links line up across cards whose copy
   wraps to a different number of lines. */
.contact-card-link {
  margin-top: auto;
  font-weight: 700;
  color: var(--colour-brand);
}

/* 6.9: "app shell, 768 for mission and values" -- the hero and the values
   sections read as prose, so they take the prose measure rather than the
   full app width.

   That cap used to be listed per section here. It is `.page.prose-page`'s job
   now: the column itself is capped and centred, and `.prose` fills it, so a
   second cap inside would only pull the text back off its own headings --
   the exact mismatch that rule was added to fix on the General pages. */

/* About used to keep the shell's left edge, because its methodology section
   was a two-column band and the stat tiles run four across, and a narrow
   centred column underneath them would have started 340px right of the hero
   introducing it. The band is gone and the page is a single column, so it
   takes `prose-page` like the General pages: the tiles and the contact cards
   now sit in that column rather than the column sitting inside them.

   `prose-page` carries the ragged-edge balancing for h1/h2/.lede/.prose, so
   the only thing left to name here is the hero's own lede, which is not
   `.prose`. */
.about-hero .lede {
  text-wrap: pretty;
}

/* ---------------------------------------------------------------------------
   6.7 s4 — nearby / lateral linking row on the entity page.

   A separate component from §5.6's `.venue-card` on purpose. That one is the
   directory unit: a tall card with a 3:2 image panel above its body, sized to
   carry a blurb and a chip row. This is the lateral row the doc draws at
   "430px each" -- a compact horizontal strip whose whole job is to get the
   reader to the next venue. Reusing the tall card here made the section
   several times its designed height and buried the link list it replaced.

   No star rating: 5.10 and 12.2 hide the star row until first-party reviews
   exist, and rule 11 forbids importing anyone else's.
   --------------------------------------------------------------------------- */
.nearby-cards {
  display: grid;
  /* auto-fit rather than a fixed three: the aside makes this column narrower
     than the page, and on a phone the row has to become a column. */
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--sp-16);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nearby-card {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  height: 100%;
  padding: var(--sp-12);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-md);
  color: inherit;
  text-decoration: none;
  transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}

/* Same hover and focus treatment as the directory card, so the two read as
   the same family even though their geometry differs. */
.nearby-card:hover {
  border-color: var(--colour-ink);
  box-shadow: var(--shadow-xs);
}

.nearby-card:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.nearby-card:hover .nearby-card-name,
.nearby-card:focus-visible .nearby-card-name {
  text-decoration: underline;
}

/* The thumbnail slot. Square and fixed so every card in the row aligns
   whatever the venue name does. */
.nearby-card-photo {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--sp-48);
  height: var(--sp-48);
  background: var(--colour-surface-alt);
  border-radius: var(--radius-sm);
  color: var(--colour-muted);
}

.nearby-card-photo svg {
  display: block;
  width: var(--sp-24);
  height: var(--sp-24);
  opacity: 0.55;
}

/* min-width: 0 so a long venue name ellipsizes instead of forcing the card
   wider than its grid track. */
.nearby-card-body {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: var(--sp-4);
}

.nearby-card-name {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.4286;
  letter-spacing: -0.025em;
  color: var(--colour-ink);
}

.nearby-card-meta {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-family: var(--font-body);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
}

.nearby-card-meta svg {
  flex: none;
  width: 0.875rem;   /* 14px, matching 5.6's meta row */
  height: 0.875rem;
}

/* The doc's "View all" link, drawn as the reference draws it: a centred pill
   rather than the inline text link `.view-all` gives the best-of pages. */
.nearby-all {
  display: flex;
  justify-content: center;
  margin-top: var(--sp-24);
}

.nearby-all .view-all {
  padding: var(--sp-12) var(--sp-24);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-pill);
  color: var(--colour-ink);
}

.nearby-all .view-all:hover,
.nearby-all .view-all:focus-visible {
  border-color: var(--colour-ink);
  text-decoration: none;
}

/* ---------------------------------------------------------------------------
   5.16 Table of contents, Listicle variant — the venue page's jump links.

   A pill row rather than the doc's 256px sticky aside: the entity page already
   has a sticky info card in its aside (6.7 3h), and two sticky columns
   competing for the same edge of the viewport is worse than either alone.

   Deliberately NOT sticky for the same reason.
   --------------------------------------------------------------------------- */
.section-tabs {
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-20) var(--container-pad);
}

.section-tabs ul {
  display: flex;
  /* Wraps rather than scrolls. A venue with every section gets a dozen tabs,
     and a horizontal scroller hides the tail behind a gesture with no
     affordance -- on a directory page the reader cannot know what they missed. */
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

.section-tab {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-12) var(--sp-20);
  background: var(--colour-surface-alt);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  color: var(--colour-ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.4286;
  letter-spacing: -0.025em;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 150ms ease-out, border-color 150ms ease-out;
}

.section-tab:hover {
  background: var(--colour-surface);
  border-color: var(--colour-rule);
}

.section-tab:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

/* The anchor targets sit under a page that may grow a sticky header later, and
   a jump link that lands with the heading flush against the viewport edge reads
   as having overshot. Applied to the sections themselves, not the tabs. */
.venue .section,
.guide-group {
  scroll-margin-top: var(--sp-24);
}

/* ---------------------------------------------------------------------------
   5.14 Search field, as a progressive enhancement.

   `.search-fallback` is what ships in the HTML and what a reader without
   JavaScript keeps. `search.js` sets `data-ready` on the container, which is
   what hides the fallback -- so the links are only ever hidden once something
   is there to replace them.
   --------------------------------------------------------------------------- */
.search {
  /* The panel below is absolutely positioned against this box, so the box
     needs its own width rather than inheriting one from the widest
     suggestion -- `.hero-split > div` is `align-items: start`, which sizes
     every child to its content. Without this the field would collapse to the
     input's intrinsic width the moment the list left the flow, and before
     that it was the list that set the width, so the box grew and shrank as
     the reader typed. */
  position: relative;
  width: 100%;
  max-width: 32rem;
}

.search-fallback {
  margin: 0;
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-ink);
}

.search[data-ready] .search-fallback {
  display: none;
}

.search-field {
  display: flex;
  align-items: stretch;
  background: var(--colour-surface);
  border: 2px solid var(--colour-rule);
  box-shadow: var(--shadow-sm);
}

.search-input {
  flex: 1;
  min-width: 0;
  padding: var(--sp-12) var(--sp-16);
  border: 0;
  background: transparent;
  color: var(--colour-ink);
  font-family: var(--font-body);
  font-size: var(--body-size);
}

.search-input:focus {
  outline: none;
}

.search-field:focus-within {
  border-color: var(--colour-brand);
}

/* The suggestion list, floating over the page rather than sitting in the flow.
   In flow it pushed the band's bottom edge down as the reader typed, so the
   hero resized under the cursor on every keystroke.

   Nothing between here and the page root clips it: `.band`, `.hero-split` and
   `.hero-split > div` set no overflow, and `.hero-figure`'s `overflow: hidden`
   is on a sibling column. The z-index stays below `.skip-link`'s 10 -- a
   suggestion list must never cover the keyboard escape hatch.

   The panel gets --shadow-md because it now sits ON the page instead of in it;
   a border alone reads as part of the layout. The max-height is what keeps a
   full 15-suggestion list from running off a short viewport with no way back. */
.search-results {
  position: absolute;
  top: calc(100% + var(--sp-8));
  left: 0;
  right: 0;
  z-index: 5;
  max-height: 60vh;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-md);
}

.search-results:empty {
  display: none;
}

.search-group {
  padding: var(--sp-8) var(--sp-16) var(--sp-4);
  color: var(--colour-muted);
  font-family: var(--font-display);
  font-size: var(--caption-size);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.search-option {
  display: block;
  padding: var(--sp-8) var(--sp-16);
  color: var(--colour-ink);
  text-decoration: none;
  cursor: pointer;
}

.search-option:hover,
.search-option[aria-selected="true"] {
  background: var(--colour-surface-alt);
}

.search-option-hint {
  color: var(--colour-muted);
  font-size: var(--caption-size);
}

.search-empty {
  padding: var(--sp-12) var(--sp-16);
  color: var(--colour-muted);
  font-size: var(--body-size);
}

/* The three browse links appended below a "No matches" row, so a mistyped
   query is never a dead end -- see search.js's render(). */
.search-empty-links {
  padding: 0 var(--sp-16) var(--sp-12);
  color: var(--colour-ink);
  font-size: var(--body-size);
}

/* ------------------------------------------------------------------------
   design.md 6.8 -- the Article Guide. docs/superpowers/specs/
   2026-09-11-hub-guides-design.md. App shell; 768 prose beside a 256 sticky
   table of contents.
   ------------------------------------------------------------------------ */
.guide-inner {
  gap: var(--sp-32);
}

/* `discover` and `how-we-research` each get the full `.band-inner` rhythm
   (96/64/48 padding) by default, which is sized for a band with a heading
   and a row of cards -- here it leaves a large empty gap around a single
   line of pills and a short note, between the FAQ above and the navy CTA
   below. Tightened for just these two bands, not `.band-inner` itself.
   Not in design.md; a judgement call per CLAUDE.md, one step down the same
   --sp-* scale `.band-inner` already steps through at the same
   breakpoints. */
.guide-inner-tight {
  padding-top: var(--sp-48);
  padding-bottom: var(--sp-48);
}

@media (max-width: 63.99em) {
  .guide-inner-tight {
    padding-top: var(--sp-32);
    padding-bottom: var(--sp-32);
  }
}

@media (max-width: 47.99em) {
  .guide-inner-tight {
    padding-top: var(--sp-24);
    padding-bottom: var(--sp-24);
  }
}

/* A preview-only banner, in the placeholder red because it means what the
   red tag means: this is not what a reader will see. */
.guide-draft {
  align-self: flex-start;
  margin: 0;
  padding: var(--sp-8) var(--sp-16);
  border: 1px dashed var(--colour-placeholder);
  color: var(--colour-placeholder);
  font-family: var(--font-display);
  font-size: var(--label-caps-size);
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  font-weight: 700;
  text-transform: uppercase;
}

.guide-header {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  max-width: var(--container-prose);
}

.guide-category {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  font-weight: 700;
  color: var(--colour-brand);
}

/* 6.8: "display-2-ish H1". */
.guide-header h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--display-2-size);
  line-height: var(--display-2-leading);
  letter-spacing: var(--display-2-tracking);
  color: var(--colour-brand-ink);
}

/* Not in 6.8, which doesn't cover mobile. Same breakpoint the hero's own H1
   steps down at (line ~3638); a judgement call per CLAUDE.md, but one that
   reuses an existing step rather than inventing a size -- display-2 (48px)
   wraps an ordinary title to five lines at 390px, and heading-1 (36px) is
   the next-smallest heading role the file already defines. */
@media (max-width: 47.99em) {
  .guide-header h1 {
    font-size: var(--heading-1-size);
    line-height: var(--heading-1-leading);
    letter-spacing: var(--heading-1-tracking);
  }
}

.guide-byline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8) var(--sp-24);
  margin: 0;
  padding-bottom: var(--sp-16);
  border-bottom: 1px solid var(--colour-rule);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.guide-hero {
  margin: 0;
}

/* The frame's hero is a wide band. A ratio rather than a height, so it
   scales with the column instead of cropping to a fixed strip. */
.guide-hero img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 7;
  object-fit: cover;
  border-radius: var(--radius-lg);
}

.guide-hero figcaption {
  margin-top: var(--sp-8);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-muted);
  text-align: center;
}

.guide-layout {
  display: grid;
  grid-template-columns: var(--aside-toc) minmax(0, var(--container-prose));
  gap: var(--sp-48);
  align-items: start;
}

.guide-toc {
  position: sticky;
  top: var(--sp-24);
}

.guide-toc-title {
  margin: 0 0 var(--sp-12);
  font-family: var(--font-display);
  font-size: var(--label-caps-size);
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--colour-brand-ink);
}

.guide-toc ol {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

.guide-toc a {
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
  text-decoration: none;
}

.guide-toc a:hover,
.guide-toc a:focus-visible {
  color: var(--colour-brand);
}

/* One column below the tablet breakpoint: the contents list sits above the
   article and stops following the reader down the page. */
@media (max-width: 63.99em) {
  .guide-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-24);
  }

  .guide-toc {
    position: static;
  }
}

.guide-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  font-size: var(--body-size);
  line-height: var(--body-leading);
  color: var(--colour-ink);
}

.guide-body p,
.guide-body ul {
  margin: 0;
}

.guide-body h2 {
  margin: var(--sp-24) 0 0;
  font-family: var(--font-display);
  font-size: var(--heading-3-size);
  line-height: var(--heading-3-leading);
  letter-spacing: var(--heading-3-tracking);
  color: var(--colour-brand-ink);
  scroll-margin-top: var(--sp-24);
}

.guide-body h3 {
  margin: var(--sp-8) 0 0;
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  line-height: var(--heading-4-leading);
  letter-spacing: var(--heading-4-tracking);
  color: var(--colour-brand-ink);
}

.guide-body a {
  color: var(--colour-link);
}

.guide-answer {
  font-size: var(--lede-size);
  line-height: var(--lede-leading);
  color: var(--colour-brand-ink);
}

.guide-takeaways {
  padding: var(--sp-24);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
}

.guide-callout-title {
  margin: 0 0 var(--sp-8);
  font-family: var(--font-display);
  font-size: var(--heading-5-size);
  line-height: var(--heading-5-leading);
  font-weight: 700;
  color: var(--colour-brand-ink);
}

.guide-takeaways ul {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding-left: var(--sp-20);
}

/* The frame's insider tip is amber. design.md has no amber, and adding a
   colour for one callout would widen the palette for a single use, so the
   nearest token is the brand wash -- a judgement call, recorded per
   CLAUDE.md. */
.guide-tip {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  padding: var(--sp-16) var(--sp-24);
  background: var(--colour-brand-wash);
  border-left: var(--sp-4) solid var(--colour-brand);
}

.guide-tip .guide-callout-title {
  margin: 0;
}

.guide-quote {
  margin: 0;
  padding-left: var(--sp-24);
  border-left: var(--sp-4) solid var(--colour-brand);
}

.guide-quote blockquote {
  margin: 0;
  font-size: var(--body-lg-size);
  line-height: var(--body-lg-leading);
  font-style: italic;
  color: var(--colour-brand-ink);
}

.guide-quote figcaption {
  margin-top: var(--sp-8);
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.guide-venue {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  box-shadow: var(--shadow-xs);
  color: inherit;
  text-decoration: none;
}

.guide-venue:focus-visible {
  outline: 2px solid var(--colour-brand);
  outline-offset: 2px;
}

.guide-venue-photo {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--colour-surface-alt);
}

.guide-venue-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.guide-venue-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-16) var(--sp-20);
}

.guide-venue-label {
  font-family: var(--font-display);
  font-size: var(--label-caps-size);
  line-height: var(--label-caps-leading);
  letter-spacing: var(--label-caps-tracking);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--colour-brand);
}

.guide-venue-name {
  font-family: var(--font-display);
  font-size: var(--heading-4-size);
  line-height: var(--heading-4-leading);
  font-weight: 700;
  color: var(--colour-brand-ink);
}

.guide-venue-where,
.guide-venue-blurb {
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

.guide-venue-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
}

.guide-venue-badge {
  padding: 0 var(--sp-8);
  border-radius: var(--radius-pill);
  background: var(--colour-open-wash);
  font-size: var(--caption-size);
  line-height: var(--caption-leading);
  color: var(--colour-ink);
}

@media (max-width: 47.99em) {
  .guide-venue {
    grid-template-columns: minmax(0, 1fr);
  }
}

.guide-faq {
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
  max-width: var(--container-prose);
  margin: 0;
}

.guide-faq > div {
  padding: var(--sp-20) var(--sp-24);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
}

.guide-faq dt {
  font-family: var(--font-display);
  font-size: var(--heading-5-size);
  line-height: var(--heading-5-leading);
  font-weight: 700;
  color: var(--colour-brand-ink);
}

.guide-faq dd {
  margin: var(--sp-8) 0 0;
  color: var(--colour-muted);
}

.guide-faq dd p,
.guide-faq dd ul {
  margin: 0;
}

.guide-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* design.md 5.4's facet pill, as a plain link. */
.guide-pills a {
  display: inline-flex;
  padding: var(--sp-8) var(--sp-16);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: var(--radius-pill);
  font-size: var(--ui-size);
  line-height: var(--ui-leading);
  color: var(--colour-ink);
  text-decoration: none;
}

.guide-pills a:hover,
.guide-pills a:focus-visible {
  border-color: var(--colour-ink);
}

.guide-note {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
  max-width: var(--container-prose);
  padding: var(--sp-24);
  background: var(--colour-surface-alt);
  border: 1px solid var(--colour-rule);
}

.guide-note-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-5-size);
  line-height: var(--heading-5-leading);
  color: var(--colour-brand-ink);
}

.guide-note p {
  margin: 0;
  font-size: var(--body-sm-size);
  line-height: var(--body-sm-leading);
  color: var(--colour-muted);
}

/* The frame's navy "Ready to plan your family visit?" band. */
.guide-cta {
  background: var(--colour-brand-ink);
}

.guide-cta-inner {
  align-items: center;
  text-align: center;
}

.guide-cta h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--heading-2-size);
  line-height: var(--heading-2-leading);
  letter-spacing: var(--heading-2-tracking);
  color: var(--colour-surface);
}

.guide-cta p {
  max-width: var(--container-prose);
  margin: 0;
  font-size: var(--body-lg-size);
  line-height: var(--body-lg-leading);
  color: var(--colour-on-dark-body);
}

.guide-cta-link {
  display: inline-flex;
  padding: var(--sp-12) var(--sp-24);
  background: var(--colour-brand);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--colour-surface);
  text-decoration: none;
}

.guide-cta-link:hover,
.guide-cta-link:focus-visible {
  background: var(--colour-brand-dark);
}

.guide-cta-link:focus-visible {
  outline: 2px solid var(--colour-surface);
  outline-offset: 2px;
}
