/* --- 00-fonts.css --- */
/* Self-hosted: no visitor IP reaches Google. latin + latin-ext only. */
@font-face{font-family:'Lora';font-style:italic;font-weight:400;font-display:swap;src:url('../fonts/lora-400-italic-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Lora';font-style:italic;font-weight:400;font-display:swap;src:url('../fonts/lora-400-italic-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Lora';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/lora-400-normal-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Lora';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/lora-400-normal-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Lora';font-style:normal;font-weight:500;font-display:swap;src:url('../fonts/lora-500-normal-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Lora';font-style:normal;font-weight:500;font-display:swap;src:url('../fonts/lora-500-normal-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Playball';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/playball-400-normal-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Playball';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/playball-400-normal-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Ruluko';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/ruluko-400-normal-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Ruluko';font-style:normal;font-weight:400;font-display:swap;src:url('../fonts/ruluko-400-normal-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}

/* --- 01-tokens.css --- */
/* ==========================================================================
   Schmidt Buffetservice — design tokens
   Palette and font families are inherited from the original identity:
   gold #cbad6a / #b59257 / #a18127, cream #e5d8c3 on charcoal #292a25.
   ========================================================================== */

:root {
  /* --- ground and surfaces: charcoal card stock, not flat black --------- */
  --ground-deep: #080807;
  --ground: #0e0e0d;
  --surface: #161614;
  --surface-raised: #1d1d19;
  --surface-sunk: #0a0a09;

  /* --- gold: the ink -------------------------------------------------- */
  --gold: #cbad6a;
  --gold-mid: #b59257;
  --gold-deep: #a18127;
  --gold-bright: #e8d4a3;

  /* The small gold marks - list bullets, the crumb slash, the eyebrow dot,
     quote marks, buffet numbers - sit on ground that the composed light
     lifts. --gold-deep is the darkest ink in the identity and was the one
     token holding the whole lighting scheme down: at 4.5:1 it capped every
     source on the page. One step up the same palette clears that, and the
     brand ink stays what it is wherever it is not competing with light. */
  --gold-mark: var(--gold-mid);

  /* --- text ----------------------------------------------------------- */
  --cream: #e5d8c3;
  --cream-dim: #bdb3a2;
  --muted: #94918a;

  /* --- rules: gold hairlines at three strengths ----------------------- */
  --rule: rgba(203, 173, 106, 0.20);
  --rule-mid: rgba(203, 173, 106, 0.34);
  --rule-strong: rgba(203, 173, 106, 0.58);

  /* --- semantic ------------------------------------------------------- */
  --focus: #f0dcae;
  --error: #d98b6a;
  --ok: #9db87e;

  /* --- type families: the original three ------------------------------ */
  --font-script: "Playball", "Brush Script MT", cursive;
  --font-serif: "Lora", "Iowan Old Style", Georgia, serif;
  --font-sans: "Ruluko", "Gill Sans", "Segoe UI", system-ui, sans-serif;

  /* --- fluid type scale ----------------------------------------------- */
  --t-micro: clamp(0.7rem, 0.68rem + 0.1vw, 0.78rem);
  --t-eyebrow: clamp(0.72rem, 0.69rem + 0.14vw, 0.84rem);
  --t-body: clamp(1rem, 0.965rem + 0.18vw, 1.1rem);
  --t-lead: clamp(1.1rem, 1.02rem + 0.4vw, 1.35rem);
  --t-h4: clamp(1.05rem, 0.98rem + 0.34vw, 1.28rem);
  --t-h3: clamp(1.25rem, 1.1rem + 0.72vw, 1.7rem);
  --t-h2: clamp(1.7rem, 1.3rem + 2.1vw, 3.3rem);
  --t-h1: clamp(2.05rem, 1.15rem + 3.1vw, 4.5rem);
  --t-script: clamp(1.5rem, 1.1rem + 1.85vw, 3rem);
  --t-display: clamp(3rem, 1.9rem + 5vw, 7rem);

  /* --- spacing -------------------------------------------------------- */
  --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: 4.5rem;
  --s-9: 7rem;
  --s-section: clamp(3.5rem, 2.25rem + 4.6vw, 7.5rem);
  --s-gutter: clamp(1.25rem, 0.6rem + 2.6vw, 3.5rem);

  /* --- measure and containers ----------------------------------------- */
  /* Left edge of the page container at any viewport width. Lets full-bleed
     elements line themselves up with the grid instead of guessing in vw. */
  --page-inset: max(var(--s-gutter), calc(50vw - (var(--w-page) / 2)));

  --measure: 62ch;
  --w-page: 1400px;
  --w-text: 760px;
  --w-wide: 1160px;

  /* --- depth: pressed into card stock, never floating ------------------ */
  --emboss: inset 0 1px 0 rgba(232, 212, 163, 0.10),
            inset 0 -1px 0 rgba(0, 0, 0, 0.6);
  --emboss-strong: inset 0 1px 0 rgba(232, 212, 163, 0.18),
                   inset 0 -1px 0 rgba(0, 0, 0, 0.7);
  --lift: 0 26px 64px -34px rgba(0, 0, 0, 0.95);
  --lift-near: 0 10px 30px -18px rgba(0, 0, 0, 0.85);

  /* --- motion --------------------------------------------------------- */
  --dur-1: 180ms;
  --dur-2: 420ms;
  --dur-3: 900ms;
  /* The step between items that arrive one after another rather than all at
     once. Here with the durations, so the reduced-motion block below can
     switch the whole of motion off in one place. */
  --stagger: 70ms;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* --- the hero's light: three placed sources rather than one chased one.
     The colours only - the geometry belongs to the hero and is composed in
     06-components.css, where the stacked and the two-column layout need
     different aims for the same three lamps.

     Key is the amber the pointer-driven candle used, kept because it is the
     warmth these photographs were graded against. Ember is a step deeper, so
     the low pool reads as a second source and not as the same lamp twice.
     Counter is almost brown: at its alpha it does nothing except stop the
     falloff from ending on flat black. ----------------------------------- */
  --hero-key-rgb: 228, 186, 108;
  --hero-ember-rgb: 198, 150, 66;
  --hero-counter-rgb: 120, 96, 54;

  /* --- composed light: the placed pools that carry the warmth below the
     hero. Two sources per section, a key and a counter, in the same golds
     the nav card and the action band already use.

     The strengths are not eyeballed. Every source sits with its centre off
     the section box, so the brightest pixel *inside* the box is far below
     the nominal alpha - which is what lets these run at 0.18-0.38 where a
     centred source would have to stop near 0.13. The exact ceiling per
     composition was solved against the weakest body token, --gold-deep, at
     4.6:1. Raising an alpha here without moving its centre back out will
     break that. ---------------------------------------------------------- */
  --key-rgb: 161, 129, 39;
  --pool-rgb: 188, 148, 62;

  /* --- layering scale --------------------------------------------------
     The hero's light is not on this scale: it is painted into the hero's own
     background, which puts it under every child of it by construction. ---- */
  --z-glow: 1;
  --z-grain: 2;
  --z-content: 3;
  --z-sticky: 40;
  --z-header: 50;
  --z-overlay: 60;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-1: 1ms;
    --dur-2: 1ms;
    --dur-3: 1ms;
    /* Not 1ms: a delay is not a duration. Left at 70ms the dishes would
       still arrive one after another, just each one instantly - which is
       the sequence somebody asked not to see. */
    --stagger: 0ms;
  }
}

/* --- 02-base.css --- */
/* ==========================================================================
   Base — reset, ground, typography, the grain and touch-light layers
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  min-height: 100%;
  background-color: var(--ground);
  color: var(--cream);
  font-family: var(--font-serif);
  font-size: var(--t-body);
  line-height: 1.72;
  font-weight: 400;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* --- the page's light ---------------------------------------------------
     One field over the whole document, not a wash per section.

     Each section used to paint its own two sources into its own box. Two
     things came out of that. The light stopped dead at every section edge,
     which is a container boundary and reads as one. And because the sources
     were sized in percentages of the box, the same composition was stretched
     over whatever height the copy happened to need - on the homepage the
     sections run from 403 to 3283 px, a factor of eight, so the long ones had
     their light pulled so thin that nothing arrived in the middle. Measured
     over the homepage below the hero: 81% of it sat on bare ground, and there
     were six stretches of 900 to 1900 px with no light in view at all.

     Here the sources are tiled down the document instead. Their height is in
     rem, so a patch is the same size on a 400 px section and a 3300 px one,
     and they cross every boundary because they know nothing about boxes.

     Four tiles of deliberately unrelated heights - 147, 166, 187, 211rem.
     One source each, so nothing repeats on any page this site will ever have:
     the four cycles only realign after a distance no document approaches.
     Sides alternate, so the light walks down the page rather than running in
     a stripe.

     The four phases are not free choices. Left where they fell, two of the
     centres landed 2rem apart - a doubled-up pair, which is wasted light and
     which sets the peak that caps every alpha on the page, while the gap it
     leaves elsewhere is where the page goes flat. Spread as evenly as four
     incommensurate periods allow, the closest pair is 6rem and the widest gap
     drops from 147 to 113rem, so the same ceiling buys 12% more alpha.

     One source per tile, centred in it, and every tile is at least 1.45x the
     source's vertical radius. A background layer is CLIPPED at its tile edge,
     not wrapped, so a source taller than its tile lays a hard horizontal line
     across the page. The phases are set with background-position instead,
     which moves the tiling origin and leaves the source centred.

     repeat-y, not repeat: the horizontal radius is wider than the viewport on
     purpose, and repeating it sideways would wrap the falloff back in at the
     opposite edge.

     The strengths are held to what the section washes already put on the
     ground - peak 0.0118 relative luminance, which is --gold-deep at 4.60:1,
     the value every one of those four compositions was solved to, and it
     holds at every width from 375 to 2560. So no pixel on the site gets
     brighter than it was. What changes is how much of the page has any light
     on it at all: on the homepage below the hero, from 6% to 44%, with the
     share sitting on bare ground down from 81% to 18%.

     Nothing composites while scrolling: this is one painted background.
     ---------------------------------------------------------------------- */
  background-repeat: repeat-y;
  background-size:
    100% 147rem,
    100% 166rem,
    100% 187rem,
    100% 211rem;
  background-position:
    0 0,
    0 88rem,
    0 108rem,
    0 180rem;
  background-image:
    radial-gradient(155% 101rem at 92% 50%,
      rgba(var(--key-rgb), 0.065) 0%, rgba(var(--key-rgb), 0) 70%),
    radial-gradient(155% 114rem at 6% 50%,
      rgba(var(--pool-rgb), 0.049) 0%, rgba(var(--pool-rgb), 0) 70%),
    radial-gradient(155% 129rem at 98% 50%,
      rgba(var(--key-rgb), 0.057) 0%, rgba(var(--key-rgb), 0) 70%),
    radial-gradient(155% 145rem at 10% 50%,
      rgba(var(--pool-rgb), 0.046) 0%, rgba(var(--pool-rgb), 0) 70%);
}

/* --- the touch light: a column down the middle of the screen that the page
   travels through. On a phone there is no pointer to give a light to, so it
   is given to the reader's own movement instead: it does not move, the page
   moves past it.

   Fixed and never transformed, so it rasterises once and a scrolled frame
   costs nothing - which is the whole difference from the scroll-driven light
   it replaced, and why it can run on a phone at all.

   0.12 is the ceiling. It sits under the copy column on a narrow screen,
   where every line of text eventually passes over its brightest point, so
   unlike the section pools it cannot be aimed away from the type. Solved
   against the section washes it stacks with; see 03-layout.css. ---------- */
.glow {
  position: fixed;
  inset: 0;
  z-index: var(--z-glow);
  pointer-events: none;
  display: none;
  background: radial-gradient(
    62% 400% at 50% 50%,
    rgba(214, 172, 96, 0.12) 0%,
    rgba(214, 172, 96, 0) 70%
  );
}

/* Narrow screens only. Its 0.12 was solved for a column of copy passing
   over its brightest point, and on a wide screen the hero's own light
   and the section washes already carry the page. */
@media (hover: none), (pointer: coarse) {
  .glow { display: block; }
}

/* --- grain: 3% noise over everything. Not decoration — it hides the JPEG
   banding in photographs shot between 2006 and 2012. --------------------- */
.grain {
  position: fixed;
  inset: -50%;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Everything authored sits above the light. */
.site-header,
.site-main,
.site-footer { position: relative; z-index: var(--z-content); }

/* --- German typography --------------------------------------------------
   Compounds like "Anbieterkennzeichnung" and "Umsatzsteuer-Identifikations-
   nummer" are longer than a 320px screen at display sizes. German sets
   hyphenated by tradition, so this is the correct typographic answer rather
   than a workaround; overflow-wrap is the last-resort net beneath it.
   ------------------------------------------------------------------------ */
p, li, dd, blockquote, figcaption, td, th, summary {
  hyphens: auto;
  -webkit-hyphens: auto;
  /* 10 5 5: only words of ten characters or more break, and never with fewer
     than five letters on either side. The looser 8 4 4 produced fragments
     like "Roast-beef" and "…buf-fets" — technically legal syllable breaks,
     but not where a German compound wants to be divided. */
  hyphenate-limit-chars: 10 5 5;
  overflow-wrap: break-word;
}

/* Display type is not hyphenated. Headings are short, authored and few, so a
   heading that will not fit is a copy problem to be fixed rather than a break
   to be inserted — _build/check_html.py fails the build on any heading word
   too long for the narrowest column. overflow-wrap stays as the last resort,
   and a soft hyphen can still be placed by hand where a legal term has to
   stay verbatim (see "Schadensminderungs&shy;pflicht" in legal.py). */
h1, h2, h3, h4, h5, h6, .script {
  hyphens: manual;
  -webkit-hyphens: manual;
  overflow-wrap: break-word;
}

/* Display lines break between words, never inside them, and the rag is
   balanced rather than left to fill each line to the edge. */
.script { text-wrap: balance; }

/* The claim breaks at its sense joint — "…garantiert" / "in aller Munde
   bleibt." — rather than wherever the measure happens to run out. Blocks
   rather than a <br>, so each half still wraps internally if it has to. */
.script__line { display: block; }

/* Never break these: tracked uppercase labels, tabular figures and prices
   lose their meaning the moment they hyphenate. */
.eyebrow, .meta, .num, .axis-row__price, .axis-row__min,
.facts dd, .buffet__price, .btn, .nav__link, .brand__name, .crumbs {
  hyphens: manual;
  -webkit-hyphens: manual;
}

/* --- headings -------------------------------------------------------- */
h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.012em;
  text-wrap: balance;
}

h1 { font-size: var(--t-h1); line-height: 1.19; letter-spacing: -0.022em; }
h2 { font-size: var(--t-h2); line-height: 1.08; letter-spacing: -0.018em; }
h3 { font-size: var(--t-h3); }
h4 { font-size: var(--t-h4); }

p { margin: 0 0 1.15em; max-width: var(--measure); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

/* A dish list sits in a 267px column on a telephone, so a name like
   "Saftiges Poulardenbrustfilet in Paprikarahm" has to take two lines. Where
   it breaks is the whole question. `pretty` stops the second line being a
   single short word; the non-breaking spaces that `formel.py` binds into the
   markup stop a line ending on a preposition, which is the worse of the two
   faults and the one CSS cannot fix. */
li { text-wrap: pretty; }

strong { font-weight: 500; color: var(--gold-bright); }
em { font-style: italic; }

/* --- links ----------------------------------------------------------- */
a { color: var(--gold); text-decoration-color: var(--rule-mid); text-underline-offset: 0.22em; }
/* :not(.btn) is load-bearing, not tidiness. `a:hover` is (0,1,1) and outranks
   `.btn`'s (0,1,0) `color: var(--btn-fg)`, so unscoped it repainted every
   button that is a link in --gold-bright on hover. On .btn--gold that is the
   same colour as the gradient underneath: the label vanished at 1.0:1. Each
   button variant states its own hover foreground; none of them wants this. */
a:not(.btn):hover { color: var(--gold-bright); text-decoration-color: currentColor; }

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

/* --- lists ----------------------------------------------------------- */
ul, ol { margin: 0 0 1.15em; padding-left: 1.15em; }
li { margin-bottom: 0.35em; }
li::marker { color: var(--gold-mark); }

img, svg, video { max-width: 100%; height: auto; display: block; }

hr {
  border: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--rule-mid) 20%, var(--rule-mid) 80%, transparent);
  margin: var(--s-7) 0;
}

::selection { background: var(--gold-deep); color: var(--ground-deep); }

/* --- utility type roles ----------------------------------------------- */
.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--t-eyebrow);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--gold-mid);
  line-height: 1.4;
  margin: 0 0 var(--s-4);
  display: block;
}

