/* ============================================================================
   BAR MANDYS
   Middle Eastern café and bar — Palm Beach, Sydney
   ----------------------------------------------------------------------------
   01  Tokens
   02  Reset & base
   03  Utilities
   04  Typography primitives
   05  Wordmark
   06  Header & nav
   07  Feature: hero
   08  Feature: menu bands
   09  Feature: menu panels
   10  About: tabs
   11  About: disclosure rows
   12  Footer
   13  Giant wordmark
   14  Motion
   15  Responsive
   16  Loader
   ----------------------------------------------------------------------------
   Progressive enhancement
     The document gets a .js class before first paint. Everything that depends
     on JavaScript is written so the no-JS state is the default and .js opts
     into the enhanced behaviour — never the reverse. That way a failed script
     leaves a usable page rather than hidden content.
   ========================================================================== */


/* ============================================================================
   01  TOKENS
   ========================================================================== */

:root {

  /* --- Colour --------------------------------------------------------------
     Brown and gold carry over from the original brand. Every surface colour is
     paired with the ink that sits on it. */

  --c-paper:      #faf8f3;   /* warm off-white page base                     */
  --c-paper-deep: #f3f0e8;   /* one step down, sits behind the hero          */
  --c-ink:        #0d0d0c;   /* near-black, never pure #000                  */
  --c-ink-60:     rgba(13, 13, 12, 0.60);
  --c-ink-35:     rgba(13, 13, 12, 0.35);
  --c-ink-12:     rgba(13, 13, 12, 0.12);

  --c-brown:      #83492c;   /* brand brown                                  */
  --c-gold:       #e9a930;   /* brand gold                                   */
  --c-wine:       #68313d;   /* drinks band; About tab hover                 */
  --c-on-band:    #f7f3ea;   /* text sitting on a saturated band             */

  /* --- Type ----------------------------------------------------------------
     Cossette Titre throughout. Condensed display face with a subtle inverted
     contrast. It ships in 300 / 400 / 700 only — no 500, no 600, no variable
     axes — so nothing here may ask for a weight that would be synthesised.

     The role tokens all resolve to the same family today. They are kept apart
     so one role can diverge later without hunting through the file: to widen
     running copy, load Cossette Texte and repoint --f-text at it. */

  --f-titre:     "Cossette Titre", "Arial Narrow", "Helvetica Neue Condensed", sans-serif;
  --f-display:   var(--f-titre);
  --f-condensed: var(--f-titre);
  --f-text:      var(--f-titre);

  /* Fluid scale. Each curve is pinned at two viewports: the first value is
     reached at 380px and the last at 1280px, with nothing growing past either
     end. Change one number here and it propagates everywhere.

     The desktop ends were raised together so the relationships hold. They did
     not all move by the same amount: the reading sizes gained the most, the
     small caps labels the least. Scaling the labels as hard would have cost
     the thing that makes the page feel current — that its utility type stays
     quiet while the editorial type carries the page.

         nav +10%   band +9%   micro +9%   tab +10%
         index +11%   question +16%   body +11%

     The 380px ends are untouched, so nothing on a phone changes. The middles
     were re-derived rather than nudged: with the first value fixed and the
     last known, the slope is (last - first) * 16 / 9 vw, and the offset is
     first - 0.2375 * slope. */
  --t-nav:      clamp(0.75rem, 0.535rem + 0.907vw, 1.26rem);
  --t-band:     clamp(0.68rem, 0.503rem + 0.747vw, 1.10rem);
  --t-micro:    clamp(0.60rem, 0.444rem + 0.658vw, 0.97rem);
  --t-tab:      clamp(0.70rem, 0.518rem + 0.764vw, 1.13rem);
  --t-index:    clamp(0.95rem, 0.625rem + 1.369vw, 1.72rem);
  --t-question: clamp(1.05rem, 0.586rem + 1.956vw, 2.15rem);
  --t-body:     clamp(0.90rem, 0.664rem + 0.996vw, 1.46rem);

  /* --- Space ---------------------------------------------------------------
     One rhythm, used everywhere. */

  /* The drinks columns. The measure labels are set at --t-micro and the
     figures under them at --t-index, so a column given in `em` resolved
     against each element's own font size and came out two different widths —
     which is why GLASS and BTL never sat over their prices. Deriving both from
     --t-index gives one absolute width that both can use, and it still scales
     with the viewport because --t-index is a clamp. */
  --price-col: calc(var(--t-index) * 3.4);
  --price-gap: calc(var(--t-index) * 0.6);

  --gutter: clamp(1.25rem, 0.9rem + 1.6vw, 2.5rem);
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 1rem;
  --s-4: 1.5rem;
  --s-5: 2.5rem;
  --s-6: 4rem;
  --s-7: 6rem;
  --s-8: 9rem;

  /* --- First-screen fit ----------------------------------------------------
     The hero takes whatever is left of the viewport once the header and the
     menu bands are accounted for, so the bands land exactly on the bottom edge
     of the screen on load. These feed both the components that set those
     heights and the hero's calc, so the two cannot drift apart.
     The logo is 1269 x 417, hence the 3.0432 divisor for its height. Swap the
     mark and this number has to be re-derived, or the header reserves the
     wrong amount of room for it. */

  --logo-w:   clamp(104px, 13vw, 176px);
  --band-h:   clamp(3rem, 2.2rem + 3.4vw, 3.9rem);
  --header-h: calc((var(--s-4) * 2) + (var(--logo-w) / 3.0432));
  --bands-h:  var(--band-h);          /* one row across on desktop */
  --hero-h:   calc(100vh - var(--header-h) - var(--bands-h));

  /* --- Motion --------------------------------------------------------------
     Four eases, each with a job:
       --ease        general purpose decelerate
       --ease-hover  gentle, for small hover moves
       --ease-panel  symmetric, so the panel feels the same up as down
       --ease-out    strong decelerate, for entrances
       --ease-in     --ease-out played backwards, for exits

     --ease-in is not a guess. Reversing a cubic-bezier means mirroring its two
     control points through the centre — (x1,y1,x2,y2) becomes
     (1-x2, 1-y2, 1-x1, 1-y1) — so --ease-out's (0.16, 1, 0.3, 1) gives
     (0.7, 0, 0.84, 0). Anything leaving on it is running the entrance
     backwards exactly, which is the whole point of the close. */

  --ease:       cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-hover: cubic-bezier(0.33, 1, 0.68, 1);
  --ease-panel: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in:    cubic-bezier(0.7, 0, 0.84, 0);

  --dur-fast:  140ms;
  --dur:       280ms;
  --dur-slow:  520ms;
  --dur-panel: 720ms;
  --dur-close: 840ms;    /* the curtain leaves a little slower than it arrives */
}

