/* ==========================================================================
   Chef's Table — design system
   --------------------------------------------------------------------------
   Dark, editorial, slightly theatrical: a laboratory crossed with a candlelit
   dining room. Restrained, expensive, a little uncanny.

   Five rules the whole sheet obeys:
     1. Near-black ground with a grain texture. Never flat #000.
     2. One loud accent, used sparingly. Nothing else is coloured.
     3. Typography carries the design. Four faces, one job each.
     4. Full-bleed media does the emotional work. Text sits over it.
     5. No boxes. Space and 1px hairlines separate content — not cards.

   Written as one stylesheet with no build step, matching the rest of this
   codebase. Tokens first, then primitives, then components. Nothing below the
   token block hardcodes a colour or a spacing value.
   ========================================================================== */

/* --------------------------------------------------------------- 1. TOKENS */

:root {
  /* ---- Ground -----------------------------------------------------------
     Near-black, not black. #000 reads as a dead screen; #0a0a0a reads as an
     unlit room. The grain overlay in the reset does the rest. */
  --ink:        #0a0a0a;   /* page background */
  --ink-raised: #111111;   /* footer bottom bar, secondary bands */
  --ink-header: #212324;   /* sticky header once it leaves the hero */

  /* ---- Text -------------------------------------------------------------
     Pure white on near-black. Meta and captions drop to 70%, which is the
     lowest this palette goes and still clears AA on the ground. */
  --paper:      #ffffff;
  --paper-70:   rgba(255, 255, 255, 0.70);
  --paper-60:   rgba(255, 255, 255, 0.60);   /* placeholders only */

  /* ---- The one accent ---------------------------------------------------
     Two calibrated stops of a single signal red, for contrast reasons that
     are not negotiable:

       --accent      #ED1B24 on --ink        = 4.51:1  (AA normal text)
       --accent-fill #E01019 under white     = 4.93:1  (AA normal text)

     Using one value for both fails one way or the other: #ED1B24 behind a
     white label is 4.39:1, and #E01019 as text on the ground is 4.01:1.
     Same hue, imperceptibly different, both legible. Use --accent for text,
     borders and underlines; --accent-fill only where white sits on top. */
  --accent:      #ED1B24;
  --accent-fill: #E01019;

  /* Muted copper. Rare secondary hover and close icons. Not a second accent
     — it never appears on a CTA. */
  --accent-2:   #cb6037;

  /* ---- Hairlines --------------------------------------------------------
     The only separator in the system. There are no borders around anything;
     there are lines between things. */
  --hairline:      rgba(255, 255, 255, 0.25);
  --hairline-soft: rgba(255, 255, 255, 0.12);

  /* ---- Type -------------------------------------------------------------
     Four faces, each with exactly one job. Mixing them is the design; using
     one where another belongs is how it falls apart.

       Aboreto        display, headings, links, buttons — UPPERCASE, 400 only
       Raleway        body, nav, form fields — 300/400, italic 300 for asides
       Special Elite  hero headline + eyebrows, ONLY over imagery
       Source Code Pro  menu descriptions, data, credits */
  --font-display: 'Aboreto', 'Times New Roman', Times, serif;
  --font-body:    'Raleway', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-type:    'Special Elite', 'Courier New', Courier, monospace;
  --font-mono:    'Source Code Pro', ui-monospace, 'SF Mono', Menlo, monospace;
  /* Optional fifth, for editorial serif section titles. Never in the same
     block as Aboreto. */
  --font-serif:   'Cormorant Infant', Georgia, 'Times New Roman', serif;

  /* ---- Scale ------------------------------------------------------------
     Fluid between the brief's desktop and mobile ends, so nothing wraps badly
     at an in-between width. Floor is 13px everywhere — nothing smaller. */
  --t-hero:    clamp(1.25rem,  0.72rem + 2.30vw, 3rem);      /* 48 -> 20 */
  --t-h1:      clamp(1.625rem, 1.24rem + 1.18vw, 2.75rem);   /* 44 -> 26 */
  --t-h2:      clamp(1.5rem,   1.28rem + 0.66vw, 2.125rem);  /* 34 -> 24 */
  --t-h3:      clamp(1rem,     0.96rem + 0.16vw, 1.125rem);  /* 18 -> 16 */
  --t-h4:      1.0625rem;                                     /* 17 */
  --t-body:    1rem;                                          /* 16 */
  --t-caption: 1.125rem;                                      /* 18 */
  --t-btn:     0.9375rem;                                     /* 15 */
  --t-min:     0.8125rem;                                     /* 13 — the floor */

  /* ---- Tracking ---------------------------------------------------------
     Wide is the whole point. Aboreto without tracking looks like a mistake. */
  --track-display: 0.05em;
  --track-wide:    0.10em;
  --track-btn:     0.20em;
  --track-num:     0.23em;   /* carousel numerals */

  /* ---- Space ------------------------------------------------------------
     Large steps. Cramped spacing breaks the whole look — the brief is
     explicit, and it is right. */
  --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;     --s-7: 3rem;     --s-8: 4rem;
  --s-9: 5.5rem;   --s-10: 8rem;    --s-11: 12rem;

  /* Section rhythm: 60px tight, 130px generous. */
  --band:      clamp(3.75rem, 2rem + 5vw, 8.125rem);

  /* The first section of an inner page sits UNDER the fixed header, so its
     top padding has to carry the header's height before any of it is visible
     as space. The header measures ~104px at rest on desktop and ~92px on a
     phone, where the CTA block is not in the bar.

     This was 12rem, copy-pasted inline into thirteen views, which left a 96px
     void under the bar — the header's height again, and then some. These
     numbers put roughly 40px of visible air under the header on a desktop and
     28px on a phone, which reads as deliberate rather than as a gap. */
  --pad-first: clamp(7.5rem, 5.5rem + 4vw, 9rem);
  --pad-side:  clamp(1.25rem, -1rem + 7vw, 5.625rem);   /* 20 -> 90px */

  /* ---- Corners ----------------------------------------------------------
     Square. All of them. --r-full survives only for genuine circles (the
     back-to-top ring), which is a shape, not a rounded rectangle. */
  --r-sm: 0;
  --r-md: 0;
  --r-lg: 0;
  --r-xl: 0;
  /* Every rectangle is square. --r-full used to mean "pill" and was applied to
     buttons, tabs and chips; all of those are now square, so it resolves to 0
     and the pills disappear wherever they were. A circle is a shape rather
     than a rounded rectangle, so the handful of genuinely round things — the
     back-to-top ring, the social marks — ask for --r-circle explicitly. */
  --r-full: 0;
  --r-circle: 999px;

  /* ---- Depth ------------------------------------------------------------
     There is none. No shadows, no bevels, no elevation. Kept as no-op tokens
     so the component sections below can keep referring to them while they are
     migrated section by section. */
  --shadow-sm:    none;
  --shadow-md:    none;
  --shadow-lg:    none;
  --shadow-flame: none;
  --bevel:        none;

  /* ---- States ----------------------------------------------------------
     The brief allows one accent and says nothing else is coloured, so an
     error borrows the signal red rather than introducing a second hue — red
     is what the accent already means. Success deliberately has no colour of
     its own: the wording carries it, backed by a white hairline. That is
     also the more accessible choice, since colour was never the only cue. */
  --error: var(--accent);
  --ok:    var(--paper);
  --warn:  var(--accent-2);

  /* The one permitted gradient: the scrim behind a transparent header. */
  --scrim: linear-gradient(180deg, #000 0%, transparent 100%);

  /* ---- Motion -----------------------------------------------------------
     Slow and few. 200ms on colour, 1000ms on hero crossfades, nothing else. */
  --ease:     cubic-bezier(0.32, 0.72, 0, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast:   200ms;
  --dur:        200ms;
  --dur-reveal: 700ms;   /* scroll-in fades */
  --dur-slow:   1000ms;  /* hero crossfades only */

  --container: 1400px;
  --measure:   65ch;

  /* Systemic layers only. No arbitrary z-index anywhere below. */
  --z-base: 1;
  --z-sticky: 100;
  --z-nav: 200;
  --z-overlay: 300;
  --z-grain: 400;

}
/* ---------------------------------------------------------------- 2. RESET */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

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

/* Respect people who get motion-sick. Not optional. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--t-body);
  line-height: 1.7;
  letter-spacing: 0.02em;
  color: var(--paper);
  background: var(--ink);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis: none;
  overflow-x: hidden;
}

img, picture, video, canvas, svg { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4 { overflow-wrap: break-word; }

/* Film grain. Fixed and pointer-events-none so it never repaints on scroll
   and never eats a click. It exists to stop the flat areas reading as
   digital vector fill. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------------ 3. TYPOGRAPHY */

/* Aboreto has one weight and no italic. Everything it sets is uppercase with
   open tracking — at sentence case and default tracking it reads as a browser
   fallback, which is the opposite of the intent. */
h1, h2, h3, h4, .display {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper);
  text-wrap: balance;
}

h1 { font-size: var(--t-h1); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }
h4 {
  font-size: var(--t-h4);
  letter-spacing: var(--track-display);
  margin: 25px 0;
}

p {
  text-wrap: pretty;
  max-width: var(--measure);
  line-height: 1.7;
}

/* The typewriter face. Over imagery only — on a plain dark band it reads as a
   broken font rather than as a deliberate one. */
.typewriter {
  font-family: var(--font-type);
  text-transform: uppercase;
  line-height: 1.3;
  letter-spacing: var(--track-display);
}

/* Asides and checkbox labels. The brief's one italic. */
.aside {
  font-family: var(--font-body);
  font-weight: 300;
  font-style: italic;
}

.lede {
  font-size: var(--t-caption);
  line-height: 1.6;
  color: var(--paper-70);
}

.caption {
  font-family: var(--font-body);
  font-size: var(--t-caption);
  font-weight: 300;
  color: var(--paper-70);
}

/* Menu item descriptions, data, credits. */
.mono {
  font-family: var(--font-mono);
  font-weight: 300;
  letter-spacing: var(--track-display);
}

/* The eyebrow above a section heading. Typewriter over media, Raleway
   everywhere else — .eyebrow is the plain one, .eyebrow--type the overlay. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-body);
  font-size: var(--t-min);
  font-weight: 400;
  letter-spacing: var(--track-num);
  text-transform: uppercase;
  color: var(--accent);
}

.eyebrow--type {
  font-family: var(--font-type);
  letter-spacing: var(--track-display);
}

/* A hairline lead-in, not a gradient — the gradient version was the last
   trace of the old palette's glow. */
.eyebrow::before {
  content: '';
  width: 1.75rem;
  height: 1px;
  background: var(--accent);
}

/* The one phrase a headline is allowed to accent, written *between asterisks*
   by the operator and turned into an <em> by Ui::headline(). It lives here
   rather than on .hero__title because every page has a headline and only the
   home page has a hero — on About it was rendering as plain white text, which
   is to say as nothing at all.

   Not italic: Aboreto ships one upright weight and no oblique, so a browser
   asked for italic shears the uprights and it shows. The colour is the mark. */
h1 em, h2 em, h3 em, .display em {
  font-style: normal;
  color: var(--accent);
}

.muted { color: var(--paper-70); }

/* Tabular figures wherever numbers are compared down a column — seat counts,
   prices, dates. Proportional digits make a price list look ragged. */
.tnum { font-variant-numeric: tabular-nums; }

/* Every link goes to the accent on hover. That is the only hover colour in
   the system; anything else fading to a different tone is a bug. */
a {
  color: var(--paper);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color var(--dur) var(--ease);
}
a:hover { color: var(--accent); }

/* Editorial lists: a hexagon instead of a bullet. The molecular-gastronomy
   motif, and the one place a glyph is allowed to carry meaning. */
.list-hex { list-style: none; padding-left: 0; }

.list-hex li {
  position: relative;
  padding-left: var(--s-5);
  margin-bottom: var(--s-3);
}

.list-hex li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 9px;
  height: 10px;
  background: var(--accent);
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}
/* ---------------------------------------------------------------- 4. LAYOUT */