.script {
  font-family: var(--font-script);
  font-size: var(--t-script);
  color: var(--gold);
  line-height: 1.25;
  letter-spacing: 0.01em;
  font-weight: 400;
}

.lead {
  font-size: var(--t-lead);
  line-height: 1.62;
  color: var(--cream-dim);
}

.meta {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

.num {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: 0.02em;
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  top: 0; left: 50%;
  transform: translate(-50%, -120%);
  z-index: var(--z-overlay);
  background: var(--gold);
  color: var(--ground-deep);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: var(--s-3) var(--s-5);
  text-decoration: none;
  transition: transform var(--dur-1) var(--ease);
}
.skip-link:focus { transform: translate(-50%, 0); }

/* --- 03-layout.css --- */
/* ==========================================================================
   Layout — containers, the editorial grid, and the overlap system
   Overlaps are built from grid areas that deliberately share cells, so they
   reflow instead of breaking. Nothing here uses absolute positioning.
   ========================================================================== */

/* Grid and flex children default to min-width:auto, which lets an item
   refuse to shrink below its content and push the page wider than the
   viewport. Every layout primitive here opts out explicitly. */
.shell {
  width: min(100% - (var(--s-gutter) * 2), var(--w-page));
  margin-inline: auto;
}

.shell--text { width: min(100% - (var(--s-gutter) * 2), var(--w-text)); margin-inline: auto; }
.shell--wide { width: min(100% - (var(--s-gutter) * 2), var(--w-wide)); margin-inline: auto; }

.section { padding-block: var(--s-section); position: relative; }
.section--tight { padding-block: calc(var(--s-section) * 0.6); }
.section--flush-top { padding-top: 0; }

/* Alternating ground so long pages breathe without adding colour.
   background-color, not the shorthand: the wash below is a background-image
   on the same boxes and the shorthand would reset it. */
/* A scrim rather than the flat colour: over --ground this resolves to the
   same #0a0a09 the token names, but it is translucent, so the touch light
   behind it carries down the page instead of stopping dead at every second
   section the way the old fixed candle did. */
/* A flat scrim with a real edge. It was briefly faded over 5rem at each end,
   on the reasoning that a hard rectangle over the field slices any patch that
   straddles it - true, but what it produced was a smear where this site
   otherwise draws lines. The band is a change of surface and is allowed to
   start somewhere; the field is what carries continuity, and it does that
   underneath. */
.section--sunk { background-color: rgba(0, 0, 0, 0.29); }
.section--raised { background-color: var(--surface); }

/* --- the light --------------------------------------------------------------
   Sections no longer carry any. Each one used to paint a key and a counter
   pool into its own box, which stopped the light at every boundary and sized
   it by the length of the copy rather than by the page. It is one field over
   the whole document now; see the note on `body` in 02-base.css.

   What is left here is the two grounds, and they matter more than they did:
   the field is painted behind the sections, so a scrim laid on one of them
   now dims the light travelling underneath as well as the ground it sits on.
   That is what gives the page its beat - lit stretches and quieter ones -
   instead of every section lighting itself identically.

   background-color, not the shorthand: it has to leave the field alone.
   -------------------------------------------------------------------------- */

/* Hairline separators that fade at both ends, like a pressed rule. */
.section--ruled::before {
  content: "";
  position: absolute;
  inset-inline: var(--s-gutter);
  top: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--rule-mid) 22%, var(--rule-mid) 78%, transparent);
}

/* --- section heads ----------------------------------------------------- */
.section-head {
  display: grid;
  gap: var(--s-4);
  margin-bottom: var(--s-7);
  max-width: 46rem;
}
.section-head--center { margin-inline: auto; text-align: center; justify-items: center; }
.section-head p { color: var(--cream-dim); }

/* --- the ornament, redrawn from the original scrollwork ---------------- */
.ornament {
  display: block;
  width: clamp(150px, 20vw, 230px);
  height: auto;
  margin: var(--s-5) 0;
  opacity: 0.75;
  color: var(--gold-mid);
}
.ornament--center { margin-inline: auto; }

/* --- .overlay-pair: text plate that sits over the corner of an image ---- */
.overlay-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

.overlay-pair > .overlay-pair__media { grid-area: 1 / 1; }
.overlay-pair > .overlay-pair__plate {
  grid-area: 1 / 1;
  align-self: end;
  justify-self: start;
  z-index: 2;
  margin: 0 var(--s-4) calc(var(--s-5) * -1) var(--s-4);
}

@media (min-width: 48rem) {
  .overlay-pair > .overlay-pair__plate {
    margin: 0 0 calc(var(--s-6) * -1) calc(var(--s-7) * -1);
    max-width: 30rem;
  }
  .overlay-pair--right > .overlay-pair__plate {
    justify-self: end;
    margin: 0 calc(var(--s-7) * -1) calc(var(--s-6) * -1) 0;
  }
}

/* --- .band: full-bleed photograph with a text plate on its lower edge.
   Mobile first: the photograph is a bounded band and the plate is pulled up
   over its edge, so the copy is always on a solid surface. From 60rem the
   two share one grid cell and the plate floats on the image.

   Both layers carry min-width: 0. Without it a frame that has an
   aspect-ratio and height:100% derives its WIDTH from the row height and
   refuses to shrink, which pushed the document to 1229px at a 390px
   viewport. Never let an aspect-ratio box be height-driven inside a grid.
   ------------------------------------------------------------------------ */
/* No breakout. The band is a direct child of <main> on every page that has
   one, and <main> is already the full content width - so 100vw plus a
   negative margin did not widen anything, it just made the section a
   scrollbar wider than the box it sits in and hung 8px off each side. That
   is where the document's horizontal overflow came from. vw counts the
   scrollbar; the content box does not. */
.band {
  display: grid;
}
.band > * { min-width: 0; }

.band__media .frame { aspect-ratio: 3 / 2; }

.band__plate {
  position: relative;
  z-index: 2;
  width: min(100% - (var(--s-gutter) * 2), var(--w-page));
  margin: calc(var(--s-7) * -1) auto 0;
  padding-bottom: var(--s-6);
  display: grid;
}
.band__plate > * { max-width: 36rem; }

@media (min-width: 60rem) {
  .band { grid-template-areas: "band"; }
  .band > * { grid-area: band; }

  .band__media { align-self: stretch; }
  .band__media .frame {
    aspect-ratio: auto;
    height: 100%;
    min-height: 28rem;
  }

  .band__plate {
    align-self: end;
    margin-top: 0;
    padding-block: var(--s-7);
  }
  .band--right .band__plate { justify-items: end; }
}

/* --- .split: asymmetric editorial two-column ---------------------------- */
.split {
  display: grid;
  gap: var(--s-6);
  align-items: center;
}

/* Kopf ueber dem Text statt mitten im Textblock: den Abstand Kopf -> Text
   uebernimmt die Zeilenluecke des Grids, sonst summiert er sich doppelt. */
.split--head-above > .section-head { margin-bottom: 0; }

@media (min-width: 56rem) {
  .split { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(2rem, 5vw, 5.5rem); }
  .split--wide-left { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .split--wide-right { grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); }
  .split--top { align-items: start; }

  /* Drei Kinder, zwei Zeilen: Kopf oben links, darunter der Fliesstext, das
     Bild daneben. So setzt das Bild an der Textoberkante an und nicht neben
     der Eyebrow. Gestapelt greift das nicht - da stehen die drei
     untereinander und die Zeilenluecke macht den Abstand. */
  .split--head-above { row-gap: var(--s-7); }
  .split--head-above > :nth-child(1) { grid-column: 1; grid-row: 1; }
  .split--head-above > :nth-child(2) { grid-column: 1; grid-row: 2; }
  .split--head-above > :nth-child(3) { grid-column: 2; grid-row: 2; }
  .split__media--pull { margin-block: calc(var(--s-7) * -1); }

  /* The picture starts at the heading, not at the eyebrow above it.
     The offset is not a measured pixel value but the eyebrow's own
     box restated: its line box, plus the margin it carries, plus the
     row gap .section-head puts between its children - both apply, and
     together they are what separates the eyebrow from the h2. Written
     this way it tracks --t-eyebrow, which is a clamp and changes with
     the viewport, where a fixed 50px would drift away from it. */
  .split--media-at-heading > :first-child {
    margin-top: calc(var(--t-eyebrow) * 1.4 + var(--s-4) * 2);
  }

  /* The picture keeps pace with the reading instead of scrolling off after
     the first paragraph, and lets go at the foot of the text. Only from this
     breakpoint up: below it the columns are stacked, and a picture pinned
     over its own text would be in the way rather than alongside it. Needs
     align-items:start above - a stretched grid item is already the height of
     its row and has nothing left to travel in. */
  .split--sticky > :first-child {
    position: sticky;
    /* Well clear of the header, so the picture takes up its travelling
       position early rather than drifting most of the way up the screen
       first. Raise this to pin it sooner still, lower it to let it run. */
    top: calc(var(--s-8) + var(--s-7));
  }
}

/* --- .bento: deliberately uneven, never a uniform card grid ------------- */
.bento {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: 1fr;
}

@media (min-width: 44rem) {
  .bento { grid-template-columns: repeat(6, 1fr); grid-auto-rows: minmax(9rem, auto); }
  .bento > :nth-child(1) { grid-column: span 4; grid-row: span 2; }
  .bento > :nth-child(2) { grid-column: span 2; }
  .bento > :nth-child(3) { grid-column: span 2; }
  .bento > :nth-child(4) { grid-column: span 3; }
  .bento > :nth-child(5) { grid-column: span 3; }
}

/* --- full-bleed band --------------------------------------------------- */
.bleed {
  position: relative;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* --- simple flow helpers ----------------------------------------------- */
.flow > * + * { margin-top: var(--s-4); }
.flow-lg > * + * { margin-top: var(--s-6); }
/* A cluster is a row of controls, not prose, so it does not take the
   reading measure that p carries. Seven of these are written as <p> in the
   page templates, and p is capped at --measure: a .cluster--center inside
   one was centring its button inside a 678px box that itself sat at the left
   of a 1400px section, which reads as "not centred" because it is not. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: center;
  max-width: none;
}
.cluster--between { justify-content: space-between; }
.cluster--center { justify-content: center; }

/* On a phone the buttons in a cluster take the full column, the way
   the hero's do. Measured at 375px the hero's button is 335 wide and
   this one was 187, sitting at the left of a 335 box - the same action
   in two sizes, which reads as two different kinds of control. Full
   width is also the bigger target for a thumb.

   Not --plain: that variant has no box at all, it is a text link
   wearing the button class, and stretching its hit area across the
   column would catch taps nowhere near the words. */
@media (max-width: 43.99rem) {
  .cluster .btn:not(.btn--plain) { width: 100%; }
}

/* --- 04-media.css --- */
/* ==========================================================================
   Media — the veil system.
   Current customer photography leads; the older archive remains available.
   Content photos receive a light tint. Hero masks keep photography clear of
   the text, preserving the existing contrast and page identity.
   ========================================================================== */

.frame {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--surface-sunk);
  isolation: isolate;
}

.frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: contrast(1.03);
  transition: transform var(--dur-3) var(--ease-expo),
              filter var(--dur-2) var(--ease);
}

/* Tint — the constant. Pushes every photo toward the charcoal ground. */
.frame::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--ground-deep);
  opacity: var(--veil, 0.12);
  transition: opacity var(--dur-2) var(--ease);
}

/* Veil — directional, so type always lands on the darkest part. */
.frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: var(--veil-dir, linear-gradient(
    to top,
    rgba(8, 8, 7, 0.92) 0%,
    rgba(8, 8, 7, 0.45) 38%,
    rgba(8, 8, 7, 0.08) 72%,
    rgba(8, 8, 7, 0.3) 100%
  ));
}

.frame--from-left {
  --veil-dir: linear-gradient(to right, rgba(8,8,7,.94) 0%, rgba(8,8,7,.6) 34%, rgba(8,8,7,.12) 68%, rgba(8,8,7,.36) 100%);
}
.frame--from-right {
  --veil-dir: linear-gradient(to left, rgba(8,8,7,.94) 0%, rgba(8,8,7,.6) 34%, rgba(8,8,7,.12) 68%, rgba(8,8,7,.36) 100%);
}
/* Content photographs carry no type, so they only need a light unifying
   tint — enough to sit in the palette, not enough to hide the food. The heavy
   treatments are reserved for frames that actually have text over them. */
.frame--even {
  --veil: 0.08;
  --veil-dir: linear-gradient(to top, rgba(8, 8, 7, 0.4), rgba(8, 8, 7, 0.06) 45%, rgba(8, 8, 7, 0.14));
}
.frame--light { --veil: 0.04; }
.frame--heavy { --veil: 0.5; }

/* Gold hairline, inset — the frame is pressed into the card, not stuck on. */
.frame--ruled {
  box-shadow: inset 0 0 0 1px var(--rule), var(--lift);
}

/* Aspect ratios — declared so nothing shifts while images load. */
.frame--wide { aspect-ratio: 16 / 9; }
.frame--cinema { aspect-ratio: 21 / 9; }
.frame--photo { aspect-ratio: 3 / 2; }
.frame--square { aspect-ratio: 1 / 1; }
.frame--portrait { aspect-ratio: 4 / 5; }
.frame--tall { aspect-ratio: 3 / 4; }

/* In-view: only a ruled frame has an edge to catch the light. */
.frame--ruled.is-lit { box-shadow: inset 0 0 0 1px var(--rule-strong), var(--lift); }
.frame.is-lit img { filter: contrast(1.05) saturate(1.04); }

/* Interactive frames (gallery, partner cards) */
a.frame:hover img,
.frame-link:hover .frame img { transform: scale(1.035); filter: saturate(1.06) contrast(1.06); }
a.frame:hover::before,
.frame-link:hover .frame::before { opacity: calc(var(--veil, 0.42) - 0.16); }

/* --- caption sitting inside the frame, on the dark edge ---------------- */
.frame__caption {
  position: absolute;
  z-index: 3;
  left: 0; right: 0; bottom: 0;
  padding: var(--s-5);
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--cream-dim);
}