/* svh tracks the visible viewport while mobile browser chrome is showing, so
   the bands stay on the bottom edge rather than under the address bar. */
@supports (height: 100svh) {
  :root { --hero-h: calc(100svh - var(--header-h) - var(--bands-h)); }
}

/* Lets <details> animate from 0 to its intrinsic height. Progressive — engines
   without it simply snap open. */
:root { interpolate-size: allow-keywords; }


/* ============================================================================
   02  RESET & BASE
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor jumps clear the sticky header instead of hiding under it. */
  scroll-padding-top: calc(var(--header-h) + var(--s-3));
}

body {
  margin: 0;
  background: var(--c-paper);
  color: var(--c-ink);
  font-family: var(--f-text);
  font-size: var(--t-body);
  line-height: 1.35;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

h1, h2, h3, h4, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }

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

a { color: inherit; text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  border: 0;
  margin: 0;
  background: none;
  -webkit-appearance: none;
          appearance: none;
}

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

/* Focus rings on a saturated band need to be light to stay visible. */
.menu-band:focus-visible,
.menu-panel :focus-visible {
  outline-color: var(--c-on-band);
}
.menu-band--lunch:focus-visible,
.menu-panel--lunch :focus-visible {
  outline-color: var(--c-ink);
}


/* ============================================================================
   03  UTILITIES
   ========================================================================== */

/* Holds a run of words together so no line of a menu can end up carrying one
   or two of them alone. nowrap rather than &nbsp; because it also suppresses
   the break after a hyphen. Applied by main.js — see "No widows" there. */
.nowrap { white-space: nowrap; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  left: var(--gutter);
  top: 0;
  z-index: 200;
  padding: var(--s-2) var(--s-3);
  background: var(--c-ink);
  color: var(--c-paper);
  font-family: var(--f-condensed);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transform: translateY(-120%);
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus-visible { transform: translateY(0); }


/* ============================================================================
   04  TYPOGRAPHY PRIMITIVES
   ========================================================================== */

/* Condensed uppercase — the workhorse for labels and headings. */
.tabs__tab,
.row__index,
.row__title,
.footer-col,
.skip-link,
.site-nav a,
.menu-band,
.menu-panel__title,
.menu-group__title,
.menu-measures,
.menu-item__name,
.menu-item__prices {
  font-family: var(--f-condensed);
  text-transform: uppercase;
}

/* Running copy renders as written, with a capital at the start of every dish
   description and every sentence. This was a blanket lowercase rule; it read
   well on the holding page, where the only running copy was three FAQ answers,
   but it was also lowercasing product names — "the og falafel" — once the real
   menus and the opening-hours copy arrived. The display type above is still
   uppercased by the group in this section; only the running copy is affected. */


/* ============================================================================
   05  WORDMARK
   ========================================================================== */

.wordmark { display: block; }

/* Logo is 1269 x 417 (3.04:1). --logo-w also feeds --header-h. */
.wordmark__img {
  width: var(--logo-w);
  height: auto;
  transition: opacity var(--dur) var(--ease);
}

.wordmark:hover .wordmark__img { opacity: 0.7; }


/* ============================================================================
   06  HEADER & NAV
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--gutter);
  background: var(--c-paper);
  /* Hairline fades in once the page has moved, so the header separates from
     content scrolling beneath it. Toggled by main.js. */
  box-shadow: 0 1px 0 rgba(13, 13, 12, 0);
  transition: box-shadow var(--dur) var(--ease);
}

.site-header.is-stuck {
  box-shadow: 0 1px 0 var(--c-ink-12);
}

.site-nav ul {
  display: flex;
  gap: clamp(1rem, 0.5rem + 1.8vw, 2.25rem);
}

.site-nav a {
  position: relative;
  display: inline-block;
  font-size: var(--t-nav);
  letter-spacing: 0.01em;
  padding-block: 0.35em;
}

/* Rule wipes in from the left on hover, and back out to the right on leave. */
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.1em;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur) var(--ease);
}

.site-nav a:hover::after,
.site-nav a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}


/* ============================================================================
   07  FEATURE: HERO
   ========================================================================== */

/* Hero, panels and bands share this positioning context so a panel can be
   sized and placed against the hero exactly.

   Three menus means two vertical lines rather than one. --split-1 and
   --split-2 are cumulative positions across the row, so each band takes the
   distance between its neighbours: breaky is everything up to --split-1, lunch
   the gap between the two, drinks whatever is left after --split-2.

   Bands and panels are both sized from the same pair, so a panel edge can
   never drift away from the band edge it grew out of — see section 08 for why
   hover moves them. */
.feature {
  position: relative;
  --split-1: 33.34%;
  --split-2: 66.67%;

  /* How the split moves, in one place, because the bands and the panels both
     read it and must arrive together. Hover is a small, quick adjustment.
     Opening a menu is part of the same gesture as the curtain rising, so it
     borrows the panel's timing and its symmetric ease. */
  --split-dur:   var(--dur);
  --split-ease:  var(--ease-hover);
  --split-delay: 0s;

  /* The curtain and the copy are described the same way, so that closing can
     be the opening run backwards rather than a second animation that happens
     to end where the first began. --copy-lead and --copy-step are scalars
     rather than a finished delay because the stagger multiplies --i, which
     only exists on the dish itself. */
  --curtain-dur:   var(--dur-panel);
  --curtain-delay: 0s;
  --copy-dur:      var(--dur-slow);
  --copy-ease:     var(--ease-out);
  --copy-lead:     180ms;
  --copy-step:     45ms;
}

.feature.has-open {
  --split-dur:  var(--dur-panel);
  --split-ease: var(--ease-panel);
}

/* --- Closing ---------------------------------------------------------------
   The close used to be the rough half of this interaction, for three reasons
   that all had the same root: it was never designed, it was just what was left
   when .is-open came off.

     · The dishes vanished on the frame the click landed. The rule that hides
       them carries no transition of its own, because it had only ever been the
       resting state the reveal came out of — so an empty coloured panel wiped
       down over nothing.
     · The split went home in 280ms while the panel needed 720ms to leave, so
       the list re-wrapped and re-flowed on its way out, under the eye of
       whoever was still reading it.
     · Nothing was sequenced. Opening fires the curtain and the split together
       and brings the copy in behind them; closing fired all three at once.

   So the close now runs the opening backwards, and takes a little longer over
   it. The copy leaves first on --ease-in, which is --ease-out mirrored, and
   sinks the same 0.9rem it rose. The curtain follows on --ease-panel, which is
   symmetric and so reads identically in both directions, over 840ms rather
   than 720. The split goes last, once there is nothing legible left to
   disturb. About 960ms end to end against the opening's 720.

   It needs its own class rather than the absence of .has-open: to CSS,
   "closing" and "at rest" are the same state, and at rest hover has to keep
   its quick, gentle timings. */

