/* ===========================================================================
   FOODYII — stylesheet (structure and components)

   ★ THE COLOURS, FONTS, SPACING AND TIMINGS ARE NOT IN THIS FILE.
     They are in css/theme.css, which section 01 imports. Edit that.

   HOW THIS FILE IS ORGANISED
     01  Theme import ............ where the knobs actually are
     02  Reset and base .......... sane defaults
     03  Typography .............. headings, paragraphs, links
     04  Layout helpers .......... page width, sections, spacing
     05  Site header / nav ....... top of every page
     06  Site footer ............. bottom of every page
     07  Home page ............... intro, featured review
     08  Review cards ............ the grid used on home + reviews pages
     09  Filter bar .............. controls on reviews.html
     10  Single review page ...... review.html
     11  Gallery and figures ..... photography
     12  About page .............. about.html
     13  States .................. loading, error, empty results
     14  Responsive .............. tablet and mobile
     15  Landing ................. the opening sequence on the home page
     16  Explore menu ............ the header dropdown
     17  Index controls .......... search, filters, view switch
     18  Map ..................... the "where I've eaten" view

   Use your editor's search to jump to a section, e.g. search for "08".
   =========================================================================== */


/* ===========================================================================
   01  THE THEME LIVES IN ITS OWN FILE

   Every colour, typeface, measurement and animation timing now sits in
   css/theme.css. This @import pulls it in, so that anything loading THIS
   stylesheet gets the tokens automatically and a new page cannot forget one
   of two <link> tags.

   ★ To change how the site looks, open css/theme.css. Not this file. ★

   What remains below is the rules that USE those tokens: what a card is,
   how the header sits, what a filter looks like. If you find a raw colour
   or a raw duration anywhere below, that is a bug — give it a token in
   theme.css and point at it here.
   =========================================================================== */
@import url("theme.css");



/* ===========================================================================
   02  RESET AND BASE
   ---------------------------------------------------------------------------
   Browsers apply their own default styles. These few rules flatten the
   differences so we start from a predictable place.
   =========================================================================== */

/* border-box means padding and border count INSIDE an element's stated width,
   which is almost always what you expect. */
*,
*::before,
*::after { box-sizing: border-box; }

body,
h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl, dd { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  background: var(--background);
  color: var(--text);
  font-family: var(--sans-font);
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  -webkit-font-smoothing: antialiased;

  /* Sticky footer: body fills the screen, main grows, footer sits at the end. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main { flex: 1 0 auto; }

/* Images are inline by default, which leaves a mysterious gap underneath.
   `display: block` removes it. max-width keeps them inside their container. */
img {
  display: block;
  max-width: 100%;
  height: auto;
}

ul, ol { padding: 0; list-style: none; }

/* The browser's own [hidden] rule is display:none, but ANY author rule that
   sets display beats it — so `element.hidden = true` silently does nothing on
   anything styled display:flex/grid/inline-flex. This restores the expected
   behaviour everywhere. */
[hidden] { display: none !important; }


/* Anyone navigating by keyboard needs to see where they are. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Visible only to screen readers — used for labels that would clutter the design. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Respect readers who have asked their system to reduce motion. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}


/* ===========================================================================
   03  TYPOGRAPHY
   =========================================================================== */
h1, h2, h3, h4 {
  font-family: var(--serif-font);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

h1 { font-size: clamp(2rem, 4.5vw, 3rem); }
h2 { font-size: clamp(1.5rem, 3vw, 2rem); }
h3 { font-size: 1.25rem; }

/* clamp(min, preferred, max) scales text with the viewport but never
   past the bounds. It replaces a stack of media queries. */

p { max-width: 70ch; }   /* long lines are hard to read; ~70 characters is comfortable */

a { color: inherit; }

/* Links inside body copy get the accent colour and a subtle underline. */
.prose a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}

/* A small, spaced, uppercase label. Used for dates, locations, section kickers. */
.eyebrow {
  font-family: var(--sans-font);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-text);
}

.lede {
  font-family: var(--serif-font);
  font-size: 1.25rem;
  line-height: 1.55;
  color: var(--muted-text);
}

/* Long-form body text: paragraphs separated by space rather than indentation. */
.prose > * + * { margin-top: var(--spacing-lg); }
.prose p { font-size: 1.0625rem; }


/* ===========================================================================
   04  LAYOUT HELPERS
   =========================================================================== */

/* Centres content and stops it touching the screen edge on small displays. */
.container {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--spacing-lg);
}

.container--prose { max-width: var(--max-width-prose); }

/* Caps line length for readability. Use it on a child INSIDE .container --
   never on the .container itself, or the block gets centred while its
   neighbours stay at the container's left edge. */
.measure { max-width: var(--max-width-prose); }

.section        { padding-block: var(--spacing-3xl); }
.section--tight { padding-block: var(--spacing-2xl); }
.section--alt   { background: var(--background-alt); }

/* A heading with a hairline rule beneath it. */
.section-title {
  padding-bottom: var(--spacing-xs);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--spacing-2xl);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--spacing);
  flex-wrap: wrap;
}

.text-link {
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
  transition: opacity var(--transition-fast) var(--ease);
}
.text-link:hover { opacity: 0.65; }


/* ===========================================================================
   05  SITE HEADER / NAVIGATION

       FOODYII                                    EXPLORE ⌄     ABOUT

   A wordmark, one menu and one link. Sections go in the menu, so the header
   is the same shape with three pages as with thirty.
   =========================================================================== */
.site-header {
  border-bottom: var(--rule);
  padding-block: var(--spacing-lg);
}

.site-header__inner {
  width: min(100% - (var(--page-padding) * 2), var(--content-width));
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-2xl);
}


/* --- Brand ---------------------------------------------------------------- */
.site-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-xs);
  text-decoration: none;
  color: var(--text-primary);
}

.site-brand__mark {
  display: inline-flex;
  /* The mark is 200×48. Sizing by width keeps all four symbols on one line
     and lets the height follow; a fixed height would let a replacement logo
     of a different ratio stretch the header. */
  width: 148px;
  color: var(--text-primary);
  transition: color var(--transition-fast) var(--ease);
}
.site-brand__mark svg { width: 100%; height: auto; display: block; }
.site-brand__logo     { max-height: 40px; width: auto; display: block; }

.site-brand__word {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
}

/* An uploaded logo speaks for itself; the wordmark beside it would be the
   site's name said twice. */
.site-brand:has(.site-brand__logo) .site-brand__word { display: none; }

.site-brand:hover .site-brand__mark { color: var(--accent); }


/* --- Navigation ----------------------------------------------------------- */
.site-nav {
  display: flex;
  align-items: center;
  gap: var(--spacing-2xl);
}

/* One shared look for the top-level link and the menu button, so "Explore"
   and "About" read as two of the same thing. */
.site-nav__link,
.explore__button {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-secondary);
  text-decoration: none;
  background: none;
  border: 0;
  padding: var(--spacing-2xs) 0;
  cursor: pointer;
  transition: color var(--transition-fast) var(--ease);
}

.site-nav__link:hover,
.explore__button:hover,
.explore--open .explore__button { color: var(--text-primary); }

.site-nav__link[aria-current="page"] {
  color: var(--text-primary);
  box-shadow: 0 var(--hairline) 0 var(--accent);
}


/* --- The Explore menu ----------------------------------------------------- */
.explore { position: relative; }

.explore__button {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-2xs);
}

.explore__chevron {
  width: 9px;
  height: auto;
  transition: transform var(--transition-normal) var(--ease);
}
.explore--open .explore__chevron { transform: rotate(180deg); }