/* --- duotone treatment for the weakest source photographs -------------- */
.frame--duotone img { filter: grayscale(1) contrast(1.1) brightness(0.85); }
.frame--duotone::before {
  background: linear-gradient(150deg, #1a1409 0%, #0b0b0a 60%);
  opacity: 0.66;
  mix-blend-mode: color;
}
.frame--duotone.is-lit img { filter: grayscale(0.8) contrast(1.1) brightness(0.95); }

/* --- crop control: where the subject actually sits in the source frame -- */
.frame--pos-right img { object-position: 68% 30%; }
.frame--pos-left img { object-position: 24% 45%; }
.frame--pos-top img { object-position: center 22%; }

/* --- hero: darkened from the left for the copy and from the bottom so the
   photograph dissolves into the next section instead of ending on a seam. */
/* --- hero -----------------------------------------------------------------
   The photograph is MASKED out where the copy goes, not painted over.

   Painting an opaque --ground panel inside the frame looked identical in a
   static mock but produced a hard vertical seam the moment the hero was lit:
   the light is painted into the hero's background, so it lit the bare page
   ground beside the photo while the painted panel inside the frame stayed
   unlit. Masking removes those pixels entirely, so the same lit ground shows
   through on both sides and there is no edge to see.
   ------------------------------------------------------------------------ */
.frame--hero {
  --veil: 0.16;
  --veil-dir: linear-gradient(to bottom,
    rgba(8, 8, 7, 0.34) 0%,
    rgba(8, 8, 7, 0.04) 34%,
    rgba(8, 8, 7, 0.3) 100%);
  box-shadow: none;
  -webkit-mask-image: var(--hero-mask);
  mask-image: var(--hero-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  /* Reaches fully transparent at 76%, above where the copy is pulled up to,
     so the type sits on page ground and never on a photographed pixel.
     Verified per pixel by _build/hero_pixels.py. */
  --hero-mask: linear-gradient(to bottom,
    transparent 0, rgba(0, 0, 0, 0.4) 1rem, #000 2.75rem,
    #000 44%, rgba(0, 0, 0, 0.45) 60%, transparent 76%);
}

@media (min-width: 62rem) {
  .frame--hero {
    --veil-dir: linear-gradient(to right,
      rgba(8, 8, 7, 0.5) 0%,
      rgba(8, 8, 7, 0.12) 45%,
      rgba(8, 8, 7, 0.28) 100%);
    /* Two fades intersected: out to the left where the copy sits, and out at
       the foot so the photograph dissolves into the next section instead of
       ending on a straight edge. */
    /* Absolute stops, not percentages: the cleared zone must not shrink on a
       small desktop or drift wider than it needs to on a large one.

       The first stop is what the type needs and cannot move: measured at
       1440, the widest line of copy is the eyebrow, whose right edge reaches
       127px into the picture against a cleared 144px. Seventeen pixels of
       slack, so the clearance stays at overlap + 2rem.

       The ramp behind it is a different matter. It used to run to
       overlap + 20rem - 432px - before the photograph was fully itself, long
       past anything with type on it. Pulled in to 12rem the picture arrives
       128px further left and the type still sits on cleared ground. */
    --hero-mask:
      linear-gradient(to right,
        transparent 0,
        transparent calc(var(--hero-overlap) + 2rem),
        rgba(0, 0, 0, 0.5) calc(var(--hero-overlap) + 6rem),
        #000 calc(var(--hero-overlap) + 12rem), #000 100%),
      linear-gradient(to bottom,
        transparent 0, rgba(0, 0, 0, 0.35) 3rem, #000 9rem,
        #000 64%, rgba(0, 0, 0, 0.4) 88%, transparent 100%);
  }
}

/* Safari before 15.4 ignores mask-image on a compositing layer often enough
   that it is worth a floor: without the mask the directional veil alone still
   darkens the copy side. */
@supports not ((mask-image: linear-gradient(#000, transparent)) or
               (-webkit-mask-image: linear-gradient(#000, transparent))) {
  .frame--hero { --veil: 0.55; }
  @media (min-width: 62rem) {
    .frame--hero {
      --veil-dir: linear-gradient(to right, var(--ground) 0%, var(--ground) 30%,
        rgba(8, 8, 7, 0.5) 52%, rgba(8, 8, 7, 0.2) 100%);
    }
  }
}

/* --- course icon, from the original icon set --------------------------- */
/* The class sits on <picture> so the WebP source can win; the <img> fills it. */
.course-icon {
  display: block;
  width: 2.5rem;
  height: 2.5rem;
  flex: 0 0 auto;
  opacity: 0.9;
}
.course-icon img { width: 100%; height: 100%; }

/* --- the hero loop --------------------------------------------------------
   The film lies inside the same frame as the photograph, not in place of it.
   The <picture> keeps its fetchpriority and stays the element the browser
   measures for LCP; the video sits on top of it, invisible until it is
   actually playing, and inherits the frame's mask and veil like any other
   pixel in there.

   That ordering is the whole safety net. Every reason not to play - reduced
   motion, a data saver, a slow radio, a browser that refuses to autoplay, a
   blocked script - ends as the video simply never reaching .is-playing, and
   what the visitor gets is the page exactly as it was before it had one.

   z-index 0 rather than auto: the tint and the veil are 1 and 2, and the
   picture is static. The loop has to land above the photograph and below
   both of the layers that make a photograph fit this page.
   ------------------------------------------------------------------------ */
.frame__video {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 45%;
  filter: contrast(1.03);
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease);
}

.frame__video.is-playing { opacity: 1; }

/* The wide stage shows roughly the middle two thirds of the file and then
   masks its left half away for the copy, so what survives is the right of
   what is left. Pushing the object window right puts the subject of every
   shot inside that survivor - the shots are cut and cropped for this number
   in _build/video.py, so the two belong together. */
@media (min-width: 62rem) {
  .hero__media .frame__video { object-position: 72% 45%; }
}

/* --- the image film ------------------------------------------------------
   The one video on this site that is asked for rather than served. It keeps
   the browser's own controls: a hand-built play button would be one more
   thing to make keyboard-reachable and screen-reader-legible, and it would
   still be worse than the one the operating system already ships.

   No veil and no mask here. Those exist so type can sit on a photograph;
   nothing sits on this, and dimming a film somebody chose to watch would be
   decoration working against the content.
   ------------------------------------------------------------------------ */
.film { margin: 0; }

.film__video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  /* The poster is 16:9 and so is the film, but the element is sized before
     either has loaded. Stating the ratio is what keeps the page from
     jumping when they arrive. */
  background: var(--ground-deep);
}

/* The credit is .meta like every other small line on the site; all it needs
   from here is to sit under the middle of the film. */
.film__caption { margin-top: var(--s-4); text-align: center; }

/* --- 05-chrome.css --- */
/* ==========================================================================
   Chrome — header, navigation, buttons, footer
   ========================================================================== */

/* --- header ------------------------------------------------------------ */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: linear-gradient(to bottom, rgba(8, 8, 7, 0.96), rgba(8, 8, 7, 0.78) 70%, rgba(8, 8, 7, 0));
  transition: background var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}

/* The blur is desktop-only on purpose. backdrop-filter makes an element the
   containing block for its position: fixed descendants, so on mobile it
   redefined the menu panel's inset against the 72px header instead of the
   viewport — the panel came out 80px tall. The mobile menu does not exist
   above this breakpoint, so the blur is safe here. */
@media (min-width: 68rem) {
  .site-header {
    backdrop-filter: blur(10px) saturate(1.1);
    -webkit-backdrop-filter: blur(10px) saturate(1.1);
  }
}

.site-header.is-stuck {
  background: rgba(10, 10, 9, 0.96);
  box-shadow: 0 1px 0 var(--rule), 0 18px 40px -30px rgba(0, 0, 0, 1);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  min-height: 4.5rem;
  padding-block: var(--s-3);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  text-decoration: none;
  flex: 0 0 auto;
}
/* The logo is 188x190, not square: contain keeps it undistorted in the
   square slot the header reserves for it. */
.brand img {
  width: auto;
  height: 2.75rem;
  position: relative;
  /* The tray the mark carries sits on the same line as the tray under the
     cloche in the menu trigger. Re-measured against the vector master: its
     tray band spans 261-277 of the 426.4-unit artwork, so at a 44px render
     the tray sits 27.76px below the image's top edge. That lands on 48.06
     against the trigger's 47.60; the 0.46 comes off here. */
  top: 5.88px;
}
.brand__name {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--cream);
  line-height: 1.3;
}
.brand__name b { display: block; font-weight: 400; color: var(--gold); letter-spacing: 0.3em; }
.brand:hover .brand__name b { color: var(--gold-bright); }

/* --- primary navigation -------------------------------------------------
   Mobile first: the disclosure is the default navigation. The horizontal bar
   is the enhancement, not the other way round. ---------------------------- */
.nav { display: none; }

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 1.4vw, 1.6rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__link {
  position: relative;
  display: block;
  padding: var(--s-2) 0;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--cream-dim);
  text-decoration: none;
  white-space: nowrap;
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-2) var(--ease-expo);
}
.nav__link:hover { color: var(--gold-bright); }
.nav__link:hover::after,
.nav__link[aria-current="page"]::after { transform: scaleX(1); }
.nav__link[aria-current="page"] { color: var(--gold); }

/* --- the disclosure ------------------------------------------------------
   The mobile navigation is the menu card, not a drawer: numbered rows on
   hairline rules, the same shape as the price mirror and the pillars. It is a
   <details>, so it opens and closes with JavaScript disabled.
   ------------------------------------------------------------------------ */
.nav-toggle { display: block; margin-left: auto; }

.nav-toggle > summary {
  position: relative;
  /* 4.8px puts "Menü" and the cloche's tray on the same baseline as
     "Buffetservice" in the wordmark opposite. Measured, not guessed. */
  top: 4.8px;
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.375rem;
  min-width: 6.375rem;
  min-height: 3rem;
  padding: 0.5rem 0.125rem 0.65rem;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
}
.nav-toggle > summary::-webkit-details-marker { display: none; }

.nav-toggle__label {
  position: relative;
  /* The word sits on the cloche's tray. 3.5px is the measured gap between the
     label's baseline and the top edge of the tray stroke at this size — see
     the pixel check in the build notes. Not a transform: a non-replaced
     inline element ignores those. */
  top: 3.5px;
}

/* No line under the label. The word is simply set on the tray's height, so
   the cloche's own stroke is the only line in the control. The element and
   its rules stay because the build gate requires them — inert, not forgotten. */
.nav-toggle__line { display: none; }

.nav-toggle__line {
  position: absolute;
  left: 0;
  right: -0.55rem;
  bottom: 0;
  height: 1.65px;
  background: currentColor;
  transform: translateX(0);
  transition: transform 420ms cubic-bezier(.2, .9, .2, 1),
              background 180ms var(--ease);
}

.nav-toggle > summary:hover .nav-toggle__line,
.nav-toggle > summary:focus-visible .nav-toggle__line {
  background: var(--gold-bright);
}

.nav-toggle[open] .nav-toggle__line {
  transform: translateX(calc(50% + 1.25rem));
}

/* The trigger is the cloche from the logo rather than a burger. Three rules
   of unequal length read as a rendering fault at this size; the mark the
   business already owns does not, and it says "food" without a word. */
.nav-toggle__icon {
  position: relative;
  display: block;
  width: 1.75rem;
  height: 1.75rem;
  flex: 0 0 auto;
  margin-bottom: -0.125rem;
  background: var(--ground-deep);
}
.nav-toggle__icon svg {
  /* The lifted lid carries its finial above the top of the viewBox, where the
     default overflow clips it away. */
  overflow: visible;
  width: 100%;
  height: 100%;
}

.nav-toggle__lid {
  transform-box: fill-box;
  transform-origin: 72% 100%;
  transition: transform 420ms cubic-bezier(.2, .9, .2, 1);
}

.nav-toggle[open] .nav-toggle__lid {
  transform: translate(4px, -9px) rotate(17deg);
}

.nav-toggle[open] > summary { color: var(--gold-bright); }

.nav-toggle.is-closing .nav-toggle__line { transform: translateX(0); }
.nav-toggle.is-closing .nav-toggle__lid { transform: none; }
.nav-toggle.is-closing > summary { color: var(--gold); }

@media (prefers-reduced-motion: reduce) {
  .nav-toggle__line,
  .nav-toggle__lid { transition: none; }
}

/* --- the panel ---------------------------------------------------------- */
.nav-toggle__panel {
  position: fixed;
  inset: 4.5rem 0 0 0;
  z-index: var(--z-header);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-6);
  overflow-y: auto;
  padding: var(--s-6) var(--s-gutter) calc(var(--s-7) + env(safe-area-inset-bottom));
  /* Opaque, so contrast does not depend on whatever the page is showing
     behind it, with the same warm light the rest of the site uses. */
  background:
    radial-gradient(120% 60% at 82% 0%, rgba(161, 129, 39, 0.18), transparent 62%),
    var(--ground-deep);
}

/* Keep the native disclosure rendered while JavaScript waits for this exit.
   Removing the closing state reverses the transition on a quick second tap. */
@media (prefers-reduced-motion: no-preference) {
  .nav-toggle__panel {
    transition: opacity 240ms var(--ease), transform 240ms var(--ease);
  }
  .nav-toggle.is-closing .nav-toggle__panel {
    opacity: 0;
    transform: translate3d(-14px, 0, 0);
    pointer-events: none;
  }
}

.nav-card ul { list-style: none; margin: 0; padding: 0; }
.nav-card li { border-bottom: 1px solid var(--rule); }
.nav-card li:first-child { border-top: 1px solid var(--rule-mid); }

.nav-card a {
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-4) 0;
  text-decoration: none;
  color: var(--cream);
  transition: color var(--dur-1) var(--ease);
}
.nav-card__no {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.2em;
  color: var(--gold-mid);
  min-width: 2.4ch;
}
.nav-card__label {
  font-family: var(--font-serif);
  font-size: var(--t-h4);
  line-height: 1.2;
}
.nav-card a:hover { color: var(--gold-bright); }
.nav-card a[aria-current="page"] { color: var(--gold); }
.nav-card a[aria-current="page"] .nav-card__no { color: var(--gold); }

/* --- the foot: the two things a phone is actually for ------------------- */
.nav-card__foot { display: grid; gap: var(--s-4); justify-items: center; }
.nav-card__foot .btn { margin-bottom: var(--s-1); }

.nav-card__tel {
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 1.1rem + 2vw, 2rem);
  color: var(--gold);
  text-decoration: none;
  letter-spacing: 0.02em;
}
.nav-card__tel:hover { color: var(--gold-bright); }
.nav-card__foot .ornament { margin: 0; opacity: 0.5; }

/* --- the rows arrive one after another --------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .nav-toggle[open] .nav-card li {
    animation: nav-row var(--dur-3) var(--ease-expo) backwards;
    animation-delay: calc(var(--i, 0) * 42ms);
  }
  .nav-toggle[open] .nav-card__foot {
    animation: nav-row var(--dur-3) var(--ease-expo) backwards;
    animation-delay: 420ms;
  }
}

@keyframes nav-row {
  from { opacity: 0; transform: translate3d(-14px, 0, 0); }
}

@media (min-width: 68rem) {
  .nav { display: block; margin-left: auto; }
  .nav-toggle { display: none; }
}

/* --- buttons ----------------------------------------------------------- */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--gold);
  --btn-rule: var(--rule-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  padding: 0.85em 1.55em;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-sans);
  /* Sentence case, barely tracked, and a size up from the eyebrow.
     Set as small caps at 0.2em the buttons read as labels among the
     other labels on the page - the eyebrow, the meta lines, the facts
     band all share that treatment - and the one element that is meant
     to be pressed looked like the ones that are meant to be read.
     The face stays Ruluko: this is a change of setting, not of type. */
  font-size: clamp(0.9rem, 0.86rem + 0.2vw, 1rem);
  letter-spacing: 0.01em;
  text-transform: none;
  line-height: 1;
  text-decoration: none;
  border: 0;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--btn-rule);
  transition: background var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease);
}

.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }

.btn--gold {
  --btn-bg: linear-gradient(160deg, var(--gold-bright), var(--gold) 42%, var(--gold-deep));
  --btn-fg: #17130a;
  --btn-rule: transparent;
  box-shadow: var(--emboss-strong), 0 14px 30px -18px rgba(203, 173, 106, 0.6);
}
.btn--gold:hover {
  --btn-bg: linear-gradient(160deg, #f4e3ba, var(--gold-bright) 45%, var(--gold));
  box-shadow: var(--emboss-strong), 0 18px 38px -18px rgba(203, 173, 106, 0.75);
}

.btn--ghost:hover { --btn-bg: rgba(203, 173, 106, 0.08); --btn-fg: var(--gold-bright); --btn-rule: var(--gold-mid); }
.btn--plain { box-shadow: none; padding-inline: 0; }
.btn--plain:hover { color: var(--gold-bright); }
.btn--sm { padding: 0.65em 1.15em; font-size: 0.82rem; letter-spacing: 0.01em; }
.btn--full { width: 100%; }

/* --- footer ------------------------------------------------------------ */
.site-footer {
  background: var(--ground-deep);
  border-top: 1px solid var(--rule);
  /* Keep the footer clear of the device's home indicator. */
  padding-block: var(--s-8) calc(var(--s-6) + env(safe-area-inset-bottom));
}

@media (min-width: 68rem) {
  .site-footer { padding-block: var(--s-8) var(--s-6); }
}

.site-footer__grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: 1fr;
}