.feature.is-closing {
  --copy-dur:      260ms;
  --copy-ease:     var(--ease-in);
  --copy-lead:     0ms;
  --copy-step:     14ms;   /* the cascade compresses; 22 dishes cannot take 1s */

  --curtain-delay: 120ms;  /* the curtain waits for the copy to start clearing */
  --curtain-dur:   var(--dur-close);

  --split-delay:   380ms;  /* and the row re-proportions behind the curtain    */
  --split-dur:     560ms;
  --split-ease:    var(--ease-panel);
}

.hero {
  background: var(--c-paper-deep);
  overflow: hidden;      /* contains the pull-back below */
}

.hero__img {
  width: 100%;
  height: var(--hero-h);
  object-fit: cover;
  object-position: center 55%;
  transition:
    transform var(--curtain-dur) var(--ease-panel) var(--curtain-delay),
    filter    var(--curtain-dur) var(--ease-panel) var(--curtain-delay);
}

/* While a menu is open the photograph draws back and softens, so the menu
   reads as the front layer and the hero as the room behind it. It settles
   again on close. A panel only ever covers its own share of the row, so the
   movement stays visible alongside the open menu. */
.js .feature.has-open .hero__img {
  transform: scale(1.06);
  filter: blur(3px) saturate(0.85);
}


/* ============================================================================
   08  FEATURE: MENU BANDS
   ========================================================================== */

.menu-bands {
  position: relative;
  z-index: 2;          /* keeps the bands above the panel layer */
  display: flex;

  /* Closes the sub-pixel seam above the row. --hero-h resolves to a fraction
     of a pixel, so the photograph and the band round to either side of the
     same row and the paper behind the hero shows through it as a pale line.
     The bands are already the top layer here, so a pixel of overlap covers it
     without moving anything that can be seen. */
  margin-top: -1px;
}

.menu-band {
  --band: var(--c-brown);

  position: relative;
  flex: 0 0 auto;              /* the width below is the whole story */
  display: grid;
  place-items: center;
  min-height: var(--band-h);
  padding: var(--s-3);
  background: var(--band);
  color: var(--c-on-band);
  font-size: var(--t-band);
  letter-spacing: 0.04em;
  text-align: center;
  cursor: pointer;
  overflow: hidden;
  transition: width var(--split-dur) var(--split-ease) var(--split-delay);
}

/* Lunch keeps the gold it has always had; brown comes across from the retired
   tapas band, and wine is the third. Gold is the only one light enough to take
   dark ink. */
.menu-band--breaky { --band: var(--c-brown); width: var(--split-1); }
.menu-band--lunch  { --band: var(--c-gold);  color: var(--c-ink);
                     width: calc(var(--split-2) - var(--split-1)); }
.menu-band--drinks { --band: var(--c-wine);  width: calc(100% - var(--split-2)); }

.menu-band__label { transition: opacity var(--dur-fast) var(--ease); }

/* An open menu and the band it came out of are the same colour, so the join
   between them disappears and the panel reads as running off the bottom of the
   screen. A hairline at 10% of the ink already on that band puts the edge back
   — enough to separate two fields of one colour, not enough to read as a rule.
   Drawn from currentColor, so it is light on the brown and the wine and dark on
   the gold without being told which is which.

   Only while open: closed, the band sits against the photograph and the edge is
   already there. */
.menu-band::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: currentColor;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.menu-band.is-open::after { opacity: 0.1; }

/* Hover moves the dividing line rather than scaling anything: the band under
   the cursor claims a little of its neighbour, and because the panels are
   sized from the same --split, an open menu widens with its own band instead
   of parting company with it.

   Doing this with flex-grow instead would look right and measure wrong — each
   band's padding sits on top of its share, so the bands land a few pixels off
   the panels, by a different few at every viewport.

   Hover is gated behind a hover-capable pointer so a tap on a phone cannot
   leave a band stuck wide. Keyboard focus is not gated.

   The chosen band goes to 52% and the two it leaves behind take 24% each. It
   claims more than the old pair's 61/39 did because a third of the window is
   too narrow to set a wine list in: the drinks panel needs the room to keep a
   bottle name and its two prices on one line. */
@media (hover: hover) {
  .feature:not(.has-open):has(.menu-band--breaky:hover) { --split-1: 52%; --split-2: 76%; }
  .feature:not(.has-open):has(.menu-band--lunch:hover)  { --split-1: 24%; --split-2: 76%; }
  .feature:not(.has-open):has(.menu-band--drinks:hover) { --split-1: 24%; --split-2: 48%; }
}

.feature:not(.has-open):has(.menu-band--breaky:focus-visible) { --split-1: 52%; --split-2: 76%; }
.feature:not(.has-open):has(.menu-band--lunch:focus-visible)  { --split-1: 24%; --split-2: 76%; }
.feature:not(.has-open):has(.menu-band--drinks:focus-visible) { --split-1: 24%; --split-2: 48%; }

/* Opening pins the split at the width the band grew to under the cursor, and
   holds it there for as long as the menu is open. Without this the hover rules
   above switch themselves off the moment .has-open lands, and the band the
   reader just chose would shrink back to half while its menu opens on top.

   Because the split is now a property of which menu is open rather than of the
   cursor, hovering the other band cannot move it — so the open panel never
   narrows and re-wraps its dishes under the reader's eye.

   These sit after the hover rules and match their specificity, so they win. */
.feature:has(.menu-band--breaky.is-open) { --split-1: 52%; --split-2: 76%; }
.feature:has(.menu-band--lunch.is-open)  { --split-1: 24%; --split-2: 76%; }
.feature:has(.menu-band--drinks.is-open) { --split-1: 24%; --split-2: 48%; }

/* Without JS the bands cannot do anything, so they are not shown; the panels
   below render as ordinary sections instead. */
html:not(.js) .menu-bands { display: none; }


/* ============================================================================
   09  FEATURE: MENU PANELS
   ----------------------------------------------------------------------------
   No-JS default: ordinary stacked sections, in flow, always readable.
   With JS: each panel is pinned over its half of the hero and is uncovered by
   a curtain wipe rising out of its band.
   ========================================================================== */

.menu-panel {
  --band: var(--c-brown);
  background: var(--band);
  color: var(--c-on-band);
}

.menu-panel--breaky { --band: var(--c-brown); }
.menu-panel--lunch  { --band: var(--c-gold); color: var(--c-ink); }
.menu-panel--drinks { --band: var(--c-wine); }

.menu-panel__inner {
  padding: clamp(1.5rem, 0.8rem + 3vw, 3.5rem) var(--gutter);
  max-width: 60rem;
  margin-inline: auto;
}