.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--s-5);
}

@media (min-width: 768px) { .wrap { padding-inline: var(--s-7); } }

/* Section rhythm. Bottom padding is slightly heavier than top — optically
   they then read as equal, which mathematically equal padding does not. */
.section { padding-block: var(--s-9) calc(var(--s-9) + var(--s-5)); }
.section--tight { padding-block: var(--s-8); }

@media (min-width: 900px) {
  .section { padding-block: var(--s-10) calc(var(--s-10) + var(--s-6)); }
}

/* Clears the fixed header. One class rather than thirteen inline copies of the
   same magic number — which is how they drifted out of step with the header in
   the first place.

   Declared AFTER the media query above deliberately. Both selectors are one
   class, so they carry identical specificity and source order is the whole
   tie-break: written before it, this rule lost to `.section` on every screen
   wider than 900px and the token did nothing there. */
.section--first { padding-top: var(--pad-first); }

.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }

/* Skip link — keyboard users land here first. */
.skip {
  position: absolute;
  left: var(--s-4);
  top: calc(-1 * var(--s-10));
  z-index: var(--z-overlay);
  padding: var(--s-3) var(--s-5);
  background: var(--accent-fill);
  color: var(--paper);
  border-radius: 0;
  font-family: var(--font-display);
  font-size: var(--t-btn);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  transition: top var(--dur-fast) var(--ease);
}
.skip:focus { top: var(--s-4); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* --------------------------------------------------------- 5. THE FRAME

   There used to be a double bezel here: an outer tray with its own hairline
   and a large radius, holding an inner core with a concentric radius and a
   top-edge highlight. It was the right idea for a warm, raised, candlelit
   surface and it is the wrong idea for this one — the brief's whole third
   rule is that content is separated by space and hairlines, not by boxes.

   The classes survive because sixteen views name them, and deleting a class
   used in sixteen places is a different change from restyling it. They now
   resolve to a plain full-bleed frame: no tray, no fill, no border, no
   radius, no shadow. The photograph meets the page directly.               */

.bezel {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.bezel__core {
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: hidden;
  height: 100%;
}

.bezel--flat { padding: 0; border-radius: 0; }
.bezel--flat .bezel__core { border-radius: 0; }

/* ---------------------------------------------------------------- 6. BUTTONS */

/* Outlined by default: the accent is a line, not a fill. Filling every button
   with red would spend the one loud colour on everything and leave nothing to
   signal the primary action.

   White is the label colour on every red fill in the system, and the fill is
   --accent-fill rather than --accent so that white clears AA on it (4.93:1
   against 4.39:1). Dark-on-red passes too, but reads as a warning label; this
   is a dinner reservation. */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--accent);
  --btn-bd: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  padding: 11px 40px;
  border: 1px solid var(--btn-bd);
  border-radius: 0;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-display);
  font-size: var(--t-btn);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  box-shadow: none;
  /* Colour only. No transform, no scale, no lift — the brief asks for slow and
     few, and a button that jumps on hover is the single most common tell of a
     templated interface. */
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}