@media (min-width: 50rem) {
  .site-footer__grid { grid-template-columns: 1.4fr 1fr 1fr; gap: var(--s-7); }
}

.site-footer h2 {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--gold-mid);
  margin-bottom: var(--s-4);
}

.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: var(--s-2); }
.site-footer a { color: var(--cream-dim); text-decoration: none; }
.site-footer a:hover { color: var(--gold-bright); }

.site-footer__legal {
  margin-top: var(--s-7);
  padding-top: var(--s-5);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4) var(--s-5);
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  color: var(--muted);
  letter-spacing: 0.1em;
}

address { font-style: normal; }

/* --- 06-components.css --- */
/* ==========================================================================
   Components
   ========================================================================== */

/* --- HERO ----------------------------------------------------------------
   No card. The photograph dissolves into the page ground, and the type sits
   on the ground it dissolved into — so the copy is on a known surface without
   a box around it.

   Why not simply lay type over the photo: _build/hero_veil.py samples the
   region the copy occupies in each hero image and reports the 99.5th
   percentile luminance at 0.87-1.00. These photographs carry blown highlights
   (glassware, white plates) straight through the text area, so no region of
   them is reliably dark and no veil under ~0.97 clears 4.5:1. Rather than
   scrim the picture into mud, the veil runs to full --ground where the type
   goes and leaves the rest of the photograph alone.

   Mobile: photo band on top, fading out over its lower quarter; the copy
   rises into that faded zone. Desktop: photo on the right, fading out to the
   left; the copy sits in the left column and overlaps the faded edge.
   ------------------------------------------------------------------------ */
.hero {
  /* Restated here, and deliberately not in vw. The token on :root
     computes from 50vw, which counts the scrollbar; .shell centres
     inside the content box, which does not. Above ~1500px that put
     the hero 8px further out than everything below it - two edges
     almost but not quite the same, which reads worse than two that
     differ on purpose. 100% here is the hero's own containing
     block, the same box .shell measures against. */
  --page-inset: max(var(--s-gutter),
                    calc((100% - var(--w-page)) / 2));
  display: grid;
  grid-template-columns: 1fr;
  position: relative;
  padding-bottom: var(--s-7);
  /* Its own ground, opaque, so the page-wide field stops at the hero and does
     not add itself to a composition solved without it. The same colour the
     body carries, so nothing changes - including behind the photograph, which
     is masked away to exactly this. Nothing has to be faded out at the foot:
     the light is composed to have died there, within a hair of what the field
     puts on the ground just below. */
  background-color: var(--ground);
  /* Put here for the pointer light that used to live in the hero: a 64rem
     square whose right half always landed outside it, which on a 360 px phone
     reached x=714 and made the document's scrollable width twice the screen.
     That light is gone, and measured at 375 and at 1440 nothing in the hero
     overflows it any more - the rail inside the wedding heroes does carry
     items past the screen edge, but it scrolls them itself.

     Kept as a guard all the same: it costs nothing, and a horizontal
     scrollbar on a phone is the kind of regression that reaches the client
     before it reaches us. `clip` rather than `hidden`, which would turn the
     hero into a scroll container; horizontal only, so a source composed past
     the hero's top edge still bleeds up out of it. */
  overflow-x: clip;
}
.hero > * { min-width: 0; }

/* --- the hero's light -----------------------------------------------------
   Three placed sources, painted into the hero's own background. There used to
   be one, a 64rem radial that followed the pointer; it is gone. A light that
   tracks the mouse announces on every page that it is an effect, comes to
   rest wherever the visitor last left the cursor, and cannot be aimed.
   Painted, it rasterises once with the rest of the hero, costs nothing to
   scroll past, and lands where the composition wants it.

   Into the background rather than over it: the photograph is masked away from
   the copy's side (04-media.css), so this light shows through the cleared
   zone and grazes the edge where the picture dissolves.

   The spot is top left and the counter is the far corner, so there is one
   diagonal to read along. The third sits at the photograph's fading edge at a
   quarter of the key's strength - enough that the picture still comes out of
   light rather than ending in nothing.

   Composed to have died by the hero's foot: 0.009 relative luminance there at
   desktop and 0.008 stacked, against the 0.0118 the page-wide field puts on
   the ground immediately below. The hero hands over to the page with no step,
   and nothing has to be faded out to hide one. That is what the top left
   position buys - with the bright source at the bottom the handover needed a
   mask, and the mask was a smear.

   The strengths are not eyeballed. Solved against the four tokens the hero
   sets type in - gold, cream, cream-dim, gold-bright - the weakest clears
   4.75:1 in both layouts, against a 4.5:1 floor. The binding pixel is the top
   left corner of the copy itself, which is why the key's centre sits above
   and outside that corner rather than on the copy.

   The far stop of each is its own colour at zero alpha rather than
   `transparent`, which interpolates through transparent black and greys the
   falloff.
   ------------------------------------------------------------------------ */
.hero--lit {
  /* key - the spot, top left. In the stack the top left corner is the
     photograph, which is opaque, so the nearest thing to it that can be seen
     is the left of the band the picture dissolves into: the centre sits on
     that line and the light reads as coming in from above and to the left. */
  --h-k-x: 6%; --h-k-y: 26%; --h-k-w: 58%; --h-k-h: 30%; --h-k-a: 0.42;
  /* dissolve - a quarter as strong, at the photograph's fading edge, so the
     picture still comes out of light instead of ending in nothing */
  --h-d-x: 72%; --h-d-y: 20%; --h-d-w: 46%; --h-d-h: 24%; --h-d-a: 0.17;
  /* counter - the far corner from the key, almost nothing, so the diagonal
     has somewhere to go */
  --h-c-x: 98%; --h-c-y: 94%; --h-c-w: 56%; --h-c-h: 26%; --h-c-a: 0.13;

  background-image:
    radial-gradient(var(--h-k-w) var(--h-k-h) at var(--h-k-x) var(--h-k-y),
      rgba(var(--hero-key-rgb), var(--h-k-a)) 0%,
      rgba(var(--hero-key-rgb), 0) 72%),
    radial-gradient(var(--h-d-w) var(--h-d-h) at var(--h-d-x) var(--h-d-y),
      rgba(var(--hero-ember-rgb), var(--h-d-a)) 0%,
      rgba(var(--hero-ember-rgb), 0) 68%),
    radial-gradient(var(--h-c-w) var(--h-c-h) at var(--h-c-x) var(--h-c-y),
      rgba(var(--hero-counter-rgb), var(--h-c-a)) 0%,
      rgba(var(--hero-counter-rgb), 0) 70%);
}

.hero__media { position: relative; min-width: 0; }
.hero__media .frame img { object-position: center 42%; }

.hero__media .frame { aspect-ratio: 4 / 3; }
@media (min-width: 23rem) { .hero__media .frame { aspect-ratio: 5 / 4; } }
@media (min-width: 30rem) { .hero__media .frame { aspect-ratio: 3 / 2; } }
@media (min-width: 44rem) { .hero__media .frame { aspect-ratio: 16 / 9; } }

/* Below the desktop breakpoint the hero is a stack, and left to its content
   it ended two thirds of the way down the screen with the next section
   peering over the fold. It fills the screen here as well - the header is in
   the flow at this width, hence subtracting it - and the slack goes to the
   photograph rather than opening a gap under the copy. Placed before the
   desktop query so that one still wins where both match. */
@media (max-width: 61.99rem) {
  .hero {
    min-height: calc(100svh - var(--s-8));
    /* Headline, picture, copy - and the slack goes to the PICTURE, not
       to the copy. Given to the copy it came out as empty ground under
       the buttons; given to the picture the hero ends exactly on the
       fold with the band below it out of sight, which is the point of
       holding it to a screen in the first place. */
    grid-template-rows: auto 1fr auto;
    /* The photograph and the copy together run past the bottom of a phone
       screen - see the block further down. This does not move the button up,
       nothing below it can, but it is height nobody can see and the page
       below arrives sooner for it. */
    padding-bottom: var(--s-5);
  }
  .hero .rail { margin-top: auto; }

  /* The frame drops its fixed ratio here and fills the row it was
     given. The old note against this warned about upscaling a 375x250
     still onto a square; the source behind it now is a 1200px poster
     and an 800px loop, so filling a taller box crops rather than
     stretches. The loop is cut 4:5 for exactly this box - see the
     phone rendition in _build/video.py. */
  /* Balanced line lengths, phone only. The lead and the buttons share a
     box 38ch wide, but balance shortens every line to even them out, so on
     the desktop the text stopped 44px short of the button edge below it and
     the column lost its right-hand alignment. In the narrow measure the
     trade goes the other way: there it stops the last line becoming a
     two-word orphan, and there is no button edge beside it to answer to.
     Unsupported, this falls back to normal wrapping. */
  .hero--home .hero__lead { text-wrap: balance; }

  .hero--home .hero__media { display: grid; }
  .hero--home .hero__media .frame {
    aspect-ratio: auto;
    height: 100%;
    min-height: 14rem;
  }

}

/* A phone held sideways has ~390 px of height to work with. */
@media (max-width: 39.99rem) and (max-height: 34rem) and (orientation: landscape) {
  .hero__media .frame { aspect-ratio: 21 / 9; }
  .hero__body { margin-top: -2rem; }
}

.hero__body {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  /* Rises into the band's lower edge, which the veil has already taken to
     solid --ground. The overlap is real; the background under the type is
     not the photograph. */
  margin-top: -2.5rem;
  padding-inline: var(--s-gutter);
}

/* Eyebrow and headline, above the picture on a phone and rejoined to the
   lead on the desktop. It carries the gutter itself rather than inheriting
   one, because it is a grid item in its own right at every width. */
.hero__intro {
  position: relative;
  z-index: 3;
  padding-inline: var(--s-gutter);
  /* Air under the header. The eyebrow used to start within a few
     pixels of the navigation, which read as the headline being stuck
     to it rather than placed below it. Block-start only: the picture
     follows immediately underneath and needs no gap of its own. */
  padding-block-start: var(--s-5);
}

}

/* gold rather than gold-mid: the eyebrow is the weakest colour over the
   dissolved photograph, and this lifts its measured margin from ~4.7:1 to
   ~6.3:1. See _build/hero_pixels.py. */
.hero .eyebrow { color: var(--gold); }

.hero .eyebrow__part { white-space: nowrap; }

/* --- hero scroll cue ---------------------------------------------------
   Hidden until the hero actually fills the screen; below that breakpoint the
   next section is already in view and an arrow would point at nothing. It
   targets an anchor pinned to the hero's foot rather than a section id, so
   one rule serves every page whatever follows the hero — and the anchor's
   scroll-margin lands that section below the sticky header, not behind it. */
.hero__cue {
  display: none;
  place-items: center;
  position: absolute;
  left: 50%;
  bottom: var(--s-5);
  z-index: 4;
  /* The mark is just the chevron, but the target stays 44px square: an
     invisible box is still a box to a thumb and to a pointer. */
  width: 2.75rem;
  height: 2.75rem;
  translate: -50% 0;
  color: var(--gold);
  transition: color var(--dur-1) var(--ease),
              translate var(--dur-1) var(--ease);
}

.hero__cue svg { width: 1.35rem; height: auto; }

.hero__cue:hover,
.hero__cue:focus-visible {
  color: var(--gold-bright);
  translate: -50% 0.22rem;
}

/* Sits in the flow between the hero and whatever follows, at zero height.
   This is the fallback landing, for a fragment jump with site.js absent:
   the seam comes to rest just below the header. With the script present the
   cue is taken over and aimed at the heading instead - stated there as a
   rule, because a scroll-margin is read differently by the fragment
   algorithm and by scrollIntoView, and a negative one differently again. */
.hero__anchor {
  display: block;
  height: 0;
  scroll-margin-top: var(--s-8);
}

.hero h1 { margin-bottom: var(--s-5); }
/* Playball sits in a much taller line box than Lora at the same size, which
   left a 22px hole under the script line while the Lora lines below it
   touched. Its own leading is pulled in and the residue trimmed, so the two
   gaps read as one rhythm. */
.hero h1 .script {
  display: block;
  font-size: clamp(1.9rem, 1.2rem + 3.4vw, 4.4rem);
  color: var(--gold);
  line-height: 1;
  margin-bottom: -0.16em;
  letter-spacing: 0;
}

/* The homepage slogan is one editorial sentence. Gold marks its semantic
   payoff; a second display face no longer competes with the headline. */
.hero--home .eyebrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.55rem;
  letter-spacing: 0.1em;
  font-size: 0.68rem;
}
.hero--home .eyebrow__part {
  flex-basis: auto;
  white-space: nowrap;
}
.hero--home .eyebrow__part + .eyebrow__part::before {
  content: "·";
  margin-right: 0.55rem;
  letter-spacing: 0;
  /* --gold, not --gold-mark: this is the one mark that sits inside the hero,
     where the hero's own light falls on the ground under it. The marks token
     is a step darker and would read 3.49:1 with the light behind it; the
     eyebrow's own text is already --gold, so the dot separating it simply
     matches. */
  color: var(--gold);
}
.hero--home h1 {
  font-size: clamp(2.35rem, 1.2rem + 3.8vw, 4.75rem);
  line-height: 1.04;
  letter-spacing: -0.028em;
}
.hero--home .hero__headline-accent { color: var(--gold); }

/* Slight lift on the homepage hero image. Restored after an edit of mine
   overwrote the block it lived in. */
@media (min-width: 62rem) {
  .hero--home .hero__media .frame img,
  .hero--home .hero__media .frame__video {
    transform: translateY(-0.35rem) scale(1.025);
  }
}

@media (min-width: 34rem) {
  .hero--home .eyebrow {
    gap: 0.35rem 1rem;
    letter-spacing: 0.2em;
    font-size: var(--t-eyebrow);
  }
  .hero--home .eyebrow__part + .eyebrow__part::before { margin-right: 1rem; }
}

.hero__lead { margin-bottom: var(--s-6); max-width: 38ch; }
/* The buttons stop on the same axis as the copy. Left to stretch they ran to
   the full column: 130px past the lead at desktop, up to 364px in the stacked
   range — the only thing in the hero reaching an edge nothing else touches,
   which reads as an oversight rather than a decision.

   font-size here is doing arithmetic, not typography: `ch` resolves against
   the element's own font, so 38ch on this div would come out a different
   width than 38ch on the lead. Borrowing the lead's size makes the two
   measures identical by construction and keeps them identical as the clamp
   scales. The buttons set their own font-size, so nothing inherits it. */
.hero__actions {
  display: grid;
  gap: var(--s-3);
  margin-bottom: var(--s-6);
  font-size: var(--t-lead);
  max-width: 38ch;
}

/* --- getting the enquiry button onto the first screen ----------------------
   Measured on the two handsets this was tested on. At 393x659 - an iPhone
   with Safari's toolbars showing - the hero came out 813px tall and the
   button block sat 146px below the fold: not clipped, absent. At 412x780, a
   Pixel, 40px. There is no sticky action bar on this site, so that button is
   the only way to enquire without scrolling, on a page whose whole job is
   enquiries.

   Only what sits ABOVE the button can move it: trimming the hero's foot
   padding shortens the hero but leaves the button exactly where it was. So
   the savings come from the photograph, the eyebrow, and the two margins
   under the headline and the lead - and from nowhere else.

   It has to live here rather than in the breakpoint further up, because the
   plain `.hero h1` and `.hero__lead` rules are declared after that block at
   the same specificity and were quietly winning.

   The photograph is deliberately NOT part of this. Capping its height was by
   far the largest saving available and it is not taken: the picture is the
   product on a catering site and it keeps its full band. Which means what is
   left is the joins alone, and they do not add up to enough on the shortest
   screens - see the note at the end of this block.
   -------------------------------------------------------------------------- */