.explore__menu {
  position: absolute;
  top: calc(100% + var(--spacing-xs));
  right: 0;
  z-index: var(--z-menu);

  min-width: 15rem;
  max-width: calc(100vw - (var(--page-padding-mobile) * 2));

  background: var(--background);
  border: var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
  padding: var(--spacing-2xs);

  /* The closed state. `hidden` is removed a frame before this class is added,
     so there is a starting point to animate from. */
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity var(--transition-normal) var(--ease),
              transform var(--transition-normal) var(--ease);
}

.explore__menu--shown { opacity: 1; transform: translateY(0); }

.explore__list { list-style: none; margin: 0; padding: 0; }

.explore__link {
  display: block;
  padding: var(--spacing-2xs) var(--spacing-xs);
  font-family: var(--font-display);
  font-size: var(--text-md);
  letter-spacing: 0.01em;
  color: var(--text-primary);
  text-decoration: none;
  border-radius: var(--radius);
  transition: background-color var(--transition-fast) var(--ease),
              color var(--transition-fast) var(--ease);
}

.explore__link:hover { background: var(--surface); }

.explore__link[aria-current="page"] {
  color: var(--accent-strong);
  background: var(--accent-wash);
}

/* ===========================================================================
   06  SITE FOOTER
   =========================================================================== */
.site-footer {
  border-top: 1px solid var(--border);
  margin-top: 0;
  padding-block: var(--spacing-2xl);
  font-size: 0.875rem;
  color: var(--muted-text);
}

.site-footer__inner {
  display: flex;
  justify-content: space-between;
  gap: var(--spacing);
  flex-wrap: wrap;
}

.site-footer a { text-decoration: none; border-bottom: 1px solid var(--border); }
.site-footer a:hover { color: var(--text); }


/* ===========================================================================
   07  HOME PAGE
   =========================================================================== */
.intro { padding-block: var(--spacing-3xl) var(--spacing-2xl); }

.intro h1 { margin-bottom: var(--spacing-lg); }

/* --- Featured review: one large image beside its text --------------------- */
.featured {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: var(--spacing-2xl);
  align-items: center;
}

.featured__image-link { display: block; overflow: hidden; }

.featured__image {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;   /* fills the box, cropping rather than squashing */
}

.featured__title {
  font-size: clamp(1.75rem, 3.5vw, 2.5rem);
  margin-block: var(--spacing-xs) var(--spacing-2xs);
}

.featured__title a { text-decoration: none; }
.featured__title a:hover { color: var(--accent); }

.featured__summary { color: var(--muted-text); margin-block: var(--spacing) var(--spacing-lg); }


/* ===========================================================================
   08  REVIEW CARDS
   ---------------------------------------------------------------------------
   Used on both the home page and the reviews page. Built by JavaScript in
   js/app.js -> createReviewCard(). No borders, no shadows: the photograph
   carries the card.
   =========================================================================== */
.card-grid {
  display: grid;
  gap: var(--spacing-2xl);
  /* auto-fill + minmax = "as many columns as fit, each at least 16rem".
     The layout adapts with no media queries. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
}

.card { display: flex; flex-direction: column; }

.card__image-link { display: block; overflow: hidden; background: var(--background-alt); }

.card__image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: opacity var(--transition-fast) var(--ease);
}

.card__image-link:hover .card__image { opacity: 0.88; }

.card__body { padding-top: var(--spacing); }

.card__location { margin-bottom: var(--spacing-2xs); }

.card__title {
  font-family: var(--serif-font);
  font-size: 1.375rem;
  font-weight: 500;
  line-height: 1.25;
  margin-bottom: var(--spacing-2xs);
}

.card__title a { text-decoration: none; }
.card__title a:hover { color: var(--accent); }

.card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--spacing-xs);
  font-size: 0.8125rem;
  color: var(--muted-text);
  margin-top: var(--spacing-2xs);
}

/* A thin divider between meta items, added with CSS instead of markup. */
.card__meta > * + *::before {
  content: "·";
  margin-right: var(--spacing-xs);
}

.card__summary {
  margin-top: var(--spacing-xs);
  font-size: 0.9375rem;
  color: var(--muted-text);
}

/* --- Michelin stars and score -------------------------------------------- */
.stars { color: var(--accent); letter-spacing: 0.15em; white-space: nowrap; }

.score {
  font-family: var(--serif-font);
  font-size: 1.0625rem;
  color: var(--text);
}
.score span { color: var(--muted-text); font-size: 0.75rem; }


/* ===========================================================================
   09  FILTER BAR  (reviews.html)
   =========================================================================== */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing);
  align-items: end;
  padding-bottom: var(--spacing-lg);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--spacing-2xl);
}

.filter-field { display: flex; flex-direction: column; gap: var(--spacing-2xs); }

.filter-field label {
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-text);
}

.filter-field select,
.filter-field input {
  font-family: var(--sans-font);
  font-size: 0.9375rem;
  color: var(--text);
  padding: 0.5em 0.75em;
  background: var(--background);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 10rem;
}

.filter-field select:focus,
.filter-field input:focus { border-color: var(--accent); outline: none; }

.filter-count {
  margin-left: auto;
  font-size: 0.8125rem;
  color: var(--muted-text);
}

.filter-reset {
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  padding: 0 0 2px;
  font-family: var(--sans-font);
  font-size: 0.8125rem;
  color: var(--muted-text);
  cursor: pointer;
}
.filter-reset:hover { color: var(--accent); border-bottom-color: var(--accent); }


/* ===========================================================================
   10  SINGLE REVIEW PAGE  (review.html)

   ---------------------------------------------------------------------------
   THE SHAPE OF THE PAGE

       header        location · name · stars · summary · thin meta line
       cover         one large photograph
       breakdown     the optional category scores
       body          the written review
       course journey  ← the visual centre
       gallery       whatever is not part of the meal
       facts         chef, price, address, links
       prev / next

   Everything below is hairlines, whitespace and type. There are no cards, no
   shadows and no filled blocks on this page: the photographs are the only
   colour, which is the whole idea.
   =========================================================================== */
.review-header { padding-block: var(--spacing-2xl) var(--spacing-lg); }

.review-header h1 { margin-block: var(--spacing-xs) var(--spacing-xs); }

.review-header__stars { font-size: 1.125rem; }

/* Cuisine · Visited · Score. Interpuncts are drawn by the stylesheet rather
   than being characters in the text, so a missing piece never leaves a
   dangling separator behind. */
.review-header__meta {
  margin-top: var(--spacing-md);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.review-header__meta span + span::before {
  content: "·";
  margin-inline: 0.6em;
  color: var(--text-faint);
}

.review-cover {
  width: 100%;
  max-height: 70vh;
  object-fit: cover;
  margin-bottom: var(--spacing-2xl);
}


/* --- The score breakdown --------------------------------------------------
   A real table, styled down to almost nothing: a hairline between rows, the
   labels quiet, the numbers aligned in one narrow column.

   `font-variant-numeric: tabular-nums` is what makes 9.5 and 10 line up —
   most typefaces give digits different widths by default, so a column of
   proportional numerals looks subtly ragged no matter how it is aligned.
   -------------------------------------------------------------------------- */
.score-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--spacing-2xl);
  border-top: var(--rule);
}

.score-table th,
.score-table td {
  padding-block: var(--score-row-padding);
  border-bottom: var(--rule);
  vertical-align: baseline;
}

.score-table__label {
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: var(--weight-normal);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}