/* Fills on hover. That is the whole interaction. */
.btn:hover {
  --btn-bg: var(--accent-fill);
  --btn-bd: var(--accent-fill);
  --btn-fg: var(--paper);
}

.btn:active { background: var(--accent-fill); }

/* The trailing glyph. Naked, with no disc behind it — the disc was a pill-era
   detail and there are no pills left. It shifts on hover; nothing scales. */
.btn__icon {
  display: inline-block;
  font-size: 0.85em;
  line-height: 1;
  transition: transform var(--dur) var(--ease);
}
.btn:hover .btn__icon { transform: translateX(4px); }

/* The primary action: a solid red block with a white label. Exactly one of
   these belongs on screen at a time — the nav CTA, or the single decisive
   action in a form. */
.btn--primary {
  --btn-bg: var(--accent-fill);
  --btn-fg: var(--paper);
  --btn-bd: var(--accent-fill);
}
.btn--primary:hover {
  /* Already filled, so hover inverts instead: the block empties out. */
  --btn-bg: transparent;
  --btn-fg: var(--accent);
  --btn-bd: var(--accent);
}

/* Secondary. Muted copper outline — the brief's one permitted second stroke,
   and it never becomes a fill on a page that already has a primary. */
.btn--secondary {
  --btn-fg: var(--accent-2);
  --btn-bd: var(--accent-2);
}
.btn--secondary:hover {
  --btn-bg: var(--accent-2);
  --btn-bd: var(--accent-2);
  --btn-fg: var(--paper);
}

/* Quietest of the three: a white hairline, for actions that must be reachable
   but must not compete. */
.btn--ghost {
  --btn-fg: var(--paper);
  --btn-bd: var(--hairline);
}
.btn--ghost:hover {
  --btn-bg: transparent;
  --btn-fg: var(--accent);
  --btn-bd: var(--accent);
}

.btn--lg { padding: 15px 52px; font-size: var(--t-body); }
.btn--sm { padding: 8px 26px; font-size: var(--t-min); }
.btn--block { display: flex; width: 100%; }

.btn[disabled], .btn[aria-disabled='true'] {
  opacity: 0.45;
  pointer-events: none;
}

/* A text link that behaves like a tertiary action, so a page does not need a
   third outlined rectangle to offer a third choice. */
.btn-text {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0;
  border: 0;
  background: none;
  color: var(--paper);
  font-family: var(--font-display);
  font-size: var(--t-btn);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}