@media (max-width: 61.99rem) {
  .hero__body { margin-top: -3.25rem; }
  .hero .eyebrow { margin-bottom: var(--s-2); }
  .hero h1 { margin-bottom: var(--s-4); }
  .hero__lead { margin-bottom: var(--s-5); line-height: 1.5; }
  /* Nothing follows the last block but the hero's own foot padding, so its
     margin is 32px of guaranteed nothing. */
  .hero__body > :last-child { margin-bottom: 0; }
}

/* What the above does and does not fix, measured:

     393x659  iPhone, toolbars shown   button block 146px under -> 97px under
     412x780  Pixel                                  40px under ->   9px clear
     375x812                                         23px under ->  72px clear
     360x640                                          95px clear -> 115px clear

   So on the shortest state of an iPhone the enquiry button is still below the
   fold, and cannot be brought up without either shortening the photograph or
   shortening the copy. It returns as soon as Safari's toolbar collapses on
   the first scroll, which gives back about 85px. If it has to be reachable
   without scrolling at all, the answer is a sticky action bar on phones, not
   a tighter hero. */

/* Reading order stays eyebrow, headline, lead, action, rail. Every child
   needs a number: an unnumbered one defaults to 0 and jumps the queue. */
.hero .eyebrow { order: 1; }
.hero h1 { order: 2; }
.hero__lead { order: 3; }
.hero__actions { order: 4; }
.hero .rail { order: 5; }

/* Below roughly 640 px of viewport height there is not room for all of it
   above the fold, so the order of importance takes over: headline, then the
   action, then the detail. The lead carries no links, so visual order and
   focus order stay in agreement. */
@media (max-height: 40rem) {
  .hero__actions { order: 3; margin-bottom: var(--s-5); }
  .hero__lead { order: 4; margin-bottom: 0; }
  .hero h1 { margin-bottom: var(--s-4); }
}

/* --- from 62rem: the photograph moves to the right and the copy takes the
   left column, overlapping the edge the mask has already dissolved.

   Two absolute values drive the whole desktop hero, so the relationship
   between copy and photograph is identical at 1024 px and at 2560 px: the
   picture begins one --hero-overlap before the copy ends, and its mask stays
   fully transparent for --hero-overlap + 2rem. The copy therefore always
   finishes exactly 2rem inside the cleared zone, at every width, instead of
   drifting the way a percentage does.
   ------------------------------------------------------------------------ */
@media (min-width: 62rem), (min-width: 40rem) and (max-height: 30rem) {
  .hero {
    /* 38rem, measured rather than picked. The headline is written to
       break into three lines; at 36rem it still did so at 1440 but fell
       to four at 1920, where the type is at the top of its clamp and the
       column is not. 37rem is the exact tipping point, so 38rem is the
       narrowest value with a step of headroom - and still hands the
       photograph 32px it did not have at 40rem. */
    --hero-text: min(38rem, 46vw);
    --hero-overlap: 7rem;

    /* Four rows for two blocks. The headline and the lead are separate
       elements only because a phone needs the picture between them; here
       they stack back into the one block they read as.

       The empty 1fr rows above and below are what centre that pair, rather
       than align-content: center. Both centre the text identically - but
       align-content shrinks the grid to its content, and the photograph
       spans the rows, so it came out 591px tall in a 900px hero with page
       ground above and below it. Spanning 1 to -1 of a grid that fills the
       hero gives the picture its full height back. */
    grid-template-rows: 1fr auto auto 1fr;
    /* A full screen, so the fold lands exactly on the hero's foot. That is
       what makes the cue at the bottom a promise rather than decoration. */
    min-height: 100svh;
    /* Breathing room against the header. The copy is nearly as tall as the
       hero itself, so centring alone left it hard up under the navigation. */
    padding-block: var(--s-7) var(--s-6);
  }
  .hero > * { grid-column: 1; }
  .hero__intro { grid-row: 2; }
  .hero__body { grid-row: 3; }
  .hero__media,
  .hero__cue { grid-row: 1 / -1; }

  .hero__media {
    align-self: stretch;
    justify-self: end;
    /* 100%, not 100vw, for the same reason as the inset above: mixing
       the two made the picture a scrollbar's width too wide, which
       overflow-x:clip then hid rather than fixed. */
    width: calc(100% - var(--page-inset) - var(--hero-text) + var(--hero-overlap));
    /* The padding above is for the copy; the photograph must not inherit it,
       or it begins on a hard horizontal line below the navigation — its own
       near-black top edge against the lit ground. Pulling it up by the hero's
       own top padding is enough: the mask fades the picture in over its first
       9rem, so what sits behind the header is the transparent part of that
       fade and the top edge is never a visible cut. It used to be pulled up
       by the header's height on top of this, which rode the photograph higher
       than the composition wants. */
    margin-block: calc(var(--s-7) * -1) calc(var(--s-6) * -1);
  }
  /* Taken out of the height calculation entirely. With height:100% against an
     auto-height row the image falls back to its own 1600x1336 and drives the
     hero: 1579px tall at 3440 wide, 1746px at 3840. Positioned absolutely it
     fills whatever height the copy and min-height decide. */
  .hero__media .frame {
    position: absolute;
    inset: 0;
    aspect-ratio: auto;
    height: auto;
  }

  .hero__body,
  .hero__intro {
    justify-self: start;
    padding-inline: 0;
    width: var(--hero-text);
    margin: 0 0 0 var(--page-inset);
  }

  .hero--page { min-height: 100svh; }
  .hero__cue { display: grid; }

  /* Here the top left corner is the copy's own corner, not the photograph's,
     so the key can sit in it - centred on the corner itself, which puts its
     brightest part above where the copy starts and leaves only the falloff
     for the type. The photograph has moved right, so the dissolve light goes
     with it, and the counter drops to the bottom right under the picture's
     own fade, so the diagonal runs corner to corner. */
  .hero--lit {
    --h-k-x: 2%; --h-k-y: 2%; --h-k-w: 48%; --h-k-h: 56%; --h-k-a: 0.42;
    --h-d-x: 64%; --h-d-y: 34%; --h-d-w: 34%; --h-d-h: 52%; --h-d-a: 0.17;
    --h-c-x: 96%; --h-c-y: 98%; --h-c-w: 44%; --h-c-h: 40%; --h-c-a: 0.13;
  }
}

/* --- FACTS BAND ---------------------------------------------------------
   The three figures a caller actually asks for. The label introduces the
   figure and the figure answers it at display size — set the other way round,
   or both at body size, they read as spreadsheet cells, which a ruled box
   around them only made worse.

   The separator is the same rotated square used as the bullet in the menu
   lists, so the band borrows a mark the page already owns instead of
   inventing furniture.
   ------------------------------------------------------------------------ */
.facts-band { position: relative; z-index: 3; padding-block: var(--s-7); }

.facts {
  --facts-gap: var(--s-7);
  display: grid;
  gap: var(--facts-gap);
  justify-items: center;
  text-align: center;
}

.facts > div {
  position: relative;
  display: grid;
  gap: var(--s-3);
}

.facts dd {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(1.75rem, 1.25rem + 1.9vw, 2.6rem);
  line-height: 1;
  color: var(--gold-bright);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -0.012em;
}

.facts dt {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The mark sits in the gap between items: above each one when stacked, */
.facts > div + div::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--facts-gap) / -2);
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* A closing mark under the last one, so the stack ends rather than just
   stopping. Only while stacked — in a row it would trail off the end. */
.facts > div:last-child::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(var(--facts-gap) / -2);
  width: 5px;
  height: 5px;
  margin: 0 0 -2.5px -2.5px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* and beside it once they sit in a row. */
@media (min-width: 44rem) {
  .facts > div:last-child::after { display: none; }

  .facts {
    --facts-gap: clamp(2.5rem, 7vw, 7rem);
    grid-auto-flow: column;
    justify-content: center;
    align-items: start;
  }
  /* In a row the mark hangs off the figure rather than off the item, so it
     centres on the numeral instead of on the label-plus-numeral block — and
     it stays centred if a label ever wraps to two lines. The columns are
     content-sized, so half the grid gap lands exactly between the two
     figures. */
  .facts > div + div::before { display: none; }

  .facts dd { position: relative; }
  .facts > div + div > dd::before {
    content: "";
    position: absolute;
    top: 50%;
    left: calc(var(--facts-gap) / -2);
    width: 5px;
    height: 5px;
    margin: -2.5px 0 0 -2.5px;
    background: var(--gold-deep);
    transform: rotate(45deg);
  }
}

/* --- PILLARS -------------------------------------------------------------
   Three parallel services, so no numbering: 01/02/03 asserted a sequence that
   does not exist, and every attempt to place those figures well — ghosted
   behind the heading, then breaking the rule above it — read as decoration
   looking for a job.

   The marks are drawn in the same hand as the menu trigger: 1.3px gold
   strokes on a 24 unit grid. The original course icons could not be reused —
   that set only covers food courses, so it has nothing for drinks or for
   crockery, and its detail would fight the hairlines.

   Rules run only between the columns, never around them.
   ------------------------------------------------------------------------ */
.pillars {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.pillar {
  position: relative;
  margin: 0;
  padding: var(--s-6) 0;
}
.pillar + .pillar { border-top: 1px solid var(--rule); }

.pillar__icon {
  display: block;
  width: 30px;
  height: 30px;
  color: var(--gold);
  margin-bottom: var(--s-5);
}

.pillar h3 {
  margin-bottom: var(--s-4);
  color: var(--cream);
}
.pillar p { color: var(--cream-dim); max-width: 34ch; }

@media (min-width: 56rem) {
  .pillars { grid-template-columns: repeat(3, 1fr); }

  .pillar { padding: 0 var(--s-6) 0 0; }
  .pillar + .pillar {
    border-top: 0;
    border-left: 1px solid var(--rule);
    padding-left: var(--s-6);
  }
}

/* --- PANEL: surfaces used by the signature band and the contact column -- */
.panel {
  background: linear-gradient(165deg, var(--surface-raised), var(--surface));
  box-shadow: var(--emboss), var(--lift-near);
  padding: var(--s-6);
  display: grid;
  gap: var(--s-4);
  align-content: start;
}
.panel h3 { color: var(--cream); }
.panel p { color: var(--cream-dim); font-size: 0.98em; }
.panel__index {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.24em;
  color: var(--gold-mark);
}

/* --- PRICE AXIS ("Der Preisspiegel") ------------------------------------
   Every buffet row draws its price at its true position on one shared
   scale, so reading down the list shows the whole price landscape as a
   single drawing. --pos is set per row in the markup. -------------------- */
.axis { display: grid; gap: 0; }

.axis__scale {
  display: none;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  color: var(--muted);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--rule-mid);
}

@media (min-width: 56rem) {
  .axis__scale { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: var(--s-6); }
  .axis__scale span:last-child { display: flex; justify-content: space-between; }
}

.axis-row {
  display: grid;
  gap: var(--s-3);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
  transition: background var(--dur-1) var(--ease);
}
.axis-row:hover { background: rgba(203, 173, 106, 0.045); }

.axis-row__name {
  font-family: var(--font-serif);
  font-size: var(--t-h4);
  color: var(--cream);
  transition: color var(--dur-1) var(--ease);
}
.axis-row:hover .axis-row__name { color: var(--gold-bright); }

.axis-row__dishes {
  font-size: 0.94em;
  color: var(--muted);
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The track */
.axis-row__track {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-4);
}

.axis-row__line {
  position: relative;
  flex: 1 1 auto;
  height: 1px;
  background: var(--rule);
}
.axis-row__line::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--pos, 50%);
  background: linear-gradient(90deg, transparent, var(--gold-deep));
  transform: scaleX(var(--draw, 1));
  transform-origin: left;
  transition: transform var(--dur-3) var(--ease-expo);
}
.axis-row__line::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--pos, 50%);
  width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px;
  background: var(--gold);
  transform: rotate(45deg) scale(var(--draw, 1));
  transition: transform var(--dur-3) var(--ease-expo), background var(--dur-1) var(--ease);
  box-shadow: 0 0 0 4px rgba(203, 173, 106, 0.1);
}
.axis-row:hover .axis-row__line::after { background: var(--gold-bright); }

.axis-row__price {
  flex: 0 0 auto;
  font-family: var(--font-sans);
  font-size: var(--t-h4);
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  min-width: 5.5ch;
  text-align: right;
}
.axis-row__min {
  flex: 0 0 auto;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  color: var(--muted);
  white-space: nowrap;
}

/* Mobile default: the shared axis is unreadable at 360px, so each row gets
   its own fill bar instead. The comparison survives; the shared scale does
   not pretend to work. */
.axis-row__line { max-width: 9rem; }
.axis-row__line::after { display: none; }
.axis-row__line::before {
  background: linear-gradient(90deg, var(--gold-deep), var(--gold));
  height: 2px;
  top: -0.5px;
  min-width: 1.4rem;
}

@media (min-width: 56rem) {
  .axis-row {
    grid-template-columns: minmax(0, 1fr) 22rem;
    align-items: center;
    gap: var(--s-6);
  }
  .axis-row__name { grid-column: 1; }
  .axis-row__dishes { grid-column: 1; }
  .axis-row__track { grid-column: 2; grid-row: 1 / span 2; }

  /* Back to one shared scale: every price at its true position. */
  .axis-row__line { max-width: none; background: var(--rule); }
  .axis-row__line::before {
    background: linear-gradient(90deg, transparent, var(--gold-deep));
    height: 1px;
    top: 0;
  }
  .axis-row__line::after { display: block; }
}

/* --- MENU LEAF: one course of one buffet ------------------------------- */
.leaf {
  display: grid;
  gap: var(--s-4);
  padding: var(--s-6) 0;
  border-bottom: 1px solid var(--rule);
}
.leaf:last-child { border-bottom: 0; }

.leaf__head { display: flex; align-items: center; gap: var(--s-4); }
.leaf__head h3 {
  font-family: var(--font-sans);
  font-size: var(--t-eyebrow);
  font-weight: 400;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--gold-mid);
}
.leaf__rule { flex: 1 1 auto; height: 1px; background: var(--rule); }

.leaf ul { list-style: none; padding: 0; margin: 0; columns: 1; }
@media (min-width: 34rem) { .leaf ul { columns: 2; column-gap: var(--s-6); } }
.leaf li {
  break-inside: avoid;
  padding-left: 1.1em;
  position: relative;
  margin-bottom: 0.55em;
  color: var(--cream);
}
.leaf li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.72em;
  width: 4px; height: 4px;
  background: var(--gold-deep);
  transform: rotate(45deg);
}

/* --- BUFFET CARD: a whole numbered menu on the offers page ------------- */
.buffet {
  display: flex;
  flex-direction: column;
  background-image: linear-gradient(168deg,
    var(--surface-raised) 0%, var(--surface) 100%);
  box-shadow: var(--emboss), var(--lift-near);
  padding: clamp(1.5rem, 1rem + 2.4vw, 3rem);
  scroll-margin-top: 6rem;
  transition:
    background-image var(--dur-2) var(--ease),
    box-shadow var(--dur-2) var(--ease);
}
.buffet ul { margin-bottom: 0; }

/* --- the card under the pointer ------------------------------------------
   Built only from tokens the card already uses. At rest it runs
   surface-raised into surface, darker at the foot; on hover both stops go to
   surface-raised, so the lower half lifts toward the light rather than a new
   colour appearing. The inset highlight goes to its strong variant and the
   shadow from the near one to the far one, which is what reads as the card
   coming forward.

   :focus-within alongside :hover - the card holds a link on the buffets page
   and will hold more; a state a mouse can reach and a keyboard cannot is
   half a state.

   No cursor change and no transform. The card is an <article>, not a
   control: a pointer cursor would promise a click that nothing answers, and
   a lift would move type that people are in the middle of reading.
   ------------------------------------------------------------------------ */