.menu-panel__title {
  font-size: var(--t-index);
  font-weight: 400;
  letter-spacing: 0.02em;
  margin-bottom: clamp(1.25rem, 0.8rem + 1.8vw, 2.5rem);
}

.menu-panel__pending {
  font-size: var(--t-body);
  opacity: 0.75;
}

/* --- Enhanced: overlay behaviour ------------------------------------------ */

.js .menu-panels {
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--hero-h);
  overflow: hidden;            /* hides the panels below until opened */
  pointer-events: none;
  z-index: 1;
}

/* Each panel covers its own share of the row, so opening one leaves the hero
   showing through the other two.

   The panel is uncovered by a clip-path rising from the bottom edge rather
   than slid up into place. It reads as the menu being revealed where it
   already was: the type never travels, so nothing can overshoot or blur, and
   it looks the same coming back down. */
.js .menu-panel {
  position: absolute;
  top: 0;
  bottom: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  clip-path: inset(100% 0 0 0);
  visibility: hidden;
  transition:
    clip-path  var(--curtain-dur) var(--ease-panel) var(--curtain-delay),
    width      var(--split-dur) var(--split-ease) var(--split-delay),
    left       var(--split-dur) var(--split-ease) var(--split-delay),
    visibility 0s linear calc(var(--curtain-delay) + var(--curtain-dur));
}

/* Lunch is the middle panel, so it is placed from the left edge at --split-1
   rather than pinned to either side. */
.js .menu-panel--breaky { left: 0; width: var(--split-1); }
.js .menu-panel--lunch  { left: var(--split-1);
                          width: calc(var(--split-2) - var(--split-1)); }
.js .menu-panel--drinks { right: 0; width: calc(100% - var(--split-2)); }

.js .menu-panel.is-open {
  clip-path: inset(0 0 0 0);
  visibility: visible;
  pointer-events: auto;
  transition:
    clip-path  var(--curtain-dur) var(--ease-panel) var(--curtain-delay),
    width      var(--split-dur) var(--split-ease) var(--split-delay),
    left       var(--split-dur) var(--split-ease) var(--split-delay),
    visibility 0s;
}

/* Items settle in behind the panel as it arrives, and sink back the same
   0.9rem as it leaves. The delay is indexed off --i, set once in the markup
   order by main.js, so adding a dish to the list needs no CSS change.

   The transition sits on this rule rather than on .is-open, which is the whole
   of the close fix. This rule is not only where an item starts — it is also
   where it returns to, and with no transition on it the copy had nothing to
   animate on the way out and simply disappeared. Declared here it serves both
   directions, and the --copy-* variables above decide which one is running. */
.js .menu-panel .menu-item,
.js .menu-panel .menu-group__title,
.js .menu-panel .menu-panel__title {
  opacity: 0;
  transform: translateY(0.9rem);
  transition:
    opacity   var(--copy-dur) var(--copy-ease)
              calc(var(--copy-lead) + (var(--i, 0) * var(--copy-step))),
    transform var(--copy-dur) var(--copy-ease)
              calc(var(--copy-lead) + (var(--i, 0) * var(--copy-step)));
}

.js .menu-panel.is-open .menu-item,
.js .menu-panel.is-open .menu-group__title,
.js .menu-panel.is-open .menu-panel__title {
  opacity: 1;
  transform: none;
}

/* --- Items ---------------------------------------------------------------- */

/* The rule between dishes is drawn from the ink already on the band, held at
   20% so it separates without competing with the type. The flat currentColor
   line comes first for engines without color-mix, which get a solid hairline
   rather than none. */
.menu-item + .menu-item {
  border-top: 1px solid currentColor;
  border-top-color: color-mix(in srgb, currentColor 20%, transparent);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
}

.menu-item__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4em 1.5em;
  margin-bottom: 0.35em;
}

.menu-item__name,
.menu-item__prices {
  font-size: var(--t-index);
  font-weight: 400;
  letter-spacing: 0.02em;
}

/* Fixed em columns, flush to the right edge, so the figures line up down the
   whole list rather than ragging. The default pair is a unit and its price —
   the lunch "Fries 10" — and an item with no unit to name leaves that first
   cell empty, which costs nothing because the grid is right-aligned either
   way. Drinks override the columns below; they carry two figures a line.

   justify-items keeps 115 and 16 sharing a right edge rather than a left
   one. */
.menu-item__prices {
  display: grid;
  grid-template-columns: 4.6em 2.2em;
  column-gap: 0.6em;
  justify-items: end;
  margin-left: auto;
}

.menu-item__prices b { font-weight: 400; }

/* --- Two-serve dishes ------------------------------------------------------
   A lunch dish that comes both as a pocket and as a plate has two sets of
   ingredients and two prices, which will not fit the single head above. The
   name takes its own line and each serve gets a row: what is in it on the
   left, the figure on the right in the same .menu-item__prices grid the rest
   of the list uses, so every price stays in one column. */

.menu-item__name + .menu-item__variants { margin-top: 0.35em; }

.menu-item__variant {
  display: flex;
  align-items: baseline;
  gap: 0.4em 1.5em;
}

.menu-item__variant + .menu-item__variant { margin-top: 0.5em; }

/* --- Groups ----------------------------------------------------------------
   Only the drinks menu groups its items — by wine style — so the heading is
   utility type a step under the panel title rather than another dish name. */

/* A line closes each style off from the next, so Sparkling, Rosé, White and
   Red each read as a section rather than as one long list with headings in it.
   Held at 80% against the 20% between dishes, so the two rules read as two
   different weights of break rather than the same line used twice: a change of
   style stops the eye, the next bottle does not.

   The measures row counts as the start of the wine, so the line goes above it
   rather than between it and Sparkling — that pair belongs together. */
.menu-group + .menu-group__head--standalone,
.menu-group + .menu-group,
.menu-group + .menu-list {
  border-top: 1px solid currentColor;
  border-top-color: color-mix(in srgb, currentColor 80%, transparent);
  margin-top: var(--s-5);
  padding-top: var(--s-5);
}

.menu-group__head {
  display: flex;
  align-items: baseline;
  gap: 0.4em 1.5em;
  margin-bottom: var(--s-2);
}

.menu-group__title {
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: 0.12em;
  opacity: 0.75;
}

/* --- Drinks -----------------------------------------------------------------
   Nearly every line here carries two figures, a glass and a bottle. Naming the
   measure on each one would set the same two words seventeen times over, so the
   columns hold bare figures and the measures are named once, on the group
   heading above them — which is how the printed menu does it.

   A wine sold only by the bottle leaves the glass cell empty and its figure
   stays under the bottle heading. */

.menu-panel--drinks .menu-item__prices,
.menu-measures {
  grid-template-columns: var(--price-col) var(--price-col);
  column-gap: var(--price-gap);
}