.btn-text:hover { color: var(--accent); }
/* ------------------------------------------------------------------ 7. HEADER

   A divided header: links | wordmark | links, in a 40/20/40 grid. The mark is
   centred by the GRID, not by flex spacing, so it stays optically centred no
   matter how long the Albanian labels run — with space-between it drifts left
   the moment one side has more text than the other, and Albanian is reliably
   longer than English.

   Over the hero it is transparent and sits ON TOP of the media, with a scrim
   so white labels survive a bright frame. Once the page scrolls it goes solid.
   The previous header was a floating blurred pill; there are no pills left. */

.hdr {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  background: transparent;
  transition: background-color var(--dur) var(--ease);
}

/* The one permitted gradient in the system. It exists so the labels stay
   legible over a bright hero frame without tinting the photograph. */
.hdr::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 180%;
  background: var(--scrim);
  opacity: 0.85;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

/* Scrolled. Solid bar, no bottom border — a hairline under a solid header
   reads as a seam. The scrim goes with it; there is nothing left to scrim. */
.hdr[data-scrolled='true'] { background: var(--ink-header); }
.hdr[data-scrolled='true']::before { opacity: 0; }

.hdr__inner {
  /* Positioned, so it paints ABOVE the scrim. Without this the ::before is the
     only positioned box in the header and an absolutely-positioned element
     paints over unpositioned in-flow content — so the black gradient was
     sitting on top of the wordmark and the CTA, greying out the whole bar and
     turning the red block maroon. */
  position: relative;
  z-index: var(--z-base);
  display: grid;
  grid-template-columns: 2fr 1fr 2fr;   /* 40 / 20 / 40 */
  align-items: center;
  gap: var(--s-5);
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--s-5) var(--pad-side);
  transition: padding var(--dur) var(--ease);
}

/* The mark shrinks on scroll, per the brief. Padding tightens with it so the
   bar loses height rather than just changing type size. */
.hdr[data-scrolled='true'] .hdr__inner { padding-block: var(--s-3); }

.hdr__side {
  display: none;              /* the burger carries navigation until 1024px */
  align-items: center;
  gap: var(--s-5);
  min-width: 0;
}
.hdr__side--l { justify-content: flex-start; }
.hdr__side--r { justify-content: flex-end; }

@media (min-width: 1024px) {
  .hdr__side { display: flex; }
}

.hdr__mark {
  grid-column: 2;
  justify-self: center;
  font-family: var(--font-display);
  font-size: var(--t-h3);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  white-space: nowrap;
  /* Aboreto has thin strokes and no bolder weight to reach for, so at header
     size it optically reads lighter than the Raleway links beside it even at
     the same pure white. A hair of stroke evens them up. */
  -webkit-text-stroke: 0.35px var(--paper);
  transition: font-size var(--dur) var(--ease), color var(--dur) var(--ease);
}
.hdr__mark:hover { color: var(--accent); }
.hdr[data-scrolled='true'] .hdr__mark { font-size: var(--t-body); }

/* Raleway, not Aboreto: the brief gives nav items to the body face, and a bar
   set entirely in the display face has no hierarchy left for the wordmark. */
.hdr__link {
  position: relative;
  font-family: var(--font-body);
  font-size: var(--t-min);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  white-space: nowrap;
  padding-block: var(--s-2);
  transition: color var(--dur) var(--ease);
}
.hdr__link:hover { color: var(--accent); }

/* Current page. A hairline that is simply present — it does not animate in,
   because it is a statement of where you are, not an entrance. */
.hdr__link::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.hdr__link[aria-current='page'] { color: var(--accent); }
.hdr__link[aria-current='page']::after { opacity: 1; }

/* The one filled block in the header, and the only solid accent above the
   fold. White label — see the token block for why the fill is --accent-fill. */
.hdr__cta {
  display: inline-flex;
  align-items: center;
  padding: 15px;
  padding-inline: var(--s-5);
  background: var(--accent-fill);
  color: var(--paper);
  font-family: var(--font-display);
  font-size: var(--t-btn);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.hdr__cta:hover { background: transparent; box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); }

/* Language pair. Furthest left, where the brief puts its search icon — this
   site has no search, and a control that does nothing is worse than no
   control. Underline marks the active language; there is no filled chip,
   because a filled chip up here would compete with the CTA. */
.lang {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  flex: 0 0 auto;
  padding-right: var(--s-4);
  border-right: 1px solid var(--hairline-soft);
}
.lang a {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  font-family: var(--font-mono);
  font-size: var(--t-min);
  font-weight: 300;
  letter-spacing: var(--track-display);
  color: var(--paper-70);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.lang a:hover { color: var(--accent); }
.lang a[aria-current='true'] {
  color: var(--paper);
  border-bottom: 1px solid var(--accent);
}

/* Two bars, per the brief. Hairline-thin and wide, which is what separates a
   considered menu control from the default three-bar stack. */
.hdr__burger {
  display: grid;
  align-content: center;
  gap: 7px;
  width: 56px;
  height: 2.75rem;          /* 44px of target; the bars themselves are 1px */
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.hdr__burger span {
  display: block;
  width: 56px;
  height: 1px;
  background: var(--paper);
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}
.hdr__burger:hover span { background: var(--accent); }
.hdr__burger[aria-expanded='true'] span:nth-child(1) { transform: translateY(4px) rotate(45deg); }
.hdr__burger[aria-expanded='true'] span:nth-child(2) { transform: translateY(-4px) rotate(-45deg); }

/* The compact booking link, phone only. Plain white text rather than a second
   solid block or a second red thing: the bar is already carrying a centred
   wordmark and the burger, and at this width a filled rectangle crowds both
   while accent text competes with the wordmark for the eye. Set small, it
   reads as a quiet way in — the loud one is the block in the overlay.

   Hidden above the breakpoint, where the full --accent-fill block takes over
   on the right. */
.hdr__cta-sm {
  display: none;
  align-items: center;
  min-height: 2.75rem;        /* 44px of thumb, per iOS, on a 13px label */
  font-family: var(--font-display);
  font-size: var(--t-min);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
.hdr__cta-sm:hover { color: var(--accent); }

/* On a phone the mark is centred, the compact CTA sits left, and the burger
   is the only other control. The full CTA block leaves the bar rather than
   shrinking — it is also the last thing in the overlay, where it has room to
   stay a real target. */
@media (max-width: 1023px) {
  .hdr__side--l,
  .hdr__side--r { display: flex; }
  .hdr__side--l .lang,
  .hdr__side--l .hdr__link,
  .hdr__side--r .hdr__link,
  .hdr__side--r .hdr__cta { display: none; }
  .hdr__cta-sm { display: inline-flex; }
}

/* ------------------------------------------------------------- 7b. MENU

   Full-screen overlay with a staggered reveal. Laid out as bar / links / CTA
   so the close button has a fixed home at the top and the links stay optically
   centred between it and the footer. */

.menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: var(--s-5) var(--pad-side) var(--s-8);
  background: var(--ink);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease), visibility var(--dur);
}
.menu[data-open='true'] { opacity: 1; visibility: visible; }

/* Mirrors the header it covers, so the mark does not appear to jump. */
.menu__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: 3rem;
}