.buffet:hover,
.buffet:focus-within {
  background-image: linear-gradient(168deg,
    var(--surface-raised) 0%, var(--surface-raised) 100%);
  box-shadow: var(--emboss-strong), var(--lift);
}

.buffet:hover .buffet__no,
.buffet:focus-within .buffet__no { color: var(--gold-bright); }

.buffet:hover .buffet__title,
.buffet:focus-within .buffet__title { border-bottom-color: var(--rule-strong); }

.buffet:hover .buffet__price,
.buffet:focus-within .buffet__price { border-color: var(--rule-strong); }

/* grid-auto-rows:1fr gives every row the height of the tallest card in the
   whole grid, not just its own row, so all fifteen cards match and every
   price - pinned to its card's floor - lands on the same line down the page.
   Without it the rows size themselves and the prices step about. */
.buffet-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  grid-auto-rows: 1fr;
}

/* Inline flow, not flex. A flex row breaks between its items, which on a
   narrow card left "Fingerfood-Buffet" on one line and the figure stranded
   alone on the next. Inline, the non-breaking space in the markup welds the
   figure to the word before it, so the line can only break where the name
   itself allows — after the hyphen in "Fingerfood-Buffet". */
.buffet__title {
  display: block;
  padding-bottom: var(--s-5);
  border-bottom: 1px solid var(--rule-mid);
  margin-bottom: var(--s-5);
}

/* An inline-block so its own line-height governs how much room it claims:
   at 0.9 the figure sets the height of the heading's line box instead of the
   heading's leading stretching around it. Both offsets are in the figure's
   own em, so it stays proportional as the clamp scales it. The script face
   carries almost no right side bearing, hence the margin.

   There used to be a translateY(0.16em) here, an optical drop from before
   the figure carried a full stop. A full stop sits ON the baseline, so it
   made the mismatch legible: measured, the figure's box ran 9.9px below the
   name's. Without it they are 0.7px apart, which is as close as two faces
   at two sizes get. */
.buffet__no {
  display: inline-block;
  font-family: var(--font-script);
  font-size: clamp(2.2rem, 1.6rem + 2.4vw, 3.6rem);
  color: var(--gold-mark);
  line-height: 0.9;
  /* The figure now closes the line instead of opening it, so the space
     the script face does not carry goes in front of it. */
  margin-inline-start: 0.12em;
}
/* The card's closing line: terms on the left, the sum on the right, sharing
   a baseline across the full width. margin-top:auto drops the whole line to
   the floor, and because the grid already stretches every card in a row to a
   common height, the prices in a row land on one line however many dishes
   each buffet happens to list. That alignment is the reason the price sits
   here rather than beside the name. */
.buffet__price {
  margin-top: auto;
  padding-top: var(--s-5);
  border-top: 1px solid var(--rule-mid);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem var(--s-4);
}
.buffet__price b {
  /* auto margin rather than space-between: it also holds the sum to the right
     on a narrow card, where the terms take the line to themselves. */
  margin-left: auto;
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--t-h3);
  color: var(--gold-bright);
  font-variant-numeric: tabular-nums;
}
.buffet__price span {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- QUOTE / testimonial ----------------------------------------------- */
.quote {
  display: grid;
  gap: var(--s-4);
  padding: var(--s-7) var(--s-6) var(--s-6);
  background: var(--surface);
  box-shadow: var(--emboss);
  position: relative;
}
.quote::before {
  content: "„";
  position: absolute;
  top: 0.1rem; left: 1.1rem;
  font-family: var(--font-script);
  font-size: 4rem;
  color: var(--gold-mark);
  opacity: 0.42;
  line-height: 1;
  pointer-events: none;
}
.quote blockquote { margin: 0; font-style: italic; color: var(--cream); position: relative; }
.quote figcaption {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold-mid);
}
.quote figcaption span { color: var(--muted); }

/* --- CONDITION LIST: the honest small print, shown not hidden ---------- */
.conditions { display: grid; gap: 0; }
.conditions > div {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--rule);
}
@media (min-width: 46rem) {
  .conditions > div { grid-template-columns: 15rem minmax(0, 1fr); gap: var(--s-6); align-items: baseline; }
}
.conditions dt {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-mid);
}
.conditions dd { margin: 0; color: var(--cream-dim); }
.conditions dd b { color: var(--gold-bright); font-weight: 400; font-variant-numeric: tabular-nums; }

/* Homepage conditions: a ruled editorial ledger, grouped by the decision a
   customer is making. The shared conditions list above stays unchanged. */
.conditions-section .section-head { max-width: 42rem; }
.condition-groups {
  display: grid;
  border-top: 1px solid var(--rule-mid);
  border-bottom: 1px solid var(--rule-mid);
}
.condition-group { padding-block: var(--s-6); }
.condition-group + .condition-group { border-top: 1px solid var(--rule); }
.condition-group__head {
  padding-bottom: var(--s-5);
}
.condition-group__head h3 {
  font-size: clamp(1.35rem, 1.2rem + 0.7vw, 1.8rem);
  color: var(--cream);
}
.conditions--grouped > div {
  display: block;
  padding: var(--s-4) 0;
}
.conditions--grouped > div:first-child { border-top: 1px solid var(--rule); }
.conditions--grouped > div:last-child { border-bottom: 0; }
.conditions--grouped dt { margin-bottom: var(--s-2); }
.conditions--grouped dd {
  max-width: 42ch;
  font-size: 0.98em;
  line-height: 1.62;
}

@media (min-width: 44rem) and (max-width: 59.99rem) {
  .condition-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .condition-group { padding: var(--s-6); }
  .condition-group:nth-child(2) {
    border-top: 0;
    border-left: 1px solid var(--rule-mid);
  }
  .condition-group:nth-child(3) { grid-column: 1 / -1; }
  .condition-group:nth-child(3) .conditions--grouped {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    border-top: 1px solid var(--rule);
  }
  .condition-group:nth-child(3) .conditions--grouped > div {
    padding-inline: var(--s-5);
    border-top: 0;
    border-bottom: 0;
  }
  .condition-group:nth-child(3) .conditions--grouped > div:first-child { padding-left: 0; }
  .condition-group:nth-child(3) .conditions--grouped > div:last-child { padding-right: 0; }
  .condition-group:nth-child(3) .conditions--grouped > div + div {
    border-left: 1px solid var(--rule);
  }
}

@media (min-width: 60rem) {
  .condition-groups { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .condition-group { padding: var(--s-6); }
  .condition-group:first-child { padding-left: 0; }
  .condition-group:last-child { padding-right: 0; }
  .condition-group + .condition-group {
    border-top: 0;
    border-left: 1px solid var(--rule-mid);
  }
}

/* --- PRICE TABLE (Leihartikel) ----------------------------------------- */
.price-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.price-table caption { text-align: left; margin-bottom: var(--s-4); color: var(--muted); font-family: var(--font-sans); font-size: var(--t-micro); letter-spacing: 0.16em; text-transform: uppercase; }
.price-table th, .price-table td { padding: var(--s-3) var(--s-2); text-align: left; border-bottom: 1px solid var(--rule); }
.price-table thead th {
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-mid);
  border-bottom-color: var(--rule-mid);
}
.price-table td:last-child, .price-table th:last-child { text-align: right; color: var(--gold); white-space: nowrap; }
.price-table tbody tr:hover { background: rgba(203, 173, 106, 0.04); }

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* --- FORM --------------------------------------------------------------- */
.form { display: grid; gap: var(--s-5); }
.form__row { display: grid; gap: var(--s-5); }
@media (min-width: 40rem) { .form__row--2 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 40rem) { .form__row--3 { grid-template-columns: 1fr 1fr 1fr; } }

.field { display: grid; gap: var(--s-2); }
.field > label {
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-mid);
}
.field > label .req { color: var(--gold-mark); }

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.85rem 1rem;
  background: var(--surface-sunk);
  color: var(--cream);
  font-family: var(--font-serif);
  font-size: 1rem;
  border: 0;
  box-shadow: inset 0 0 0 1px var(--rule);
  transition: box-shadow var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.field textarea { min-height: 9rem; resize: vertical; line-height: 1.6; }
.field input::placeholder, .field textarea::placeholder { color: #6a675f; }
.field input:hover, .field select:hover, .field textarea:hover { box-shadow: inset 0 0 0 1px var(--rule-mid); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  background: #0d0d0b;
  box-shadow: inset 0 0 0 1px var(--gold-mid), 0 0 0 3px rgba(203, 173, 106, 0.16);
}
.field input:user-invalid,
.field select:user-invalid,
.field textarea:user-invalid {
  box-shadow: inset 0 0 0 1px rgba(217, 139, 106, 0.75);
}
.field input:user-valid { box-shadow: inset 0 0 0 1px rgba(157, 184, 126, 0.4); }

.field--check { grid-template-columns: auto 1fr; align-items: start; gap: var(--s-3); }
.field--check input { width: 1.1rem; height: 1.1rem; margin-top: 0.35rem; accent-color: var(--gold); box-shadow: none; padding: 0; }
.field--check label { text-transform: none; letter-spacing: 0.01em; font-family: var(--font-serif); font-size: 0.95rem; color: var(--cream-dim); }

.form__note { font-size: 0.9rem; color: var(--muted); }
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.notice {
  padding: var(--s-5);
  box-shadow: inset 0 0 0 1px var(--rule-mid);
  background: var(--surface);
}
.notice--ok { box-shadow: inset 0 0 0 1px rgba(157, 184, 126, 0.5); }
.notice--error { box-shadow: inset 0 0 0 1px rgba(217, 139, 106, 0.5); }

/* --- GALLERY ------------------------------------------------------------ */
.gallery {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: 1fr;
}

.gallery-archive { margin-top: var(--s-12, 6rem); }
.gallery a.frame { cursor: zoom-in; }
.gallery a.frame:focus-visible { outline-offset: 4px; }

@media (min-width: 34rem) {
  .gallery {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
    grid-auto-rows: 11rem;
    grid-auto-flow: dense;
  }
  /* Fixed row height instead of a per-tile aspect ratio: a double-width tile
     is also double-height, so it has to claim two rows or the tiles beside it
     leave a hole under themselves. */
  .gallery > .frame { aspect-ratio: auto; height: 100%; }

  /* Every seventh tile takes a double square, so the grid never settles into
     a uniform card matrix. */
  .gallery > *:nth-child(7n + 1) { grid-column: span 2; grid-row: span 2; }
  /* Portrait food details keep enough height to show the whole arrangement. */
  .gallery > .gallery__portrait { grid-column: span 1; grid-row: span 2; }
}

/* --- PARTNER / reference cards ----------------------------------------- */
.card-link {
  display: grid;
  gap: var(--s-4);
  /* Cards are equal height, so without this the default stretch shares the
     leftover space out between the rows — and because the paragraphs differ
     in length it shares out a different amount per card, which pushes the
     headings to different heights. Packing to the top keeps image, label,
     heading and text on one line across the row. */
  align-content: start;
  text-decoration: none;
  color: inherit;
  padding-bottom: var(--s-5);
  border-bottom: 1px solid var(--rule);
}
.card-link h3 { color: var(--cream); transition: color var(--dur-1) var(--ease); }
.card-link:hover h3 { color: var(--gold-bright); }
.card-link p { color: var(--cream-dim); font-size: 0.96em; }
.card-link .meta { color: var(--gold-mid); }

/* --- REFERENCE COLUMNS: the long client list, set as running text ------ */
.ref-list { columns: 1; column-gap: var(--s-7); list-style: none; padding: 0; margin: 0; }
@media (min-width: 40rem) { .ref-list { columns: 2; } }
@media (min-width: 64rem) { .ref-list { columns: 3; } }
.ref-list li {
  break-inside: avoid;
  padding: var(--s-2) 0 var(--s-2) 1.1em;
  position: relative;
  color: var(--cream-dim);
  font-size: 0.96em;
  border-bottom: 1px solid rgba(203, 173, 106, 0.09);
}
.ref-list li::before { content: "·"; position: absolute; left: 0.2em; color: var(--gold-mark); }

/* --- ACTION BAND: the repeated route to the enquiry -------------------- */
.action-band {
  background:
    radial-gradient(70% 130% at 22% 0%, rgba(161, 129, 39, 0.22), transparent 62%),
    var(--surface-sunk);
  border-top: 1px solid var(--rule-mid);
  border-bottom: 1px solid var(--rule-mid);
  text-align: center;
}
.action-band .shell--text { display: grid; gap: var(--s-5); justify-items: center; }
.action-band h2 { max-width: 20ch; }

/* --- PAGE NAV (sub navigation on menu pages) --------------------------- */
/* --- RAIL: a set that swipes, with arrows that stay --------------------
   The track is a native scroll container, so a swipe on touch and a trackpad
   gesture on desktop both work with site.js absent. The arrows are the part
   a scroll container cannot give itself; they are revealed under .js, and go
   dim rather than disappearing at the ends so their place never shifts. */
.rail {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-5);
  min-width: 0;
}
.rail__track {
  display: flex;
  gap: 0;
  margin: 0;
  padding: 0.2rem 0;
  list-style: none;
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: 1.25rem;
  scrollbar-width: none;
  /* The set dissolves at whichever edge still has something behind it, so a
     half-visible item reads as "there is more" rather than as a mistake -
     and the fade lifts once you reach an end, where a soft edge would be
     promising something that is not there. site.js flips the attributes;
     without it both fades simply stay on. */
  --fade-s: 1.25rem;
  --fade-e: 1.25rem;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-s),
    #000 calc(100% - var(--fade-e)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-s),
    #000 calc(100% - var(--fade-e)), transparent 100%);
}
.rail[data-at-start] .rail__track { --fade-s: 0px; }
.rail[data-at-end] .rail__track { --fade-e: 0px; }
.rail__track::-webkit-scrollbar { display: none; }

/* No gap: the items sit shoulder to shoulder so their bottom rules read as
   one continuous line running under the whole index. */
.rail__track li { margin: 0; flex: 0 0 auto; scroll-snap-align: center; }

/* An index on a rule, not a row of buttons. Boxes with their own fills turn
   into four grey rectangles on a photograph and read as a form; the site
   already says this sort of thing with tracked capitals and a hairline. */
.rail__item {
  display: block;
  padding: 0 1.5em 0.7em;
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--cream-dim);
  border-bottom: 1px solid var(--rule);
  transition: color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
}

/* The index starts flush with the paragraph above it. */
.rail__track li:first-child .rail__item { padding-left: 0; }

.rail__item:hover,
.rail__item:focus-visible {
  color: var(--gold-bright);
  border-bottom-color: var(--rule-strong);
}

/* The page you are on doubles its rule in gold. A second line drawn just
   below rather than a thicker border, so nothing shifts by a pixel. */
.rail__item[aria-current="page"] {
  color: var(--gold-bright);
  border-bottom-color: var(--gold);
  box-shadow: 0 1px 0 var(--gold);
}
.rail__arrow {
  display: none;
  place-items: center;
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--gold);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease),
              opacity var(--dur-1) var(--ease);
}
.js .rail__arrow { display: grid; }
.rail__arrow svg { width: 0.72rem; height: auto; }
.rail__arrow:hover:not(:disabled) { color: var(--gold-bright); }
.rail__arrow:disabled { opacity: 0.26; cursor: default; }

/* --- PAGER: the two edges of the page, as a way through the set ---------
   Real links, so they work with site.js absent and open in a new tab like
   anything else. The set is a ring, so neither arrow is ever dead - an
   always-visible control that sometimes does nothing is worse than one that
   comes and goes.

   Height is the whole viewport only from the desktop breakpoint up, where
   the columns sit in the empty outer gutter. Full height on a phone would
   put a 3rem link over the left and right edge of every paragraph and eat
   taps meant for the text underneath; there the arrows shrink to a band at
   eye level and the swipe carries the rest. */