.score-table__value {
  text-align: right;
  width: var(--score-value-column);
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The overall score. Set apart by a heavier rule above it and slightly larger
   type — it is the number the categories are explaining, not one of them. */
.score-table__overall th,
.score-table__overall td {
  border-top: 1px solid var(--border-strong);
  border-bottom: 0;
  padding-top: var(--spacing-md);
  color: var(--text-primary);
}
.score-table__overall .score-table__value { font-size: var(--text-lg); }


/* --- Facts: chef, price, address, links ----------------------------------- */
/* Bottom rule only. The section heading above already draws a line, and two
   rules with an empty band between them read as a missing row. */
.review-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--spacing-lg);
  padding-bottom: var(--spacing-lg);
  border-bottom: var(--rule);
}

/* The three closing sections, each given the same distance from what came
   before it, so the foot of the page has a rhythm rather than an accident. */
#review-gallery-section,
#review-facts-section { margin-top: var(--section-spacing); }

/* The heading's own margin-bottom is the gap; the grid does not need a second
   one on top of it. */
#review-gallery-section .gallery { margin-top: 0; }

.review-facts dt {
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--spacing-2xs);
}

.review-facts dd {
  font-family: var(--font-display);
  font-size: 1.0625rem;
}

.review-facts a { color: var(--accent); text-decoration: none; border-bottom: 1px solid currentColor; }


/* ===========================================================================
   10b  THE COURSE JOURNEY

   The meal, in order. Each course is:

       01                  a small number
       HIRAMASA            the dish
       an optional line    the subtitle
       [ photograph ]      large, given room
       commentary          the writing
       Score 9.4           optional, quiet

   No borders around a course, no background, no card. What separates two
   courses is the gap between them — the largest gap on the site — plus one
   hairline. That is what stops a long menu reading as a table of rows.
   =========================================================================== */
.course-journey {
  margin-top: var(--section-spacing);
  margin-bottom: var(--section-spacing);
}

.course {
  /* Its own container, so a full-width course photograph can be wider than
     the text it belongs to without either one leaving the page gutters. */
  width: min(100% - (var(--page-padding) * 2), var(--content-width));
  margin-inline: auto;
  padding-top: var(--course-gap);
}

/* A hairline between courses, and never above the first one: the section
   heading is already the line above that. */
.course + .course { border-top: var(--rule); }

.course__number {
  font-family: var(--font-body);
  font-size: var(--course-number-size);
  letter-spacing: var(--tracking-wider);
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--spacing-xs);
}

.course__name {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  line-height: var(--line-height-tight);
  margin: 0;
}

.course__subtitle {
  margin-top: var(--spacing-2xs);
  font-size: var(--text-md);
  color: var(--text-secondary);
  font-style: italic;
}

/* Stacked — on a phone, on a full-width course, or on any course with no
   photograph to put beside anything. The spread above overrides the cap. */
.course__figure {
  margin: var(--spacing-lg) 0 0;
  max-width: var(--course-image-max);
}

/* THE PHOTOGRAPH IS NEVER CROPPED AND NEVER STRETCHED.

   `width: 100%` with `height: auto` is the whole rule: the picture fills the
   column it is in and takes whatever height its own proportions ask for.
   There is no fixed height anywhere on a course photograph, which is why
   `object-fit` is not needed — nothing is ever asked to fit a box it does not
   match, so nothing is ever cut off the side of a dish. A size preset changes
   how WIDE the column is; the file and its shape are untouched. */
.course__figure img {
  width: 100%;
  height: auto;
  display: block;
}

.course__text { margin-top: var(--spacing-lg); max-width: var(--measure); }

.course__commentary { margin: 0 0 var(--spacing-md); }
.course__commentary:last-child { margin-bottom: 0; }

/* The score sits under the commentary as a quiet label/number pair, not as a
   badge and not as a bar. */
.course__score {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-xs);
  margin: var(--spacing-md) 0 0;
  padding-top: var(--spacing-xs);
  border-top: var(--rule);
  max-width: 12rem;
}
.course__score-label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
}
.course__score-value {
  margin-left: auto;
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-variant-numeric: tabular-nums;
}

/* --- THE EDITORIAL SPREAD --------------------------------------------------

   Above 820px, a course with a photograph and a size other than `full`
   becomes two columns: the picture on one side, and the WHOLE of the writing
   on the other — number, dish, subtitle, commentary and score together, as
   one piece of text against one picture.

   Everything below hangs off three rules:

   1. ONE DOM ORDER, ALWAYS: heading, figure, text. That is the order the page
      reads with no CSS, on a phone, and to a screen reader. grid-template-
      areas moves the regions; the markup never moves. It is also why left and
      right differ by one line each rather than by re-ordering anything.

   2. `grid-template-rows: auto 1fr` with `align-items: start`. Two auto rows
      would share a tall photograph equally between them, pushing the
      commentary halfway down the column and opening a hole under the dish
      name. Sizing the first row to the heading and letting the second absorb
      the rest puts the text directly under the heading, at the TOP of the
      photograph, whatever height the photograph is.

   3. NOTHING CONSTRAINS THE TEXT. No height, no max-height, no overflow.
      Write four paragraphs and the column simply grows past the picture,
      which is what a magazine does. There is no scroll box, and nothing is
      ever truncated.
   -------------------------------------------------------------------------- */
@media (min-width: 820px) {
  .course--split {
    display: grid;
    column-gap: var(--course-split-gap);
    row-gap: var(--spacing-md);
    align-items: start;
    grid-template-rows: auto 1fr;
  }

  /* The three proportions, as two grid tracks. `minmax(0, …)` rather than a
     bare fraction: a track's minimum defaults to its content, and one long
     unbroken word would otherwise push the column past the page. */
  .course--size-small.course--photo-left {
    grid-template-columns: minmax(0, var(--course-photo-small)) minmax(0, var(--course-text-small));
  }
  .course--size-small.course--photo-right {
    grid-template-columns: minmax(0, var(--course-text-small)) minmax(0, var(--course-photo-small));
  }
  .course--size-medium.course--photo-left {
    grid-template-columns: minmax(0, var(--course-photo-medium)) minmax(0, var(--course-text-medium));
  }
  .course--size-medium.course--photo-right {
    grid-template-columns: minmax(0, var(--course-text-medium)) minmax(0, var(--course-photo-medium));
  }
  .course--size-large.course--photo-left {
    grid-template-columns: minmax(0, var(--course-photo-large)) minmax(0, var(--course-text-large));
  }
  .course--size-large.course--photo-right {
    grid-template-columns: minmax(0, var(--course-text-large)) minmax(0, var(--course-photo-large));
  }

  .course--split.course--photo-left {
    grid-template-areas:
      "figure heading"
      "figure text";
  }
  .course--split.course--photo-right {
    grid-template-areas:
      "heading figure"
      "text    figure";
  }

  .course--split .course__heading { grid-area: heading; }
  .course--split .course__text    { grid-area: text; margin-top: 0; max-width: none; }

  .course--split .course__figure {
    grid-area: figure;
    margin-top: 0;
    /* The column IS the size. A cap meant for a stacked picture would fight
       it — a Large photograph in an 1100px row is over 46rem wide. */
    max-width: none;
  }
}

/* A FULL-WIDTH photograph takes the whole content width. This is the one size
   that is a hero rather than a spread: the heading sits above it and the
   writing follows underneath, at the page's normal reading measure. */
.course--size-full .course__figure { max-width: var(--course-image-max-full); }


/* --- Previous / next ------------------------------------------------------
   Two link blocks on one row: Previous on the left, Next on the right. When
   only one exists it keeps its own side — `grid-column` pins the Next link to
   the second column so a first review does not have its single Next link
   sitting on the left where Previous would have been.
   -------------------------------------------------------------------------- */