.menu__close {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;          /* a 44px target: the iOS minimum, and this is the
                               only way out of a full-screen overlay */
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  color: var(--paper);
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}
/* The brief's one use for the copper: a close icon. */
.menu__close:hover { color: var(--accent-2); }

.menu__list {
  display: grid;
  gap: var(--s-5);
  align-content: center;
  justify-items: start;
  list-style: none;
  padding: 0;
  margin: 0;
}

/* The CTA and the language pair, stacked with room between them so neither
   is a mis-tap for the other. */
.menu__foot {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-6);
}

/* The bar's version has a divider on its right separating it from the links.
   In here it is the last thing on the page, so there is nothing to divide. */
.menu__lang {
  padding-right: 0;
  border-right: 0;
  gap: var(--s-5);
}
.menu__lang a { font-size: var(--t-btn); }

.menu__list a {
  font-family: var(--font-display);
  font-size: var(--t-h1);
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
  opacity: 0;
  transform: translateY(1.5rem);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out),
              color var(--dur) var(--ease);
}
.menu__list a:hover { color: var(--accent); }

/* The page you are on, marked here too — inside a full-screen overlay there is
   no other context telling you where you are. */
.menu__list a[aria-current='page'] { color: var(--accent); }

.menu[data-open='true'] .menu__list a { opacity: 1; transform: none; }
.menu[data-open='true'] li:nth-child(1) a { transition-delay: 60ms; }
.menu[data-open='true'] li:nth-child(2) a { transition-delay: 120ms; }
.menu[data-open='true'] li:nth-child(3) a { transition-delay: 180ms; }
.menu[data-open='true'] li:nth-child(4) a { transition-delay: 240ms; }
.menu[data-open='true'] li:nth-child(5) a { transition-delay: 300ms; }
.menu[data-open='true'] li:nth-child(6) a { transition-delay: 360ms; }
/* ------------------------------------------------------------------ 8. HERO

   Full-bleed, full-height, text bottom-left. Not a centred stack, not a
   split with a picture beside it — the media is the page and the headline
   sits on top of it.                                                       */

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  /* dvh, not vh: on iOS Safari the address bar makes 100vh taller than the
     visible viewport, so a full-height hero is cropped until you scroll. */
  min-height: 100dvh;
  overflow: hidden;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}

/* <picture> is an inline wrapper with auto height, so an <img> inside it has
   nothing for height:100% to resolve against — the still filled only its own
   intrinsic height and left the rest of the hero black. Most visible on a
   narrow screen, where a 2.3:1 crop is a fraction of a 100dvh box. */
.hero__media picture {
  display: block;
  width: 100%;
  height: 100%;
}

.hero__vid {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Centred, because the hero still is cropped to the opening rather than
     being the whole plate. The full photograph is a wall with a hole in it,
     and two thirds of it is plaster — nudging object-position around only
     chose which band of plaster to show. The crop lives in the asset. */
  object-position: 50% 50%;
}

/* Darkened only where the type is.

   This used to carry a wash across the whole frame — 0.25 even at the very
   top — which dulled the entire photograph to protect two lines of text
   sitting in the bottom eighth of it. The weight now falls away completely by
   the midpoint, so the room, the lit ceiling and the wine racks are seen at
   something close to their real exposure.

   The top of the frame needs nothing: the header draws its own scrim for its
   own labels, so the hero was darkening that band twice. */
.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    /* The bottom strip stays weighted: the crop ends on pale plaster, and the
       seat line sits right on it. Everything above 30% is essentially clear. */
    rgba(10, 10, 10, 0.88) 0%,
    rgba(10, 10, 10, 0.62) 10%,
    rgba(10, 10, 10, 0.28) 22%,
    rgba(10, 10, 10, 0.08) 34%,
    transparent 50%);
}

.hero__inner {
  position: relative;
  z-index: var(--z-base);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  /* ~100px inset at the bottom-left, per the brief, collapsing to the page
     gutter on a phone where 100px of nothing is most of the screen. */
  padding: 0 var(--pad-side) clamp(2.5rem, 1rem + 5vw, 6.25rem);
}

.hero__title {
  font-size: var(--t-hero);
  max-width: 18ch;
  color: var(--paper);
  /* A soft dark halo, not a drop shadow — no offset, wide blur, low alpha, so
     it reads as the letters sitting in the room's light rather than as type
     with a shadow under it.

     This is what lets the scrim above stay light: legibility no longer
     depends on the whole photograph being dimmed, which matters most once a
     video goes in here and the frame behind the headline stops being one
     known, mostly-dark corner of one known photograph. */
  text-shadow: 0 0 28px rgba(10, 10, 10, 0.75), 0 1px 3px rgba(10, 10, 10, 0.5);
}