.pager__arrow {
  position: fixed;
  /* svh, not %. A percentage here resolves against the initial containing
     block, and on a phone that block is resized every time the browser's
     address bar retracts or comes back - so the arrows stepped half the
     bar's height up and down the screen the whole way through a scroll.
     The small-viewport unit is defined as the size with those interfaces
     expanded and must not change when they retract, so the arrows hold
     still. It centres them against the bar-visible screen, which is the
     state that keeps them inside the visible band in either case. */
  top: 50svh;
  /* Just the mark, at a thumb's size. A tall column here would lie over the
     left and right edge of every line of the copy and swallow taps meant for
     the text; the swipe already carries this on a phone. */
  height: 2.75rem;
  width: 2rem;
  transform: translateY(-50%);
  /* Keep the viewport navigation above the moving page, below the header. */
  z-index: calc(var(--z-sticky) + 1);
  --mark: 2.5rem;
  --pad: 0.1rem;
  /* The chevron alone is centred, and the name is taken out of the flow to
     hang beneath it. Laying both out together centred the pair instead, which
     pushed the chevron itself off the middle of the screen by half the name's
     height - the thing that looked wrong. */
  display: grid;
  align-content: center;
  text-decoration: none;
  color: var(--gold);
  opacity: 0.62;
  transition: opacity var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease),
              background var(--dur-2) var(--ease);
}

/* Pushed to the very edge of the screen: the chevron sits against the glass
   and the name reads inward from it, so the control takes the margin nobody
   was using rather than any of the measure. */
/* Pushed to the very edge of the screen: the chevron sits against the glass
   and the name reads inward from it, so the control takes the margin nobody
   was using rather than any of the measure. */
.pager__arrow--prev { left: 0; justify-content: start; padding-left: var(--pad); }
.pager__arrow--next { right: 0; justify-content: end; padding-right: var(--pad); }

.pager__mark {
  display: grid;
  place-items: center;
  width: 100%;
  height: var(--mark);
  transition: translate var(--dur-1) var(--ease);
}

.pager__mark svg { width: 0.8rem; height: auto; }

/* The name of what is on the other side. Written down the column, reading
   bottom-up on the left and top-down on the right, so both run outward from
   the page rather than one of them upside down. */
.pager__name {
  position: absolute;
  top: calc(50% + var(--mark) / 2 + var(--s-3));
  font-family: var(--font-sans);
  font-size: var(--t-micro);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cream-dim);
  writing-mode: vertical-rl;
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
  /* Revealed only by hover, which a touch screen has none of. */
  display: none;
}

/* Centred on the chevron's own axis, not the column's: the chevron is pinned
   to the outer edge, so half the mark in from the padding is where it is. */
.pager__arrow--prev .pager__name {
  left: calc(var(--pad) + var(--mark) / 2);
  translate: -50% 0;
  rotate: 180deg;
}

.pager__arrow--next .pager__name {
  right: calc(var(--pad) + var(--mark) / 2);
  translate: 50% 0;
}

.pager__arrow:hover,
.pager__arrow:focus-visible {
  opacity: 1;
  color: var(--gold-bright);
}

.pager__arrow:hover .pager__name,
.pager__arrow:focus-visible .pager__name { opacity: 1; }

.pager__arrow--prev:hover .pager__mark { translate: -0.25rem 0; }
.pager__arrow--next:hover .pager__mark { translate: 0.25rem 0; }

@media (min-width: 62rem) {
  /* The gutter is empty here, so the target can be the full height of the
     screen: anywhere down the edge moves you on. */
  .pager__arrow {
    top: 0;
    height: 100svh;
    transform: none;
    width: clamp(3.5rem, 6vw, 6.5rem);
    opacity: 0.5;
  }
  .pager__mark { width: var(--mark); }
  .pager__name { display: block; }
  .pager__arrow--prev {
    background: linear-gradient(to right, rgba(8, 8, 7, 0.55), transparent);
  }
  .pager__arrow--next {
    background: linear-gradient(to left, rgba(8, 8, 7, 0.55), transparent);
  }
  .pager__arrow--prev:hover,
  .pager__arrow--prev:focus-visible {
    background: linear-gradient(to right, rgba(8, 8, 7, 0.82), transparent);
  }
  .pager__arrow--next:hover,
  .pager__arrow--next:focus-visible {
    background: linear-gradient(to left, rgba(8, 8, 7, 0.82), transparent);
  }
}

/* --- PAGE SLIDE: two documents, one movement -----------------------------
   The incoming page is fetched, laid over the current one and slid in from
   the side the gesture came from, and only then does the content actually
   change hands. No document swap happens in view, so there is no flash and
   no jump to the top to watch - which is the whole point.

   It sits under the header on purpose: the chrome holds still and the page
   travels beneath it. Nothing here is load-bearing - if the fetch fails, the
   links are followed the ordinary way. */
.slide-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-sticky);
  overflow: hidden;
  pointer-events: none;
}

.slide-layer__panel {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--ground);
  /* A whole page is moving; both sides get their own layer up front rather
     than being promoted mid-animation. */
  will-change: transform;
  contain: paint;
}

/* The outgoing page must not paint over the arriving one where they cross. */
.is-sliding-out {
  position: relative;
  z-index: 0;
  will-change: transform, opacity;
}

/* --- BREADCRUMB --------------------------------------------------------- */
.crumbs { font-family: var(--font-sans); font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s-2); margin: 0; padding: 0; }
.crumbs li + li::before { content: "/"; margin-right: var(--s-2); color: var(--gold-mark); }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--gold); }

/* --- prose block for legal pages --------------------------------------- */
.prose h2 { font-size: var(--t-h3); margin-top: var(--s-7); margin-bottom: var(--s-4); color: var(--gold-bright); }
.prose h3 { font-size: var(--t-h4); margin-top: var(--s-6); margin-bottom: var(--s-3); color: var(--cream); }
.prose > :first-child { margin-top: 0; }
.prose p, .prose li { color: var(--cream-dim); }
.prose ol { padding-left: 1.3em; }
.prose ol > li { margin-bottom: var(--s-4); }

/* --- FAQ: each question is an H2 so it is independently retrievable, but it
   is set at question scale, not section scale. --------------------------- */
.faq { display: grid; gap: var(--s-6); }
.faq h2 {
  font-size: var(--t-h4);
  color: var(--gold-bright);
  letter-spacing: -0.006em;
  margin-bottom: var(--s-2);
}
.faq p { color: var(--cream-dim); }

/* Homepage FAQ: disclosure is native, so keyboard, name and state work even
   when JavaScript fails. The contact-page FAQ above remains plain text. */
.faq-section__layout {
  display: grid;
  gap: var(--s-7);
  align-items: start;
}
.faq-section .section-head { margin-bottom: 0; max-width: 30rem; }
.faq--accordion {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--rule-mid);
}
.faq--accordion .faq__item { border-bottom: 1px solid var(--rule-mid); }
.faq--accordion summary {
  min-height: 4.75rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 2.5rem;
  gap: var(--s-5);
  align-items: center;
  padding-block: var(--s-4);
  color: var(--cream);
  cursor: pointer;
  list-style: none;
}
.faq--accordion summary::-webkit-details-marker { display: none; }
.faq--accordion summary::marker { content: ""; }
.faq__question {
  font-family: var(--font-serif);
  font-size: clamp(1.08rem, 1rem + 0.42vw, 1.35rem);
  line-height: 1.3;
  transition: color var(--dur-1) var(--ease);
}
.faq__icon {
  position: relative;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--rule-strong);
  transition: border-color var(--dur-1) var(--ease),
              background-color var(--dur-1) var(--ease);
}
.faq__icon::before,
.faq__icon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.85rem;
  height: 1px;
  background: var(--gold);
  transform: translate(-50%, -50%);
  transition: transform var(--dur-1) var(--ease);
}
.faq__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq__item[open] .faq__icon {
  border-color: var(--gold-deep);
  background: rgba(203, 173, 106, 0.06);
}
.faq__item[open] .faq__icon::after { transform: translate(-50%, -50%) rotate(90deg) scaleX(0); }
.faq__item[open] .faq__question,
.faq--accordion summary:hover .faq__question { color: var(--gold-bright); }
.faq--accordion summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}
.faq__answer {
  max-width: 62ch;
  padding: 0 calc(2.5rem + var(--s-5)) var(--s-5) 0;
}
.faq__answer p { margin: 0; line-height: 1.7; }

@media (min-width: 58rem) {
  .faq-section__layout {
    grid-template-columns: minmax(16rem, 0.72fr) minmax(0, 1.28fr);
    gap: clamp(3rem, 7vw, 7rem);
  }
}

/* --- the buffet card as a wide row ---------------------------------------
   Three columns inside one row - name, dishes, price - instead of three
   cards beside each other. It exists because the homepage had a three
   column grid of pillars and then, one section later, a three column grid
   of cards: the same shape twice in a row reads as a template rather than
   as a composition.

   The price column is what this buys. Stacked, the three sums sit under
   each other in one vertical line, which is the comparison the section is
   for; side by side they sit at three different heights because the cards
   are as tall as their longest dish list.

   Only from 56rem. Below it the row has no width to divide and falls back
   to the stacked card, which is what a phone wants anyway.
   ------------------------------------------------------------------------ */
.buffet-grid--rows {
  grid-template-columns: 1fr;
  grid-auto-rows: auto;
}

@media (min-width: 56rem) {
  .buffet--wide {
    display: grid;
    grid-template-columns: minmax(0, 13rem) minmax(0, 1fr) minmax(0, 10rem);
    gap: clamp(1.5rem, 3vw, 3.5rem);
    align-items: start;
  }

  /* The dish list takes the width it was given rather than running one long
     column down a wide row. */
  .buffet--wide ul { columns: 2; column-gap: var(--s-6); }

  /* The rule and the auto margin are what pin the price to a card's floor.
     In a row there is no floor to pin it to - it sits in its own column, so
     the border moves to the left edge and becomes the seam between the
     dishes and the sum. */
  /* Bottom right, not top right. align-items:start above holds the name and
     the dishes to the top of the row, which is where they belong; the price
     was following them and ending up with the row's whole spare height under
     it, reading as unanchored. Stretching this one column and pushing its
     content to the end puts the sum on the row's last line, and the rule
     beside it becomes a full-height seam rather than a short tick. */
  .buffet--wide .buffet__price {
    align-self: stretch;
    align-content: end;
    margin-top: 0;
    padding-top: 0;
    padding-inline-start: var(--s-5);
    border-top: 0;
    border-inline-start: 1px solid var(--rule-mid);
    display: grid;
    justify-items: end;
    text-align: right;
    gap: 0.35rem;
  }
  .buffet--wide .buffet__price b { margin: 0; }
}

/* --- 07-motion.css --- */
/* ==========================================================================
   Motion — one language: content rises a short distance into the light.
   Compositor properties only (transform, opacity). Every animated element
   is fully visible when JavaScript never runs, because .reveal only takes
   effect once the document is marked .js.
   ========================================================================== */

.reveal-ready .reveal {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity var(--dur-3) var(--ease-expo),
    transform var(--dur-3) var(--ease-expo);
  transition-delay: var(--delay, 0ms);
  will-change: opacity, transform;
}

.reveal-ready .reveal.is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* The axis rows draw their price line only once they are read. */
.reveal-ready .axis-row { --draw: 0; }
.reveal-ready .axis-row.is-in { --draw: 1; }

/* The cue nods in the direction it sends you. Slow and shallow: it is an
   invitation at the foot of the screen, not a blinking sign. */
@keyframes cue-nod {
  0%, 62%, 100% { transform: translateY(0); }
  80% { transform: translateY(0.24rem); }
}

.hero__cue svg { animation: cue-nod 3.2s var(--ease) infinite; }

@media (prefers-reduced-motion: reduce) {
  .reveal-ready .reveal { opacity: 1; transform: none; transition: none; }
  .reveal-ready .axis-row { --draw: 1; }
  .frame img { transition: none; }
  a.frame:hover img, .frame-link:hover .frame img { transform: none; }
  .btn:hover { transform: none; }
  .hero__cue svg { animation: none; }
}

/* ==========================================================================
   Page transitions. The site is a set of static documents, and a
   cross-document view transition is what lets two of them read as one
   surface instead of a reload. Nothing here is load-bearing: a browser
   without the feature navigates exactly as it did before.

   Durations come from the tokens, which collapse to 1ms under
   prefers-reduced-motion - so the whole thing turns itself off there.
   ========================================================================== */
@view-transition { navigation: auto; }

/* The chrome is the same from page to page, so it is lifted out of the page
   snapshot and holds still while the content travels under it. */
.site-header { view-transition-name: site-header; }

::view-transition-group(root) { animation-duration: var(--dur-2); }

/* An ordinary link keeps the default cross-fade. A rail move is lateral, so
   its pages travel the way the gesture went - site.js writes the direction
   before it leaves and the arriving page reads it back. */
html[data-nav="forward"]::view-transition-old(root) {
  animation: page-leave-left var(--dur-2) var(--ease-expo) both;
}
html[data-nav="forward"]::view-transition-new(root) {
  animation: page-enter-right var(--dur-2) var(--ease-expo) both;
}
html[data-nav="back"]::view-transition-old(root) {
  animation: page-leave-right var(--dur-2) var(--ease-expo) both;
}
html[data-nav="back"]::view-transition-new(root) {
  animation: page-enter-left var(--dur-2) var(--ease-expo) both;
}

@keyframes page-leave-left  { to   { transform: translateX(-3.5%); opacity: 0; } }
@keyframes page-enter-right { from { transform: translateX(3.5%);  opacity: 0; } }
@keyframes page-leave-right { to   { transform: translateX(3.5%);  opacity: 0; } }
@keyframes page-enter-left  { from { transform: translateX(-3.5%); opacity: 0; } }

/* ==========================================================================
   Print — the menu card, actually printable. Gold on charcoal becomes ink
   on paper; the atmosphere layers are removed rather than wasting toner.
   ========================================================================== */