.review-nav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--spacing-lg);
  margin-top: var(--section-spacing);
  margin-bottom: var(--section-spacing);
  padding-top: var(--spacing-lg);
  border-top: var(--rule);
}

.review-nav__link {
  text-decoration: none;
  display: block;
  padding-block: var(--spacing-xs);
  color: inherit;
}
.review-nav__next { grid-column: 2; text-align: right; }

.review-nav__direction {
  display: block;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--transition-fast) var(--ease);
}

/* The arrow slides a couple of pixels on hover — the smallest gesture that
   still says "this goes somewhere". */
.review-nav__arrow {
  display: inline-block;
  transition: transform var(--transition-fast) var(--ease);
}
.review-nav__previous .review-nav__arrow { margin-right: 0.4em; }
.review-nav__next     .review-nav__arrow { margin-left: 0.4em; }

.review-nav__link:hover .review-nav__previous .review-nav__arrow,
.review-nav__previous:hover .review-nav__arrow,
.review-nav__previous:focus-visible .review-nav__arrow { transform: translateX(-3px); }
.review-nav__next:hover .review-nav__arrow,
.review-nav__next:focus-visible .review-nav__arrow { transform: translateX(3px); }

.review-nav__name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  margin-top: var(--spacing-2xs);
  transition: color var(--transition-fast) var(--ease);
}

.review-nav__location {
  display: block;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-top: var(--spacing-3xs);
}

.review-nav__link:hover .review-nav__name,
.review-nav__link:focus-visible .review-nav__name { color: var(--accent); }
.review-nav__link:hover .review-nav__direction { color: var(--text-primary); }


/* ===========================================================================
   10c  COMPARISON  (compare.html)

   ---------------------------------------------------------------------------
   ONE ATTRIBUTE PER ROW, ONE RESTAURANT PER COLUMN

   Hairlines between rows, nothing between columns, and no fill anywhere. The
   only heavy element on the page is the photograph at the top of each column.

   ONE STICKY EDGE, AND WHY NOT TWO

   The LABEL COLUMN sticks to the left, so scrolling across never loses which
   row you are reading. That is the one that matters on a phone.

   The head row is NOT sticky, and cannot be. `position: sticky` sticks to the
   nearest scrolling ancestor, and CSS does not allow `overflow-x: auto` with
   `overflow-y: visible` — the visible axis is forced to `auto`. So the moment
   this box scrolls sideways it becomes a vertical scroll container too, and a
   head row set to `top: 0` sticks inside a box that never scrolls vertically:
   it does nothing. Worse, Chromium counts those stuck cells into
   documentElement.scrollWidth, which reports a horizontal overflow the page
   does not actually have. Left off deliberately, not forgotten.

   `position: sticky` on a table cell is only reliable with
   `border-collapse: separate`. With `collapse`, the borders belong to the
   table rather than to the cells, and a stuck cell leaves its own borders
   behind. So: separate, zero spacing, and every rule drawn as a cell border.

   THE OVERFLOW IS ON .compare-scroll, NOT ON THE PAGE. Four columns on a
   390px phone scroll inside that box; <body> never gains a horizontal
   scrollbar, which is the difference between a wide table and a broken page.
   =========================================================================== */
.compare-scroll {
  overflow-x: auto;

  /* `overflow-x: auto` alone is not quite enough. A table wider than this box
     is clipped and the page does not actually scroll — but Chromium still
     counts the table's width into documentElement.scrollWidth, which is what
     a responsive check reads to decide whether a page overflows. `contain:
     paint` says explicitly "nothing here paints outside this box", and the
     reported width then matches what the page really does. It costs nothing:
     the box already clips, and there is nothing positioned inside it for the
     new containing block to affect. */
  contain: paint;

  /* Momentum scrolling on iOS, and a scrollbar that does not overlay the
     last row on macOS. */
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--spacing-xs);
  margin-bottom: var(--spacing-2xl);
}

.compare-table {
  --compare-columns: 2;
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  /* Wide enough that the columns stay readable; the wrapper scrolls when
     that is more than the screen. */
  min-width: calc(var(--compare-label-column) + (var(--compare-column) * var(--compare-columns)));
  table-layout: fixed;
}

.compare-table th,
.compare-table td {
  border-bottom: var(--rule);
  padding: var(--compare-row-padding) var(--spacing-md);
  vertical-align: top;
  text-align: left;
}

/* --- The head: photograph, name, place, remove ---------------------------- */
.compare-table__head,
.compare-table__corner {
  background: var(--background);
  border-bottom: 1px solid var(--border-strong);
  font-weight: var(--weight-normal);
  padding-top: var(--spacing-md);
}

.compare-table__image {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  margin-bottom: var(--spacing-sm);
  background: var(--surface);
}
.compare-table__image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.compare-table__name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: var(--line-height-tight);
  margin: 0;
}
.compare-table__name a { color: inherit; text-decoration: none; }
.compare-table__name a:hover { color: var(--accent); }

.compare-table__location {
  margin: var(--spacing-3xs) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.compare-table__remove {
  margin-top: var(--spacing-xs);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  padding: 0 0 2px;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--transition-fast) var(--ease);
}
.compare-table__remove:hover { color: var(--danger); border-bottom-color: var(--danger); }

/* --- The label column ------------------------------------------------------
   Sticky to the left, on top of the cells that scroll under it, and painted
   with the page colour so they really do disappear behind it rather than
   showing through.
   -------------------------------------------------------------------------- */
.compare-table__label,
.compare-table__corner {
  position: sticky;
  left: 0;
  width: var(--compare-label-column);
  background: var(--background);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  padding-left: 0;
}
/* The corner belongs to the head row as well as to the label column, so it
   sits above the plain labels below it. */
.compare-table__corner { z-index: 2; }
.compare-table__label  { z-index: 1; }

.compare-table__cell {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.compare-table__cell--empty { color: var(--text-faint); }

/* --- The breakdown group -------------------------------------------------- */
.compare-table__group-heading th {
  padding-top: var(--spacing-xl);
  padding-left: 0;
  border-bottom: 1px solid var(--border-strong);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  background: var(--background);
}

.compare-table tfoot td { border-bottom: 0; padding-top: var(--spacing-lg); }

.compare-table__read {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}
.compare-table__read:hover { color: var(--accent-strong); }

/* --- Anything that could not be shown ------------------------------------- */
.compare-note {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding-block: var(--spacing-md);
  border-top: var(--rule);
}
.compare-note__slugs { font-family: var(--font-body); color: var(--text-primary); }

/* --- The picker ------------------------------------------------------------
   Text buttons in a quiet grid. Not cards: choosing a restaurant to compare
   is a list of names, and a grid of boxes would compete with the comparison
   it is meant to feed.
   -------------------------------------------------------------------------- */
.compare-picker { margin-top: var(--section-spacing-tight); }

.compare-picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: var(--hairline);
  background: var(--border);
  border-block: var(--rule);
}

.compare-picker__item {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--background);
  border: 0;
  padding: var(--spacing-md) var(--spacing-sm);
  cursor: pointer;
  font-family: inherit;
  color: inherit;
  transition: background var(--transition-fast) var(--ease);
}
.compare-picker__item:hover { background: var(--accent-wash); }