/* Special Elite is a typewriter face: it has no true bold and its strokes are
   already irregular, so the accent is carried by colour alone. */
.hero__title em { color: var(--accent); }

.hero__meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-5);
  font-size: var(--t-min);
  text-transform: uppercase;
  color: var(--paper-70);
  text-shadow: 0 0 20px rgba(10, 10, 10, 0.85);
}

.hero__meta-sep { color: var(--paper-60); }   /* was --hairline: a border value used as type */

/* --------------------------------------------------------- 9. SEATS COUNTER

   The number that decides whether someone books tonight or forgets. It gets
   its own component and its own weight.                                      */

.seats {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
}

/* The number, in the accent, carrying its own weight without a panel behind
   it. This was a blurred glass box with a border and a shadow — three of the
   things the brief rules out, stacked on one element. */
.seats__num {
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.seats__label {
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper-70);
}

.seats--out .seats__num { color: var(--paper-70); }

/* A slow pulse when only a couple remain. Scarcity, stated once, quietly. */
.seats--low .seats__num { animation: emberPulse 2.8s var(--ease) infinite; }

@keyframes emberPulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.62; }
}

/* Inline chair pictogram — twenty marks, filled as seats go. Communicates
   "one table of twenty" faster than the sentence does. */
.chairs { display: flex; flex-wrap: wrap; gap: 4px; }
.chairs i {
  width: 7px; height: 14px;
  border-radius: 2px;
  background: var(--hairline-soft);
  transition: background-color var(--dur) var(--ease);
}
.chairs i[data-taken='1'] { background: var(--accent); }

/* ------------------------------------------------------------- 10. THE BENTO

   Asymmetric grid. Not three equal columns.                                   */

.bento { display: grid; gap: var(--s-4); }

@media (min-width: 900px) {
  .bento { grid-template-columns: repeat(12, 1fr); grid-auto-rows: minmax(180px, auto); }
  .bento__lg { grid-column: span 7; grid-row: span 2; }
  .bento__sm { grid-column: span 5; }
  .bento__md { grid-column: span 6; }
  .bento__wide { grid-column: span 12; }
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 200px;
  padding: var(--s-6);
  overflow: hidden;
}

.tile__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.tile__bg img { width: 100%; height: 100%; object-fit: cover; }

/* Scrim so type stays legible over any photograph, however bright. */
.tile__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(10, 10, 10, 0.92) 12%, rgba(10, 10, 10, 0.35) 55%, rgba(10, 10, 10, 0.15));
}

.tile > *:not(.tile__bg) { position: relative; z-index: var(--z-base); }

.tile img { transition: transform var(--dur-slow) var(--ease); }
.tile:hover img { transform: scale(1.045); }

/* ------------------------------------------------- 10b. CATEGORY TABS

   An underlined strip, not pills. These were pills when the page was full of
   rounded trays and they matched; with the boxes gone a row of outlined
   capsules was the last thing on the page still drawing its own container.

   Wraps rather than scrolling. It used to be a horizontal scroller, which
   works when a strip is obviously cut off mid-item — but these tabs end on a
   count, so a row ending in "WORKSHOPS 1" at the screen edge looks finished
   rather than truncated, and the two tabs past it were simply never found.
   Wrapping costs a line of height and shows every filter at once.

   Row gap is smaller than the column gap on purpose: wrapped rows want to
   read as one block, and equal gaps make them read as two separate strips. */

.cat-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  padding: var(--s-2) 0;
  margin-top: var(--s-6);
}

/* Tighter columns where a phone would otherwise wrap after two. */
@media (max-width: 575px) {
  .cat-tabs { gap: var(--s-2) var(--s-5); }
}

.cat-tab {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s-2);
  flex: 0 0 auto;
  min-height: 2.5rem;
  padding: var(--s-2) 0;
  border: 0;
  border-bottom: 1px solid transparent;
  background: transparent;
  color: var(--paper);
  font-family: var(--font-body);
  font-size: var(--t-min);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.cat-tab:hover { color: var(--accent); }

.cat-tab.is-on {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Nothing upcoming: still listed, plainly not clickable. "No workshops right
   now" is information; a tab that vanishes is a puzzle. */
.cat-tab.is-empty { opacity: 0.4; cursor: default; }

/* The count, as a plain numeral rather than a filled chip.

   This was --hairline, which is white at 25% alpha — a value meant for a 1px
   rule, not for type. Over the near-black ground it computes to roughly
   2.1:1, so the numbers were technically present and practically invisible.
   --paper-70 is the dimmest thing in this system that is still text. */
.cat-tab__n {
  font-family: var(--font-mono);
  font-size: var(--t-min);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--paper-70);
  background: none;
  padding: 0;
  border-radius: 0;
}
.cat-tab.is-on .cat-tab__n { color: var(--accent); }

/* The same word on a listing card, quieter. */
.cat-chip {
  padding: 0;
  border: 0;
  font-family: var(--font-mono);
  font-size: var(--t-min);
  font-weight: 300;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--accent);
}

/* A plain statement inside a form — the place where a control used to be and
   a sentence now does the job better. Reads as a note, not as a warning. */

/* A hairline above, not a coloured stripe down the left: "no left-border
   accent stripes" is on the brief's do-not list by name, and the tinted fill
   behind it was a filled panel besides. */
.note-quiet {
  padding: var(--s-5) 0 0;
  border-top: 1px solid var(--accent);
  background: none;
}

.note-quiet p {
  margin: 0;
  color: var(--paper-70);
  font-size: var(--t-min);
  line-height: 1.6;
}

/* File inputs. The browser's native button is the one control that ignores
   everything else on the page, so the ::file-selector-button is restyled to
   match .btn rather than left sitting there in Chrome grey. */

.input[type="file"] {
  padding: var(--s-3);
  font-size: var(--t-min);
  cursor: pointer;
}