@media print {
  .grain, .site-header, .nav-toggle,
  .hero__media, .frame, .rail, .btn, .hero__cue, .pager { display: none !important; }
  /* The light and the field are backgrounds, so hiding boxes misses them. */
  .hero--lit { background-image: none; }
  body { background-image: none; }

  body { background: #fff; color: #1a1a1a; font-size: 11pt; line-height: 1.5; }
  a { color: #1a1a1a; text-decoration: none; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 8pt; color: #666; }

  .section { padding-block: 1rem; break-inside: avoid; }
  .buffet { box-shadow: none; border: 1pt solid #999; padding: 0.6rem; break-inside: avoid; margin-bottom: 0.6rem; }
  .buffet__no, .script { color: #6b5720; }
  .leaf__head h3, .eyebrow, .conditions dt { color: #6b5720; }
  h1, h2, h3 { color: #111; }
  .price-table th, .price-table td { border-bottom: 0.5pt solid #bbb; }
  .price-table td:last-child { color: #111; }
  .axis-row__track { display: none; }
}

/* --- the dishes arrive one after another ---------------------------------
   The card already reveals as a whole: it carries .reveal, so the observer
   in site.js gives it .is-in when it comes into view. This adds the list
   inside it, each line a step behind the one above, so a buffet reads as
   something being set down rather than a block appearing.

   Shorter and shallower than the card's own entrance - 6px against 18, and
   --dur-2 against --dur-3. The lines move inside a card that is itself
   still arriving; matching its travel would read as two separate animations
   fighting over the same corner.

   Eight rules because eight is the longest dish list in the data. A ninth
   dish would simply arrive with the eighth, which is a far better failure
   than a line that never appears - so no :nth-child(n+9) catch-all is
   needed, only this note if the menus ever grow.

   Everything here is under .reveal-ready, which site.js sets. Script absent
   or blocked: nothing is ever hidden, and the list is just a list.
   ------------------------------------------------------------------------ */
.reveal-ready .buffet li {
  opacity: 0;
  transform: translate3d(0, 6px, 0);
  transition:
    opacity var(--dur-2) var(--ease-expo),
    transform var(--dur-2) var(--ease-expo);
}

.reveal-ready .buffet.is-in li {
  opacity: 1;
  transform: none;
}

.reveal-ready .buffet.is-in li:nth-child(1) { transition-delay: calc(var(--stagger) * 1); }
.reveal-ready .buffet.is-in li:nth-child(2) { transition-delay: calc(var(--stagger) * 2); }
.reveal-ready .buffet.is-in li:nth-child(3) { transition-delay: calc(var(--stagger) * 3); }
.reveal-ready .buffet.is-in li:nth-child(4) { transition-delay: calc(var(--stagger) * 4); }
.reveal-ready .buffet.is-in li:nth-child(5) { transition-delay: calc(var(--stagger) * 5); }
.reveal-ready .buffet.is-in li:nth-child(6) { transition-delay: calc(var(--stagger) * 6); }
.reveal-ready .buffet.is-in li:nth-child(7) { transition-delay: calc(var(--stagger) * 7); }
.reveal-ready .buffet.is-in li:nth-child(8) { transition-delay: calc(var(--stagger) * 8); }

/* --- 08-hero-portal.css --- */
/* ==========================================================================
   The portal hero — home-portal.html, desktop only.

   A second composition for the same hero, kept beside the first rather than
   replacing it. The idea in one sentence: the film is seen through a
   dome-topped opening, and the dome is the one in the logo.

   Two weights across the screen: the headline in heavy type on the left, and
   the film moving behind a dome-topped opening that takes everything else -
   from the end of the type column to the right edge of the screen, and off
   the bottom of it. At 1440 that is 857 px of opening against 496 px of
   type. An opening cut by the fold reads as a room that continues; a
   medallion floating in the middle would read as ceremony.

   A column of figures was tried between the opening and the right edge and
   taken out again. The three numbers already have a band directly under the
   hero, and buying them a second showing cost the opening 270 px and its
   edge - which is the whole difference between a window in a wall and a
   panel parked on a page.

   What it costs: the headline drops from 4.75rem to 4.3rem and breaks into
   four lines. At 1440 the three-line setting measures 570 px and the column
   here is 496 px. Four narrow lines beside a tall narrow opening is also the
   better pairing - both columns then have the same vertical.

   EVERYTHING that makes a portal is inside the 62rem query, and below it the
   markup is left alone: .portal and .portal__opening are two plain divs with
   no styles of their own, the frame inside carries frame--hero, and the
   section still carries hero--home. So under 992 px this page is not a
   degraded portal - it is the first hero, exactly, at every width it was
   built for.
   ========================================================================== */

/* The stroked dome is markup that exists at every width, and below the
   breakpoint there is no aperture for it to trace. Hidden here, and the path
   also carries fill="none" in the markup: without either, an inline <svg>
   whose fill is only set inside a media query renders as a solid black dome
   over the phone's hero. */
.portal__edge { display: none; }

@media (min-width: 62rem) {
  /* --- the layout ---------------------------------------------------------
     Three columns: type, gap, opening - and the opening is the only one
     that flexes, so it takes the whole of the right-hand side.
     -------------------------------------------------------------------- */
  .hero--portal {
    --portal-gap: clamp(2rem, 2.8vw, 3.25rem);
    --portal-type: min(31rem, 39vw);
    /* The cap is always this fraction of the opening's width: the dome in
       the emblem measures 529.12 x 247.78, and hero_portal_shape.py prints
       the ratio it wrote. Stated here so the SVG is never distorted. */
    --portal-cap-ratio: 0.46829;
    /* Measured from the media cell, which is already pulled up by the
       hero's own padding - so this is the gap between the header's foot and
       the dome's crown. 13 px at 900, 22 px at 1440 tall. */
    --portal-top: clamp(0.75rem, 1.5vh, 1.75rem);

    display: grid;
    grid-template-columns:
      minmax(0, var(--portal-type))
      var(--portal-gap)
      minmax(0, 1fr);
    /* Two 1fr rows centring the pair between them, the same device the first
       hero uses - align-content would shrink the grid to its content and
       take the opening's height with it. */
    grid-template-rows: 1fr auto auto 1fr;
    /* The header is sticky and sits in the flow, so a full 100svh would put
       the hero's foot that far below the fold - and then the opening is not
       cut by the screen edge, it just ends somewhere nobody sees. */
    min-height: calc(100svh - var(--header-h, 4.5rem));
    /* No inset on the right at all: the opening's own edge is the screen's.
       The left one stays, because the headline has to line up with every
       section under it or the page looks mis-set. */
    padding-inline: var(--page-inset) 0;
    padding-block: var(--s-6);
    column-gap: 0;
  }

  .hero--portal > * { grid-column: auto; }

  .hero--portal .hero__intro,
  .hero--portal .hero__body {
    grid-column: 1;
    width: auto;
    margin: 0;
    padding-inline: 0;
  }
  .hero--portal .hero__intro { grid-row: 2; }
  .hero--portal .hero__body { grid-row: 3; }

  .hero--portal .hero__media {
    grid-column: 3;
    grid-row: 1 / -1;
    position: relative;
    align-self: stretch;
    justify-self: stretch;
    width: auto;
    /* The opening runs past the hero's foot and off the screen. The hero
       clips horizontally only, so nothing here has to be told to stay
       inside - the mask has faded it out long before then. */
    margin-block: calc(var(--s-6) * -1);
    /* So the dome's height can be worked out from the opening's own width,
       which nothing else in the layout knows: this column is the flexible
       one. */
    container-type: inline-size;
  }

  /* Centred on the opening, not on the screen. left and right name the
     opening's own column and the auto margins do the centring - which works
     where a percentage cannot, because the opening's width is whatever the
     other two columns leave over. */
  .hero--portal .hero__cue {
    display: grid;
    left: calc(var(--page-inset) + var(--portal-type) + var(--portal-gap));
    right: 0;
    margin-inline: auto;
    translate: 0;
  }
  .hero--portal .hero__cue:hover,
  .hero--portal .hero__cue:focus-visible { translate: 0 0.22rem; }

  /* --- the aperture -------------------------------------------------------
     Two mask layers, added together: the dome cap at the top, a shaft under
     it. The cap is assets/media/brand/portal-dome.svg, which
     _build/hero_portal_shape.py lifts out of the emblem - the outline the
     designer drew, not a border-radius approximating it.

     The cap's height is computed from the rendered width rather than stated,
     because the opening's column is the flexible one: 100cqw of the media
     cell times the dome's own ratio. Container units rather than a
     percentage, because a percentage in mask-size resolves against the
     masked box's height, not its width.

     The shaft layer is a gradient rather than a solid, because the opening
     has to die out before the hero's foot: a hard edge there would show as a
     cut the moment the page scrolls. Same reasoning as the stacked hero's
     bottom fade in 04-media.css.

     It reaches zero at 89 %, not at 100 %. Ending exactly on the hero's foot
     left the last stretch at two or three per cent alpha - too little to
     read as picture, more than enough to read as a straight line where the
     section stops. The last tenth of the shaft is now nothing at all, and
     the dissolve finishes inside the hero instead of at its edge.
     -------------------------------------------------------------------- */
  .hero--portal .portal {
    position: absolute;
    inset: var(--portal-top) 0 0 0;
    --portal-cap: calc(100cqw * var(--portal-cap-ratio));
  }

  .hero--portal .portal__opening {
    position: absolute;
    inset: 0;
    /* Light on the floor of the opening, below where the film ends. Without
       it the shaft is the same charcoal as the room and the opening stops
       being an opening. */
    background:
      radial-gradient(120% 44% at 50% 4%,
        rgba(var(--hero-ember-rgb), 0.17) 0%,
        rgba(var(--hero-ember-rgb), 0) 72%),
      linear-gradient(to bottom,
        rgba(var(--hero-key-rgb), 0.07) 0%,
        rgba(var(--hero-key-rgb), 0) 55%);

    -webkit-mask-image: url(../media/brand/portal-dome.svg),
                        linear-gradient(to bottom, #000 0, #000 48%,
                                        rgba(0, 0, 0, 0.5) 70%,
                                        rgba(0, 0, 0, 0) 89%);
            mask-image: url(../media/brand/portal-dome.svg),
                        linear-gradient(to bottom, #000 0, #000 48%,
                                        rgba(0, 0, 0, 0.5) 70%,
                                        rgba(0, 0, 0, 0) 89%);
    /* The shaft starts one pixel above the cap's foot and is one pixel
       taller for it. Butted exactly, the two layers left a hairline seam
       right across the opening where the dome's anti-aliased bottom edge met
       the shaft's hard top one - invisible under most frames of the film and
       plain as day under a flat fill. mask-composite is add, which clamps at
       full alpha, so an overlap costs nothing. */
    -webkit-mask-size: 100% var(--portal-cap),
                       100% calc(100% - var(--portal-cap) + 1px);
            mask-size: 100% var(--portal-cap),
                       100% calc(100% - var(--portal-cap) + 1px);
    -webkit-mask-position: left top, left calc(var(--portal-cap) - 1px);
            mask-position: left top, left calc(var(--portal-cap) - 1px);
    -webkit-mask-repeat: no-repeat, no-repeat;
            mask-repeat: no-repeat, no-repeat;
  }

  /* The film fills the opening. It used to sit in the top 5:6 of it with
     the shaft left dark under it - a lit kitchen seen through a door, with
     an unlit floor below. That was an answer to a narrow opening: at 524 px
     a 5:6 picture was 628 px tall and there were 190 px of aperture left
     over. At 857 px the same picture would be 1028 px tall, taller than the
     hero itself, so there is no floor left to leave dark and the film simply
     is the opening.

     What that costs is vertical crop: 5:6 source into a box near 1:1 means
     object-fit takes about a fifth of the frame's height. It is affordable
     here and it was not in the other direction - see the note in
     _build/video.py on why the encode is 900x1080 of a 1920x1080 source and
     not a narrower window.

     No mask of its own: the opening already carries one, and two bottom
     fades over each other made a band rather than a dissolve.

     Specificity has to match .hero__media .frame in 06-components.css, which
     pins every hero frame to inset:0 at this width - and frame--hero, which
     hands it a mask. */
  .hero--portal .hero__media .frame--portal {
    position: absolute;
    inset: 0;
    height: auto;
    aspect-ratio: auto;
    /* Almost no veil. The veil system exists so type can sit on a
       photograph; nothing sits on this one. What is left is the two per cent
       that keeps it in the palette, and a breath at the foot to help the
       picture into the page. */
    --veil: 0.02;
    --veil-dir: linear-gradient(to bottom,
      rgba(8, 8, 7, 0.14) 0%,
      rgba(8, 8, 7, 0) 26%,
      rgba(8, 8, 7, 0.16) 100%);
    box-shadow: none;
    -webkit-mask-image: none;
            mask-image: none;
  }

  /* --- the gold on the opening --------------------------------------------
     The curve is stroked, so the hairline is the same line the mask cuts.
     vector-effect keeps it at one pixel whatever the opening is scaled to; a
     stroke-width in user units comes out 0.79 px at 1440 and 1.2 px at 2560.
     -------------------------------------------------------------------- */
  .hero--portal .portal__edge {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--portal-cap);
    z-index: 3;
    overflow: visible;
    fill: none;
    stroke: var(--rule-mid);
    pointer-events: none;
  }

  /* The shaft's two sides, carrying the dome's line down and then letting it
     go. Gradients rather than borders: a border would stop dead where the
     mask has already faded the opening away. */
  .hero--portal .portal::before,
  .hero--portal .portal::after {
    content: "";
    position: absolute;
    top: var(--portal-cap);
    width: 1px;
    height: calc(100% - var(--portal-cap));
    background: linear-gradient(to bottom,
      var(--rule-mid) 0%, var(--rule) 42%, rgba(203, 173, 106, 0) 82%);
    pointer-events: none;
  }
  .hero--portal .portal::before { left: 0; }
  .hero--portal .portal::after { right: 0; }

  /* --- the light, re-aimed ------------------------------------------------
     The first hero's key sits in the top left corner because the copy's
     corner is there and the photograph is away to the right. Here the
     opening is the subject, so the second source goes behind it - an
     aperture has to read as lit from within, or it is a hole. The key stays
     over the headline, wider and weaker than before because it no longer
     carries a corner on its own, and the counter drops under the opening's
     own fade. The ember is aimed at 60 %, which is where the two stated
     columns leave the opening's centre.
     -------------------------------------------------------------------- */
  .hero--portal.hero--lit {
    --h-k-x: 3%;  --h-k-y: 30%; --h-k-w: 38%; --h-k-h: 54%; --h-k-a: 0.34;
    --h-d-x: 60%; --h-d-y: 24%; --h-d-w: 32%; --h-d-h: 46%; --h-d-a: 0.30;
    --h-c-x: 93%; --h-c-y: 96%; --h-c-w: 44%; --h-c-h: 38%; --h-c-a: 0.12;
  }

  /* --- the type ----------------------------------------------------------- */
  /* Measured rather than estimated: "Feier garantiert", the longest line,
     sets 424 px at 61.44 px in a 496 px column - 72 px of slack that a
     reckoned character width had hidden. So the headline goes back up. At
     4.3rem the longest line is 475 px and the column is still 496. */
  .hero--portal h1 {
    font-size: clamp(2.35rem, 1.5rem + 3.05vw, 4.3rem);
    line-height: 1.05;
    letter-spacing: -0.028em;
    margin-bottom: 0;
  }
  /* The spans are only groupings in the source - nothing styles them on the
     first hero, where the column width happens to break the lines in the
     same places. Here the breaks have to be exact, because every line has to
     clear a 496 px column, so the groupings become the lines. */
  .hero--portal .hero__headline-line { display: block; }
  .hero--portal .hero__headline-accent { color: var(--gold); }
  .hero--portal .hero__lead {
    margin-top: var(--s-5);
    margin-bottom: var(--s-5);
    max-width: 30ch;
  }
  .hero--portal .hero__actions { margin-bottom: 0; max-width: 24rem; }

  /* --- the one move: the cloche lifts -------------------------------------
     The opening settles upward while the hairline draws itself along the
     curve. That is the whole of the motion on this page - no parallax, no
     second reveal, nothing bound to the scroll.

     Transform only, never opacity: the still inside the opening is the LCP
     element and it is painted on the first frame either way. An opacity fade
     would move the measurement to the end of the animation.

     pathLength="1" on the path means the dash length is 1 whatever the curve
     actually measures, so nothing has to ask the DOM for getTotalLength.
     -------------------------------------------------------------------- */
  .hero--portal .portal {
    animation: portal-rise 1200ms var(--ease-expo) both;
  }
  .hero--portal .portal__edge path {
    stroke-dasharray: 1;
    animation: portal-draw 900ms 220ms var(--ease) both;
  }
}

@keyframes portal-rise {
  from { transform: translateY(2.2%); }
  to   { transform: translateY(0); }
}

@keyframes portal-draw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}

/* The still version is a finished composition, not the same one with the
   movement switched off: the opening is where it comes to rest, and the
   hairline is drawn. */
@media (prefers-reduced-motion: reduce) {
  .hero--portal .portal,
  .hero--portal .portal__edge path { animation: none; }
  .hero--portal .portal__edge path { stroke-dashoffset: 0; }
}

/* Without mask support the aperture cannot exist, and a rectangle of film
   with a gold curve floating over it is worse than no curve at all. The
   opening becomes a plain ruled panel, the dome goes, and the composition
   still stands: three columns, the film in the middle. */
@supports not ((mask-image: linear-gradient(#000, transparent)) or
               (-webkit-mask-image: linear-gradient(#000, transparent))) {
  .hero--portal .portal__edge { display: none; }
  .hero--portal .hero__media .frame--portal {
    box-shadow: inset 0 0 0 1px var(--rule);
  }
}

/* 07-motion.css takes the hero apart for print; the figures follow it rather
   than being set as a tally on paper. */
@media print {
  .hero--portal .hero__facts { display: block; }
  .hero--portal .hero__facts > div::before { display: none; }
  .hero--portal .hero__facts dd { justify-self: start; text-align: left; }
}