.compare-picker__name {
  display: block;
  font-family: var(--font-display);
  font-size: 1.0625rem;
}
.compare-picker__location {
  display: block;
  margin-top: var(--spacing-3xs);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.compare-picker__note { color: var(--text-secondary); font-size: var(--text-sm); }


/* ===========================================================================
   10d  THE COMPARISON TICK AND TRAY  (reviews.html)

   The tick sits on a review card; the tray appears along the bottom of the
   window once something is ticked. The tray is fixed rather than sticky so it
   does not push the last row of the grid around as it appears and disappears.
   =========================================================================== */
.compare-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-2xs);
  margin-top: var(--spacing-sm);
  cursor: pointer;
  user-select: none;
}

/* The real checkbox, kept in the page (never display:none, which would take
   it out of the tab order) and drawn as a small square outline. */
.compare-toggle__input {
  appearance: none;
  -webkit-appearance: none;
  width: 0.85rem;
  height: 0.85rem;
  margin: 0;
  border: 1px solid var(--border-strong);
  background: var(--background);
  cursor: pointer;
  display: grid;
  place-content: center;
  transition: border-color var(--transition-fast) var(--ease);
}
.compare-toggle__input::after {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  background: var(--accent);
  transform: scale(0);
  transition: transform var(--transition-fast) var(--ease);
}
.compare-toggle__input:checked { border-color: var(--accent); }
.compare-toggle__input:checked::after { transform: scale(1); }

.compare-toggle__text {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--transition-fast) var(--ease);
}
.compare-toggle:hover .compare-toggle__text { color: var(--text-primary); }

.compare-tray {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-menu);
  background: var(--background);
  border-top: 1px solid var(--border-strong);
  box-shadow: var(--shadow-menu);
}

.compare-tray__inner {
  width: min(100% - (var(--page-padding) * 2), var(--content-width));
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: var(--spacing-lg);
  padding-block: var(--spacing-sm);
}

.compare-tray__count {
  margin: 0;
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
}

.compare-tray__open {
  margin-left: auto;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}
.compare-tray__open[aria-disabled="true"] {
  color: var(--text-faint);
  cursor: default;
  border-bottom-color: transparent;
}

.compare-tray__clear {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  cursor: pointer;
}
.compare-tray__clear:hover { color: var(--danger); }


/* ===========================================================================
   11  GALLERY AND FIGURES
   ---------------------------------------------------------------------------
   Gallery images keep their natural proportions, so a portrait photograph
   stays portrait instead of being cropped to match its neighbours.
   =========================================================================== */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: var(--spacing-lg);
  margin-top: var(--spacing-3xl);
  align-items: start;
}

figure { margin: 0; }

figcaption {
  margin-top: var(--spacing-xs);
  font-size: 0.8125rem;
  color: var(--muted-text);
  line-height: 1.5;
}


/* ===========================================================================
   12  ABOUT PAGE
   =========================================================================== */



/* ===========================================================================
   13  STATES — loading, error, empty
   ---------------------------------------------------------------------------
   Every page that loads data can end up in one of these. Styling them
   properly is part of the job, not an afterthought.
   =========================================================================== */
.state {
  padding-block: var(--spacing-3xl);
  text-align: center;
  color: var(--muted-text);
}

.state h2 { margin-bottom: var(--spacing-xs); color: var(--text); }
.state p { margin-inline: auto; }

.state code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--background-alt);
  padding: 0.15em 0.4em;
}

.state--error h2 { color: var(--accent); }


/* ===========================================================================
   14  RESPONSIVE
   ---------------------------------------------------------------------------
   The grids above already reflow on their own. These few rules handle the
   layouts that need an explicit change on smaller screens.
   =========================================================================== */

/* --- Tablet and below ----------------------------------------------------- */
@media (max-width: 800px) {
  .featured   { grid-template-columns: 1fr; gap: var(--spacing-lg); }
}

/* --- Phone ---------------------------------------------------------------- */
@media (max-width: 560px) {
  :root {
    --spacing: 0.875rem;          /* tighten the whole spacing scale at once */
    --font-size-base: 1rem;
  }

  /* The header stays one row on a phone — a wordmark and a menu button fit
     comfortably. Only the gaps and the mark shrink. */
  .site-header__inner { gap: var(--spacing-lg); }
  .site-nav           { gap: var(--spacing-lg); }
  .site-brand         { gap: var(--spacing-2xs); }
  .site-brand__mark   { width: 96px; }
  .site-brand__word   { font-size: var(--text-md); }

  .filters       { flex-direction: column; align-items: stretch; }
  .filter-field select,
  .filter-field input { width: 100%; }
  .filter-count  { margin-left: 0; }

  /* One column, Previous above Next. `grid-column: auto` undoes the desktop
     rule that pins Next to the second column — without it a first review's
     lone Next link would be placed in a column that no longer exists. */
  .review-nav        { grid-template-columns: 1fr; gap: var(--spacing-md); }
  .review-nav__next  { grid-column: auto; text-align: left; }

  /* The gaps between courses come down with the rest of the spacing scale,
     but not as far — the sequence still has to read as separate movements. */
  .course        { padding-top: var(--course-gap-mobile); }
  .course__name  { font-size: var(--text-lg); }
  .course__text  { margin-top: var(--spacing-md); }

  /* ON A PHONE THE SIZE PRESETS DO NOT APPLY.

     The spread above is already off below 820px, so every course is one
     column here — number, dish, subtitle, photograph, commentary, score, in
     DOM order. What this adds is the guarantee that a Small photograph is
     not a small photograph on a phone: `max-width: none` lets the picture
     take the content width whatever size it asked for, because a 38%
     column at 390px is a thumbnail, not a layout. Left and right stop
     meaning anything, which is correct — there is only one column to be on.

     The proportions are unchanged; only the width the picture may take is. */
  .course__figure { max-width: none; }

  .review-cover { max-height: 50vh; }

  /* --- Comparison on a phone ---------------------------------------------
     The table keeps all four columns and scrolls sideways INSIDE
     .compare-scroll, with the label column stuck to the left edge. The
     alternative — a picker that shows two at a time — hides half the answer
     behind another control; a swipe does not.

     The columns narrow but never below a width that holds "18/20" and a
     two-word cuisine on one line. */
  .compare-table {
    --compare-column: 8.5rem;
    --compare-label-column: 6.5rem;
  }
  .compare-table th,
  .compare-table td { padding: var(--spacing-sm) var(--spacing-xs); }
  .compare-table__name { font-size: 1rem; }

  .compare-tray__inner { gap: var(--spacing-md); }
  .compare-tray__count { font-size: var(--text-xs); }
}

/* The tray floats over the page, so the page needs somewhere for its last row
   to go. The class is put on <body> by js/compare-select.js rather than being
   inferred with :has(), which not every browser in use understands — and the
   consequence of it not working would be the tray sitting on the last card. */
body.has-compare-tray { padding-bottom: 4.5rem; }