/* Dish names wrap evenly rather than filling each line and stranding whatever
   is left — which is how "Roncier Blanc de Blancs Brut NV" ended on a line of
   its own reading "NV". Balance spreads the words across the lines it needs, so
   the last one is never left short, and unlike an unbreakable run it cannot
   push past the column. Engines without it fall back to ordinary wrapping. */
.menu-item__name { text-wrap: balance; }

/* Wine names run long — a producer, a grape and a vintage — and several are
   wider than the panel. The shared head would drop the figures onto a row of
   their own, which on a list this size happens often enough to read as an
   accident. So here the row holds and the name wraps inside it instead: the
   figures stay on the first line, against the heading that names them. */
.menu-panel--drinks .menu-item__head { flex-wrap: nowrap; }

.menu-panel--drinks .menu-item__name { min-width: 0; }

.menu-measures {
  display: grid;
  justify-items: end;
  margin-left: auto;
  font-size: var(--t-micro);
  letter-spacing: 0.12em;
  opacity: 0.75;
}



.menu-item__desc {
  font-size: var(--t-body);
  line-height: 1.3;
  max-width: 40em;
}


/* ============================================================================
   10  ABOUT: TABS
   ----------------------------------------------------------------------------
   No-JS default: every panel visible in sequence, tab buttons hidden.
   With JS: real ARIA tabs, one panel at a time.
   ========================================================================== */

.info {
  position: relative;      /* the marks below are positioned against this */
  padding-block: clamp(3rem, 1.6rem + 6vw, var(--s-8));
}

/* --- Background marks ------------------------------------------------------
   The two hand-drawn marks sit behind the copy at low opacity, hung off the
   left and right edges so they read as pressed into the paper rather than
   placed on it.

   They arrive as masks rather than <img> because the artwork is solid black:
   as a mask the shape can be tinted to the brand brown, which sits into the
   warm paper. Black at the same opacity would go grey and read as a smudge.

   Clipping is on this wrapper, not on .info, so the marks can hang past the
   edges without adding horizontal scroll — and without cropping a focus ring
   on the tabs or rows in front of them. */

.info__marks {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.info__mark {
  position: absolute;
  display: block;
  background-color: var(--c-brown);
  opacity: 0.07;

  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}

/* Each mark keeps its own aspect ratio, so the width is the only size given
   and the artwork can never be squashed. */

.info__mark--wordmark {
  top: 4%;
  left: -7%;
  width: clamp(19rem, 48vw, 46rem);
  aspect-ratio: 1237 / 391;
  transform: rotate(-4deg);
  -webkit-mask-image: url("../assets/mark-wordmark.svg");
          mask-image: url("../assets/mark-wordmark.svg");
}

.info__mark--tag {
  bottom: 5%;
  right: -9%;
  width: clamp(15rem, 38vw, 36rem);
  aspect-ratio: 1130 / 290;
  transform: rotate(5deg);
  -webkit-mask-image: url("../assets/mark-tag.svg");
          mask-image: url("../assets/mark-tag.svg");
}

/* Centred in the page rather than pushed in from the left. The old 22% indent
   is gone: it cost the block its left edge and left short questions floating
   in an empty field, which is the whole reason the section read as unbalanced. */
.info__inner {
  /* 64rem, not the 78rem the block used to have. Dropping the indent widened
     every row by the 22% it used to give away, which put the open/close sign
     a long way from the question it belongs to. This lands the rows at about
     the width they read at before, now centred rather than pushed right. */
  width: min(100% - (var(--gutter) * 2), 64rem);
  margin-inline: auto;

  /* Lifts the copy off the background marks. */
  position: relative;
  z-index: 1;
}

html:not(.js) .tabs__list { display: none; }

html:not(.js) .tabs__panel + .tabs__panel {
  margin-top: clamp(2rem, 1.2rem + 3.4vw, var(--s-6));
}

.tabs__list {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.25rem, 0.7rem + 2.2vw, 3rem);
  margin-bottom: clamp(2rem, 1.2rem + 3.4vw, var(--s-6));
}

.tabs__tab {
  position: relative;
  font-size: var(--t-tab);
  font-weight: 400;
  letter-spacing: 0.03em;
  color: var(--c-ink-35);
  cursor: pointer;
  padding-block: 0.2em;
  transition: color var(--dur) var(--ease);
}

.tabs__tab:hover { color: var(--c-wine); }

.tabs__tab[aria-selected="true"] { color: var(--c-ink); }

/* Underline slides between tabs rather than blinking on and off. */
.tabs__tab::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.tabs__tab[aria-selected="true"]::after { transform: scaleX(1); }

.js .tabs__panel[hidden] { display: none; }

/* Panel fades up when its tab is chosen. */
.js .tabs__panel:not([hidden]) {
  animation: rise var(--dur-slow) var(--ease-out) both;
}

/* A panel is focusable so keyboard users land in the content after the tab;
   it should not draw a ring when reached by mouse. */
.tabs__panel:focus { outline: none; }


/* ============================================================================
   11  ABOUT: DISCLOSURE ROWS
   ========================================================================== */

/* A hairline for every row, so an open answer is fenced to the question it
   belongs to rather than floating between two of them. */
.row { border-top: 1px solid var(--c-ink-12); }
.row:last-of-type { border-bottom: 1px solid var(--c-ink-12); }

.row__head {
  display: grid;
  grid-template-columns: clamp(5rem, 3.4rem + 6vw, 7.5rem) 1fr auto;
  gap: var(--s-3);
  align-items: baseline;
  cursor: pointer;
  list-style: none;
  /* The whole row is the target, not just the words. */
  padding: var(--s-4) var(--s-3) var(--s-4) 0;
}

/* Suppress the native disclosure triangle in every engine. */
.row__head::-webkit-details-marker { display: none; }
.row__head::marker { content: ""; }

.row__index {
  font-size: var(--t-index);
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--c-ink);
}

.row__title {
  font-size: var(--t-question);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: 0.005em;
}

.row[open] .row__title { color: var(--c-ink); }

/* --- Hover -----------------------------------------------------------------
   The whole of it: the type turns maroon and steps a little to the left. It
   used to tint the entire row a shade of paper and push the question and its
   answer to the right, which was a lot of furniture moving to say "this is the
   one you are pointing at".

   The step is a transform rather than padding, because the row starts flush to
   the left margin and there is no padding left to take away. Transform also
   costs no layout, so nothing below it can be nudged.

   Gated on a real pointer so a tap cannot leave a row parked off its margin,
   and held off the static rows in Hours and Catering + Events, which are copy
   rather than controls. */

.row__index,
.row__title,
.row__sign {
  transition:
    color     var(--dur-fast) var(--ease),
    transform var(--dur) var(--ease);
}