.input[type="file"]::file-selector-button {
  margin-right: var(--s-4);
  padding: var(--s-2) var(--s-5);
  border: 1px solid var(--accent);
  border-radius: 0;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-family: var(--font-display);
  font-size: var(--t-min);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.input[type="file"]::file-selector-button:hover {
  background: var(--accent-fill);
  color: var(--paper);
}

/* ----------------------------------------------------- 10b. TRAVELING CHEF

   Evenings cooked by a visiting chef are marked twice: an accent hairline
   above the card body, and a small tag by the chef's name. The line alone
   would be meaningless to anyone who has not been told what it means, and the
   tag alone is easy to miss when scanning a grid — together they read.

   This used to be a red gradient filling the card's tray. With the tray gone
   the fill had nothing to sit inside and washed straight across the
   photograph, which is both the "no filled panels" rule and the "do not tint
   photography with the accent" rule broken by one declaration. A line does
   the same job and takes nothing away from the picture.                    */

.bezel--visiting .card__body { border-top-color: var(--accent); }

/* White label inside an accent outline. Red-on-red was the tag drawing
   attention to itself twice and reading clearly neither time — the border
   already says "this one is different", so the words only have to be
   readable. */
.visiting-tag {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--accent);
  font-family: var(--font-mono);
  font-size: var(--t-min);
  font-weight: 400;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper);
  white-space: nowrap;
}

/* ------------------------------------------------------------- 11. EVENT CARD

   Not a card. An image with text under it, separated from the next one by
   space and a single hairline above the body — which is the only rule the
   brief allows between pieces of content.                                   */

.card {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* Edge to edge in its column, never inset inside a rounded container. */
.card__media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--ink-raised);
}
.card__media img { width: 100%; height: 100%; object-fit: cover; }

/* The photograph does not move. A card that zooms its own image on hover is
   the single most recognisable templated-interface gesture there is; the
   title going accent is the whole hover state. */
.card:hover .card__title { color: var(--accent); }

.card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--s-3);
  padding: var(--s-5) 0 0;
  border-top: 1px solid var(--hairline-soft);
  margin-top: var(--s-5);
}

.card__title { transition: color var(--dur) var(--ease); }

/* CTAs pinned to the bottom so a row of cards forms one clean baseline
   regardless of how long each title runs. */
.card__foot {
  margin-top: auto;
  padding-top: var(--s-5);
}

.card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--t-min);
  font-weight: 300;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper-70);
}

/* Sits on the photograph, so it needs a ground of its own to stay readable
   over a bright frame. Square, no border, no blur — a solid block is honest
   about being an overlay where a frosted panel pretends to be glass. */
.badge {
  position: absolute;
  top: 0; right: 0;
  padding: 0.45rem 0.8rem;
  background: var(--ink);
  font-family: var(--font-display);
  font-size: var(--t-min);
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--paper);
}
.badge--out { background: var(--accent-fill); color: var(--paper); }
.badge--few { color: var(--accent); }

/* -------------------------------------------------------------- 12. THE MENU */

.courses { list-style: none; padding: 0; display: grid; gap: 0; }

.course {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-5);
  align-items: baseline;
  padding-block: var(--s-5);
  border-bottom: 1px solid var(--hairline-soft);
}
.course:last-child { border-bottom: 0; }

.course__num {
  font-family: var(--font-mono);
  font-size: var(--t-min);
  color: var(--accent);
  letter-spacing: 0.1em;
}

.course__name {
  font-family: var(--font-display);
  font-size: var(--t-h4);
  color: var(--paper);
  line-height: 1.2;
}

.course__desc { font-size: var(--t-min); color: var(--paper-70); margin-top: var(--s-2); }

/* --------------------------------------------------------------- 13. FORMS */

.field { display: grid; gap: var(--s-2); }
.field + .field { margin-top: var(--s-4); }

.label {
  font-family: var(--font-body);
  font-size: var(--t-min);
  font-weight: 400;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--paper-70);
}

/* Fully transparent, with a white hairline around them. A filled field on a
   dark ground reads as a disabled control; the line is enough to say "you can
   type here", and it keeps the photograph or the ground visible behind the
   form the way the rest of the page does. */
.input, .select, .textarea {
  width: 100%;
  padding: var(--s-4);
  border: 1px solid var(--paper);
  border-radius: 0;
  background: transparent;
  color: var(--paper);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--t-btn);
  letter-spacing: 0.02em;
  transition: border-color var(--dur) var(--ease);
}

.input::placeholder, .textarea::placeholder { color: var(--paper-60); }

/* No fill and no glow on focus — the border goes accent and that is all. The
   3px ring this used to grow was the only soft shadow left in the system. */
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.input[aria-invalid='true'] { border-color: var(--error); }

.textarea { min-height: 7rem; resize: vertical; }

.select {
  appearance: none;
  padding-right: var(--s-7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ffffff' stroke-width='1.25' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
}

.error-text { font-size: var(--t-min); color: var(--error); }
.hint {
  font-family: var(--font-body);
  font-weight: 300;
  font-style: italic;      /* the brief's one italic: asides and hints */
  font-size: var(--t-min);
  color: var(--paper-70);
}

/* Seat stepper — a segmented control, not a bare number input. Choosing how
   many people are coming should feel like a decision, not a form field. */
.stepper {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-2);
  border: 1px solid var(--paper);
  border-radius: 0;
  background: transparent;
  width: max-content;
}