/* --- Print ---------------------------------------------------------------- */
@media print {
  .site-nav, .filters, .review-nav,
  .compare-tray, .compare-toggle, .compare-table__remove, .compare-picker { display: none; }
  /* A printed comparison cannot be scrolled, so let it be as wide as it is
     and let the page break it. */
  .compare-scroll { overflow: visible; }
  body { background: #fff; color: #000; }
}

/* ===========================================================================
   15  SITE SETTINGS PRESETS  (Stage 6.5)
   ---------------------------------------------------------------------------
   The admin's layout controls store a WORD — "center", "wide", "large" — and
   the frontend turns each word into one of the class names below. No CSS value
   ever comes from the database.

   That is what keeps the layout controls safe. The worst a bad value can do is
   produce a class name that matches nothing here, leaving the default.
   =========================================================================== */

/* --- Home hero: alignment ------------------------------------------------ */
.intro--align-left   { text-align: left; }
.intro--align-center { text-align: center; }
.intro--align-right  { text-align: right; }

/* Centring the text also has to centre the block itself, or a narrow column
   sits centred-text but left-placed. */
.intro--align-center .measure { margin-inline: auto; }
.intro--align-right  .measure { margin-inline: auto 0; }

/* p has max-width: 70ch from the typography section, which would otherwise
   keep the paragraph left-hugging inside a centred block. */
.intro--align-center .measure p { margin-inline: auto; }
.intro--align-right  .measure p { margin-inline: auto 0; }

/* --- Home hero: width ---------------------------------------------------- */
.intro--width-narrow .measure { max-width: var(--max-width-prose); }
.intro--width-normal .measure { max-width: 52rem; }
.intro--width-wide   .measure { max-width: 100%; }

/* --- Home hero: spacing -------------------------------------------------- */
.intro--space-above-small  { padding-top: var(--spacing-lg); }
.intro--space-above-medium { padding-top: var(--spacing-2xl); }
.intro--space-above-large  { padding-top: var(--spacing-3xl); }

.intro--space-below-small  { padding-bottom: var(--spacing-lg); }
.intro--space-below-medium { padding-bottom: var(--spacing-2xl); }
.intro--space-below-large  { padding-bottom: var(--spacing-3xl); }

/* --- Footer layout presets ----------------------------------------------- */
.site-footer__inner--split {
  flex-direction: row;
  justify-content: space-between;
  align-items: baseline;
}

.site-footer__inner--centered {
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--spacing-xs);
}

.site-footer__inner--stacked {
  flex-direction: column;
  gap: var(--spacing-xs);
}

.site-footer--align-left   .site-footer__inner { text-align: left; }
.site-footer--align-center .site-footer__inner { text-align: center; }
.site-footer--align-right  .site-footer__inner { text-align: right; }

.site-footer--align-center .site-footer__inner--stacked { align-items: center; }
.site-footer--align-right  .site-footer__inner--stacked { align-items: flex-end; }

/* The footer's link row, when there is more than one. */
.site-footer__links {
  display: flex;
  gap: var(--spacing-lg);
  flex-wrap: wrap;
}
.site-footer__inner--centered .site-footer__links,
.site-footer--align-center .site-footer__links { justify-content: center; }
.site-footer--align-right .site-footer__links { justify-content: flex-end; }

.site-footer__text { color: var(--muted-text); max-width: 60ch; }

/* --- Informational pages -------------------------------------------------- */
.page-body { padding-block: var(--spacing-3xl); }
.page-body h1 { margin-bottom: var(--spacing-lg); }
.page-body .lede { margin-bottom: var(--spacing-2xl); }


/* ===========================================================================
   15  THE LANDING

   A full-viewport cream field with the mark on it, which dissolves into the
   page underneath. See js/landing.js for the sequence and theme.css for the
   timings — there are no durations written in this section, only tokens.
   =========================================================================== */
.landing {
  position: fixed;
  inset: 0;
  z-index: var(--z-landing);

  display: grid;
  place-items: center;
  background: var(--background);

  opacity: 1;
  transition: opacity var(--landing-fade-out) var(--ease);
}

/* The exit. The page underneath rises at the same time (below), so the two
   cross rather than one replacing the other. */
.landing--leaving { opacity: 0; pointer-events: none; }

.landing__inner {
  width: min(100% - (var(--page-padding-mobile) * 2), var(--content-width));
  display: grid;
  place-items: center;
}

.landing__mark {
  /* Width only. Height follows from the artwork's own proportions, so the
     mark can never be stretched, squashed or cropped at any screen size —
     and --landing-mark-width in theme.css is the one place its size is set. */
  width: var(--landing-mark-width);
  max-width: calc(100vw - (var(--page-padding-mobile) * 2));
  color: var(--text-primary);

  opacity: 0;
  transform: translateY(var(--landing-mark-travel));
  transition: opacity var(--landing-fade-in) var(--ease),
              transform var(--landing-fade-in) var(--ease);
}

/* ONE mark, whether it arrives as a file or as the placeholder SVG. The four
   symbols are never animated separately: the whole thing fades in together,
   because it is one piece of artwork and not four icons. */
.landing__mark img,
.landing__mark svg {
  display: block;
  width: 100%;
  height: auto;
}

.landing--in .landing__mark { opacity: 1; transform: translateY(0); }

/* While the landing is up: hold the page still underneath, and start it very
   slightly down and transparent so it can rise into place as the landing
   goes. `is-landing` is only ever added by JavaScript, so a browser that does
   not run it never hides anything. */
.is-landing { overflow: hidden; }
.is-landing body > *:not(.landing) { opacity: 0; transform: translateY(8px); }

.is-landing-done body > *:not(.landing) {
  opacity: 1;
  transform: none;
  transition: opacity var(--landing-fade-out) var(--ease),
              transform var(--landing-fade-out) var(--ease);
}


/* ===========================================================================
   16  THE INDEX CONTROLS

   Search, filters and the view switch on reviews.html. Meant to read as the
   head of an index rather than as a control panel: no boxes, no shadows, no
   pill buttons — a rule above and a rule below, and a lot of air.
   =========================================================================== */
.index-controls {
  border-block: var(--rule);
  padding-block: var(--spacing-lg);
  margin-bottom: var(--spacing-2xl);
}

/* --- The search line ------------------------------------------------------ */
.index-search {
  position: relative;
  margin-bottom: var(--spacing-lg);
}

.index-search input {
  width: 100%;
  border: 0;
  border-bottom: var(--hairline) solid var(--border);
  background: transparent;
  padding: var(--spacing-xs) 0;

  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--text-primary);

  transition: border-color var(--transition-normal) var(--ease);
}
.index-search input::placeholder { color: var(--text-faint); }
.index-search input:focus        { outline: none; border-bottom-color: var(--text-primary); }

/* The browser's own clear button in WebKit is a grey circle that belongs to a
   different design. */
.index-search input::-webkit-search-cancel-button { display: none; }


/* --- The filter row ------------------------------------------------------- */
.index-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--spacing-lg);
  align-items: end;
}

.index-field { display: flex; flex-direction: column; gap: var(--spacing-3xs); }

.index-field label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A select with its chrome taken off: no border, no box, no arrow of the
   operating system's choosing. The chevron is a background image so that the
   element stays a real <select>, with the platform's own dropdown, its
   keyboard behaviour and its mobile picker — all of which are better than
   anything a custom control would reimplement. */
.index-field select {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background-color: transparent;
  padding: var(--spacing-3xs) 1.25rem var(--spacing-3xs) 0;

  font-family: var(--font-display);
  font-size: var(--text-md);
  color: var(--text-primary);
  cursor: pointer;

  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1 5 5 9 1' fill='none' stroke='%236B6B70' stroke-width='1.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0 center;
  background-size: 9px 6px;

  transition: color var(--transition-fast) var(--ease);
}
.index-field select:focus { outline: none; }

/* A filter that is doing something looks different from one that is not. */
.index-field select:not([data-default="true"]) { color: var(--accent-strong); }


/* --- The bottom line: count, clear, view switch --------------------------- */
.index-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-lg);
  flex-wrap: wrap;
  margin-top: var(--spacing-lg);
}

.index-status__left {
  display: flex;
  align-items: baseline;
  gap: var(--spacing-md);
  flex-wrap: wrap;
}

.filter-count {
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  color: var(--text-secondary);
  margin: 0;
}

.filter-clear {
  background: none;
  border: 0;
  padding: 0;
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  border-bottom: var(--hairline) solid currentColor;
  transition: color var(--transition-fast) var(--ease);
}
.filter-clear:hover { color: var(--accent-strong); }