@media (hover: hover) {
  .row:not(.row--static):hover .row__index,
  .row:not(.row--static):hover .row__title,
  .row:not(.row--static):hover .row__sign {
    color: var(--c-wine);
    transform: translateX(-0.35rem);
  }
}

/* The sign: two bars, the upright one rotating onto the flat one as the row
   opens, so + becomes − without swapping a glyph. Built from borders rather
   than characters so it lines up with the type at any size. */
.row__sign {
  position: relative;
  align-self: center;
  width: 0.85em;
  height: 0.85em;
  font-size: var(--t-index);
  color: var(--c-ink-35);
}

.row__sign::before,
.row__sign::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur) var(--ease);
}

.row__sign::after { transform: rotate(90deg); }

.row[open] .row__sign::after { transform: rotate(0deg); }

.row[open] .row__sign { color: var(--c-ink); }

.row__body {
  /* Aligns the copy with the question column above it. */
  padding-left: calc(clamp(5rem, 3.4rem + 6vw, 7.5rem) + var(--s-3));
  padding-right: var(--s-3);
  padding-bottom: var(--s-5);
  padding-top: 0;
  max-width: 42em;
  color: var(--c-wine);
  font-size: var(--t-body);
  line-height: 1.32;
}

.row__body p + p { margin-top: 1.05em; }

.row__body a {
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  text-decoration-color: currentColor;
  transition: color var(--dur-fast) var(--ease);
}

.row__body a:hover { color: var(--c-ink); }

/* --- Single-row panels -----------------------------------------------------
   Hours and Catering + Events carry one row each. Every piece of furniture on a
   row exists to do a job that needs a second row to make sense of it: the
   hairlines fence one answer off from the next, the step across on hover marks
   which of several you are about to open, and the reveal shows copy that was
   hidden. With nothing above, below or closed, all three are decoration around
   a single paragraph.

   So those two rows are plain markup rather than a <details> — see index.html —
   and this strips the rest. FAQ keeps the lot, because it has three questions
   and needs them. */

.tabs__panel--plain .row,
.tabs__panel--plain .row:last-of-type { border: 0; }

.js .tabs__panel--plain:not([hidden]) { animation: none; }

/* A static row is copy, not a control: no pointer, and no hover — the hover
   rule in section 11 already excludes it.

   It also carries no index label and no sign, so the three-column head has
   nothing to put in two of them. One column, and the answer lines up under the
   title rather than under a question column that is no longer there. */
.row--static .row__head {
  cursor: auto;
  grid-template-columns: 1fr;
}

.row--static .row__body,
.row--static:hover .row__body { padding-left: 0; }

/* Smooth open/close where ::details-content is supported. */
@supports selector(details::details-content) {
  .row::details-content {
    block-size: 0;
    overflow: hidden;
    opacity: 0;
    transition:
      block-size var(--dur-slow) var(--ease),
      opacity    var(--dur) var(--ease),
      content-visibility var(--dur-slow) allow-discrete;
  }
  .row[open]::details-content {
    block-size: auto;
    opacity: 1;
  }
}


/* ============================================================================
   12  FOOTER
   ========================================================================== */

.site-footer { padding-top: clamp(2rem, 1.2rem + 3.4vw, var(--s-6)); }

.footer-cols {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--s-4);
  padding-inline: var(--gutter);
  font-size: var(--t-micro);
  line-height: 1.25;
  letter-spacing: 0.045em;
}

.footer-col a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--c-ink-35);
  transition: text-decoration-color var(--dur-fast) var(--ease);
}

.footer-col a:hover { text-decoration-color: currentColor; }


/* ============================================================================
   13  GIANT WORDMARK
   Full-bleed sign-off, gutter to gutter, with the "y" descender complete.
   ========================================================================== */

.wordmark-giant {
  margin-top: clamp(1rem, -0.4rem + 5vw, 3rem);
  padding-inline: var(--gutter);
  padding-bottom: 20px;
  overflow: hidden;
  user-select: none;
}

.wordmark-giant__img {
  display: block;
  width: 100%;
  height: auto;
}


/* ============================================================================
   14  MOTION
   ----------------------------------------------------------------------------
   Scroll-linked reveals use animation-timeline: view(), which needs no
   JavaScript and no observer. Applied only to top-level blocks — never to
   anything inside a panel that can be hidden, because an element that never
   enters a scrollport would stay stuck at opacity 0 when later revealed.
   ========================================================================== */