.stepper button {
  width: 2.4rem; height: 2.4rem;
  border: 0;
  border-radius: var(--r-full);
  background: rgba(255, 255, 255, 0.06);
  color: var(--paper);
  font-size: var(--t-caption);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.stepper button:hover:not(:disabled) { background: var(--accent-fill); color: var(--paper); }
.stepper button:active:not(:disabled) { transform: scale(0.92); }
.stepper button:disabled { opacity: 0.3; cursor: not-allowed; }

.stepper output {
  min-width: 2.5ch;
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-variant-numeric: tabular-nums;
  color: var(--paper);
}

/* --------------------------------------------------------------- 14. NOTICES */

.notice {
  display: flex;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  border-left: 2px solid;
  font-size: var(--t-min);
}
.notice--error { background: rgba(237, 27, 36, 0.1); border-color: var(--error); color: var(--accent); }
.notice--ok    { background: rgba(255, 255, 255, 0.1); border-color: var(--ok); color: var(--paper); }
.notice--info  { background: rgba(237, 27, 36, 0.09); border-color: var(--accent); color: var(--paper); }

/* Empty states get composed, not left blank. */
.empty {
  display: grid;
  gap: var(--s-4);
  justify-items: center;
  text-align: center;
  padding: var(--s-9) var(--s-5);
  color: var(--paper-70);
}
.empty__mark { font-size: var(--t-h1); opacity: 0.18; font-family: var(--font-display); }

/* --------------------------------------------------------------- 15. TICKET */

.ticket { max-width: 420px; margin-inline: auto; text-align: center; }

.ticket__qr {
  padding: var(--s-5);
  background: var(--paper);
  border-radius: var(--r-lg);
  display: inline-block;
  line-height: 0;
}

.ticket__code {
  font-family: var(--font-mono);
  font-size: var(--t-h4);
  letter-spacing: 0.16em;
  color: var(--accent);
}

/* Perforation between the stub and the body of a ticket. */
.ticket__perf {
  height: 1px;
  margin-block: var(--s-5);
  background: repeating-linear-gradient(90deg, var(--hairline-soft) 0 6px, transparent 6px 12px);
}

/* --------------------------------------------------------------- 16. FOOTER

   Two bands, per the brief: a textured upper band carrying the columns, and a
   tighter --ink-raised bar underneath for the legal line. The grain is the
   same overlay the page wrapper uses, at a slightly higher opacity — enough
   that the band separates from the page without a rule between them.        */

.footer {
  position: relative;
  padding-block: var(--band) var(--s-7);
  background: var(--ink);
}

/* The texture. Sits under the content and takes no clicks. */
.footer::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

.footer > * { position: relative; z-index: var(--z-base); }

.footer__grid { display: grid; gap: var(--s-7); }
@media (min-width: 800px) { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr; } }

/* Institutional support: the grant line and the two logos, centred under the
   columns. Both logos are white variants made for the dark band. */
.footer__support {
  display: grid;
  justify-items: center;
  gap: var(--s-5);
  margin-top: var(--band);
  padding-top: var(--s-7);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  text-align: center;
}
.footer__logos {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--s-7);
}
.footer__logo { display: block; width: auto; }
.footer__logo--mei { height: 112px; }
.footer__logo--sa  { height: 38px; }
.footer__support-text { max-width: 52ch; }

/* The second band. Full-bleed to the window edges rather than sitting inside
   the wrap, so it reads as a bar under the footer instead of a boxed strip
   inside it. */
.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  justify-content: space-between;
  align-items: center;
  margin-top: var(--band);
  margin-inline: calc(50% - 50vw);
  padding: 10px max(var(--pad-side), calc(50vw - var(--container) / 2 + var(--pad-side)));
  background: var(--ink-raised);
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: var(--t-min);
  letter-spacing: var(--track-display);
  color: var(--paper-70);
}

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

/* A footer link is 20px of text — fine to click, awkward to thumb. On touch
   the padding grows to a usable target without changing how the footer looks
   on a desktop, where the pointer is precise and the extra space would just
   loosen the column. */
@media (hover: none) and (pointer: coarse) {
  .footer a,
  .footer__bottom a {
    display: inline-flex;
    align-items: center;
    min-height: 2.5rem;
    padding-block: 0.25rem;
  }
  .footer__bottom p { gap: var(--s-4); }
}

/* Marks, not buttons. The ring and the lift went with the rest of the boxes;
   the glyph itself going accent is the hover. */
.social { display: flex; gap: var(--s-5); }
.social a {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--paper-70);
  transition: color var(--dur) var(--ease);
}
.social a:hover { color: var(--accent); }

@media (hover: none) and (pointer: coarse) {
  .social a { width: 2.75rem; height: 2.75rem; }
}

/* ------------------------------------------------------- 17. SCROLL REVEAL

   Driven by IntersectionObserver in app.js, never by a scroll listener.
   Transform and opacity only — nothing that triggers layout.                 */

/* Hiding is OPT-IN, gated on `.reveal-ready`, which app.js adds to <html>
   only once it is running and has an IntersectionObserver to un-hide with.
   The obvious way round — hide by default, show via `.no-js` — fails badly:
   it covers "JavaScript is disabled" but NOT "JavaScript threw before it got
   here", and the failure mode there is a completely blank page on a site
   whose whole job is taking bookings. Inverted, the worst case is that the
   content simply appears without animating.

   A fade and nothing else. The rise this used to have was a 2.2rem translate
   over a full second, which on a long page means everything is permanently
   arriving from somewhere — and the brief limits reveals to fades and the
   title underline for exactly that reason. */
.reveal-ready [data-reveal] {
  opacity: 0;
  transition: opacity var(--dur-reveal) var(--ease-out);
}
.reveal-ready [data-reveal].is-in { opacity: 1; }

.reveal-ready [data-reveal-delay='1'] { transition-delay: 90ms; }
.reveal-ready [data-reveal-delay='2'] { transition-delay: 180ms; }
.reveal-ready [data-reveal-delay='3'] { transition-delay: 270ms; }
.reveal-ready [data-reveal-delay='4'] { transition-delay: 360ms; }

/* ------------------------------------------------------------- 18. UTILITIES */

.center { text-align: center; }
.hide { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.rule { height: 1px; background: var(--hairline-soft); border: 0; }

.grid-2 { display: grid; gap: var(--s-5); }
.grid-3 { display: grid; gap: var(--s-5); }
@media (min-width: 700px) { .grid-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

.flow-between { display: flex; flex-wrap: wrap; gap: var(--s-5); align-items: end; justify-content: space-between; }