/* --- Catalog / Map -------------------------------------------------------- */
.view-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--spacing-md);
}

.view-switch__label {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
}

.view-switch__options { display: inline-flex; gap: var(--spacing-md); }

.view-switch__option {
  background: none;
  border: 0;
  padding: var(--spacing-3xs) 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  cursor: pointer;
  border-bottom: var(--hairline) solid transparent;
  transition: color var(--transition-fast) var(--ease),
              border-color var(--transition-fast) var(--ease);
}
.view-switch__option:hover { color: var(--text-secondary); }

/* aria-pressed is the state a screen reader reads, so it is also the state
   the eye reads. One source of truth, no parallel "active" class. */
.view-switch__option[aria-pressed="true"] {
  color: var(--text-primary);
  border-bottom-color: var(--accent);
}


/* ===========================================================================
   17  THE MAP
   =========================================================================== */
.map { margin: 0; position: relative; }

/* --- The surface: the map's frame ----------------------------------------

   A plain wrapper. No cursor, no touch-action and no pointer handlers
   anywhere: the map is a fixed canvas, so wheel, drag and pinch are simply
   never intercepted and the page scrolls over it exactly as it does over a
   photograph. */
.map__surface {
  position: relative;
  display: block;
  border: var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}

.map__canvas {
  display: block;
  width: 100%;
  /* The drawing adopts whatever shape this box has (see map.js), so this is
     free to be chosen for reading rather than for the map's proportions. */
  height: clamp(20rem, 46vh, 32rem);
  background: var(--map-sea);
}

.map__sea  { fill: var(--map-sea); }
.map__land { fill: var(--map-land); stroke: var(--map-coast); stroke-width: 0.6; }

/* --- Markers ---------------------------------------------------------------
   A marker is a <g> holding two circles that share one centre: an invisible
   hit target, and the dot you see inside it.

   The three radii arrive as custom properties on the <svg>, set by js/map.js,
   which converts the screen-pixel sizes in theme.css into the canvas's units.
   That is the only reason they are not written here directly.

   NOTHING HERE MOVES A MARKER. There is no transform, no translate and no
   transform-origin on any of these rules — hover and selection change `r`,
   and a circle's radius grows around its own cx/cy, so the centre is fixed by
   construction. Scaling with a transform would not be: an SVG element's
   default transform-origin is the viewport's (0,0), so a hovered dot would
   leap across the map.
   -------------------------------------------------------------------------- */
.map__marker { cursor: pointer; }

/* Transparent, not `none`: a fill of `none` is not hit-tested, and
   `pointer-events: all` makes the target work regardless. */
/* No `r` here on purpose. Each hit circle carries its own radius as an
   attribute, because it is capped by how close that marker's nearest
   neighbour is (see hitRadiusFor in js/map.js) — a blanket CSS radius would
   override all of them back to the full size and put Cádiz's target back on
   top of Lisbon. */
.map__marker-hit {
  fill: transparent;
  pointer-events: all;
}

.map__marker-dot {
  fill: var(--map-marker);
  stroke: var(--background);
  stroke-width: 1.5;
  /* The ring stays a hairline at every window width, like the coastline. */
  vector-effect: non-scaling-stroke;
  r: var(--map-marker-r);
  /* The hit circle above already covers this area; letting the dot take
     events too would mean mouseleave firing as the pointer crossed between
     them. */
  pointer-events: none;
  /* The frame is fixed, so a marker's radius only ever changes because it
     was hovered or chosen — one at a time, which is exactly when animating
     it looks right. */
  transition: r var(--transition-normal) var(--ease),
              fill var(--transition-fast) var(--ease);
}

.map__marker--hover .map__marker-dot,
.map__marker--active .map__marker-dot {
  fill: var(--map-marker-hover);
  r: var(--map-marker-r-active);
}


/* --- The preview ---------------------------------------------------------- */
.map__preview-slot { position: absolute; left: var(--spacing-md); bottom: var(--spacing-md); }

.map-preview {
  display: flex;
  gap: var(--spacing-md);
  align-items: stretch;

  width: min(24rem, calc(100% - var(--spacing-lg)));
  background: var(--background);
  border: var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-float);
  overflow: hidden;
}

.map-preview__image { flex: 0 0 6.5rem; }
.map-preview__image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.map-preview__body {
  padding: var(--spacing-md);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-3xs);
  min-width: 0;
}

.map-preview__name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  margin: 0;
}

.map-preview__place,
.map-preview__facts {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.map-preview__link {
  margin-top: var(--spacing-2xs);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  border-bottom: var(--hairline) solid currentColor;
  align-self: flex-start;
}


/* --- The list beside the map ---------------------------------------------- */
.map-list {
  list-style: none;
  margin: var(--spacing-lg) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--spacing-3xs) var(--spacing-lg);
}

.map-list__button {
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: var(--hairline) solid var(--border);
  padding: var(--spacing-xs) 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  transition: border-color var(--transition-fast) var(--ease);
}
.map-list__button:hover { border-bottom-color: var(--border-strong); }

.map-list__name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  color: var(--text-primary);
}
.map-list__place { font-size: var(--text-xs); color: var(--text-secondary); }

.map-list__item--active .map-list__name   { color: var(--accent-strong); }
.map-list__item--active .map-list__button { border-bottom-color: var(--accent); }