@keyframes rise {
  from { opacity: 0; transform: translateY(1.5rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes settle {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The background marks drift against the scroll. Written on the `translate`
   property rather than `transform`, so each mark keeps the static rotation set
   in section 10 and the two do not have to be maintained in one declaration.

   The two travel different distances, which is the whole effect: matching
   amounts would slide them as one sheet, and the difference is what makes them
   sit at different depths in the paper. */
@keyframes drift-a {
  from { translate: 0  3.5rem; }
  to   { translate: 0 -3.5rem; }
}

@keyframes drift-b {
  from { translate: 0 -2rem; }
  to   { translate: 0  2rem; }
}

@media (prefers-reduced-motion: no-preference) {

  /* Page arrival — the hero settles rather than snapping in. */
  .hero__img { animation: fade-in 700ms var(--ease-out) both; }

  @supports (animation-timeline: view()) {
    .menu-bands,
    .info__inner,
    .footer-cols {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }

    .wordmark-giant__img {
      animation: settle linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 90%;
    }

    /* `cover`, not `entry`: the drift runs for the whole time the mark is on
       screen, so it is always mid-travel while the section is being read. An
       entry range would finish the movement before the copy is reached and
       then hold still. */
    .info__mark--wordmark {
      animation: drift-a linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    .info__mark--tag {
      animation: drift-b linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ============================================================================
   15  RESPONSIVE
   ========================================================================== */

/* --- Tablet --------------------------------------------------------------- */
@media (max-width: 62rem) {

  .footer-cols {
    grid-template-columns: repeat(2, 1fr);
    row-gap: var(--s-4);
  }
}

/* --- Phone ---------------------------------------------------------------- */
@media (max-width: 40rem) {

  :root {
    /* Header padding drops to --s-3 here and the bands stack three deep, so
       both inputs to the hero's height change with them. */
    --band-h:   3.25rem;
    --header-h: calc((var(--s-3) * 2) + (var(--logo-w) / 3.0432));
    --bands-h:  calc(var(--band-h) * 3);
  }

  .site-header {
    align-items: flex-end;
    padding-block: var(--s-3);
  }

  /* Bands stack, so a panel takes the full width rather than sitting beside
     the hero — and the splits have nothing left to divide. These match the
     specificity of the rules they replace and win on order. */
  .menu-bands { flex-direction: column; }

  .menu-band--breaky,
  .menu-band--lunch,
  .menu-band--drinks { width: 100%; }

  .js .menu-panel--breaky,
  .js .menu-panel--lunch,
  .js .menu-panel--drinks {
    width: 100%;
    left: 0;
    right: 0;
  }

  /* Index label moves above the question instead of beside it. The sign keeps
     its own column on the right and spans both, so it stays centred on the
     pair however many lines the question runs to. */
  .row__head {
    grid-template-columns: 1fr auto;
    gap: var(--s-1) var(--s-3);
  }

  .row__index,
  .row__title { grid-column: 1; }

  .row__sign {
    grid-column: 2;
    grid-row: 1 / span 2;
  }

  /* The answer runs to the gutter here rather than sitting under the question
     column, which has moved above it. Without this the wide rule would still
     win on specificity and shove the copy 8rem in on a narrow window. */
  .row__body { padding-left: 0; }

  /* --- An open menu keeps its place in the stack --------------------------
     On a phone the bands stack three deep, and a panel that opens above all
     three loses its position in the sequence — nothing tells you how far down
     the list you are. So the bands ahead of the open one rise above the panel
     and the panel drops to meet them: breaky / lunch / panel / close, rather
     than panel / breaky / lunch / close.

     Every rising band travels exactly --hero-h, and that is not a coincidence
     worth re-deriving later. At rest band i sits at --hero-h + (i * --band-h),
     because the panels occupy --hero-h above them. Subtract --hero-h and it
     lands at (i * --band-h) — flush under the header, in its original order.
     One distance for all three, whichever menu is open.

     The panel keeps its full --hero-h and simply drops by the same amount, so
     its bottom edge lands exactly on the open band: at rest that band sits at
     --hero-h + (k * --band-h), and the panel now runs from (k * --band-h) for
     --hero-h. Shrinking it instead leaves the risen bands' vacated layout
     space showing as a strip of paper above the close band — transform moves
     the paint, not the box.

     No :has() support means --open-i stays 0 and no band rises, which is the
     behaviour this replaces — so the fallback is the current design, not a
     broken one. */

  .js .menu-panels {
    /* The -1px pairs with .menu-bands' own margin-top: -1px. The bands sit a
       pixel above the grid to cover a sub-pixel seam, so a panel placed on the
       unshifted grid lands a pixel below the band above it and the paper shows
       through as a pale line. Same trick, same reason, same direction. */
    top: calc((var(--open-i, 0) * var(--band-h)) - 1px);
    transition: top var(--curtain-dur) var(--ease-panel) var(--curtain-delay);
  }

  /* Breaky is index 0 and needs no rule for --open-i — nothing sits ahead of
     it — but it still names its colour, for the switch case below. */
  .js .feature:has(.menu-band--breaky.is-open) { --open-band: var(--c-brown); }
  .js .feature:has(.menu-band--lunch.is-open)  { --open-i: 1; --open-band: var(--c-gold); }
  .js .feature:has(.menu-band--drinks.is-open) { --open-i: 2; --open-band: var(--c-wine); }

  /* What the rising bands uncover. They vacate their space in the bands row but
     transform leaves the box behind, so the paper the row sits on shows through
     as a white strip until the curtain climbs high enough to cover it — about
     83% of the way in, because the strip is the full height of the bands that
     left. Painting the row itself means the gap is the colour of the menu
     arriving from the first frame, with nothing extra in the markup.

     The row is z-index 2, above the panel layer, so this sits behind the bands
     that stay and is invisible until one moves off it.

     --hold is why closing does not flash. The band loses .is-open the moment it
     is dismissed, so :has() stops matching and the colour would revert while
     the bands are still travelling down. A 0s transition with a delay holds the
     old value for the whole close and only then drops it. Opening leaves --hold
     at 0s, so the colour is there immediately. */
  .js .menu-bands {
    background: var(--open-band, transparent);
    transition: background-color 0s linear var(--hold, 0s);
  }

  .js .feature.is-closing {
    --hold: calc(var(--curtain-delay) + var(--curtain-dur));
  }

  /* Set by main.js for two frames when switching between open menus. See
     snapBands() there for why the reshuffle is a cut rather than a move. */
  .js .feature.switching .menu-band,
  .js .feature.switching .menu-panels {
    transition: none;
  }

  .js .feature:has(.menu-band--lunch.is-open)  .menu-band--breaky,
  .js .feature:has(.menu-band--drinks.is-open) .menu-band--breaky,
  .js .feature:has(.menu-band--drinks.is-open) .menu-band--lunch {
    transform: translateY(calc(-1 * var(--hero-h)));
  }

  /* --curtain-dur, not --split-dur. --split-dur is var(--dur) at rest and only
     becomes var(--dur-panel) once .has-open lands — and that class arrives in
     the same frame the transform changes, so the transition resolves against
     the resting value and the band finishes in 280ms while the curtain is
     still wiping for 720ms. The curtain's own tokens are defined on .feature
     at rest, so they read correctly opening and closing both. Describing the
     band the way the curtain is described is what keeps them together. */
  .js .menu-band {
    transition: transform var(--curtain-dur) var(--ease-panel) var(--curtain-delay);
  }

  .footer-cols { grid-template-columns: 1fr; }
}


/* ============================================================================
   16  LOADER
   ----------------------------------------------------------------------------
   Plays once on arrival and fades into the page underneath it.

   Two acts. The three lock-ups write themselves onto the paper, overlapping
   into one cluster; then five photographs open out from the middle of the frame
   in quick succession, each covering the last. The fifth reaches full bleed and
   the whole sheet fades away — the site is already behind it, so there is
   nothing to slide off and nothing to wait for.

   The cluster is a block, not a set of positions. The prototype placed each
   lock-up as a percentage of a 1400 x 800 frame, which only holds at that one
   ratio: `top` resolves against height and `width` against width, so on a
   taller window the type shrinks while the gaps between it grow and the cluster
   comes apart. Here the three are stacked in flow inside one box and the overlap
   is a negative margin — a percentage of the box's own width — so the whole
   composition scales as one thing at any shape of screen.

   Two of the lock-ups arrive already pale from the supplied files. Nothing is
   tinted here; it is black type reading over two tints of its own family.

   The five photographs are 1.0–1.8MB each as supplied. They are preloaded in
   the head, but they want compressing before this goes live — the sequence is
   on a fixed schedule and will not wait for a slow one to arrive.
   ========================================================================== */

.loader {
  position: fixed;
  inset: 0;
  /* Over everything: the sticky header sits at 100 and the skip link at 200,
     and the loader is first in the body, so at an equal z-index it would lose to
     both on document order. */
  z-index: 300;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--c-paper);
}

/* No JavaScript, no loader. The page underneath is complete without it, so the
   failure case is simply not showing it — never a sheet of paper left over the
   site. */
html:not(.js) .loader { display: none; }

/* Taken out for good once it has faded, so it cannot intercept anything. The
   attribute alone would lose to .loader's display above. */
.loader[hidden] { display: none; }

/* Nothing scrolls behind it while it is up. */
html.is-loading { overflow: hidden; }

/* --- The cluster ---------------------------------------------------------- */

.loader__type {
  /* --lt-overlap and the widths are measured off Opening Window Example 2,
     expressed against this block's width so they travel together.

     --lt-gap is the one number worth touching: it is the distance from the
     bottom of "our doors are open" to the top of the address. Both lock-ups are
     cropped tight to their ink — measured, not assumed — so this is the whole of
     the gap. Near zero puts the address just under the descenders of "open",
     which is where Example 2 has it, without the two boxes overlapping. */
  /* 40vw is the proportion the composition was drawn at — "our doors are open"
     is 38.7% of the frame in Example 2. The cap keeps it from becoming a
     billboard on a very wide screen, and 44rem holds close to that proportion
     out to 1920 rather than shrinking to a third of it. */
  --lt-w:       clamp(17rem, 40vw, 44rem);
  --lt-overlap: 10.3%;
  --lt-gap:     0.8%;

  position: relative;
  z-index: 1;
  width: var(--lt-w);
}

.lt {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0;
}

/* A shade left of centre, as it sits in the example. */
.lt--script  { width: 86.7%; margin-left: -1.4%; }

/* Up into the script: this overlap is what carries the layout. */
.lt--doors   { width: 100%; margin-top: calc(var(--lt-overlap) * -1); }

.lt--address { width: 72%; margin-top: var(--lt-gap); margin-inline: auto; }

/* --- The photographs ------------------------------------------------------ */

.loader__shot {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  transform-origin: center;
}

.loader__shot img { width: 100%; height: 100%; object-fit: cover; }

.loader__shot--1 { z-index: 2; }
.loader__shot--2 { z-index: 3; }
.loader__shot--3 { z-index: 4; }
.loader__shot--4 { z-index: 5; }
.loader__shot--5 { z-index: 6; }

/* --- Grain ----------------------------------------------------------------
   Generated rather than supplied: an feTurbulence tile multiplied over the
   paper, stepped frame by frame so it crawls the way film grain does instead of
   sitting still like a texture. One background-image to repoint if a scan
   arrives. */

.loader__grain {
  position: absolute;
  inset: -50%;
  z-index: 7;
  pointer-events: none;
  mix-blend-mode: multiply;
  opacity: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* --- Motion ---------------------------------------------------------------
   Written rather than wiped. The stops are uneven on purpose — a hand does not
   travel at a constant rate, and a linear reveal is the thing that makes a
   drawn-on effect read as a mask sliding across instead. */

@keyframes loader-write {
  0%   { opacity: 1; clip-path: inset(0 100% 0 0); }
  14%  { clip-path: inset(0 79% 0 0); }
  26%  { clip-path: inset(0 71% 0 0); }
  40%  { clip-path: inset(0 52% 0 0); }
  52%  { clip-path: inset(0 43% 0 0); }
  68%  { clip-path: inset(0 26% 0 0); }
  84%  { clip-path: inset(0 11% 0 0); }
  100% { opacity: 1; clip-path: inset(0 0 0 0); }
}

/* Scaling the layer rather than uncovering it is what makes it read as coming
   from something small rather than a window widening. --ease-panel is an S, so
   the layer gathers pace and settles instead of leaving at full speed the way
   --ease-out did — its near-vertical start read as a snap.

   scale(0.28) rather than the 0.10 this began at. Distance is the other half of
   why a zoom reads as violent: 0.10 is a tenfold move, and no easing makes ten
   times in half a second calm. 0.28 still opens from something small in the
   middle of the screen, over a third of the distance.

   The fade is a second animation rather than a keyframe inside this one, and that
   is load-bearing. A timing function is applied between adjacent keyframes, not
   across the whole animation, so an intermediate stop splits the run into two
   eased intervals and the scale gets eased in twice — once at 0% and again at the
   stop. Measured, that cost 84ms of a 280ms move to a stall before it whipped
   out. Two keyframes, one interval, one S. */
@keyframes loader-open {
  from { transform: scale(0.28); }
  to   { transform: scale(1); }
}

@keyframes loader-appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes loader-grain-crawl {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-6%, 3%); }
  40%  { transform: translate(4%, -5%); }
  60%  { transform: translate(-3%, -2%); }
  80%  { transform: translate(5%, 4%); }
  100% { transform: translate(0, 0); }
}

@keyframes loader-grain-settle {
  0%   { opacity: 0.30; }
  55%  { opacity: 0.20; }
  100% { opacity: 0.10; }
}

.loader.is-playing .lt--script  { animation: loader-write 620ms linear both; }
.loader.is-playing .lt--doors   { animation: loader-write 700ms linear both 380ms; }
.loader.is-playing .lt--address { animation: loader-write 520ms linear both 900ms; }

/* The cluster is held for a second once it is written, so the copy is read
   before the photographs take the screen. */
.loader.is-playing .loader__shot   {
  animation:
    loader-open   520ms var(--ease-panel) both,
    loader-appear  80ms linear            both;
}
.loader.is-playing .loader__shot--1 { animation-delay: 2500ms; }
.loader.is-playing .loader__shot--2 { animation-delay: 2740ms; }
.loader.is-playing .loader__shot--3 { animation-delay: 2980ms; }
.loader.is-playing .loader__shot--4 { animation-delay: 3220ms; }
.loader.is-playing .loader__shot--5 { animation-delay: 3460ms; }  /* full bleed at 3980ms */

/* 520ms at 240ms apart means each photograph is still opening when the next one
   starts, so two are in the air at once. That is the trade for the slower move:
   less a row of hand-offs than a stack building up. RUN in main.js is the
   number to keep in step with the last delay.

   The fade stays short — 80ms against a 520ms move — precisely because of that
   overlap. Lengthening it in proportion was the obvious move and the wrong one:
   from scale(0.28) the layer is already large, so a slow fade shows a
   see-through panel sitting over the photograph beneath it, which reads as a
   mistake rather than a stack. Opaque early, then travel. */

.loader.is-playing .loader__grain {
  animation:
    loader-grain-crawl 900ms steps(6, end) infinite,
    loader-grain-settle 3980ms var(--ease) both;
}

/* Straight into the site: the page is already behind the sheet, so the sheet
   fades and there is nothing to move. */
.loader.is-out {
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms var(--ease);
}

/* Anyone who has asked for less movement gets the page, not a performance. The
   sequence is collapsed rather than skipped so the fade still reads as a
   transition and not a flash. */
@media (prefers-reduced-motion: reduce) {
  .loader.is-playing .lt,
  .loader.is-playing .loader__shot,
  .loader.is-playing .loader__grain {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
}