/* --- Reviews with no coordinates ------------------------------------------ */
.map-missing {
  margin-top: var(--spacing-2xl);
  padding-top: var(--spacing-lg);
  border-top: var(--rule);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.map-missing__intro { margin: 0 0 var(--spacing-xs); }
.map-missing__list  { margin: 0; padding-left: var(--spacing-md); }
.map-missing__list a { color: var(--text-primary); }


/* ===========================================================================
   18  FOCUS

   One focus treatment for the whole site, defined once. :focus-visible rather
   than :focus, so it appears for keyboard users and not on every mouse click.
   =========================================================================== */
a:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible,
[tabindex]:focus-visible {
  outline: var(--hairline) solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: var(--radius);
}


/* ===========================================================================
   19  RESPONSIVE — the additions above
   =========================================================================== */
@media (max-width: 760px) {
  .index-filters  { grid-template-columns: repeat(2, 1fr); gap: var(--spacing-md); }
  .index-status   { flex-direction: column; align-items: flex-start; }

  /* Over the map on a phone the preview would cover most of the world, so it
     sits underneath instead. */
  .map__preview-slot { position: static; margin-top: var(--spacing-md); }
  .map-preview       { width: 100%; }

}

@media (max-width: 460px) {
  .index-filters { grid-template-columns: 1fr 1fr; }
  .index-search input { font-size: var(--text-md); }
}


/* ===========================================================================
   20  SCROLL REVEAL

   ⚠️ Everything here is scoped to `.reveal-ready`, a class that ONLY
   js/reveal.js adds, and only once it knows it can finish the job. Without
   the script — or with reduced motion, or without IntersectionObserver —
   this whole section does nothing and every element is simply visible.

   That scoping is the entire safety mechanism. A rule that hid content
   unconditionally would take the page down with any script failure.

   Only opacity and transform are animated. Neither affects layout, so the
   page is exactly as tall before a reveal as after it and nothing shifts.
   =========================================================================== */

.reveal-ready [data-reveal="pending"] {
  opacity: 0;
  transform: translateY(var(--reveal-distance));
}

.reveal-ready [data-reveal] {
  /* On the element rather than only on the hidden state, so the element
     animates INTO place rather than snapping there. */
  transition: opacity var(--reveal-duration) var(--ease),
              transform var(--reveal-duration) var(--ease);

  /* A hint that these two properties are about to change. Left permanently
     it would keep a layer alive for every revealed element on the page, so
     it is removed once shown, below. */
  will-change: opacity, transform;
}

.reveal-ready [data-reveal="shown"] {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* The last word, for anyone whose browser applied the class before the
   script could decide not to. */
@media (prefers-reduced-motion: reduce) {
  .reveal-ready [data-reveal="pending"] { opacity: 1; transform: none; }
  .reveal-ready [data-reveal]           { transition: none; }
}


/* ===========================================================================
   21  A PAGE'S PHOTOGRAPH

   One image per informational page, positioned by a preset the admin chooses
   (js/page.js sets the class; lib/site.js decides which are allowed).

   The no-image case is the default and needs no rules at all: without
   .page-body--with-image the container is the plain measured column it has
   always been. Nothing here reserves space for a picture that is not there.
   =========================================================================== */

/* THE CONTAINER IS NOT THE GRID.

   This is the whole fix. The obvious version — picture in one grid column,
   every word of the page in the other — costs the writing the width of the
   picture plus a gap AT EVERY WIDTH. On a 900px laptop that took the reading
   column from 640px to 436px, and a long section like "Scoring Guidelines"
   became a gutter.

   So the container stays a plain block holding two things: an OPENING that
   may be two columns, and a run of SECTIONS that always keeps the measure.
   A photograph narrows the title and the lede beside it. It never narrows
   the long-form writing underneath. */
.page-body--with-image { display: block; }

/* ONE COLUMN IS THE DEFAULT, and it is written first on purpose.

   All of these are single-class selectors, so they have identical
   specificity and the LAST one wins — and a media query adds none. Written
   the other way round, the narrow-screen rule would override the wide-screen
   one at every width and the picture would always be full width. */
.page-opening {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--spacing-2xl);
  align-items: start;
  margin-bottom: var(--spacing-2xl);
}

/* Stacked, the picture is an illustration rather than a poster. The same cap
   "above" uses, so all three presets agree once there is only one column. */
.page-opening .page-figure { max-width: 34rem; }

/* Wide enough for two — and not one pixel narrower.

   900px is where a 20rem picture, a gap and a title still leave the opening
   text about 476px, which is a comfortable line for a lede. Below it the two
   simply stack, which is always better than two columns that are each too
   narrow to read.

   minmax(0, …) rather than a bare fraction — a grid track's minimum defaults
   to its content, which one long unbroken word would push past the column
   and out of the page. */
@media (min-width: 900px) {
  .page-body--image-left  .page-opening { grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); }
  .page-body--image-right .page-opening { grid-template-columns: minmax(0, 1fr) minmax(0, 20rem); }

  .page-body--image-left  .page-opening .page-figure,
  .page-body--image-right .page-opening .page-figure { max-width: none; }

}

/* "Above" is one column at every width, by never gaining a second one. */

/* The long-form writing. THIS is the element a photograph must not narrow:
   the same measure the page has with no photograph at all. */
.page-sections { max-width: var(--measure); }

/* The opening text and the title keep the same cap, so no line anywhere on
   the page runs longer than the reading measure — with a picture beside it
   the column is wider than the measure, and a lede that ran the full width of
   it would be the one over-long line on an otherwise careful page. */
.page-opening__text { max-width: var(--measure); }
.page-body--with-image > h1 { max-width: var(--measure); }

/* `.prose > * + *` sets the rhythm between blocks, and it only reaches DIRECT
   children of the container. Once the writing is nested inside these two, it
   would silently lose that spacing — so both repeat it, which is what keeps
   the typography identical with and without a photograph. */
.page-opening__text > * + *,
.page-sections > * + * { margin-top: var(--spacing-lg); }

/* The lede's own bottom margin belongs between it and the sections, which is
   now the opening's margin instead. Doubling them opens a hole. */
.page-opening__text > :last-child { margin-bottom: 0; }

/* --- Get in touch ----------------------------------------------------------
   One external link at the foot of the About page. Deliberately not a button
   and deliberately not branded: no gradient, no logo, no colour that is not
   already on this site. It reads as the last line of an editorial page,
   because that is what it is.
   -------------------------------------------------------------------------- */
.page-contact {
  margin-top: var(--section-spacing-tight);
  padding-top: var(--spacing-lg);
  border-top: var(--rule);
  max-width: var(--measure);
}

.page-contact__heading { margin: 0 0 var(--spacing-md); }

/* Written as a descendant selector on purpose. This link lives inside
   `#page-content.prose`, and `.prose a` — accent-coloured and underlined —
   has specificity (0,1,1), which beats a lone class. Without the extra class
   here the link came out brown with two underlines: the prose underline and
   this border-bottom, one under the other. */
.page-contact .page-contact__link {
  display: inline-flex;
  align-items: baseline;
  gap: var(--spacing-xs);
  text-decoration: none;
  color: var(--text-primary);
  padding: var(--spacing-2xs) 0;
  border-bottom: 1px solid var(--border-strong);
  transition: color var(--transition-fast) var(--ease),
              border-color var(--transition-fast) var(--ease);
}

.page-contact__service {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.page-contact__handle {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  color: var(--text-secondary);
  transition: color var(--transition-fast) var(--ease);
}

/* The arrow moves the smallest distance that still reads as "this leaves the
   site". Two pixels, and only on hover or focus. */
.page-contact__arrow {
  font-size: 0.8em;
  transition: transform var(--transition-fast) var(--ease);
}

.page-contact .page-contact__link:hover,
.page-contact .page-contact__link:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.page-contact .page-contact__link:hover .page-contact__handle,
.page-contact .page-contact__link:focus-visible .page-contact__handle { color: var(--accent); }
.page-contact .page-contact__link:hover .page-contact__arrow,
.page-contact .page-contact__link:focus-visible .page-contact__arrow { transform: translate(2px, -2px); }

/* The site's focus ring, said explicitly here because the link has its own
   border-bottom and a ring that only replaced that border would be easy to
   miss on a page this quiet. */
.page-contact .page-contact__link:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 4px;
}

.page-figure {
  margin: 0;
  /* An editorial rectangle, not an avatar. No border-radius and no circle:
     a portrait in a journal is a photograph, not a profile picture. */
}

.page-figure img {
  display: block;
  width: 100%;
  height: auto;          /* the file's own proportions, never forced */
  border-radius: var(--radius);
}

.page-figure figcaption {
  margin-top: var(--spacing-xs);
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--text-secondary);
  max-width: 34rem;
}


/* ===========================================================================
   22  RICH TEXT

   The complete set of formatting the editor can produce. There is deliberately
   nothing else: a mark, a font or a size that is not here cannot be stored
   (lib/richtext.js) and would not be drawn (js/richtext.js).

   Bold, italic and underline are real elements — <strong>, <em>, <u> — so
   they mean something to a screen reader and need no rules at all. Only the
   two typography presets need classes, and both resolve to theme tokens.
   =========================================================================== */

.rt-font-display { font-family: var(--font-display); }
.rt-font-body    { font-family: var(--font-body); }

/* Relative to whatever the text already is, so "Large" inside a caption and
   "Large" inside a paragraph both look like a step up rather than jumping to
   one fixed size and breaking the scale. */
.rt-size-sm { font-size: 0.875em; }
.rt-size-lg { font-size: 1.2em; }
.rt-size-xl { font-size: 1.45em; }

/* Underline sits closer to the baseline than the default, which on a serif at
   reading size collides with descenders. */
u { text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
