@charset "utf-8";
/* ==========================================================================
   Lou Lou's Bistro + Foster's Bar
   ---------------------------------------------------------------------------
   THE CLIMB: Come. Sit. Stay. One continuous ascent from bistro daylight
   into bar lamplight.

   THE LAYOUT IS A HANG, not a grid of sections. Their dining room is hung
   salon style with framed sporting art at different sizes and heights, and
   that is the system: pictures take different columns, sit high or low
   against each other, and the words move side to side with them. Some acts
   carry no picture, some carry no words. What holds it together is the
   frame, the mat and the light, never a repeated skeleton.

   THE DISPLAY FACE IS MARCELLUS, and it was not chosen, it was MATCHED. Their
   own sign sets FOSTER'S and BISTRO & BAR in a classical inscriptional roman:
   cupped and flaring serifs, a kicking leg on the R, a teardrop apostrophe.
   Marcellus is that letter. It replaced Melodrama, a fashion-salon didone that
   read as somebody else's brand and dissolved at paragraph length.

   One weight, no italic, because an engraver has one chisel. Emphasis is SIZE,
   TRACKING and CASE, never a bolder cut, never a slant. And it sets DISPLAY
   ONLY: running copy, decks, trait lists and quotes belong to the text face.
   A lapidary roman at paragraph length is a gravestone, not a menu.

   Tracking runs POSITIVE here. Inscriptional capitals were cut with air around
   them; the tight negative tracking a didone wants would close this face up
   and kill the very thing it was picked for.

   Every color is pipetted from their real logo, real menu, real dining room.
   Nothing invented. See CONCEPT.md.
   ========================================================================== */

/* ==========================================================================
   1. TOKENS, the whole system, locked before a single component
   ========================================================================== */
:root {
  /* ---- Ink & paper: their logo and their printed menu ------------------- */
  /* THE INK RAMP, STATED ONCE AND ONLY ONCE.
     A light card nested in a dark stratum has to restate every token it paints
     with (sessions 5, 12, 13, 16), and those restatements MUST point back here
     with var(), never carry a second copy of the hex. Two blocks used to carry
     literal #6E8087, so darkening the ramp reached most of the site and silently
     missed the booking card. A token duplicated is a token that will drift. */
  --ink-base:       #1E3035;   /* the logo's slate-teal. never black */
  --ink-soft-base:  #3A4E54;   /* 7.8:1 on paper. functional text lives here */
  --ink-faint-base: #5A6E76;   /* 4.7:1 on paper. the faintest step a WORD may
                                  wear, was #6E8087 (3.65:1), which put every
                                  dish description under the readable line.
                                  Whisper, never illegible. */

  --ink:            var(--ink-base);
  --ink-soft:       var(--ink-soft-base);
  --ink-faint:      var(--ink-faint-base);
  --paper:          #F4F1E9;   /* the menu's warm bone */
  --paper-warm:     #EFE9DC;
  --paper-deep:     #E4DDD0;

  /* ---- The room: their own dining room photograph ----------------------- */
  --sage:           #A6BDA4;   /* block-print tablecloth ground */
  --sage-deep:      #91A591;
  --tolix:          #CB3C21;   /* the red chairs. THE one accent, <=10% */
  --tolix-deep:     #9E1E0C;
  --rattan:         #C89B62;   /* charger plates; upstairs lamplight */
  --rattan-lit:     #E9B475;
  --gold:           #B08A4F;   /* the mat line on a framed print */

  /* ---- Upstairs: Foster's dusk ----------------------------------------- */
  --dusk:           #16252A;
  --dusk-deep:      #101C21;

  /* ---- Semantic (L2, where 90% of components point) ------------------- */
  --surface:        var(--paper);
  --surface-raised: var(--paper-warm);
  --line:           color-mix(in srgb, var(--ink) 14%, transparent);
  --line-soft:      color-mix(in srgb, var(--ink) 8%,  transparent);
  --accent:         var(--tolix);
  --focus:          var(--tolix);

  /* ---- Type families --------------------------------------------------- */
  --display:    'Marcellus', 'Optima', 'Palatino', Georgia, serif;
  --display-sc: 'Marcellus SC', 'Marcellus', 'Optima', Georgia, serif;
  --text:    'Albert Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* ---- Spacing: ONE 4-based ramp. Nothing between these, ever. ---------- */
  --space-1:4px;   --space-2:8px;    --space-3:12px;   --space-4:16px;
  --space-5:24px;  --space-6:32px;   --space-7:48px;   --space-8:64px;
  --space-9:96px;  --space-10:128px; --space-11:160px; --space-12:224px;

  /* ---- Type scale: 1.333 editorial, generated, never hand-picked -------- */
  --ms-dn2: 0.563rem; --ms-dn1: 0.75rem;
  --ms-0:   1rem;     --ms-1: 1.333rem; --ms-2: 1.777rem; --ms-3: 2.369rem;
  --ms-4:   3.157rem; --ms-5: 4.209rem; --ms-6: 5.61rem;  --ms-7: 7.478rem;

  /* Roles, one treatment each. The vw terms are large on purpose: the phone
     is the main stage and display type must actually GROW there instead of
     pinning to a floor. 19vw at 390px = 74px, not the old 48px. */
  --t-hero:    clamp(4.25rem, 19vw, 13rem);      /* the one word: Come/Sit/Stay */
  --t-display: clamp(2.6rem,  7.6vw, 5.61rem);   /* page H1 */
  --t-h2:      clamp(2rem,    4.4vw, 3.157rem);
  --t-h3:      clamp(1.45rem, 2.4vw, 2.369rem);
  --t-sub:     clamp(1.1rem,  1.5vw, 1.777rem);
  --t-body:    1rem;
  --t-lead:    clamp(1.06rem, 1.15vw, 1.125rem);
  --t-label:   var(--ms-dn1);
  --t-caption: 0.72rem;

  /* ---- Grid: 12 columns, declared first --------------------------------- */
  --edge:   clamp(1.25rem, 0.8rem + 2.2vw, 4.5rem);
  --gutter: clamp(1.25rem, 0.8rem + 2vw, 3rem);
  --shell-max: 84rem;
  --shell:  min(100% - 2 * var(--edge), var(--shell-max));
  /* The distance from the shell's edge to the screen's edge. Viewport-relative
     on purpose: --shell resolves against its own parent, so it is useless for
     bleed maths once you are inside a grid column. */
  --bleed:  calc(-1 * max(var(--edge), (100vw - var(--shell-max)) / 2));
  --measure: 65ch;
  --section-y: clamp(3.5rem, 2rem + 4.4vw, 7rem);

  /* ---- Radius: sharp house lane. A bistro prints on a squared card. ----- */
  --r-xs:2px; --r-sm:3px; --r-md:4px; --r-pill:999px; --r-full:50%;

  /* ---- Shadow: one light source, straight down, ink-tinted, 4 rungs ----- */
  --e1: 0 1px 2px color-mix(in srgb,var(--ink) 12%,transparent), 0 1px 1px color-mix(in srgb,var(--ink) 8%,transparent);
  --e2: 0 1px 2px color-mix(in srgb,var(--ink) 12%,transparent), 0 4px 8px color-mix(in srgb,var(--ink) 10%,transparent);
  --e3: 0 1px 2px color-mix(in srgb,var(--ink) 14%,transparent), 0 12px 20px color-mix(in srgb,var(--ink) 12%,transparent);
  --e4: 0 1px 2px color-mix(in srgb,var(--ink) 16%,transparent), 0 24px 40px color-mix(in srgb,var(--ink) 12%,transparent);

  /* ---- Motion: two eases, four durations, ONE reveal verb (rise) --------
     The tempo note is Campbell's, twice: "peaceful jazz, not hip-hop."
     Grace comes from long travel, an expressive ease, a wide stagger, and
     from scrubbing the big moments to the reader's own scroll, never from
     a longer duration. Nothing is yanked; everything is invited. */
  --ease-standard:   cubic-bezier(.22,.61,.36,1);
  --ease-expressive: cubic-bezier(.16,1,.30,1);
  --dur-micro:120ms; --dur-element:200ms; --dur-section:320ms; --dur-cinematic:600ms;
  --stagger:75ms;
}

/* Root font size: one continuous curve, 16@360 -> 19@1440. No snap. */
html { font-size: clamp(1rem, 0.93rem + 0.31vw, 1.1875rem); -webkit-text-size-adjust: 100%; }
*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--text);
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* CLIP, NOT HIDDEN. `overflow-x: hidden` on body makes body a scroll container,
   and on iOS Safari that produces a phantom scrollable area BELOW the footer. Campbell scrolled past the footer into a screen of dead white. `clip` stops
   the overflow without creating a scroll container, so there is nothing left to
   scroll into. Declared on both elements because Safari propagates body's
   overflow to the viewport only when html's is `visible`. */
html, body { overflow-x: clip; }

/* ==========================================================================
   THE OVERSCROLL CANVAS, the white void under the footer.

   Campbell, on his phone: "you can scroll past the footer and there's a whole
   other section of just white space." It is not a layout box and no measurement
   finds it: scrollHeight, the footer's bottom, and every headless engine all
   report zero dead space, because iOS rubber-band overscroll is not part of the
   scrollable area at all.

   What he was seeing is the CANVAS. `html` carried no background, so the canvas
   inherited body's bone paper, and bouncing past a near-black footer revealed a
   band of white paper behind it. A rendering artifact that looks exactly like a
   broken empty section.

   Fixed by painting the canvas itself: dusk as the base color (so the bottom
   bounce shows the footer's own darkness and the page simply feels deeper than
   the screen), with a paper layer over the top portion (so the pull-down at the
   top still shows paper, not a black flash above the masthead). No element, no
   height, nothing added to the document.
   ========================================================================== */
html {
  background-color: var(--dusk-deep);
  background-image: linear-gradient(var(--paper), var(--paper));
  background-repeat: no-repeat;
  background-size: 100% 70%;
}
img, svg, video { max-width: 100%; display: block; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--rattan) 40%, transparent); }

/* ==========================================================================
   2. LAYOUT PRIMITIVES, one shell token, one measure token, no second name
   ========================================================================== */
.shell { width: var(--shell); margin-inline: auto; }
.grid  { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--gutter); }
.measure { max-width: var(--measure); }

/* THE HANG, the layout idea, and the reason this stopped being a template.

   Their dining room is hung salon style: framed sporting art at different
   sizes and different heights along a white wall, nothing in a row, nothing
   the same size, and it still reads as composed rather than scattered. That
   is the layout system. Each act is HUNG, not generated: pictures take
   different columns, sit at different heights, and the words move with them
   like an engraved wall label.

   The first pass of this build held every block on column 1 with the picture
   always right. That is a stricter template than the zig-zag it replaced, and
   it read stale. So: the words genuinely alternate side, the pictures sit
   high or low against each other, and some acts have no picture or no words
   at all. What holds it together is the frame, the mat and the light, not a
   repeated skeleton. */

/* A hung picture sits off its neighbor's baseline, the way a real salon wall
   staggers. Done with ALIGNMENT inside the row, never transform: a transform
   moves the paint but leaves the space behind, and the page fills with holes.

   The column spans live HERE and not in a style attribute, because an inline
   grid-column applies at every width and collapses a framed portrait into a
   60px thumbnail on a phone. Mobile stacks; the hang only exists once there
   is a wall wide enough to hang on. */
.hang-row { display: grid; grid-template-columns: 1fr; gap: var(--space-7); }
.hang-row > * { min-width: 0; }

@media (min-width: 60rem) {
  .hang-row {
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--gutter); row-gap: var(--space-8);
    align-items: stretch;
  }
  .hang--high { align-self: start; }
  .hang--mid  { align-self: center; }
  .hang--low  { align-self: end; }
  .hang--drop { align-self: end; margin-bottom: var(--space-7); }

  /* THE SALON PAIR, and the rows are NAMED, for the same reason .split names
     them. Auto-placement does not backtrack: the portrait took columns 8-12,
     so the name card asking for 1-6 fell to row 2 and the small print to row
     3. Lou Lou's act was rendering 1445px tall as three stacked bands each
     with an empty half beside it. That is the "not formatted correctly" in
     Campbell's screenshot, and it was never a spacing problem.

     Composed properly: the tall portrait holds the full height on one side,
     the name card sits beside it, and the smaller print hangs low beneath the
     card, which is what a real salon wall does. */
  .hang-row--pair  > :nth-child(1) { grid-column: 1 / 7; grid-row: 1 / 3; }
  .hang-row--pair  > :nth-child(2) { grid-column: 8 / -1; grid-row: 1; align-self: end; }
  .hang-row--pair  > :nth-child(3) { grid-column: 8 / -1; grid-row: 2; align-self: start; }
  /* mirrored: her portrait crosses to the right so the page alternates
     R, L, R, L, R instead of running left three acts in a row. */
  .hang-row--pair-rev > :nth-child(1) { grid-column: 7 / -1; grid-row: 1 / 3; }
  .hang-row--pair-rev > :nth-child(2) { grid-column: 1 / 6; grid-row: 1; align-self: end; }
  .hang-row--pair-rev > :nth-child(3) { grid-column: 1 / 6; grid-row: 2; align-self: start; }
  /* the opposite hang: label first, the portrait taking the right */
  .hang-row--rev   > :nth-child(1) { grid-row: 1; }
  .hang-row--rev   > :nth-child(2) { grid-row: 1; }
  /* the opposite hang: label first, the portrait taking the right */
  .hang-row--rev   > :nth-child(1) { grid-column: 1 / 6; }
  .hang-row--rev   > :nth-child(2) { grid-column: 7 / -1; }
  /* the dedication plate: two prints flanking centered words */
  .hang-row--plaque > * { grid-row: 1; }
  .hang-row--plaque > :nth-child(1) { grid-column: 1 / 4; }
  .hang-row--plaque > :nth-child(2) { grid-column: 5 / 10; }
  .hang-row--plaque > :nth-child(3) { grid-column: 11 / -1; }
  /* THE LETTER hangs on the same wall but needs a MEASURE, not a plate: six
     columns for the words with a print either side. The plaque's five columns
     were sized for one sentence and would have run their letter to a column of
     ragged two-word lines. */
  .hang-row--letter > * { grid-row: 1; }
  .hang-row--letter > :nth-child(1) { grid-column: 1 / 4; }
  .hang-row--letter > :nth-child(2) { grid-column: 4 / 10; }
  .hang-row--letter > :nth-child(3) { grid-column: 10 / -1; }
}
/* on a phone the flanking prints would be noise; the words carry the act */
@media (max-width: 59.99rem) {
  .hang-row--plaque > :nth-child(1),
  .hang-row--plaque > :nth-child(3) { display: none; }
  .hang-row--plaque > :nth-child(2) { text-align: left; }
  .hang-row--plaque .quote__say { margin-inline: 0; }

  /* THE LETTER ON A PHONE keeps ONE print, and it is deliberate which one.
     Hiding both (what the plaque does, and what this rule did on its first
     pass) left 1130px of unbroken body copy on bone paper with no anchor
     anywhere in the act: read at 390 it is exactly the flat, plain wall this
     build has been called out for twice. The crowd print goes, because the
     ribbon-cutting band directly above it is the same moment from the same
     afternoon. The sunflowers stay, tucked in under the signature the way a
     photograph goes in with a letter. */
  .hang-row--letter > :nth-child(1) { display: none; }
  .hang-row--letter > :nth-child(3) {
    max-width: 60%;
    margin-inline: auto;
    margin-top: var(--space-7);
  }
}
.split { display: grid; grid-template-columns: 1fr; gap: var(--space-7); align-items: start; }


.split__say { min-width: 0; }
.split__see { min-width: 0; }
.split__say > * + * { margin-top: var(--space-5); }

@media (min-width: 60rem) {
  .split { grid-template-columns: repeat(12, 1fr); gap: var(--gutter); }

  /* EVERY PICTURE IS MOUNTED NOW, so nothing bleeds off the viewport: running a
     matted print off the screen edge slices its own frame, which is worse than
     any amount of margin. The picture earns its size from COLUMNS instead, seven of twelve, so the act reads big and no dead outer margin opens up.
     The old build did the opposite: it bled the picture on some acts and inset
     it on others, which is where the empty right-hand gutters came from. */
  /* BOTH HALVES ARE PINNED TO ROW 1. This is the single most expensive line in
     the file. CSS grid auto-placement does not backtrack: on a mirrored act the
     words are placed in columns 8-12 first, and when the picture then asks for
     columns 1-7 the cursor has already passed them, so the picture is pushed
     onto a NEW ROW. Every "picture on the left" act on the site was silently
     stacking instead of sitting beside its words, which is where the big empty
     quadrants came from, an empty top-left beside the text and an empty
     bottom-right beside the picture. Naming the row fixes it at every width. */
  .split__say, .split__see { grid-row: 1; }
  .split__say { grid-column: 1 / 6; }
  .split__see { grid-column: 6 / -1; }
  /* wide say: for acts that are mostly words */
  .split--say .split__say { grid-column: 1 / 7; }
  .split--say .split__see { grid-column: 7 / -1; }
  .split--mid { align-items: center; }

  /* MIRRORED: the words take the right, the picture takes the left. This is
     now an ALTERNATION, act by act, not an exception. Campbell, 2026-07-22:
     "If there's a picture on the left, the words are on the right." */
  .split--right .split__say { grid-column: 8 / -1; }
  .split--right .split__see { grid-column: 1 / 8; }
  .split--right.split--say .split__say { grid-column: 7 / -1; }
  .split--right.split--say .split__see { grid-column: 1 / 7; }
  /* --inset is kept as a no-op alias: inset is the only behaviour now. */

  /* CENTERED PLATE: a breath. Words seated as an engraved plaque with the
     shell's own margins, used once or twice a page, never as the default. */
  .split--plate .split__say { grid-column: 3 / 11; text-align: center; }
  .split--plate .split__say .namecard__traits,
  .split--plate .split__say .quote__say { margin-inline: auto; }
  .split--plate .split__say .namecard__traits { justify-content: center; }
}

/* ==========================================================================
   THE CENTERING LAW. Campbell, 2026-07-22, verbatim:
   "All the left-aligned things that aren't alternating or just centered.
    Everything that doesn't have a picture needs to be centered. If there's a
    picture to the right, the word's on the left. If there's a picture on the
    left, the words are on the right."

   An act with a picture is a SPLIT and the picture alternates sides, act by
   act, genuinely, not a repeated skeleton with the treatment swapped, which
   is what the last pass shipped and what he caught. An act with NO picture is
   CENTERED, full stop. Quince does exactly this and it is why its text bands
   read as deliberate rests instead of as a column that ran out of picture.

   Centering is not just text-align: every child that carries its own measure
   has to re-center, or the block reads centered at the top and left-aligned in
   the body, which is worse than either.
   ========================================================================== */
.centered { text-align: center; }
.centered > * { margin-inline: auto; }

/* THE CENTRED-BLOCK LAW, MADE AUTOMATIC (2026-08-02).

   `.centered` has always re-centred its children's boxes, and everything that
   was not literally .centered did not: measured on /reservations/ at 1440, the
   two engraved band labels sat 5.4px left of their own card, because a <p> caps
   at --measure (65ch = 746.7px) inside a 757.4px column and a capped box with
   `margin-inline: 0` stays pinned to the host's left edge while its WORDS
   centre inside it. That is the collapse-to-left trap, and design-precision
   warns it "ships at 1440 exactly as at 390" precisely because nobody looks for
   it on a desktop.

   Naming the offenders would be another enumeration, and this file has now
   watched two of those rot. So the pairing INHERITS: a container that declares
   the centre declares --box-axis with it, and every capped block underneath
   picks up auto margins without knowing who its parent is. Centring the words
   and centring the box are one decision, taken once, at the top.

   The declaration that CONSUMES --box-axis deliberately lives further down,
   immediately after the `p` margin reset. A margin-inline set before a margin
   shorthand is erased by it, silently, which is the same trap that ate a
   background-colour in session 18. The consumer must come after the reset.

   THIS COMMENT WAS BROKEN AND IT TOOK THE RULE BELOW WITH IT (found 2026-08-03).
   It closed itself four lines early, so everything after that point was parsed
   as CSS: the leftover prose became a selector, a backticked margin reset
   inside it became that selector's block, and the real rule below was
   swallowed whole. The data-axis declaration therefore never applied from the
   day it was written, and nothing noticed, because .centered gets its auto
   margins from two other rules. Found by measuring, not by reading: --box-axis
   computed to EMPTY on every host on the site.

   A COMMENT MAY NEVER CONTAIN THE CLOSING SEQUENCE, not even quoted, not even
   inside backticks. Writing this note is how it happened the second time, five
   minutes after fixing the first. Third stray-delimiter fault in this file
   (session 4's brace, session 18's shorthand, this one twice). Check that a
   stylesheet PARSES before believing any rule in it is applying. */
.centered,
.enquire__in,
.book__step,
.blankcard__in,
[data-axis="center"] { --box-axis: auto; }

/* ...and the same idea for the WORDS. These containers author centred copy and
   have since session 5 (the invitation card Campbell asked for by name, the
   blank bar card, the door's live status line, the standing card, the story
   board's pinned tags, the book's service labels). They declare it once, and
   the phone's running-copy rule at the foot of this file defers to them
   instead of flattening every paragraph on the site to the left.

   The enumeration here is of CONTAINERS THAT DECLARE AN AXIS, which is a real
   and small set, not of paragraph class names, which is the list that rots. */
.enquire__in,
.blankcard__in,
.board__pin,
.book__service,
.door__say,
.stand,
.service,
.namecard,
.svc__card,
[data-axis="center"] { --copy-align: center; }
/* .namecard and .svc__card are here because a diff against the shipped build
   caught them too: the dogs' name and breed line on /our-story/ and the events
   card's copy on /services/ were both centred in session 19 and had gone left.
   Home's name cards survived only because `.hang-row .namecard__name` happens
   to carry a second class and outrank the catch-all, which is luck, not law. */
.centered p,
.centered .sub,
.centered .lead,
.centered .measure,
.centered blockquote,
.centered .quote__say { margin-inline: auto; }
.centered .display,
.centered h1, .centered h2, .centered h3 { margin-inline: auto; }
.centered .namecard__traits,
.centered .hero__intents,
.centered .bill__switch,
.centered .social { justify-content: center; }
.centered .link, .centered .btn { margin-inline: auto; }
/* a rule under a centered head is a centered rule, not a left one */
.centered .rule { margin-inline: auto; }
/* the reading measure narrows when centered, centered text over 60ch is a wall */
.centered p { max-width: 58ch; }
.centered .sub { max-width: 46ch; }

/* THE CENTERED ACT's own furniture: an engraved rule that seats the head the
   way a printed card seats a course. Used where an act opens with no picture. */
/* THE RULE CENTRES ON WHATEVER CENTRES ITS CONTENT.

   Its inline margin used to be a flat 0, with a separate `.centered .rule`
   rule putting it back to auto. That is an enumeration, and it died the same
   way every enumeration in this build has died: the events card on /services/
   centres its content through its own component CSS rather than through
   `.centered`, so the selector never reached it and a 148px ornamental
   hairline sat 285px LEFT of centre inside an 836px card, under a centred
   heading. Found by sweeping for rules off their host's axis, not by eye.

   It reads --box-axis now, the same inherited switch every capped block on the
   site already uses, so a host that declares its centre gets a centred rule
   without anyone having to remember to add it to a list. */
.rule {
  width: clamp(56px, 8vw, 96px); height: 1px; border: 0;
  margin: var(--space-6) var(--box-axis, 0);
  background: color-mix(in srgb, var(--ink) 34%, transparent);
}
.on-dusk .rule { background: color-mix(in srgb, var(--paper) 40%, transparent); }

/* ==========================================================================
   THE MOUNT, no photograph on this site is a bare rectangle.
   Their dining room is hung with matted sporting prints; that is the material
   language, so every picture is MOUNTED. The carved gilt .plate stays reserved
   for the heroes (the two dogs, the chef). This is its quiet sibling: a paper
   mat, a fine ruled fillet, and one soft shadow under glass. Cheap enough to
   put on every image on the site, which is exactly the point.
   ========================================================================== */
.mount {
  --mat: clamp(10px, 1.1vw, 18px);
  position: relative; margin: 0;
  padding: var(--mat);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--paper) 88%, #fff) 0%,
      var(--paper-warm) 100%);
  border: 1px solid color-mix(in srgb, var(--ink) 13%, transparent);
  border-radius: var(--r-xs);
  box-shadow:
    0 1px 0 color-mix(in srgb, #fff 70%, transparent) inset,
    0 2px 4px color-mix(in srgb, var(--ink) 8%, transparent),
    0 14px 28px color-mix(in srgb, var(--ink) 11%, transparent);
}
/* the fillet: the fine gold rule a framer cuts against the window */
.mount::before {
  content: ''; position: absolute; inset: calc(var(--mat) - 3px);
  border: 1px solid color-mix(in srgb, var(--gold) 52%, transparent);
  pointer-events: none; z-index: 1;
}
.mount img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  border-radius: 1px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 14%, transparent);
}
.mount figcaption {
  margin-top: var(--space-4); font-size: var(--t-caption);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint);
  text-align: left;
}
.on-dusk .mount {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--paper) 92%, #fff) 0%, var(--paper-warm) 100%);
  border-color: color-mix(in srgb, #000 30%, transparent);
  box-shadow:
    0 1px 0 color-mix(in srgb, #fff 60%, transparent) inset,
    0 18px 40px color-mix(in srgb, #000 42%, transparent);
}
.on-dusk .mount figcaption { color: color-mix(in srgb, var(--paper) 56%, transparent); }

/* ==========================================================================
   THE SHOWPIECE. Campbell, 2026-07-22: "it doesn't look like a stretched all
   the way across image... we can center it framed. And kind of make it a
   little smaller."

   The full-bleed band was the last thing on the site fighting its own material
   language: every photograph here is a mounted print, and a print does not run
   off the edge of the wall. It was also the resolution problem. A 2400px file
   stretched across a 1440 viewport is asking for 2880 physical pixels on a
   retina screen and getting 2400, visibly soft. The same file inside a framed
   column ~1080 CSS wide needs 2160 and has 2400 to spend, so it renders sharp.
   Smaller, framed, centered, and BETTER, the geometry does the work.
   ========================================================================== */
/* 52rem, not 68. At 68rem the frame was 1292px inside a 1346px shell, still
   effectively edge to edge, so it read as the same stretched band with a
   hairline round it. At 52rem there is real paper either side, it is plainly a
   hung picture, and a 2400px file rendering at ~990 CSS px has pixels to spare
   on any screen. */
/* .pane and .mount both set margin: 0 and are declared LATER in this file, so
   a bare .showpiece rule lost the centering at equal specificity and the frame
   sat hard left. Named together, it wins. */
.showpiece, .pane.showpiece, .mount.showpiece { max-width: 52rem; margin-inline: auto; }
.showpiece img { width: 100%; height: auto; aspect-ratio: 2/1; object-fit: cover; }
/* a plate needs depth, not a letterbox slot */
.showpiece--plate img { aspect-ratio: 3/2; }
@media (max-width: 47.99rem) {
  /* on a phone the frame takes the screen, and the crop turns squarer so the
     room still reads instead of becoming a letterbox slot */
  .showpiece img { aspect-ratio: 4/3; }
}

/* a full-width picture that breaks the cadence between acts */
.band { width: 100vw; margin-left: calc(50% - 50vw); overflow: hidden; margin-block: 0; }
.band img { width: 100%; object-fit: cover; aspect-ratio: 21/9; }
.band--tall img { aspect-ratio: 5/2; }
@media (max-width: 47.99rem) {
  .band img { aspect-ratio: 4/3; }
  .band--tall img { aspect-ratio: 4/3; }
}

/* THE CARD TEASE, a change of shape between the two splits. The type is the
   picture here; three engraved columns across the full shell, ruled at the
   top the way a printed card is. */
/* THE MENU CARD ON THE HOME PAGE. Campbell, 2026-07-22: "it looks too much,
   it's just plain text on white... let's frame that in a way that it looks
   unique... spread out and centered in a way that makes people want to click
   the whole menu."

   So it becomes an actual CARD, bone paper, the same mat and gold fillet that
   frames every photograph and the inquiry card, laid on the page. One
   containment language across the whole site: photographs, the invitation, and
   the menu are all mounted the same way. */
.card-tease .tease {
  --mat: clamp(14px, 1.5vw, 24px);
  position: relative; margin-inline: auto; padding: var(--mat);
  background: linear-gradient(180deg, #FCF9F2 0%, var(--paper-warm) 100%);
  border: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  border-radius: var(--r-xs);
  box-shadow:
    0 1px 0 color-mix(in srgb, #fff 70%, transparent) inset,
    0 3px 6px color-mix(in srgb, var(--ink) 8%, transparent),
    0 22px 44px color-mix(in srgb, var(--ink) 13%, transparent);
}
.card-tease .tease::before {
  content: ''; position: absolute; inset: calc(var(--mat) - 3px);
  border: 1px solid color-mix(in srgb, var(--gold) 52%, transparent);
  pointer-events: none;
}
.tease__in { padding: clamp(var(--space-7), 4vw, var(--space-10)) clamp(var(--space-5), 3.5vw, var(--space-9)); }

.card-tease__head {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-3);
  padding-bottom: var(--space-7); margin-bottom: var(--space-8);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 40%, transparent);
}
.card-tease__head .engraved { color: var(--ink-faint); font-size: var(--t-caption); }

/* GRID, not column-count. Three courses were flowing into a balanced two
   columns and leaving the third empty, a third of the card blank. One course
   per column is deterministic and it spreads, which is what he asked for. */
.card-tease .courses { column-count: unset; display: grid; gap: var(--space-8) clamp(var(--space-7), 4vw, var(--space-10)); }
@media (min-width: 48rem) { .card-tease .courses { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 72rem) { .card-tease .courses { grid-template-columns: repeat(3, 1fr); } }
.card-tease .course { margin-bottom: 0; }
.card-tease__go { display: flex; justify-content: center; margin-top: var(--space-9); }

/* THE CLIMB, the staircase owns the height and the type seats into it. */
@media (min-width: 60rem) {
  .climb .split__say { align-self: center; }
  .climb .pane img { max-height: 88svh; }
}

/* ==========================================================================
   3. TYPE ROLES, one treatment each, identical everywhere
   ========================================================================== */
.hero-word, .display, h1, h2, h3 {
  font-family: var(--display); font-weight: 400; text-wrap: balance; margin: 0;
}
/* Cut with air around the letter, the way the sign is. Positive tracking is
   not a flourish here, it is what keeps an inscriptional roman from closing up. */
/* Leading sits AT the 1.0 law floor, not under it. It was .88, which made the
   line box 12% shorter than the glyphs, measured 14px of overflow at 390 and
   46px at 1440, and the wall blocked on it (correctly: a box that does not
   contain its own letters is a box waiting to clip them the first time an
   ancestor turns on overflow). The strike is unchanged: the word is pulled back
   up by exactly the 0.06em the taller box added, so the frame's edge still cuts
   the caps at the same place it did, measured 38.6% before, 38.6% after. */
.hero-word {
  font-size: var(--t-hero);
  line-height: 1; letter-spacing: .004em;
}
.display {
  font-size: var(--t-display);
  line-height: 1.06; letter-spacing: .008em;
}
h2, .h2 {
  font-size: var(--t-h2);
  line-height: 1.12; letter-spacing: .012em;
}
h3, .h3 {
  font-size: var(--t-h3);
  line-height: 1.2; letter-spacing: .016em;
}

/* THE ENGRAVED LABEL, small caps, wide, ruled. This is the register of
   "BISTRO & BAR" on their glass, and it is how a section announces itself
   here. Not an eyebrow: it is the heading, at the heading's own weight. */
.engraved {
  font-family: var(--display-sc); font-weight: 400;
  letter-spacing: .22em; text-transform: uppercase;
  line-height: 1.3; margin: 0;
}

/* The deck belongs to the TEXT face. It used to be set in the display face at
   ~18px, where a lapidary roman goes spindly and reads as a weaker heading
   instead of a stronger sentence. */
.sub {
  font-family: var(--text); font-weight: 400;
  font-size: var(--t-sub);
  line-height: 1.45; letter-spacing: -.004em;
  color: var(--ink-soft); max-width: 34ch; margin: 0;
}
p { margin: 0; max-width: var(--measure); text-wrap: pretty; }
/* ...and the consumer of --box-axis, declared AFTER that `margin: 0` so the
   shorthand cannot erase it. See THE CENTRED-BLOCK LAW above: a container that
   centres its words hands every capped block beneath it an auto inline margin,
   so the box comes home with the text instead of staying pinned left. */
p, .sub, .lead, .fine, figcaption, blockquote, .points { margin-inline: var(--box-axis, 0); }
.lead { font-size: var(--t-lead); color: var(--ink-soft); }
.fine { font-size: var(--t-label); color: var(--ink-faint); line-height: 1.45; }

/* Labels are UI only, never a section eyebrow. The laws forbid eyebrows. */
.label { font-size: var(--t-label); font-weight: 600; letter-spacing: .12em; text-transform: uppercase; line-height: 1.2; color: var(--ink-faint); }
.num { font-variant-numeric: tabular-nums lining-nums; }

/* ==========================================================================
   4. THE LIVING GROUND, the room's own light and material, not a flat swatch.

   The old ground was bone paper with 4% noise on it: a page, not a room. This
   is the light and the cloth, and it is the single biggest reason the site
   stopped reading like a template. Every layer is a whisper; content always
   wins. Nothing here is ever allowed to compete with a word.

     · THE LIGHT, spring sun through the front glass, pooled warm and high and
       falling off toward the floor, so the paper has a top and a bottom instead
       of one flat fill. This is what turns a cream page into a lit room, and it
       is Campbell's feeling for this build in one gradient: jazz on a sunny
       spring day.
     · THE CLOTH, the hand-blocked botanical from Wesley's own tables, printed
       as a real repeating field at a whisper, with a press mis-register (a hair
       of deeper sage offset beneath it) the way a carved block actually prints.
       It was drawn for this build (assets/img/blockprint.svg) and until now only
       lived under the menu; here it is the material of the whole room.
     · THE GRAIN, paper tooth, kept, dialed to sit under the light.

   Fixed on purpose: it is the room the page travels through. The dusk strata
   are opaque and slide up over it, which IS the climb, daylight held below,
   lamplight above. The paper never has to remember which floor it is on; the
   light does. */
.ground { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
/* THE LIGHT lives on the element, so the two masked cloth layers can sit above
   it and still read as printed ON the lit paper. */
.ground::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(135% 105% at 15% -14%,
      color-mix(in srgb, var(--rattan-lit) 26%, transparent) 0%,
      color-mix(in srgb, var(--rattan-lit) 8%,  transparent) 23%,
      transparent 51%),
    radial-gradient(120% 85% at 97% -8%,
      color-mix(in srgb, var(--sage) 13%, transparent) 0%,
      transparent 45%),
    linear-gradient(to bottom,
      transparent 50%,
      color-mix(in srgb, var(--paper-deep) 44%, transparent) 100%);
}
/* THE CLOTH, masked so the SVG's currentColor becomes our own sage, plus a
   second copy offset a few px for the printer's mis-register. */
.ground__field, .ground__field::before {
  content: ''; position: absolute; inset: -80px;
  -webkit-mask: url("/assets/img/blockprint.svg") 0 0 / clamp(200px, 21vw, 300px) auto repeat;
          mask: url("/assets/img/blockprint.svg") 0 0 / clamp(200px, 21vw, 300px) auto repeat;
}
.ground__field   { background: var(--sage);      opacity: .08; }
.ground__field::before { background: var(--sage-deep); opacity: .5;
  -webkit-mask-position: 10px 8px; mask-position: 10px 8px; }
/* THE GRAIN, paper tooth. */
.ground::after {
  content: ''; position: absolute; inset: 0;
  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 baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: multiply; opacity: .05;
}
/* the light warms as the visitor descends into the meal and climbs toward the
   bar: a scroll-driven deepening, wired by grammar's scrub on <html>. Whisper
   only, and gated on the still path. */
@media (prefers-reduced-motion: no-preference) {
  .ground__field { transition: opacity 1.2s var(--ease-standard); }
}

/* ==========================================================================
   4b. THE ELEVATION INDEX. CONCEPT.md §4, the signature's edge marker
   ==========================================================================
   Two floor NAMES on the edge, the one you are standing on in ink and the other
   ghosted. Foster's is ABOVE Lou Lou's because it is the floor above: the marker
   is a section through the building, not a progress bar. No numbers, ever.

   It reads bottom-up on the left edge the way a spine does. The car travels the
   rule between the two names, that is the only moving part, and its whole job is
   to say which way is up.

   Suppressed under 1000px ON PURPOSE, and this is an authored decision rather
   than a phone afterthought: at 390px the stratum fills the entire screen, so
   the ground itself already reports the floor far louder than an edge label
   could, and the edge is thumb territory. Adding it there would be chrome
   competing with the very thing it describes. */
.elev {
  position: fixed; left: 0; top: 50%; transform: translateY(-50%);
  z-index: 30; display: none;
  flex-direction: column; align-items: center; gap: var(--space-4);
  padding: var(--space-5) var(--space-4);
  pointer-events: auto;
}
@media (min-width: 1000px) { .elev { display: flex; } }

/* g28-ui-label: these are the two FLOOR NAMES on a navigational marker, not an
   eyebrow sitting above a heading. Inscriptional capitals with positive tracking
   are the CONCEPT.md §1 rule for this face, and a vertical spine label is the one
   place the tracking is doing structural work rather than decorating a title.
   (Noted while marking this: test/laws.test.mjs SKIPs `assets`, so site.css has
   never actually been scanned by that law, only web/admin/admin.css is. Marked
   here because it is correct, not because a gate demanded it.) */
.elev__floor {
  writing-mode: vertical-rl; transform: rotate(180deg);
  font-family: var(--display-sc, var(--display));
  font-size: .68rem; letter-spacing: .3em; text-transform: uppercase;
  text-decoration: none; white-space: nowrap;
  /* GHOSTED, the floor you are not standing on.
     The first version did this with opacity: .26, and the wall was right to
     catch it: that measured 1:1, which is not a label, it is decoration
     pretending to be one. A navigational control a visitor cannot read is
     broken however good it looks. The ghost is now carried by the INK RAMP
     instead, --ink-faint is 4.7:1 on paper, so the inactive floor stays
     legible while the active one still clearly wins on weight of colour. */
  color: var(--ink-faint);
  transition: color var(--dur-section) var(--ease-standard);
}
/* THE NAMES SAY OPEN OR SHUT. THE CAR SAYS WHERE YOU ARE.

   They used to say both: the name went to full ink when you SCROLLED to that
   floor, and a red dot appeared under it when the floor was OPEN, and the car
   was a second red dot. Three marks, two meanings, overlapping. Campbell:
   "the red dot shows on Lou Lou's, moves to Foster's, but then the red dot
   also stays on Lou Lou's while it's open. That just seems confusing."

   One mark, one meaning. The car on the shaft is the only thing that reports
   altitude, which is what a car in a lift shaft is for. The two names report
   one thing only: whether you could walk into that floor right now. So a floor
   is printed when it is open and ghosted when it is shut, and that never
   changes as you scroll. (The lit colours live in house.css beside the clock
   that sets them.) */

.elev__floor:focus-visible { outline: 2px solid var(--tolix); outline-offset: 4px; }

/* the shaft. a hairline with the car riding it. */
.elev__rule {
  position: relative; width: 1px; height: clamp(56px, 9vh, 96px);
  background: color-mix(in srgb, var(--ink) 22%, transparent);
}
/* THE CAR IS POSITION, AND NOTHING ELSE.

   It was a round red dot, and so was the "this floor is open right now" mark
   under a floor's name, so the page could show three identical red dots
   meaning two different things at once. Campbell: "the red dot shows on Lou
   Lou's, moves to Foster's, but then the red dot also stays on Lou Lou's while
   it's open. That just seems confusing." He is right: one mark, one meaning.

   The car keeps the shaft and loses the accent. A short BAR riding a hairline
   is what a car in a lift shaft looks like; a circle was only ever a dot. The
   red goes back to being the seal on a full stop, which is the one job it has
   everywhere else on this site. */
.elev__car {
  position: absolute; left: 50%; width: 3px; height: 15px; border-radius: 2px;
  background: color-mix(in srgb, var(--ink) 58%, transparent);
  margin-left: -1.5px; top: 100%; margin-top: -15px;
  transition: top var(--dur-section) var(--ease-standard),
              margin-top var(--dur-section) var(--ease-standard);
}
.elev[data-floor="bar"] .elev__car { top: 0; margin-top: 0; }

/* LEGIBILITY COMES FROM THE GROUND, NOT FROM THE FLOOR.

   These colours used to hang off data-floor, which says which floor you are
   ON. That is a different question from what is painted behind a FIXED rail,
   and the difference is what Campbell saw: scrolled past the dusk stratum with
   data-floor still "bar", the active name was pure --paper on bone paper and
   simply disappeared, while the inactive one survived as a paper/dusk mid
   tone. One floor vanished and the other did not, every time.

   site.js now sets data-ground from the strata the rail is actually over, so
   the ink ramp answers the background and data-floor is left to do its own
   job: saying which of the two is active, by weight. */
.elev[data-ground="dark"] .elev__floor { color: color-mix(in srgb, var(--paper) 52%, transparent); }
.elev[data-ground="dark"] .elev__rule { background: color-mix(in srgb, var(--paper) 26%, transparent); }
.elev[data-ground="dark"] .elev__car  { background: color-mix(in srgb, var(--paper) 66%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .elev__floor, .elev__car { transition: none; }
}

/* ==========================================================================
   5. THE RAIL, navigation
   ========================================================================== */
.rail {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--space-6);
  padding: var(--space-4) var(--edge);
  padding-top: max(var(--space-4), env(safe-area-inset-top));
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-section) var(--ease-standard);
}
.rail[data-scrolled="true"] { border-bottom-color: var(--line-soft); }
.rail__mark { display: block; flex: none; line-height: 0; }
.rail__mark img { width: clamp(124px, 13vw, 186px); height: auto; }
.rail__nav { display: none; gap: var(--space-6); margin-left: auto; align-items: center; }
@media (min-width: 60rem) { .rail__nav { display: flex; } }
.rail__nav a {
  font-size: var(--t-label); font-weight: 500;
  letter-spacing: .11em; text-transform: uppercase;
  text-decoration: none; color: var(--ink-soft);
  position: relative; padding-block: var(--space-2);
  transition: color var(--dur-element) var(--ease-standard);
}
.rail__nav a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 0;
  height: 1px; background: var(--ink);
  transition: right var(--dur-section) var(--ease-expressive);
}
.rail__nav a:hover { color: var(--ink); }
.rail__nav a:hover::after, .rail__nav a[aria-current="page"]::after { right: 0; }

.rail__book {
  font-family: var(--text); font-size: var(--t-label); font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase; text-decoration: none;
  display: inline-flex; align-items: center; min-height: 44px;
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--ink); color: var(--ink);
  border-radius: var(--r-pill); white-space: nowrap;
  transition: background var(--dur-element) var(--ease-standard),
              color var(--dur-element) var(--ease-standard),
              transform var(--dur-element) var(--ease-standard);
}
.rail__book:hover { background: var(--ink); color: var(--paper); }

.rail__burger { margin-left: auto; display: grid; place-items: center; width: 48px; height: 48px; background: none; border: 0; cursor: pointer; color: var(--ink); }
@media (min-width: 60rem) { .rail__burger { display: none; } }
.rail__burger span { display: block; width: 22px; height: 1px; background: currentColor; transition: transform var(--dur-section) var(--ease-expressive), opacity var(--dur-micro); }
.rail__burger span + span { margin-top: 6px; }
.rail__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.rail__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.rail__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.drawer {
  position: fixed; inset: 0; z-index: 39; background: var(--paper);
  padding: calc(env(safe-area-inset-top) + 92px) var(--edge) var(--space-8);
  display: flex; flex-direction: column; gap: 0;
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity var(--dur-section) var(--ease-standard),
              transform var(--dur-section) var(--ease-expressive),
              visibility var(--dur-section);
}
.drawer[data-open="true"] { opacity: 1; visibility: visible; transform: none; }
.drawer a {
  font-family: var(--display); font-size: var(--ms-3);
  letter-spacing: .014em;
  text-decoration: none; padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line-soft);
}
/* The drawer's booking control was a small left-hugging link pretending to be
   a button. In a full-screen menu it is the ONE thing the visitor came for, so
   it gets real size and sits on the drawer's own axis. */
.drawer .rail__book {
  margin-top: var(--space-9); align-self: stretch; width: 100%;
  justify-content: center; text-align: center;
  font-family: var(--text); font-size: var(--ms-0); letter-spacing: .14em;
  min-height: 62px; padding-inline: var(--space-7);
  background: var(--ink); color: var(--paper); border: 1px solid var(--ink);
  border-radius: var(--r-pill); border-bottom: 1px solid var(--ink);
}
.drawer .rail__book:hover { background: transparent; color: var(--ink); }

/* ==========================================================================
   6. THE THRESHOLD, the hero as ONE object, not two blocks

   What was here: a photograph bleeding to the screen edge in one half and a
   column of text in the other. That is the literal shape of a Squarespace
   image-and-text section block, and it is why Campbell kept reading the whole
   site as a template no matter how authored the acts below it were. It was also
   the ONE bare rectangle left in a build whose entire material language is
   "every photograph is a mounted print on a wall."

   What it is now: the print is HUNG, and the word is STRUCK ACROSS ITS BOTTOM
   EDGE, so the frame's own rule cuts through the letters. One composition on
   one plane, and a broken plane, nothing beside anything, nothing stacked.
   Overlap is the move a page builder cannot make, which is exactly why it reads
   as authored the instant you land.

   It fixes the blur by geometry too, the same lesson the bands taught in
   session 6: dogs-window.jpg is 845x1500 and was being stretched across a 47vw
   column (677 CSS px, upscaled on every retina phone and laptop). Inside a
   mount it renders at ~340px on a phone and ~440px on a desktop, native or
   under, with pixels to spare.
   ========================================================================== */
.hero { position: relative; padding: var(--space-6) 0 var(--space-8); }

/* THE OVERLAP, in one token so the two widths cannot drift apart. It is a
   fraction of the word's own size, so the frame edge always cuts the letters at
   the same place in the cap height no matter how the clamp resolves. */
/* THE WEIGHTED MAT. A framer cuts the bottom of a mat deeper than the other
   three sides, the eye reads an even mat as bottom-heavy, so the bottom is
   given extra. It is the single most recognisable signal of a properly framed
   print versus a photo in a store-bought frame, and here it earns a second job:
   it is the band the word is struck into. The upper half of the letters lands
   on bone mat (legible, high contrast) and the lower half on the page's own
   paper, with the frame's border and its gold fillet ruling straight through
   the middle of the word. Ink on a dark photograph would have been the obvious
   overlap and it would have been unreadable; this one is both the bolder
   composition and the safer one. */
.hero {
  --strike: calc(var(--t-hero) * 0.381);  /* where the frame's edge cuts the caps.
       0.34 while the leading was .88. The leading is now a full 1.0, so the
       half-leading pushed the glyphs down and the word is pulled back up to
       land in the same place: measured cut 38.6% before, 38.5% after. The cut
       position is the thing held constant, never the number. */
}

.hero__see { position: relative; z-index: 1; width: var(--shell); margin-inline: auto; }
.hero__frame {
  --mat: clamp(16px, 2.2vw, 26px);   /* the hero print gets the deepest mat on the site */
  --mat-foot: calc(var(--t-hero) * 0.62);
  margin-inline: auto; width: min(100%, 21rem);
  padding: var(--mat) var(--mat) var(--mat-foot);
  contain: paint;      /* the drift keyframe scales the picture; keep it off the scroll area */
}
/* the gold fillet is ruled around the WINDOW, so it follows the weighted mat
   down instead of floating as a rectangle across the photograph */
.hero__frame::before {
  inset: calc(var(--mat) - 3px) calc(var(--mat) - 3px) calc(var(--mat-foot) - 3px);
}
/* the title cut into the mat, gallery style. On the phone the word occupies
   this band, so the engraving only appears where there is room for it. */
.hero__frame figcaption {
  position: absolute; left: var(--mat); right: var(--mat);
  bottom: calc((var(--mat-foot) - 1em) / 2);
  margin: 0; text-align: center;
  font-family: var(--display-sc); font-size: var(--t-caption);
  letter-spacing: .3em; text-transform: uppercase; color: var(--ink-faint);
  display: none;
}
/* THE CROP. The old square at object-position 62% 38% put Foster's back across
   two thirds of the first screen and cut Lou Lou in half at the left edge, a
   photograph of fur. Both dogs' heads live in the top 60% of this frame, so the
   phone gets a portrait window anchored high and you land on two faces at a
   window, which is the whole reason this picture is the hero. */
/* THE WINDOW. The picture drifts as the reader leaves it (scroll-linked, the
   reader's own tempo) and that keyframe scales it 1.07, which inside a mat
   meant the photograph grew straight over its own bone border and swallowed the
   gold fillet, so the ruled line read as a rectangle floating ON the picture.
   overflow:hidden on the figure would clip at the FRAME edge, not the mat's
   opening, so the opening gets its own element. This is what a mat actually is:
   a window cut in board, and the print moves behind it. */
.hero__win {
  display: block; overflow: hidden; border-radius: 1px; position: relative; z-index: 0;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 14%, transparent);
}
.hero__frame img { width: 100%; aspect-ratio: 5/6; object-fit: cover; object-position: 50% 5%; box-shadow: none; }

.hero__say {
  position: relative; z-index: 2;
  width: var(--shell); margin-inline: auto;
  display: flex; flex-direction: column; gap: var(--space-5);
  align-items: flex-start;
}
.hero__word .hero-word { margin-left: -.055em; }  /* optical: hang the left edge */
.hero__word { margin-top: calc(var(--strike) * -1); position: relative; }

@media (min-width: 60rem) {
  /* THE AXIS IS STILL LEFT. Campbell's composition stands: the words hold the
     left, the picture hangs right. What changed is that the picture is a hung
     print instead of a bleeding rectangle, and the word crosses its edge. */
  .hero {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 27rem);
    align-items: center; gap: var(--gutter);
    width: var(--shell); margin-inline: auto;
    min-height: min(92svh, 940px); padding-block: var(--space-9);
  }
  .hero__see { width: auto; grid-column: 2; grid-row: 1; margin: 0; }
  .hero__say { width: auto; grid-column: 1; grid-row: 1; padding-right: 0; }
  .hero__frame { width: 100%; max-width: 27rem; --mat-foot: calc(var(--mat) * 2.6); }
  .hero__frame .hero__win img { aspect-ratio: 4/5; }
  /* the weighted mat holds the engraving here, because the word is beside the
     frame rather than struck through it, one frame, the same material, the
     composition authored per width instead of collapsed from one to the other */
  .hero__frame figcaption { display: block; }
  .hero__word { margin-top: 0; }
}

.hero__intents { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-2); }

/* ==========================================================================
   7. BUTTONS
   ========================================================================== */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: 48px; padding: var(--space-3) var(--space-6);
  font-size: var(--t-label); font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase; text-decoration: none;
  border-radius: var(--r-pill); border: 1px solid var(--ink);
  background: var(--ink); color: var(--paper); cursor: pointer;
  font-family: var(--text);
  transition: transform var(--dur-element) var(--ease-standard),
              box-shadow var(--dur-element) var(--ease-standard),
              background var(--dur-element) var(--ease-standard),
              color var(--dur-element) var(--ease-standard);
}
/* the lift is gone: The Pressed owns the transform now (lean to the hand,
   compress under it). The hover keeps only its material change. */
.btn:hover { box-shadow: var(--e2); }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { background: var(--ink); color: var(--paper); }
.btn--accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn--accent:hover { background: var(--tolix-deep); border-color: var(--tolix-deep); }

.link {
  display: inline-block; text-decoration: none;
  font-size: var(--t-label); font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase;
  padding-bottom: var(--space-2); position: relative;
}
.link::after { content: ''; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%; background: var(--line); }
.link::before {
  content: ''; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: var(--ink); z-index: 1;
  transition: width var(--dur-cinematic) var(--ease-expressive);
}
.link:hover::before { width: 100%; }

/* ==========================================================================
   8. THE LIVING LINE, today, computed, never a page someone must update
   ========================================================================== */
.today {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding-top: var(--space-5); border-top: 1px solid var(--line-soft);
  font-size: var(--t-label); color: var(--ink-soft); letter-spacing: .02em;
  width: 100%; max-width: 46ch;
}
.today__dot { width: 6px; height: 6px; border-radius: var(--r-full); background: var(--ink-faint); flex: none; transform: translateY(-2px); }
.today__dot[data-open="true"] { background: var(--sage-deep); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sage) 34%, transparent); }
.today__dot[data-open="soon"] { background: var(--rattan); }

/* ==========================================================================
   9. SECTIONS + THE MOUNTED PRINT
   ========================================================================== */
.act { padding-block: var(--section-y); position: relative; }
.act--tight { padding-block: calc(var(--section-y) * .6); }

/* A picture presented as a mounted print: mat, hairline, one light source.
   Their walls are hung with framed sporting art, so a photograph here is
   hung the same way. This is the site's material idea. */
/* THE GILT FRAME.
   Their walls carry real gilt-framed sporting prints, so a photograph here is
   built like one and not like a card with a border:

     · a carved gold MOULDING, its highlight running along the top-left where
       the room's light falls and its shadow along the bottom-right, so the
       profile reads as rounded rather than printed;
     · a fine dark rebate inside the moulding, the way gilt is always laid
       over a darker bole;
     · a deep cream MAT with a gold fillet ruled around the opening;
     · the picture sat down inside, with its own inner shadow so it sits
       BEHIND the glass rather than on top of the frame;
     · and a slow specular SHIMMER that travels the gold, because gilding is
       the one material in that room that answers the light back.

   Gold is a gradient, never a flat swatch. A flat gold is a border; a graded
   one is a moulding. */
/* the hook: the frame plus its wall label. The label hangs OUTSIDE the
   moulding, the way a gallery cards a print. */
.hung { margin: 0; }
.plate {
  display: block;
  --gilt-bole:  #4A2F12;   /* the red-brown clay gilding is laid over */
  --gilt-dark:  #7A551F;
  --gilt-mid:   #BE964F;
  --gilt-lit:   #E8CB8B;
  --gilt-white: #FFF6DD;
  position: relative; margin: 0; isolation: isolate;
  padding: clamp(14px, 1.7vw, 32px);        /* a real moulding has heft */
  border-radius: 2px;
  /* THE CARVED PROFILE, read across the moulding: an outer lip that catches
     the light, a hollow that falls into shadow, a rounded rise, then a step
     down to the mat. Five bands, not a flat swatch, that is the whole
     difference between a moulding and a border. */
  background:
    linear-gradient(138deg,
      var(--gilt-mid)   0%,
      var(--gilt-white) 4%,
      var(--gilt-lit)   9%,
      var(--gilt-dark)  19%,
      var(--gilt-bole)  27%,
      var(--gilt-mid)   38%,
      var(--gilt-white) 47%,
      var(--gilt-mid)   57%,
      var(--gilt-dark)  71%,
      var(--gilt-bole)  80%,
      var(--gilt-lit)   92%,
      var(--gilt-dark) 100%);
  box-shadow:
    /* the frame stands off the wall */
    0 2px 3px color-mix(in srgb, var(--ink) 26%, transparent),
    0 18px 34px color-mix(in srgb, var(--ink) 20%, transparent),
    0 40px 70px color-mix(in srgb, var(--ink) 10%, transparent),
    /* the outer lip */
    inset 0 1px 0 color-mix(in srgb, #fff 66%, transparent),
    inset 1px 0 0 color-mix(in srgb, #fff 40%, transparent),
    inset 0 -2px 1px color-mix(in srgb, var(--gilt-bole) 78%, transparent),
    inset -2px 0 1px color-mix(in srgb, var(--gilt-bole) 58%, transparent);
}
/* the mat, deep, with a gold fillet ruled around its opening */
.plate__mat {
  position: relative; display: block;
  padding: clamp(14px, 2.4vw, 46px);
  background:
    linear-gradient(160deg,
      color-mix(in srgb, #fff 34%, var(--paper-warm)),
      var(--paper-warm) 56%,
      color-mix(in srgb, var(--paper-deep) 70%, var(--paper-warm)));
  box-shadow:
    /* the mat sits down inside the rebate */
    inset 0 2px 5px color-mix(in srgb, var(--gilt-bole) 44%, transparent),
    inset 0 0 0 1px color-mix(in srgb, var(--gilt-bole) 34%, transparent);
}
.plate__mat::after {
  content: ''; position: absolute;
  inset: clamp(8px, 1.15vw, 22px);
  border: 1px solid color-mix(in srgb, var(--gilt-mid) 88%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, #fff 62%, transparent),
              inset 0 0 0 1px color-mix(in srgb, #fff 40%, transparent);
  pointer-events: none;
}
/* the picture sits down inside, under glass */
.plate__mat img { box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 16%, transparent),
                              inset 0 2px 6px color-mix(in srgb, var(--ink) 20%, transparent); }
/* the shimmer: a narrow specular band that crosses the gilding as the frame
   travels up the screen. Scroll-linked, so it is the reader's own pace. */
.plate::before {
  content: ''; position: absolute; inset: 0; z-index: 2;
  border-radius: 2px; pointer-events: none;
  background: linear-gradient(105deg,
    transparent 34%,
    color-mix(in srgb, #FFF6DF 62%, transparent) 47%,
    color-mix(in srgb, #FFFDF4 78%, transparent) 50%,
    color-mix(in srgb, #FFF6DF 62%, transparent) 53%,
    transparent 66%);
  -webkit-mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000);
          mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000);
  -webkit-mask-clip: padding-box, border-box;
          mask-clip: padding-box, border-box;
  -webkit-mask-composite: xor; mask-composite: exclude;   /* the moulding only */
  opacity: 0; background-size: 260% 100%; background-position: 100% 0;
}
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .plate::before {
      opacity: 1; animation: gilt linear both;
      animation-timeline: view(); animation-range: entry 10% cover 78%;
    }
    @keyframes gilt {
      from { background-position: 116% 0; }
      to   { background-position: -16% 0; }
    }
  }
}
.plate img { width: 100%; height: 100%; object-fit: cover; max-height: 74svh; }
.plate--tall   img { aspect-ratio: 4/5; }
/* THE CHEF'S FRAME matches his photograph exactly (9:10), so the mount crops
   NOTHING. The source is 720x800 and both plates are already clipped by its
   own edges, the left plate at the left edge, the right plate at the bottom.
   Any frame with a different aspect eats more of them. Campbell's call was to
   keep the real chef photograph, cut head and all, and make sure both hands
   and both plates read; the only way to honor that is to show the entire
   frame and stop cropping. Upscaled 4x and brought back down to 1440x1600 so
   it stays sharp on a 2x screen. */
.plate--chef   img { aspect-ratio: 9/10; }
.plate--wide   img { aspect-ratio: 3/2; }
.plate--square img { aspect-ratio: 1/1; }
.plate--port   img { aspect-ratio: 4/5; }
/* the label under a hung picture, the way a gallery captions a print */
.hung figcaption {
  margin-top: var(--space-4); font-size: var(--t-caption);
  letter-spacing: .06em; color: var(--ink-faint); line-height: 1.4;
  text-transform: uppercase; font-weight: 500;
}
.on-dusk .hung figcaption { color: color-mix(in srgb, #F4F1E9 54%, transparent); }

/* an unframed picture that runs to the edge, used when the room should
   surround you rather than hang on a wall */
.pane { overflow: hidden; border-radius: var(--r-sm); margin: 0; }
.pane img { width: 100%; height: 100%; object-fit: cover; max-height: 80svh; }
.pane--wide   img { aspect-ratio: 3/2; }
.pane--tall   img { aspect-ratio: 4/5; }
.pane--cinema img { aspect-ratio: 2/1; }
@media (max-width: 47.99rem) { .pane--cinema img { aspect-ratio: 4/3; } .pane--tall img { aspect-ratio: 3/4; } }

/* ==========================================================================
   10. THE NAME CARD. Lou Lou / Foster. LEFT axis, never centered.
   Their own printed dog cards set the name in an italic display serif with a
   rule beneath and the traits run on below. This is that card, in live type.
   ========================================================================== */
.namecard { max-width: 48ch; }
.namecard__name {
  font-family: var(--display);
  font-size: var(--t-h2);
  letter-spacing: .012em; line-height: 1.05; margin: 0;
}
.namecard__rule { height: 1px; background: var(--ink); opacity: .3; margin: var(--space-4) 0 var(--space-3); }
.namecard__who { font-size: var(--t-label); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); }
.namecard__traits {
  margin: var(--space-5) 0 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
  font-family: var(--text); font-weight: 400;
  font-size: 1.0625rem;
  line-height: 1.5; color: var(--ink-soft);
}
/* THE SEPARATOR LEADS ITS OWN ITEM, and is part of it.

   Session 4 found a naked interpunct STARTING wrapped lines, because the dot
   was an absolutely positioned ::before on the next item and had detached from
   every word. The fix put it at the END of the preceding item instead, which
   simply moved the orphan to the other end: a dot left DANGLING at the end of
   a wrapped line, which is what Lou Lou's card was doing in a narrow column.
   Third time this build has shipped a stranded separator (the interpunct in
   session 4, the gold dot in 13, the sentence punctuation in 16).

   A dot cannot be orphaned when it is the first glyph of the item it
   introduces and is laid out in normal flow with it: on one line it reads
   "bossy · very charming", and across a wrap the next line simply opens
   "· very charming", which is correct for a run-on list either way.

   THE FOURTH FIX, AND THE LAST ONE (2026-08-02). That reasoning was wrong, and
   Campbell caught what it produced: the leading dot does not strand, but it
   RAGS THE LEFT EDGE. Measured at 1440 on Lou Lou's card, line one opens on
   "A" at x=49 while lines two and three open on "·" at x=49 with their first
   word at x=80. Three rows, two different left edges, which is exactly the
   "items that sat left and issues with how things looked" he named. It is not
   hanging punctuation either (that would put the dot OUTSIDE the column and
   align every word); it is simply a ragged column.

   Four sessions have now moved this separator from one end of the line to the
   other (4, 13, 16, 18) and each move produced the same defect facing the
   other way, because CSS cannot say "between items on the same line". So the
   separator is DELETED and the gap does the separating, at every width, which
   is what session 19 had already concluded for the phone. One rule instead of
   two, and there is no longer anything that can strand or rag. The run-on
   reading survives: the items still flow inline, they are just spaced rather
   than punctuated. */
.namecard__traits li { min-width: 0; }
.namecard__traits { column-gap: var(--space-6); }

/* ==========================================================================
   THE POINTS LIST, for the services page, where the items are phrases and
   not adjectives. Run-on interpuncts were leaving a dot dangling at the end of
   every wrapped line, which reads as a typo rather than as a list. A phrase
   list gets a real list: one per line, a gold tick on the rule, no separator
   to strand. Short adjective runs (the dogs) keep the run-on above.
   ========================================================================== */
.points { list-style: none; margin: var(--space-6) 0 0; padding: 0; max-width: 46ch; }
.points li {
  position: relative; padding: var(--space-3) 0 var(--space-3) var(--space-6);
  border-top: 1px solid var(--line-soft);
  font-size: 1.0625rem; line-height: 1.5; color: var(--ink-soft);
}
.points li:last-child { border-bottom: 1px solid var(--line-soft); }
.points li::before {
  content: ''; position: absolute; left: 0; top: 1.35em;
  width: 14px; height: 1px; background: var(--gold);
}
.on-dusk .points li { border-color: color-mix(in srgb, var(--paper) 14%, transparent); color: color-mix(in srgb, var(--paper) 78%, transparent); }
/* centered variant: the tick would strand on the left, so it becomes a rule */
.centered .points li { padding-left: 0; }
.centered .points li::before { display: none; }
.on-dusk .namecard__rule { background: var(--paper); opacity: .3; }

/* ==========================================================================
   11. UPSTAIRS, the dusk stratum
   ========================================================================== */
.on-dusk {
  background: var(--dusk); color: var(--paper);
  --ink: #F4F1E9;
  --ink-soft: color-mix(in srgb, #F4F1E9 76%, transparent);
  --ink-faint: color-mix(in srgb, #F4F1E9 54%, transparent);
  --line: color-mix(in srgb, #F4F1E9 18%, transparent);
  --line-soft: color-mix(in srgb, #F4F1E9 11%, transparent);
  --surface: var(--dusk); --surface-raised: var(--dusk-deep);
  --paper-warm: var(--dusk-deep);
}
.on-dusk .btn { background: var(--paper); color: var(--dusk); border-color: var(--paper); }
.on-dusk .btn--ghost { background: transparent; color: var(--paper); }
.on-dusk .btn--ghost:hover { background: var(--paper); color: var(--dusk); }


/* the seam between paper and dusk: light leaves the room gradually, the way
   it does on a staircase */
.seam { height: clamp(72px, 11vh, 180px); background: linear-gradient(to bottom, var(--paper), var(--dusk)); }
.seam--up { background: linear-gradient(to bottom, var(--dusk), var(--paper)); }

/* ==========================================================================
   12. THE MENU, the engraved card, in live semantic text
   --------------------------------------------------------------------------
   Via Carota prints a Victorian trade card and photographs it: dense, ruled,
   authoritative, and completely dead to a screen reader or a search engine.
   This is that density and that authority built from real text, leader dots
   drawn in CSS, tabular prices on a hard right rail, small-caps course heads
   on a hairline, and authored column breaks so no column ends in a hole.
   Density is the class. Airy is the Squarespace default.
   ========================================================================== */
.bill { position: relative; }
.bill__cloth {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: var(--sage); opacity: .15;   /* a whisper, not wallpaper */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 5%, #000 95%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 5%, #000 95%, transparent);
}
.bill > * { position: relative; z-index: 1; }

.courses { column-gap: clamp(var(--space-7), 5vw, var(--space-10)); column-count: 1; }
@media (min-width: 48rem) { .courses { column-count: 2; } }
@media (min-width: 82rem) { .courses { column-count: 3; } }

.course { break-inside: avoid; margin-bottom: var(--space-8); }

/* A SINGLE-COURSE MENU (brunch) was flowing into column one and leaving two of
   three columns empty for the whole height of the page, a course is
   break-inside: avoid, so one course can only ever occupy one column. When
   there is just the one, let it break and hold it to two centered columns so it
   reads as a card instead of a stranded list. */
.bill__panel[data-courses="1"] .courses { column-count: 2; max-width: 62rem; margin-inline: auto; }
.bill__panel[data-courses="1"] .course { break-inside: auto; }
@media (max-width: 48rem) { .bill__panel[data-courses="1"] .courses { column-count: 1; } }
.course__name {
  display: flex; align-items: baseline; gap: var(--space-3);
  font-family: var(--display-sc); font-size: var(--ms-1);
  letter-spacing: .16em; text-transform: uppercase; margin: 0 0 var(--space-4);
}
.course__name::after {
  content: ''; flex: 1 1 auto; height: 1px;
  background: color-mix(in srgb, var(--ink) 30%, transparent);
  transform: translateY(-.24em);
}
/* Add-on prices. A number somebody is charged is functional text, never a whisper. */
.course__note { display: block; font-size: var(--t-caption); letter-spacing: .04em; text-transform: none; font-family: var(--text); color: var(--ink-soft); margin: calc(var(--space-4) * -1) 0 var(--space-4); }

.dish { margin: 0 0 var(--space-4); break-inside: avoid; }
.dish__line { display: flex; align-items: baseline; gap: var(--space-2); font-size: 1rem; font-weight: 500; line-height: 1.3; }
.dish__name { flex: 0 1 auto; }
.dish__lead { flex: 1 1 auto; min-width: var(--space-5); transform: translateY(-.22em); border-bottom: 1px dotted color-mix(in srgb, var(--ink) 36%, transparent); }
.dish__price { flex: none; font-variant-numeric: tabular-nums lining-nums; font-weight: 500; color: var(--ink-soft); }
.dish__price[data-market="true"] { font-family: var(--display); letter-spacing: .04em; }
/* The dish description is the most-read functional text on the site, it is how
   somebody decides what to eat. It is not an aside and does not wear the whisper. */
.dish__of { margin: var(--space-1) 0 0; font-size: .84rem; line-height: 1.45; color: var(--ink-soft); max-width: 44ch; }

/* CENTERED. These sit under a centered heading and were the last controls on the
   site still pinned left. Campbell: "center the bistro and brunch, center those
   buttons." */
.bill__switch { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; margin-bottom: var(--space-7); }
.bill__switch button {
  font: inherit; font-family: var(--text); font-size: var(--t-label); font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase;
  min-height: 48px; padding: var(--space-3) var(--space-6);
  background: transparent; color: var(--ink-faint);
  border: 1px solid var(--line); border-radius: var(--r-pill); cursor: pointer;
  transition: color var(--dur-element) var(--ease-standard),
              border-color var(--dur-element) var(--ease-standard),
              background var(--dur-element) var(--ease-standard);
}
.bill__switch button:hover { color: var(--ink); border-color: var(--ink); }
.bill__switch button[aria-selected="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* ==========================================================================
   13. WHAT'S ON, an earned row, not a card grid
   ========================================================================== */
/* The old .whatson / .on rows were removed with the list they styled: three
   left-aligned slabs are now THE STANDING CARD at the end of this file. */

/* ==========================================================================
   14. THE QUOTE, left axis, big, one focal point
   ========================================================================== */
.quote blockquote { margin: 0; }
.quote__say {
  font-family: var(--display);
  font-size: clamp(1.45rem, 2.6vw, var(--ms-2));
  line-height: 1.42; letter-spacing: .008em; margin: 0;
  max-width: 34ch;                 /* never a ransom-note column */
}
.quote--wide .quote__say { max-width: 46ch; font-size: clamp(1.55rem, 3vw, var(--ms-3)); }

/* THE LONG TESTIMONIAL is a different object from a pull quote, and setting it
   like one was the mistake. Gabriel McDonald's review runs 100+ words; at
   display scale in a lapidary roman it became a wall of monument lettering
   nobody reads. A guest talking about the mac and cheese gets the TEXT face,
   a real measure, and room to breathe. The display face still opens it. */
.quote--long .quote__say {
  font-family: var(--text); font-weight: 300;
  font-size: clamp(1.125rem, 1.35vw, 1.375rem);
  line-height: 1.68; letter-spacing: 0;
  max-width: 58ch;
}
.quote--long .quote__say::first-line { font-weight: 400; }
.quote__who { margin-top: var(--space-6); }

/* ==========================================================================
   THE LETTER. Wesley's own words for the home page, set as a letter and not
   as a pull quote.

   The axis: the BLOCK is centred on its host and the READING MATTER is left
   inside it. That is design-precision's own worked example of earned left
   (left is earned by reading matter at width, inside a block that is itself
   centred) and it is why this does not need --copy-align: left is already the
   catch-all's default, so the letter body simply obeys the house law.

   The signature is the one thing that makes a letter a letter, so it is the
   only part that leaves the reading axis: display face, on the spine, closed
   with the site's own red seal instead of a quotation mark. It declares
   --copy-align because the phone catch-all would otherwise pull it left with
   the rest of the running copy, and a signature is not running copy.
   ========================================================================== */
.letter { min-width: 0; }
/* NO text-align on the letter's heading. There was one, written when the letter
   had an act of its own with "Why they opened the doors." over it. The letter
   now lives inside the Sit act, whose h2 is the struck hero word, so that rule
   quietly centred "Sit." at 1440 where it had always been left. Caught by
   tools/align-diff.mjs comparing against the pre-change snapshot, not by eye:
   one unintended alignment change hiding among six intended ones. */
.letter__body {
  margin-top: var(--space-6);
  margin-inline: auto;
  /* shorter than --measure: this is a letter, not an article, and 54ch keeps
     three paragraphs reading as a page rather than as a column of text. */
  max-width: 54ch;
}
.letter__body > p + p { margin-top: var(--space-5); }
.letter__sign {
  --copy-align: center;
  text-align: center;
  margin-top: var(--space-7);
  font-family: var(--display);
  font-size: var(--t-h3);
  line-height: 1.1;
  letter-spacing: .015em;
}

/* ==========================================================================
   15. THE GALLERY, a hung wall, not a camera-roll dump
   ========================================================================== */
/* grid-auto-flow: dense so a double-wide picture never leaves a hole beside it.
   The wall is hung, not dumped. */
.wall { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gutter); grid-auto-flow: dense; }
@media (min-width: 48rem) { .wall { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 75rem) { .wall { grid-template-columns: repeat(4, 1fr); } }
.wall__pic { grid-column: span 1; margin: 0; }
/* The double-wide tile spans TWO ROWS as well as two columns. Spanning columns
   only made its row taller than a 4:5 tile needs, so every frame beside it got
   a fat lopsided mat along the bottom, which reads as a broken frame, not as
   a design. Two rows keeps every mat even and gives the wall a real focal
   picture instead of a uniform contact sheet. */
.wall__pic[data-big="true"] { grid-column: span 2; grid-row: span 2; }
.wall__pic[data-big="true"] .wall__win { height: 100%; }
.wall__pic[data-big="true"] img { height: 100%; aspect-ratio: auto; }
/* Every picture on the wall is MOUNTED (see .mount). The wall used to be bare
   rounded rectangles floating on the paper, a camera roll with a border
   radius. A mat and a fillet is what turns a phone snap into a hung print,
   and it is the same language as the sporting art on their real walls. */
.wall__pic { --mat: clamp(7px, 0.7vw, 13px); }
.wall__pic .wall__win { overflow: hidden; border-radius: 1px; display: block; }
.wall__pic img {
  width: 100%; aspect-ratio: 4/5; object-fit: cover;
  transition: transform var(--dur-cinematic) var(--ease-expressive);
}
.wall__pic[data-big="true"] img { aspect-ratio: 3/2; }
.wall__pic:hover img { transform: scale(1.04); }

/* CENTERED under the gallery heading. On a phone it becomes a deliberate
   3-then-2, with the pair centered beneath the trio. Campbell's exact ask,
   and much steadier than letting five pills wrap wherever they land. */
.filters { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: center; margin-bottom: var(--space-8); }
@media (max-width: 47.99rem) {
  .filters { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--space-2); }
  /* equal columns means the longest label sets the fit, and EVERYTHING was
     touching its own pill. Tighten the tracking and the inline padding rather
     than letting the word clip. */
  .filters button { grid-column: span 2; padding-inline: var(--space-2); font-size: .68rem; letter-spacing: .06em; }
  .filters button:nth-child(4) { grid-column: 2 / 4; }
  .filters button:nth-child(5) { grid-column: 4 / 6; }
}
.filters button {
  font: inherit; font-family: var(--text); font-size: var(--t-label); font-weight: 600;
  letter-spacing: .11em; text-transform: uppercase;
  min-height: 44px; padding: var(--space-2) var(--space-5);
  background: transparent; color: var(--ink-faint);
  border: 1px solid var(--line); border-radius: var(--r-pill); cursor: pointer;
  transition: color var(--dur-element) var(--ease-standard), border-color var(--dur-element) var(--ease-standard), background var(--dur-element) var(--ease-standard);
}
.filters button:hover { color: var(--ink); border-color: var(--ink); }
.filters button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }

/* ==========================================================================
   16. FORMS
   ========================================================================== */
.field { display: grid; gap: var(--space-2); margin-bottom: var(--space-5); }
.field > label { font-size: var(--t-label); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.field input, .field textarea, .field select {
  font: inherit; font-family: var(--text); font-size: 1rem; color: var(--ink);
  background: color-mix(in srgb, var(--paper) 60%, #fff);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: var(--space-3) var(--space-4); min-height: 48px; width: 100%;
  transition: border-color var(--dur-element) var(--ease-standard);
}
.field input:focus, .field textarea:focus { border-color: var(--ink); }
.field textarea { min-height: 132px; resize: vertical; }
.formnote { font-size: var(--t-label); color: var(--ink-faint); max-width: 46ch; }

/* ==========================================================================
   THE INQUIRY CARD. Campbell, 2026-07-22: "I want the form to be framed like
   a picture... modern and feel good to fill out... invite someone to celebrate
   their moment with Lou Lou and Foster."

   So it is not a form on a page, it is an INVITATION CARD: bone paper, mounted
   in the same mat and gold fillet as every photograph on the site, laid on the
   upstairs dusk so it glows. The fields are ruled lines rather than boxes,
   because that is what a card you fill in by hand actually looks like, and
   boxes on paper read as software.
   ========================================================================== */
.enquire {
  --mat: clamp(12px, 1.3vw, 20px);

  /* THIS CARD IS A PIECE OF PAPER LYING ON THE DUSK, so it restates the light
     ground for everything inside it. .on-dusk reassigns --ink to bone and
     --paper-warm to near-black for its own stratum; inherited unchanged, the
     card's gradient faded to black down the page and its labels came out
     light-on-light. A light surface nested in a dark stratum has to own its
     tokens, never borrow them. */
  --ink:       var(--ink-base);
  --ink-soft:  var(--ink-soft-base);
  --ink-faint: var(--ink-faint-base);
  --paper-warm:#EFE9DC;
  --line:      color-mix(in srgb, #1E3035 14%, transparent);
  --line-soft: color-mix(in srgb, #1E3035 8%, transparent);
  color: var(--ink);

  max-width: 48rem; margin-inline: auto; position: relative;
  padding: var(--mat);
  background: linear-gradient(180deg, #FCF9F2 0%, var(--paper-warm) 100%);
  border: 1px solid color-mix(in srgb, #000 26%, transparent);
  border-radius: var(--r-xs);
  box-shadow:
    0 1px 0 color-mix(in srgb, #fff 70%, transparent) inset,
    0 30px 60px color-mix(in srgb, #000 46%, transparent),
    0 6px 14px color-mix(in srgb, #000 30%, transparent);
}
/* the same fine gold rule a framer cuts against the window */
.enquire::before {
  content: ''; position: absolute; inset: calc(var(--mat) - 3px);
  border: 1px solid color-mix(in srgb, var(--gold) 55%, transparent);
  pointer-events: none;
}
.enquire__in {
  position: relative;
  padding: clamp(var(--space-7), 4.5vw, var(--space-10)) clamp(var(--space-5), 4vw, var(--space-9));
  text-align: center;
}
.enquire__mark { width: 58px; height: auto; margin: 0 auto var(--space-5); opacity: .9; }
.enquire__say {
  font-family: var(--display); font-size: var(--t-h3);
  letter-spacing: .014em; line-height: 1.2; margin: 0; color: var(--ink);
}
.enquire__of {
  margin: var(--space-4) auto 0; max-width: 40ch;
  font-size: .95rem; line-height: 1.6; color: var(--ink-soft);
}
.enquire__rule {
  width: 76px; height: 1px; border: 0; margin: var(--space-7) auto;
  background: color-mix(in srgb, var(--gold) 70%, transparent);
}

/* Two up where there is room; the story of the event always runs full width. */
.enquire__fields { display: grid; gap: var(--space-5) var(--space-6); text-align: left; }
@media (min-width: 40rem) {
  .enquire__fields { grid-template-columns: 1fr 1fr; }
  .enquire__fields .field--wide { grid-column: 1 / -1; }
}

/* RULED, not boxed. A line you write on, the way a card is filled in. */
.enquire .field { margin-bottom: 0; }
.enquire .field > label { color: var(--ink-faint); letter-spacing: .14em; }
.enquire .field input,
.enquire .field textarea {
  background: transparent;
  border: 0; border-bottom: 1px solid color-mix(in srgb, var(--ink) 26%, transparent);
  border-radius: 0; padding: var(--space-3) 0;
  transition: border-color var(--dur-element) var(--ease-standard),
              box-shadow var(--dur-element) var(--ease-standard);
}
.enquire .field input:focus,
.enquire .field textarea:focus {
  outline: none;
  border-bottom-color: var(--ink);
  box-shadow: 0 1px 0 var(--ink);
}
.enquire .field textarea { min-height: 104px; }
.enquire__go { margin-top: var(--space-8); display: flex; justify-content: center; }
.enquire .cf-turnstile { display: flex; justify-content: center; margin-top: var(--space-6); }
.enquire .formnote { margin: var(--space-5) auto 0; max-width: 44ch; text-align: center; }

/* ==========================================================================
   THE ON-PAGE CTA, a real button, centered under its act.
   His note: an invitation to book or inquire is a different object from a link
   in a paragraph, and it should not sit hard against the left edge of a text
   column looking like an afterthought.
   ========================================================================== */
.act__go { display: flex; justify-content: center; margin-top: var(--space-9); }
.btn--quiet {
  background: transparent; color: var(--ink);
  border-color: color-mix(in srgb, var(--ink) 40%, transparent);
}
.btn--quiet:hover { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.on-dusk .btn--quiet { color: var(--paper); border-color: color-mix(in srgb, var(--paper) 40%, transparent); }
.on-dusk .btn--quiet:hover { background: var(--paper); color: var(--dusk); border-color: var(--paper); }

/* ==========================================================================
   17. FOOTER
   ========================================================================== */
.foot { background: var(--dusk-deep); color: var(--paper); padding-block: var(--section-y) var(--space-7); }

/* ONE AXIS, both breakpoints. Every child centers on it; nothing is allowed to
   float off to a side and open a hole, which is what the old 13-columns-in-12
   grid did to "Reach us". */
.foot__in { text-align: center; display: flex; flex-direction: column; align-items: center; }
.foot__in > * { margin-inline: auto; }

.foot__mark { width: clamp(88px, 11vw, 132px); height: auto; opacity: .92; margin-bottom: var(--space-6); }
/* the mark is deep slate on transparent, invert it onto the dusk ground */
.foot__mark { filter: brightness(0) invert(1); opacity: .82; }

.foot__say {
  font-family: var(--display); font-size: var(--ms-2); letter-spacing: .014em;
  color: var(--rattan-lit); margin: 0 0 var(--space-5); line-height: 1.15;
}
.foot__where { font-style: normal; line-height: 1.7; color: color-mix(in srgb, #F4F1E9 78%, transparent); margin: 0; }
.foot__go { margin: var(--space-5) 0 0; }

.foot h4 {
  font-family: var(--display-sc); font-size: var(--ms-0);
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--rattan); margin: 0 0 var(--space-5);
}

/* The two floors keep DIFFERENT hours, and that asymmetry is the whole point
   of the building, so they are set as a matched pair, side by side, each one
   internally aligned (day left, time right) so the times stay readable. Only
   the PAIR is centered; centering the rows themselves would destroy the column
   that makes them scannable. */
/* Sized to CONTENT, not to a guessed cap. A 15rem column broke "11 to 2 & 5
   to 9" across two lines and left the 9 stranded on its own, a restaurant's
   opening hours are the one thing on a site that must never look like a
   mistake. Columns are max-content and the times never wrap. */
.foot__floors {
  display: grid; grid-template-columns: 1fr; gap: var(--space-8);
  margin-top: var(--space-10); justify-content: center;
}
@media (min-width: 46rem) {
  .foot__floors { grid-template-columns: repeat(2, max-content); gap: var(--space-11); }
}
.foot__floor { text-align: center; }
.foot__hours {
  display: grid; grid-template-columns: max-content max-content;
  gap: var(--space-2) var(--space-6);
  margin: 0; font-size: .86rem; text-align: left; justify-content: center;
}
.foot__hours dt { color: color-mix(in srgb, #F4F1E9 62%, transparent); white-space: nowrap; }
.foot__hours dd { margin: 0; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums lining-nums; }

.foot__reach { margin-top: var(--space-10); }
.foot__lines { display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline; gap: var(--space-3); margin: 0; }
.foot__lines a { text-underline-offset: 4px; }
.foot__dot { color: color-mix(in srgb, #F4F1E9 40%, transparent); }
/* Stacked on a narrow phone, and align-items must be named: once the flex
   direction turns to column, justify-content centers on the VERTICAL axis and
   the links stretch full width, which parked them hard against the left edge
   of an otherwise perfectly centered footer. */
@media (max-width: 33rem) {
  .foot__dot { display: none; }
  .foot__lines { flex-direction: column; align-items: center; gap: var(--space-3); }
}
.foot__reach .fine { margin: var(--space-5) auto 0; max-width: 42ch; }

.foot p { color: color-mix(in srgb, #F4F1E9 74%, transparent); }
.foot__legal {
  margin-top: var(--space-9); padding-top: var(--space-5); width: 100%;
  border-top: 1px solid color-mix(in srgb, #F4F1E9 14%, transparent);
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-6); justify-content: center;
  font-size: var(--t-caption); color: color-mix(in srgb, #F4F1E9 52%, transparent);
}

.social { display: flex; gap: var(--space-4); margin-top: var(--space-8); justify-content: center; }
.social a { display: grid; place-items: center; width: 44px; height: 44px; border: 1px solid color-mix(in srgb, #F4F1E9 22%, transparent); border-radius: var(--r-full); transition: background var(--dur-element) var(--ease-standard), color var(--dur-element) var(--ease-standard); }
.social a:hover { background: var(--paper); color: var(--dusk); }
.social svg { width: 17px; height: 17px; }

/* ==========================================================================
   18. MOTION. ONE reveal verb (rise). Scrubbed where it should breathe.
   ========================================================================== */
[data-rise] {
  opacity: 0; transform: translateY(28px);
  transition: opacity var(--dur-cinematic) var(--ease-expressive),
              transform var(--dur-cinematic) var(--ease-expressive);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}
[data-rise][data-in="true"] { opacity: 1; transform: none; }

/* THE TWO DOGS KEEP DIFFERENT LAWS.
   Lou Lou is composed and a little bossy: her card settles onto its line and
   stops dead, no overshoot, because she was always going to end up in charge.
   Foster is the goodest kind of energy: his card comes in a touch hot, passes
   its mark and rocks back. Same distance, same duration, opposite character.
   Nobody will be able to say why the page feels like them. That is the point. */
[data-dog="loulou"] { transition-timing-function: cubic-bezier(.16, .84, .28, 1); }
[data-dog="foster"] { transition-timing-function: cubic-bezier(.34, 1.42, .52, 1); }

/* the hero picture drifts as you leave it, scroll-linked, so the tempo is
   the reader's own. Never a timer, never yanked. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero__see img { animation: drift linear both; animation-timeline: view(); animation-range: entry 0% exit 100%; }
    @keyframes drift { from { transform: scale(1.07) translateY(-1.4%); } to { transform: scale(1.07) translateY(1.4%); } }
  }
}

/* ==========================================================================
   THE DEVELOPMENT, every mounted photograph arrives as a print in the house
   ink and develops to full color as the reader carries it up the page. Scroll
   is the darkroom clock.

   It does two jobs at once. It turns a wall of honest phone-shot photographs
   into a curated set of PRINTS, which is most of the distance between "good
   restaurant site" and "expensive one." And it IS the climb: a picture warming
   from slate into full color as it rises is the same gesture as the room warming
   from daylight into lamplight.

   Targeted at .mount and .plate, every framed and matted photograph, and NOT
   the hero (.hero__see img), which is the LCP and must read instantly in color
   (the instrument's own forbidden line). Gated on view-timeline support AND the
   still path, so a browser without either simply shows full-color photographs;
   nothing is ever left gray. The develop finishes well before an image reaches
   center, so nothing sits inky while it is being read.

   Keyed to the SAME reveal the whole site runs on, the [data-rise] observer
   sets data-in when a figure enters, so the print RISES into the light and
   colors in as one gesture; the develop and the climb are the same motion. A
   scroll-scrub was the first instinct, but every photograph sits inside its own
   clipping frame (.mount, .wall__win, .plate__mat), which becomes the view()
   timeline's scroller and leaves progress null. Keyed to data-in it is exact and
   robust, and the safety net that flips data-in a beat after load means no
   photograph can ever stay inky. */
.mount img, .plate img {
  filter: saturate(.42) brightness(1.03) contrast(.97) sepia(.14);
  transition: filter 1.15s var(--ease-expressive);
}
.mount[data-in="true"] img,
[data-in="true"] .plate img {
  filter: saturate(1) brightness(1) contrast(1) sepia(0);
}
@media (prefers-reduced-motion: reduce) {
  .mount img, .plate img { filter: none !important; }
}

/* ==========================================================================
   THE ATELIER, the movements that make it feel bought, not rendered.
   The grammar (dealt for this build, seed laf-demo) emits the numbers; the look
   is authored here off this build's own tokens. Peaceful jazz, never hip-hop.
   ========================================================================== */

/* --- THE PRESSED (banked instrument) -------------------------------------
   Every pill leans a few pixels toward the hand (desktop), compresses honestly
   under the press (pointer and keyboard), and settles. This REPLACES the
   translateY(-2px) hover-lift, which was the one template tell left on the
   controls. The lean rides two custom properties set by atelier.js; the
   compress rides [data-pressing]. Caps are the instrument's: 4px lean here,
   3.5% compress, past them it reads as a toy. */
[data-press] {
  transform: translate(var(--press-x, 0px), var(--press-y, 0px));
  transition: transform var(--dur-element) var(--ease-expressive),
              box-shadow var(--dur-element) var(--ease-standard),
              background var(--dur-element) var(--ease-standard),
              color var(--dur-element) var(--ease-standard),
              border-color var(--dur-element) var(--ease-standard);
}
[data-press][data-pressing="true"] {
  transform: scale(.965);
  transition-duration: 90ms;
}
/* the solid pills get a soft ring that blooms as the press settles */
.btn[data-press]:active, .rail__book[data-press]:active {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  [data-press] { transform: none !important; }
}

/* --- ARRIVAL: THE ENGRAVED LIGHT (grammar sheen) -------------------------
   The one word the visitor reads (Come / Sit / Stay) catches a band of warm
   light that crosses it once as it enters the room, the way light crosses a
   letter cut into stone. Keyed to --g28-seen, which grammar flips to 1 when the
   word is in view; on the still path and without background-clip it is simply
   set in ink. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  @media (prefers-reduced-motion: no-preference) {
    /* the engraved-light base: the word is set in a gradient of its own ink with
       one warm band parked off to the right, so it reads as plain ink until the
       band is drawn across it. Robust by construction, keyed to the site's own
       signals (load and data-in), never to a fragile timeline. */
    .hero-word {
      background-image: linear-gradient(100deg,
        currentColor 0 42%,
        color-mix(in srgb, var(--rattan-lit) 88%, #fff) 50%,
        currentColor 58% 100%);
      background-size: 260% 100%;
      background-position: 122% 0;
      /* repeat-x, NOT no-repeat: a clipped gradient narrower than the word leaves
         the letters outside it TRANSPARENT (text-fill is transparent), so a
         sweeping no-repeat band erased the C mid-travel. Tiled, the word is
         always fully inked except exactly where the one bright band passes. */
      background-repeat: repeat-x;
      -webkit-background-clip: text; background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    /* Come. The arrival word, catches the light once as the page opens. */
    .hero-word:not([data-rise]) { animation: sheen-sweep 1.5s var(--ease-expressive) .55s both; }
    /* Sit. and Stay. Each catches it as the reader climbs into its act. */
    .hero-word[data-rise] { transition: background-position 1.3s var(--ease-expressive); }
    .hero-word[data-rise][data-in="true"] { background-position: -12% 0; }
    @keyframes sheen-sweep { from { background-position: 122% 0; } to { background-position: -12% 0; } }
  }
}

/* --- MEDIA: THE HUNG PICTURE ANSWERS THE HAND (grammar tilt + glare) ------
   A framed picture on a real wall leans a little toward you and its glass and
   gilding catch the light as you pass. Desktop, fine-pointer, no-reduced-motion
   only. grammar.css already gives .hung its stacking context and 3D camera. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .plate[data-g28~="tilt"] {
    transform: perspective(1600px) rotateX(var(--g28-rx, 0deg)) rotateY(var(--g28-ry, 0deg));
    transition: transform .5s var(--ease-standard);
  }
  .plate[data-g28~="glare"]::after {
    content: ''; position: absolute; inset: 0; z-index: 4; pointer-events: none;
    border-radius: 2px;
    background: radial-gradient(240px circle at calc(var(--g28-ex, .5) * 100%) calc(var(--g28-ey, .5) * 100%),
      color-mix(in srgb, #FFF7E4 66%, transparent) 0%, transparent 62%);
    opacity: calc(var(--g28-in, 0) * .4);
    mix-blend-mode: soft-light;
    transition: opacity .35s var(--ease-standard);
  }
}

/* --- NAV: THE TOUCH BLOOM (grammar ripple) --------------------------------
   A tap on a primary pill blooms once from the point of contact. Authored soft
   and warm, clipped to the pill by grammar.css. */
[data-g28-rippling]::before {
  content: ''; position: absolute; z-index: 0;
  left: var(--g28-ox, 50%); top: var(--g28-oy, 50%);
  width: 8px; height: 8px; border-radius: 50%;
  transform: translate(-50%, -50%);
  background: color-mix(in srgb, var(--paper) 42%, transparent);
  animation: press-bloom .62s var(--ease-standard);
}
.btn--ghost[data-g28-rippling]::before,
.bill__switch button[data-g28-rippling]::before,
.filters button[data-g28-rippling]::before {
  background: color-mix(in srgb, var(--ink) 20%, transparent);
}
@keyframes press-bloom { to { width: 300%; height: 900%; opacity: 0; } }
@media (prefers-reduced-motion: reduce) { [data-g28-rippling]::before { display: none; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
  [data-rise] { opacity: 1; transform: none; }
}

/* ==========================================================================
   19. UTILITIES
   ========================================================================== */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.stack > * + * { margin-top: var(--space-5); }
.stack-lg > * + * { margin-top: var(--space-7); }
.skip {
  position: absolute; left: var(--space-4); top: -100px; z-index: 100;
  background: var(--ink); color: var(--paper); padding: var(--space-3) var(--space-5);
  border-radius: var(--r-sm); text-decoration: none; font-size: var(--t-label);
  transition: top var(--dur-element) var(--ease-standard);
}
.skip:focus { top: var(--space-4); }

/* Marcellus ships no italic, so <em> inside a display line is NOT slanted, a
   synthetic oblique is a forgery of a cut the designer never drew. With one
   weight there is no lighter cut to fall back on either, so emphasis is
   carried exactly the way an engraver carries it: the tracking opens and the
   ink lifts a shade toward the lamplight. */
.display em, h1 em, h2 em, h3 em, .hero-word em {
  font-style: normal;
  letter-spacing: .06em;
  color: color-mix(in srgb, var(--ink) 78%, var(--rattan));
}
.on-dusk .display em, .on-dusk h1 em, .on-dusk h2 em, .on-dusk h3 em, .on-dusk .hero-word em {
  color: color-mix(in srgb, var(--paper) 74%, var(--rattan-lit));
}

/* ==========================================================================
   THE PHONE COMPOSITION, authored, not a collapsed desktop stack.
   Campbell, 2026-07-22: "everything is left aligned on mobile still, except
   randomly it'll go back center. We just need some alternating interesting
   ways to make things coordinate."

   He is right, and the cause is structural: in one column there is no left and
   no right, so the desktop alternation has nothing to express itself with and
   every act collapses to the same left-aligned stack under a centered page
   intro. Two moves fix it.

   1. THE SPINE IS CENTERED. Heading, deck, picture and button all sit on the
      page's center line, exactly like the intro above them, so the phone reads
      as one composition instead of a centered header bolted onto left-aligned
      slabs. Running paragraphs and the ruled points list stay left, because
      centered body copy at this measure is a wall, the eye needs a hard left
      edge to return to. Centered spine, left-aligned reading. That contrast is
      the coordination, not an accident.

   2. THE ALTERNATION BECOMES ORDER. An act that puts the picture on the LEFT
      on desktop leads with the PICTURE on the phone; the others lead with the
      words. Scrolling, you get headline-act, photograph-act, headline-act, the same rhythm as the desktop side-to-side, carried by the only axis a
      phone actually has.
   ========================================================================== */
@media (max-width: 59.99rem) {
  .split--right .split__see { order: -1; }

  /* THE WHOLE SPINE. This block lives at the END of the stylesheet on purpose:
     declared earlier it lost every tie on source order, and the failures were
     invisible in the source and obvious on the phone, the name-card rule
     computed margin-left: 0 and sat as a stray underline off to the left under
     Lou Lou and Foster, and .fine / .link stayed left while their headings
     centered. Anything centered on the phone belongs here, and nowhere else. */
  /* THE SPINE IS THE DEFAULT AND EARNED-LEFT IS THE EXCEPTION (2026-08-02).

     Session 19 wrote, correctly: "A law written as a selector list dies the day
     the markup is rebuilt." Then it wrote another selector list. Four days
     later Campbell was back with the same complaint, "items that sat left...
     the hooks and gates missed", because the services page alone carries FOUR
     act containers (.svc__panel, .svc__card, .svc, .svc__mid) and only one of
     them was ever enumerated. So "Downstairs at Lou Lou's" and its button sat
     left while "Upstairs at Foster's" directly beneath it centred: two axes,
     one page, one scroll apart. That is the whole of "centering is all over the
     place", and it will happen again on the next act anybody authors.

     So the law is inverted, and it now binds to what a thing IS rather than to
     what a container happens to be called. On a phone every heading, deck,
     signpost, caption and control inside <main> comes home to the spine. A new
     act joins it by existing. Nothing has to remember anything.

     LEFT IS STILL EARNED, and it is earned by being SCANNED rather than read
     (design-precision section 2): a ledger, a rack, a form, a key. Those
     containers are named ONCE, in the :not() below, and that list is short and
     stable because it describes structures, not sections. Running paragraphs
     are handled separately and always keep their left edge.

     And every centred thing gets margin-inline:auto in the same breath. A
     max-width block with `text-align:center` and no auto margin centres its
     WORDS while its BOX stays pinned to the host's left edge: the documented
     collapse-to-left trap, and it was live here at 768px on the stairwell's
     hours lines (89px off) and the owners' attribution. Centring a box and
     centring its text are two different declarations and they belong together
     or the rule is half-written. */
  main :is(h1, h2, h3, h4, h5, h6,
           .sub, .lead, .fine, .engraved, .deck,
           figcaption, .quote, .quote__say, .quote__who,
           .namecard__name, .namecard__who,
           .act__go, .btn, .link
          ):not(:where(.course, .courses, .dish, .points, .stand, .standing__set,
                       .avail, .ticks, .opts, .field, .book__grid, .blankcard__lines,
                       table, [data-earned-left]) *) {
    text-align: center;
    margin-inline: auto;
  }

  /* RUNNING COPY KEEPS ITS LEFT EDGE. Centred body copy at a 40ch measure is a
     wall and the eye needs a hard left edge to return to. A deck is short and
     classed (.sub, .lead, .fine); a bare paragraph is reading matter. This has
     to come AFTER the rule above, because a <p class="fine"> is both and the
     signpost reading must win only for the classed ones. */
  /* The two lists MUST name the same set. `p.engraved` is both a paragraph and
     a signpost, and leaving it out of this exclusion pulled the elevation label
     straight back to the left under its own centred heading: two axes inside
     one act, which is the exact fault this whole block exists to kill. The gate
     caught it, which is what the gate is for.

     AND IT READS --copy-align RATHER THAN OVERRIDING EVERY COMPONENT. Written
     as a flat `text-align: left`, this rule sits at the END of the stylesheet
     and therefore beat the invitation card, the blank bar card, the door's
     status line, the standing card and the story board, all of which declare
     centred copy of their own and had it since session 5. Campbell asked for
     this to be checked on his phone and that is exactly where it showed: the
     whole enquiry card went left. Proven against the session-19 build on its
     own live alias, not assumed: those paragraphs computed `center` there and
     `left` here.

     So a container that authors centred copy declares it ONCE, the same way it
     declares --box-axis, and this rule defers to it. A catch-all that cannot be
     overridden is not a law, it is a bulldozer. */
  /* .dish__line, .dish__of and .course__note joined this list on 2026-08-03:
     they are the MENU LEDGER, a name with a run of leader dots and a price on
     a rail, which is scanned and never read. When the centred-copy law went
     sitewide they were the 18 rows it wrongly swept up, and align-diff caught
     it rather than an eye. An exclusion belongs HERE, in the law's own set,
     not in a competing rule further down: a separate `main .dish__line` loses
     the specificity tie to this selector and does nothing at all. */
  main p:not(:is(.sub, .lead, .fine, .deck, .engraved, .quote__say, .quote__who,
                 .dish__line, .dish__of, .course__note)) {
    text-align: var(--copy-align, left);
  }
  /* ...but a paragraph that exists only to carry a control is not reading
     matter, it is the control's host, and the control belongs on the spine. */
  main p:has(> .link),
  main p:has(> .btn),
  main p:has(> .act__go) { text-align: center; }

  /* the ruled points list keeps its left edge: it is a list, and a centered
     list with hanging ticks reads as broken */
  .points, .points li { text-align: left; }

  /* THE TRAIT RUN-ON, CENTRED, LOSES ITS SEPARATORS.

     The dots lead their own item so they can never strand at the END of a
     wrapped line (the fix that finally killed a bug this build shipped three
     times). But CENTRED and wrapped, every new line opens with a dot at a
     different x, and six of them down a phone screen read as scattered marks
     rather than as a list. Left-aligned on the story page they are correct and
     they stay. Here the gap does the separating and nothing can strand.

     (2026-08-02: the separator is gone at EVERY width now, for the same reason
     one width further up this file, so only the phone's tighter gap is left
     here. The story page's run is no longer "correct as it stands": it ragged
     its own left edge at 1440, which is what this note used to be wrong about.) */
  .namecard__traits { gap: var(--space-2) var(--space-5); }

  .namecard { max-width: none; }
  .namecard__name, .namecard__who { text-align: center; }
  .namecard__rule { margin: var(--space-4) auto var(--space-3); width: 64px; }
  .namecard__traits { justify-content: center; text-align: center; }

  /* the hung rows stack, so their labels join the same centered spine */
  .hang-row .namecard__name,
  .hang-row .namecard__who { text-align: center; }
  .hung figcaption, .mount figcaption { text-align: center; }
}

/* ==========================================================================
   THE LIVING SURFACES, the parts of the site the owners' Desk drives.
   Guest alerts opt-in, an 86'd dish, and tonight's specials. All degrade to
   nothing when the database or push is not switched on.
   ========================================================================== */
.foot__alerts { margin-top: var(--space-9); display: flex; flex-direction: column; align-items: center; gap: var(--space-3); }
.foot__alerts .btn--ghost { color: var(--paper); border-color: color-mix(in srgb, var(--paper) 40%, transparent); }
.foot__alerts .btn--ghost:hover { background: var(--paper); color: var(--dusk); }
.foot__alerts .btn--ghost[data-on="true"] { background: var(--rattan-lit); color: var(--dusk); border-color: var(--rattan-lit); }
.foot__alerts .fine { max-width: 42ch; text-align: center; }

/* an 86'd dish on the live menu, struck through, marked, still readable */
.dish[data-off="true"] { opacity: .55; }
.dish[data-off="true"] .dish__name { text-decoration: line-through; }
.dish[data-off="true"] .dish__name::after {
  content: '86 tonight'; text-decoration: none; display: inline-block; margin-left: var(--space-2);
  font-size: .56rem; letter-spacing: .12em; text-transform: uppercase; color: var(--tolix); vertical-align: middle;
}

/* tonight's specials, live at the top of the menu page */
.specials-strip {
  max-width: 62rem; margin: 0 auto var(--space-8); padding: var(--space-5) var(--space-6);
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent); border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--rattan) 8%, var(--paper)); position: relative; z-index: 1;
}
.specials-strip__head { font-family: var(--display-sc); letter-spacing: .22em; text-transform: uppercase; font-size: var(--t-caption); color: var(--rattan); margin: 0 0 var(--space-3); }
.specials-strip ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.specials-strip li { font-size: 1.05rem; }
.specials-strip__floor { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); margin-right: var(--space-2); }
.specials-strip b { font-variant-numeric: tabular-nums; color: var(--ink-soft); }

/* ==========================================================================
   THE PRESS, the five moves that answer "too calm, too plain, too ordinary"

   Campbell, 2026-07-22, on the live site: "it still feels like a template...
   too calm, too plain, too ordinary. Too Wix or Squarespace with a touch of
   somebody tried didn't they."

   He could not name it, so here is the naming. The composition was already
   authored and the craft below the fold was real; what was missing was
   MATERIAL EVENT. Every surface on the page sat at the same tonal temperature,
   the one declared accent was used at literally zero percent, the three words
   that carry the whole concept were set at the same weight of attention as a
   section heading, and nothing on the page was ever pressed, stamped, sealed or
   struck. A page where nothing has been DONE to it reads as a page a template
   generated. Five moves, all of them material, none of them layout, because
   the layout is his and it stays.

   Every one of these runs at EVERY width. His instruction: "whatever changes
   are made for mobile should be made for the entire site." And nothing new here
   is left aligned: the seal is punctuation, the press is a material, and the
   standing card and the phone spine are centered.
   ========================================================================== */

/* --- 1. THE SEAL ---------------------------------------------------------
   Come. Sit. Stay. are dog commands, and the full stop is the most loaded
   punctuation on this site. It is now the one place the tolix red, pipetted
   from the chairs in their own dining room, declared in the tokens at "<=10%"
   and then used at 0% for eleven sessions, appears at scale. Three red marks
   down the page: the ink at the end of each command.

   The restated text-fill is not optional. .hero-word paints its sheen with
   background-clip:text and -webkit-text-fill-color:transparent, and the child
   inherits BOTH, so an unstyled span inside it is an invisible glyph, not a
   red one. Its own fill and its own background are declared here on purpose. */
.seal {
  font-style: normal;
  color: var(--tolix);
  -webkit-text-fill-color: var(--tolix);
  background: none;
  margin-left: -.02em;   /* optical: a period sits tighter than the metal allows */
}
.on-dusk .seal, .on-dusk .seal { color: var(--tolix); -webkit-text-fill-color: #D4472A; }

/* --- 2. THE STRIKE -------------------------------------------------------
   The monuments stop being large text and become an IMPRESSION in the sheet.

   Two parts, both material, neither of them layout. The still part is a
   letterpress deboss: a hairline of paper-white catching the raised lip below
   each stroke and a whisper of ink pooled above it, so the word reads as
   pressed INTO the paper rather than printed onto it. It is drawn with
   drop-shadow rather than text-shadow on purpose, the sheen above paints these
   words with background-clip:text and a transparent fill, and a text-shadow
   behind a transparent glyph becomes the glyph. drop-shadow filters the
   RENDERED result, so the two compose instead of fighting.

   The moving part is the strike itself: the word lands a touch compressed with
   its letters tight, then the ink spreads out to its set track as the sheet
   takes the impression. Marcellus is an inscriptional roman cut with air around
   the letter, so opening the track IS the press releasing. Slow, on the
   expressive ease. Peaceful jazz, never hip-hop. */
.hero-word {
  filter:
    drop-shadow(0 1px 0 color-mix(in srgb, #fff 88%, transparent))
    drop-shadow(0 -1px 1px color-mix(in srgb, var(--ink) 16%, transparent));
}
.on-dusk .hero-word {
  filter:
    drop-shadow(0 1px 0 color-mix(in srgb, var(--rattan-lit) 26%, transparent))
    drop-shadow(0 -1px 2px color-mix(in srgb, #000 55%, transparent));
}
/* BOTH animations, in ONE declaration. The sheen above already owns
   `animation` and `transition` on these exact selectors, and a second
   declaration of a shorthand does not merge with the first, it replaces it.
   Declared separately, the strike silently deleted the sheen and parked the
   word's gradient mid-band, which rendered "Come." in pale rattan light instead
   of ink. They animate different properties and compose fine; they just have to
   be written together. (Fourth specificity-class failure on this build in four
   sessions. Check the COMPUTED value, never the source.) */
@media (prefers-reduced-motion: no-preference) {
  .hero-word[data-rise] {
    letter-spacing: -.022em; transform: scaleY(.965); transform-origin: 50% 100%;
    transition: background-position 1.3s var(--ease-expressive),
                letter-spacing 1.1s var(--ease-expressive),
                transform 1.1s var(--ease-expressive);
  }
  .hero-word[data-rise][data-in="true"] { letter-spacing: .008em; transform: none; }
  @keyframes strike {
    from { letter-spacing: -.022em; transform: scaleY(.965); }
    to   { letter-spacing: .008em;  transform: none; }
  }
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-word:not([data-rise]) {
      transform-origin: 50% 100%;
      animation: sheen-sweep 1.5s var(--ease-expressive) .55s both,
                 strike 1.1s var(--ease-expressive) .12s both;
    }
  }
}
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-word:not([data-rise]) { transform-origin: 50% 100%; animation: strike 1.1s var(--ease-expressive) .12s both; }
  }
}

/* --- 3. THE THRESHOLD, ON THE PHONE -------------------------------------
   The hero was the ONE act that never joined the centered phone spine session 6
   established and Campbell approved, heading, deck, paragraph and both buttons
   all hugging the left under a photograph of fur. It was the first screen of
   the site and the last left-aligned stack on it. It joins the spine now, and
   the two buttons stop being a ragged pair of different widths.

   This block lives at the END of the stylesheet with the rest of the phone
   composition, for the reason that cost three sessions: declared earlier it
   loses every tie on source order and the failure is invisible in the source
   and obvious on the phone. */
@media (max-width: 59.99rem) {
  .hero__say { align-items: center; text-align: center; }
  .hero__say .sub, .hero__say .lead { margin-inline: auto; }
  .hero__word .hero-word { margin-left: 0; }   /* the optical hang is a LEFT-axis correction */
  .hero__intents { width: 100%; max-width: 20rem; flex-direction: column; align-items: stretch; }
  .hero__intents .btn { justify-content: center; }
  .today { justify-content: center; margin-inline: auto; }
}

/* the hero print is the LCP and reads in color instantly: the development
   instrument's own forbidden line. Its frame is a .mount, so it has to be
   excused explicitly or it would sit muted forever, .hero__frame carries no
   data-rise, so the develop-to-color rule could never fire on it. */
.hero__frame img { filter: none; }

/* --- 4. THE STANDING CARD ------------------------------------------------
   What's on was three stacked slabs of left-aligned body copy under colored
   labels: the only block on the site sitting outside its own type system, and
   the plainest thing on the page.

   TASTE.md's standing law is that a nicer list is still a list, so this is not
   restyled, it is moved into the engraved card system Campbell named himself
   ("I like the menu... a little bit more compact"). Course head, ruled entries,
   the day and time on a right rail with leader dots exactly like a price.
   Density is the class. It is what a bistro actually stands on the table, in
   the hand the site already writes in. */
/* THE TOKENS ARE RESTATED ON THE CARD ITSELF, not on its inner padding box.
   This is exactly the trap the invitation card taught in session 5, caught one
   element too high the first time: .on-dusk reassigns --ink to bone AND
   --paper-warm to near-black for its stratum, so a bone card laid on the dusk
   that borrows either one comes out wrong. Restating the inks on .tease__in
   fixed the type and left the card's own gradient running from bone at the top
   to dusk-deep at the bottom, the standing card faded into the floor and took
   Live Music at Foster's with it. A light surface nested in a dark stratum
   restates EVERY token it paints with, at the element that paints. */
.standing .tease {
  --mat: clamp(12px, 1.4vw, 20px);
  --paper: #F4F1E9; --paper-warm: #EFE9DC; --paper-deep: #E4DDD0;
  --ink: var(--ink-base); --ink-soft: var(--ink-soft-base); --ink-faint: var(--ink-faint-base);
  position: relative; margin-inline: auto; max-width: 56rem; padding: var(--mat);
  color: var(--ink);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--paper) 96%, #fff) 0%, var(--paper-warm) 100%);
  border: 1px solid color-mix(in srgb, #000 34%, transparent);
  border-radius: var(--r-xs);
  box-shadow:
    0 1px 0 color-mix(in srgb, #fff 62%, transparent) inset,
    0 20px 44px color-mix(in srgb, #000 44%, transparent);
}
.standing .tease::before {
  content: ''; position: absolute; inset: calc(var(--mat) - 3px);
  border: 1px solid color-mix(in srgb, var(--gold) 58%, transparent);
  pointer-events: none;
}
.standing .tease__in {
  padding: clamp(var(--space-7), 4vw, var(--space-9)) clamp(var(--space-5), 3.5vw, var(--space-8));
}
/* the gold ornament on the head above the card belongs to the dusk it sits on;
   the rattan variant would be invisible against bone, and vice versa */
.standing .rule::after { background: var(--rattan); }
/* Everything on this card is CENTERED, at every width. Campbell, this session:
   "left aligned should earn its place... if it's left aligned it is there by my
   choice and nothing new should be left aligned." The three entries that live
   here were the last block on the site that had never been composed, so they
   compose to the center. A leader-dot right rail was the first attempt and it
   collided: "Live Music at Foster's" beside "NOW BOOKING" does not fit a phone,
   and a display name that wraps under its own price rail is worse than no rail.
   Name, then the standing detail on one engraved line, then what it is. */
.standing__set { display: grid; gap: var(--space-7); }
.stand { text-align: center; }
.stand + .stand { padding-top: var(--space-7); border-top: 1px solid color-mix(in srgb, var(--gold) 34%, transparent); }
.stand__name {
  margin: 0; font-family: var(--display); font-size: var(--t-h3);
  letter-spacing: .014em; line-height: 1.15; color: var(--ink);
}
/* THE HOST OWNS THE AXIS. This box carries a max-width narrower than the card
   that holds it (777px inside 921px), so `margin-inline: 0` pinned it to the
   left edge and all three when/where lines sat 72px off the card's centre, text-align:center centred the WORDS inside a box that was itself off-axis.
   Measured, not guessed. `auto` puts the box back on its host's centre line. */
.stand__at {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-1);
  margin: var(--space-3) auto 0;
}
@media (min-width: 48rem) {
  .stand__at { flex-direction: row; justify-content: center; align-items: center; gap: var(--space-3); }
}
.stand__when {
  font-family: var(--display-sc); font-size: var(--t-label);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--rattan); font-variant-numeric: tabular-nums;
}
/* The separator is HIDDEN until both halves actually fit on one line. Left in
   at phone width it strands a naked gold dot at the end of the first line every
   time the location wraps, the same orphaned-interpunct fault session 4 fixed
   on the services list, which is what happens when a separator lives in its own
   box instead of travelling with the word it follows. */
.stand__dot { display: none; }
@media (min-width: 48rem) {
  .stand__dot { display: block; width: 4px; height: 4px; border-radius: var(--r-full); background: color-mix(in srgb, var(--gold) 70%, transparent); flex: none; }
}
/* THE LOCATION LINE IS A SIGNPOST, NOT AN ASIDE. Measured on the rendered
   ground (the card paints a gradient, so a probe reading background-color sees
   straight through it): --ink-faint here computed 4.49:1 against the 4.5:1 a
   12px line needs. It is the word telling somebody WHICH FLOOR to walk to, so
   it carries readable ink, exactly as the invitation labels did in session 16
   and the bar's hours line in session 18. Third time this whisper colour has
   been caught doing functional work. */
.stand__where {
  font-family: var(--display-sc); font-size: var(--t-label);
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft);
}
.stand__of {
  margin: var(--space-4) auto 0; font-size: .95rem; line-height: 1.6;
  color: var(--ink-soft); max-width: 48ch;
}
@media (min-width: 48rem) {
  .standing__set { gap: var(--space-8); }
  .stand + .stand { padding-top: var(--space-8); }
}

/* --- 5. THE PRESS AND THE SEAL, ON EVERY PAGE ---------------------------
   Campbell, this session: "whatever changes are made for mobile should be made
   for the entire site."

   So the two material moves that fixed the home page's opening are not the home
   page's. Every page's H1 is now an impression in the sheet, the same deboss,
   the same strike as the ink spreads to its set track when the line arrives, and every page's opening line ends in the same red seal. Six pages, one
   material, one mark. The layout of those pages is untouched: this is what the
   type is MADE OF, not where it sits. */
.display {
  filter:
    drop-shadow(0 1px 0 color-mix(in srgb, #fff 82%, transparent))
    drop-shadow(0 -1px 1px color-mix(in srgb, var(--ink) 13%, transparent));
}
.on-dusk .display {
  filter:
    drop-shadow(0 1px 0 color-mix(in srgb, var(--rattan-lit) 22%, transparent))
    drop-shadow(0 -1px 2px color-mix(in srgb, #000 50%, transparent));
}
@media (prefers-reduced-motion: no-preference) {
  .display[data-rise] {
    letter-spacing: -.016em;
    transition: opacity var(--dur-cinematic) var(--ease-expressive),
                transform var(--dur-cinematic) var(--ease-expressive),
                letter-spacing 1.15s var(--ease-expressive);
  }
  .display[data-rise][data-in="true"] { letter-spacing: .008em; }
}

/* --- 6. THE ENGRAVED RULE ------------------------------------------------
   Fifteen section heads across the site were seated by the same 1px gray line,
   which is the default an editor gives you and reads as one. It is the ornament
   a printed card actually carries: a gold hairline that fades out at both ends
   with a cut lozenge at its center. Same gold as the fillet ruled around every
   photograph on the site, so the furniture and the frames are one hand. */
.rule {
  position: relative; border: 0;
  width: clamp(88px, 13vw, 148px); height: 1px;
  /* reads --box-axis, so a host that declares its centre gets a centred rule.
     THIS is the .rule that wins: there is an earlier one further up the sheet,
     and editing that one changed nothing at all. Diagnosed by reading the
     COMPUTED width (148px, from the clamp below, not the earlier 96px clamp),
     which is the same lesson this build has banked three times: find the rule
     that wins, never the first one that matches. */
  margin: var(--space-6) var(--box-axis, 0);
  background: linear-gradient(90deg,
    transparent 0%,
    color-mix(in srgb, var(--gold) 78%, transparent) 26%,
    color-mix(in srgb, var(--gold) 78%, transparent) 74%,
    transparent 100%);
  overflow: visible;
}
.rule::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  background: var(--gold); transform: rotate(45deg);
}
.on-dusk .rule {
  background: linear-gradient(90deg,
    transparent 0%,
    color-mix(in srgb, var(--rattan) 72%, transparent) 26%,
    color-mix(in srgb, var(--rattan) 72%, transparent) 74%,
    transparent 100%);
}
.on-dusk .rule::after { background: var(--rattan); }

/* --- 7. THE ARRIVAL ------------------------------------------------------
   anti-slop dimension 3 asks for ONE orchestrated page-load staggered into
   assigned landing spots. This build had a scroll reveal doing double duty as
   an arrival, which is not the same thing: everything above the fold simply
   faded up together at 0ms and the page was just THERE. A page that is just
   there is a page a builder generated.

   Now every page opens in one short sequence, on the tempo Campbell set twice
   ("peaceful jazz, not hip-hop"): the mark settles into the masthead, the
   opening line is struck into the sheet, and the engraved rule is DRAWN from
   its own center lozenge outward, the way a rule is actually ruled. Nothing is
   held back, nothing blocks paint, and the whole thing is over in under a
   second. Under reduced motion the page is simply present, complete and still. */
@media (prefers-reduced-motion: no-preference) {
  /* THE HOLD. These two run off the clock rather than off a reveal, so behind
     the arrival sheet they would have finished before anyone could see them.
     They wait for it instead, and the moment the sheet is released early the
     hold goes to zero and they come straight in. (site.js holds the [data-rise]
     reveals the same beat, off the sheet's own animation.) */
  .rail__mark { animation: settle .9s var(--ease-expressive) calc(.05s + var(--arr-hold, 0s)) both; }
  @keyframes settle {
    from { opacity: 0; transform: translateY(-10px); }
    to   { opacity: 1; transform: none; }
  }

  /* the rule is RULED: the lozenge is set first, then the hairline is drawn out
     to both ends from under it. Two elements, one gesture. */
  .rule { transform: scaleX(0); transform-origin: 50% 50%; transition: transform 1.1s var(--ease-expressive) .18s; }
  .rule::after { opacity: 0; transition: opacity var(--dur-section) var(--ease-standard) .1s; }
  .rule[data-in="true"], .rule:not([data-rise]) { transform: scaleX(1); }
  .rule[data-in="true"]::after, .rule:not([data-rise])::after { opacity: 1; }
  /* a rule that is not part of a reveal (inside a card) is simply drawn on load */
  .rule:not([data-rise]) { animation: draw 1.1s var(--ease-expressive) calc(.18s + var(--arr-hold, 0s)) both; }
  @keyframes draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
}

/* --- 8. THE STRIKE IS THE SIGNATURE, NOT THE HERO'S TRICK ----------------
   A page whose opening is composed and whose body reverts to picture-then-
   heading-then-paragraph is a half-composed page, and half-composed reads as
   templated: the visitor feels the moment the authoring stops. anti-slop's rule
   is one structural signature carried through, everything else varying.

   So on the phone, where the picture leads its act, Sit. and Stay. are struck
   into their own prints exactly as Come. is struck into the hero: a weighted
   bottom mat, the word landing on it, and the frame's border ruling through the
   letters. Three monuments, one gesture, the length of the page. On desktop the
   words sit BESIDE their pictures (Campbell's approved left axis, untouched),
   so the strike belongs to the composition the phone actually has.

   The act's row gap has to go to zero first: the negative margin has to lift
   the word against the grid's own gap, and fighting a gap with a margin is how
   you get a number that only works at one width. */
@media (max-width: 59.99rem) {
  .split:has(> .split__see > .struck) { gap: 0; }
  .struck { padding-bottom: calc(var(--t-hero) * 0.62); }
  .struck::before {
    inset: calc(var(--mat) - 3px) calc(var(--mat) - 3px) calc(var(--t-hero) * 0.62 - 3px);
  }
  .split__say > .hero-word {
    margin-top: calc(var(--t-hero) * -0.381);  /* moves with --strike; see the hero */
    position: relative; z-index: 2;
  }
  /* the deck that follows the monument keeps its own air */
  .split__say > .hero-word + * { margin-top: var(--space-5); }
}


/* --- 9. THE ARRIVAL ======================================================
   Campbell, 2026-07-23: "an intro screen with the logo slowly going towards
   the viewer then gracefully fading away, like a dissolve or a more artistic
   water color type transition / ink bleed done tastefully... the logo with the
   wording appearing (like it looks but hand drawn/written) letter by letter...
   as the landing page arrives ever so inviting."

   THE SHEET IS THE SITE'S OWN PAPER. This is not a black screen with a logo
   centred on it, which is the shape every template preloader takes. It is the
   same bone sheet, the same hand-blocked sage field and the same spring
   light-fall the page below is printed on. So when it goes there is no cut:
   the ink soaks into the paper, and the page is already standing in the paper
   it soaked into.

   THE INK IS THEIRS. web/assets/img/arr/*.png are their own mark-h.png split
   along the ink islands already in it (tools/pigment.py), the dogs on one
   layer, the twenty-four letters on another, plus a wet copy of each put
   through real pigment physics: the paper's fibre pulling the edge about, the
   spread past where the nib went, the granulation as it pools, the thin wash
   standing off the edge. At rest the layers ARE their lockup, pixel for pixel.
   Nothing is redrawn and nothing is re-set in a substitute face, so the script
   really is their script.

   THEIR DESIGNER ALREADY ENCODED THE TWO DOGS IN THE LETTERFORMS, and the
   sequence obeys it. "Lou Lou's" is a flowing script and is WRITTEN, stroke
   after stroke at a hand's tempo, with the lift between the two words.
   FOSTER'S is an inscriptional roman and is CUT IN, one deliberate capital at
   a time. The charming one and the composed one: the same opposite pair the
   two dogs already carry in their motion laws on the page below. The mark
   arrives first and the name is written AROUND it, which is simply true, the
   restaurant is named after the dogs.

   EVERY LAYER IS A MASK, painted from --ink. Their lockup is one flat slate,
   so three colour channels of it would be triple the bytes for nothing; as
   grey masks the whole sequence is ~100kb instead of half a megabyte racing
   the hero photograph. It is also the only reason the wet wash can be a
   THINNER mix of the same pigment instead of a second set of files. A mask on
   an element clips its own pseudo-children, so wet and set are ::before and
   ::after, siblings, never nested.

   HOW IT MOVES, and why it costs nothing. Every frame is opacity and
   transform, which the compositor owns. The watercolour is NOT a live filter:
   an animated feTurbulence will put a phone on its knees, and this build has
   already banked one dead scroll-timeline for exactly that class of reason.
   The pigment was rendered once, offline; the sequence cross-fades wet into
   set. Twenty-four letters cost two image decodes between them, each a sprite
   window onto the same pair of files.

   The tempo lives in ARR_T in build.mjs and nowhere else, arriving here as
   --d per letter, and every frame of the sequence is CSS: the script only ever
   opens the door and takes a hand off the latch. The sheet is display:none
   until assets/arrival.js sets data-arrival, so no script means no door, not
   a door that will not open. It runs ONCE a session, on the home page only,
   never under reduced motion, and any touch, key or scroll releases it. */

.arrival { display: none; }

html[data-arrival] .arrival {
  display: block;
  position: fixed; inset: 0; z-index: 900;
  /* A light island restates its own tokens rather than borrowing them, the
     law this build learned twice, at the invitation card and again at the
     standing card. Nothing above may recolour the sheet. */
  --paper: #F4F1E9;
  --paper-deep: #E4DDD0;
  --rattan-lit: #E9B475;
  --sage: #A6BDA4;
  --sage-deep: #91A591;
  --rattan: #C89B62;
  --ink: #1E3035;
  --wash: color-mix(in srgb, var(--ink) 76%, var(--sage));
  /* HOW FAR OFF THE MARK STARTS, and how soft. These are the only two numbers
     in the sequence that answer PHYSICAL size rather than proportion: the same
     .38 and 7px that read as depth across 830px of desktop lockup erase a
     351px phone one completely, and the door opens on eight hundred
     milliseconds of blank paper, which reads as a page that has not loaded.
     The gesture is identical at both widths; only the distance is honest. */
  --arr-far: .38;
  --arr-blur: 7px;
}
@media (max-width: 46rem) {
  html[data-arrival] .arrival { --arr-far: .54; --arr-blur: 4px; }
}
html[data-arrival="on"] { --arr-hold: 3.12s; }
html[data-arrival="out"] { --arr-hold: 0s; }
html[data-arrival="on"] .arrival { animation: arr-gone 1ms linear var(--arr-end,4020ms) both; }
@keyframes arr-gone { to { visibility: hidden; pointer-events: none; } }

/* THE SHEET. Sun pooled high-left falling to a deeper floor, the block print
   whispering across the whole of it, and just enough vignette that the eye is
   held at the centre without ever being told to look there. */
html[data-arrival] .arrival__sheet {
  position: absolute; inset: 0;
  background: radial-gradient(116% 88% at 20% 2%,
    color-mix(in srgb, var(--rattan-lit) 34%, var(--paper)) 0%,
    var(--paper) 38%,
    var(--paper-deep) 78%,
    color-mix(in srgb, var(--ink) 9%, var(--paper-deep)) 100%);
}
html[data-arrival="on"] .arrival__sheet {
  animation: arr-sheet var(--arr-paper-dur,690ms) cubic-bezier(.4,0,.72,.42) var(--arr-paper,3290ms) both;
}
html[data-arrival] .arrival__sheet::before {
  content: ''; position: absolute; inset: 0;
  background: var(--sage); opacity: .135;
  /* the same repeat the page's own ground is printed at, at 520px a phone
     read as three big blooms instead of a hand-blocked cloth */
  -webkit-mask: url(/assets/img/blockprint.svg) 0 0 / clamp(200px, 21vw, 300px) auto repeat;
          mask: url(/assets/img/blockprint.svg) 0 0 / clamp(200px, 21vw, 300px) auto repeat;
}
/* THE MIS-REGISTER: a hair of the deeper sage under the print, off by a few
   pixels, the way a hand-blocked repeat never lands twice in the same place.
   The site prints its own ground this way; the sheet is the same cloth. */
html[data-arrival] .arrival__sheet::after {
  content: ''; position: absolute; inset: -14px 0 0 -10px;
  background: var(--sage-deep); opacity: .07;
  -webkit-mask: url(/assets/img/blockprint.svg) 0 0 / clamp(200px, 21vw, 300px) auto repeat;
          mask: url(/assets/img/blockprint.svg) 0 0 / clamp(200px, 21vw, 300px) auto repeat;
}
/* the room the sheet is lit in: the eye is held at the centre without ever
   being told to look there */
html[data-arrival] .arrival__stage::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(74% 58% at 50% 46%,
    transparent 38%, color-mix(in srgb, var(--ink) 13%, transparent) 100%);
}
/* it is part of the sheet and it leaves WITH the sheet. Left on its own timing
   it would hang over the revealed page and put a dark ring round the hero. */
html[data-arrival="on"]  .arrival__stage::after {
  animation: arr-sheet var(--arr-paper-dur,690ms) cubic-bezier(.4,0,.72,.42) var(--arr-paper,3290ms) both;
}
html[data-arrival="out"] .arrival__stage::after {
  animation: arr-sheet-now 560ms ease-in 60ms both;
}
@keyframes arr-sheet { to { opacity: 0; } }

html[data-arrival] .arrival__stage {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  padding: 0 5vw;
}
/* their mark keeps its own aspect at every width, so it is never stretched by
   a hair, and it is CENTRED on the phone and the desktop alike (his standing
   law: nothing new is left aligned) */
html[data-arrival] .arrival__lockup {
  position: relative;
  width: min(90vw, 52rem);
  aspect-ratio: var(--ar);
}

/* --- THE DOGS, COMING TOWARD THE VIEWER ---------------------------------
   Two copies of the one shape: the wet blot, and the ink set. It travels from
   far off and small to its own size on a long deceleration, the blot handing
   over to the ink as it gets close, which is what coming toward you actually
   looks like, a depth of field resolving, not a box being scaled up. */
html[data-arrival] .arr__sym { position: absolute; inset: 0; }
html[data-arrival="on"] .arr__sym {
  animation: arr-focus var(--arr-sym-dur,1500ms) cubic-bezier(.38,.12,.26,1) var(--arr-sym,60ms) both;
}
@keyframes arr-focus {
  from { filter: blur(var(--arr-blur, 7px)); }
  70%  { filter: blur(1.1px); }
  to   { filter: blur(0); }
}
html[data-arrival] .arr__sym::before,
html[data-arrival] .arr__sym::after {
  content: ''; position: absolute; inset: 0;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;   mask-size: 100% 100%;
  opacity: 0;
}
html[data-arrival] .arr__sym::before {
  background: var(--wash);
  -webkit-mask-image: url(/assets/img/arr/sym-wet.png);
          mask-image: url(/assets/img/arr/sym-wet.png);
}
html[data-arrival] .arr__sym::after {
  background: var(--ink);
  -webkit-mask-image: url(/assets/img/arr/sym.png);
          mask-image: url(/assets/img/arr/sym.png);
}
html[data-arrival="on"] .arr__sym::after {
  animation: arr-sym-ink var(--arr-sym-dur,1440ms) cubic-bezier(.38,.12,.26,1) var(--arr-sym,60ms) both,
             arr-sym-out var(--arr-soak,820ms) cubic-bezier(.42,0,.7,.5) var(--arr-out,3080ms) both;
}
html[data-arrival="on"] .arr__sym::before {
  animation: arr-sym-wet var(--arr-sym-dur,1440ms) cubic-bezier(.38,.12,.26,1) var(--arr-sym,60ms) both,
             arr-soak var(--arr-soak,820ms) cubic-bezier(.3,0,.66,.6) var(--arr-out,3080ms) both;
}
@keyframes arr-sym-ink {
  0%   { opacity: 0; transform: scale(var(--arr-far, .38)) translateY(4%); }
  42%  { opacity: 0; }
  100% { opacity: 1; transform: none; }
}
@keyframes arr-sym-wet {
  0%   { opacity: 0;   transform: scale(calc(var(--arr-far, .38) - .02)) translateY(4.4%); }
  9%   { opacity: 1; }
  60%  { opacity: .82; }
  100% { opacity: 0;   transform: none; }
}
/* THE DEPARTURE. The set ink goes first, it is going back to being wet, and the wet then spreads out and DOWN into the sheet, thinning as it goes. */
@keyframes arr-sym-out { to { opacity: 0; transform: scale(1.05); } }
@keyframes arr-soak {
  0%   { opacity: 0;  transform: none; }
  24%  { opacity: .82; }
  100% { opacity: 0;  transform: scale(1.36) translateY(2.8%); }
}

/* the damp spot the ink is landing in: it arrives a beat before the mark does */
html[data-arrival] .arr__bloom {
  position: absolute; left: 50%; top: 46%;
  width: 66%; aspect-ratio: 1; translate: -50% -50%;
  opacity: 0;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--sage-deep, #91A591) 26%, transparent) 0%,
    color-mix(in srgb, var(--rattan) 10%, transparent) 40%,
    transparent 68%);
}
html[data-arrival="on"] .arr__bloom {
  animation: arr-bloom var(--arr-sym-dur,1440ms) cubic-bezier(.38,.12,.26,1) var(--arr-sym,60ms) both,
             arr-bloom-out var(--arr-soak,820ms) ease-out var(--arr-out,3080ms) both;
}
@keyframes arr-bloom {
  0%   { opacity: 0;   scale: .34; }
  30%  { opacity: 1; }
  100% { opacity: .26; scale: 1; }
}
@keyframes arr-bloom-out { to { opacity: 0; scale: 1.85; } }

/* --- THE NAME, WRITTEN AROUND THEM --------------------------------------
   One <b> per letter, holding two sprite windows: ::before is the wet blot it
   lands as, ::after is the letter set. The blot's window is the wider of the
   two, because wet ink stands off from where the nib actually went.

   THE WOBBLE IS IN THE MOTION AND NEVER IN THE ARTIFACT. A letter arrives a
   touch high, a touch large and off its axis, the way a hand puts one down,
   and settles to exactly zero. The last frame is their lockup, untouched.

   On the way out the words LIFT while the mark soaks DOWN. Two opposite
   departures in one gesture, which is the same opposition the two dogs carry
   in their own motion laws on the page below. */
html[data-arrival] .arr__word { position: absolute; inset: 0; }
html[data-arrival] .arr__ch {
  position: absolute;
  left: calc(var(--x) * 100%);  top: calc(var(--y) * 100%);
  width: calc(var(--w) * 100%); height: calc(var(--h) * 100%);
}
html[data-arrival] .arr__ch::before,
html[data-arrival] .arr__ch::after {
  content: ''; position: absolute;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  opacity: 0;
  will-change: opacity, transform;
}
html[data-arrival] .arr__ch::after {
  inset: 0;
  background: var(--ink);
  -webkit-mask-image: url(/assets/img/arr/word.png);
          mask-image: url(/assets/img/arr/word.png);
  -webkit-mask-size: calc(100% / var(--w)) calc(100% / var(--h));
          mask-size: calc(100% / var(--w)) calc(100% / var(--h));
  -webkit-mask-position: calc(var(--px) * 100%) calc(var(--py) * 100%);
          mask-position: calc(var(--px) * 100%) calc(var(--py) * 100%);
}
html[data-arrival] .arr__ch::before {
  left:   calc((var(--bx) - var(--x)) / var(--w) * 100%);
  top:    calc((var(--by) - var(--y)) / var(--h) * 100%);
  width:  calc(var(--bw) / var(--w) * 100%);
  height: calc(var(--bh) / var(--h) * 100%);
  background: var(--wash);
  -webkit-mask-image: url(/assets/img/arr/word-wet.png);
          mask-image: url(/assets/img/arr/word-wet.png);
  -webkit-mask-size: calc(100% / var(--bw)) calc(100% / var(--bh));
          mask-size: calc(100% / var(--bw)) calc(100% / var(--bh));
  -webkit-mask-position: calc(var(--bpx) * 100%) calc(var(--bpy) * 100%);
          mask-position: calc(var(--bpx) * 100%) calc(var(--bpy) * 100%);
}
html[data-arrival="on"] .arr__ch::after {
  animation: arr-ch var(--arr-ch-dur,340ms) cubic-bezier(.2,.78,.3,1) var(--d) both,
             arr-lift var(--arr-soak,820ms) cubic-bezier(.4,0,.7,.5) var(--arr-out,3080ms) both;
}
html[data-arrival="on"] .arr__ch::before {
  animation: arr-ch-wet var(--arr-ch-dur,340ms) cubic-bezier(.2,.78,.3,1) var(--d) both,
             arr-lift var(--arr-soak,820ms) cubic-bezier(.4,0,.7,.5) var(--arr-out,3080ms) both;
}
@keyframes arr-ch {
  0%   { opacity: 0; transform: translateY(9%) scale(1.09) rotate(.7deg); }
  32%  { opacity: 0; }
  100% { opacity: 1; transform: none; }
}
@keyframes arr-ch-wet {
  0%   { opacity: 0;   transform: translateY(12%) scale(1.15) rotate(1deg); }
  24%  { opacity: .92; }
  100% { opacity: 0;   transform: none; }
}
@keyframes arr-lift { to { opacity: 0; transform: translateY(-13%); } }

/* --- THE RELEASE ---------------------------------------------------------
   A touch, a key or a scroll takes the rest of it away. Nobody is held at a
   door they have already opened. The sheet then runs its own exit AT SPEED
   rather than being cut, because a cut is the one thing this whole sequence
   is arguing against.

   THE EXIT KEYFRAMES ARE NAMED APART FROM THE ONES THEY REPLACE, and that is
   not tidiness. A running animation whose NAME does not change is not
   restarted when the rule changes: the browser keeps the same animation and
   only updates its timing. So handing the already-running arr-gone a delay of
   620ms when 700ms have already elapsed drops it straight into its after
   phase, animationend fires on the spot, and the sheet is CUT, which is the
   one ending this sequence exists to avoid. Proven this session in both
   engines. A fresh name is a fresh animation. The elements carrying TWO
   animations happen to be safe (their list changes at index 0, which forces a
   restart) but relying on that is how the next session gets bitten.

   Every rule here is also a single animation declaration that REPLACES the
   pair above it, a second `animation` shorthand does not merge
   with the first, it wins outright, which this build has already been bitten
   by once at the hero sheen. */
html[data-arrival="out"] .arrival        { animation: arr-gone-now 1ms linear 620ms both; }
html[data-arrival="out"] .arrival__sheet { animation: arr-sheet-now 560ms ease-in 60ms both; }
@keyframes arr-gone-now  { to { visibility: hidden; pointer-events: none; } }
@keyframes arr-sheet-now { to { opacity: 0; } }
html[data-arrival="out"] .arr__sym::after  { animation: arr-sym-out 400ms ease-in both; }
html[data-arrival="out"] .arr__sym::before { animation: arr-soak 520ms ease-out both; }
html[data-arrival="out"] .arr__bloom     { animation: arr-bloom-out 480ms ease-out both; }
html[data-arrival="out"] .arr__ch::after,
html[data-arrival="out"] .arr__ch::before { animation: arr-lift 420ms ease-in both; }

/* Nobody who has asked for less motion is shown a door that moves. The gate in
   the head never sets the attribute for them; this is the belt to that brace,
   so the sheet cannot exist at all. */
@media (prefers-reduced-motion: reduce) {
  html[data-arrival] .arrival { display: none; }
}

/* ==========================================================================
   10. THE BOOK, AND THE INVITATION (2026-07-28)
   Their own reservation book on the public side, and the catering card.
   THE LOOK, and it is law because take one of the catering form was killed on
   sight, "it looks like OpenTable... those red lines don't match the site.
   The white box looks like some nonsense":
     - the primary action is INK. Red stays the seal and nothing else.
     - A LINE, NEVER A BOX.
     - no scattered chips, no callout panels, nothing new left aligned.
   ========================================================================== */

/* A <select> was never given the ruled treatment, so every dropdown in the
   invitation would have rendered as a rounded box on paper, the exact thing
   that got take one killed. It joins its siblings. */
.enquire .field select {
  background: transparent;
  border: 0; border-bottom: 1px solid color-mix(in srgb, var(--ink) 26%, transparent);
  border-radius: 0; padding: var(--space-3) 0;
  appearance: none; -webkit-appearance: none; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, color-mix(in srgb, var(--ink) 45%, transparent) 50%),
                    linear-gradient(135deg, color-mix(in srgb, var(--ink) 45%, transparent) 50%, transparent 50%);
  background-position: right 4px center, right 0 center;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
  transition: border-color var(--dur-element) var(--ease-standard);
}
.enquire .field select:focus { outline: none; border-bottom-color: var(--ink); box-shadow: 0 1px 0 var(--ink); }

/* ------------------------------ the book --------------------------------- */
.book__step { border: 0; margin: 0 0 var(--space-6); padding: 0; text-align: center; }
.book__step legend {
  display: block; width: 100%; margin: 0 0 var(--space-3); padding: 0;
  font-size: var(--t-label); font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-faint); text-align: center;
}
.book__sizes, .book__times { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }
.size {
  width: 46px; height: 46px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  background: transparent; color: var(--ink);
  font-family: var(--display); font-size: 1.02rem; cursor: pointer;
  transition: background var(--dur-element) var(--ease-standard), color var(--dur-element) var(--ease-standard);
}
.size:hover { border-color: var(--ink); }
.size[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.tick {
  min-height: 44px; padding: var(--space-2) var(--space-4);
  border-radius: 999px; border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  background: transparent; color: var(--ink); font-family: var(--text); font-size: .96rem;
  cursor: pointer; display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  transition: background var(--dur-element) var(--ease-standard), color var(--dur-element) var(--ease-standard);
}
.tick small { font-size: .62rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.tick:hover { border-color: var(--ink); }
.tick[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.tick[aria-pressed="true"] small { color: color-mix(in srgb, var(--paper) 68%, transparent); }
.book__date {
  display: block; width: auto; min-width: 12rem; margin: 0 auto;
  background: transparent; border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 26%, transparent);
  border-radius: 0; padding: var(--space-3) var(--space-2); color: var(--ink);
  font-family: var(--text); font-size: 1rem; text-align: center;
}
.book__date:focus { outline: none; border-bottom-color: var(--ink); box-shadow: 0 1px 0 var(--ink); }
.book__hint { margin: 0; font-size: .92rem; color: var(--ink-faint); text-align: center; max-width: 40ch; margin-inline: auto; }
.book__hint .link, .book__hint button.link { font-size: inherit; }
.book__hold { margin: var(--space-4) auto 0; font-size: var(--t-label); letter-spacing: .06em;
  color: var(--ink-faint); text-align: center; min-height: 1.2em; }
.book__else { margin-top: var(--space-4); display: grid; gap: var(--space-3); }
.book__else > p { margin: 0; font-size: .88rem; color: var(--ink-faint); text-align: center; }
button.link { background: none; border: 0; cursor: pointer; font: inherit; }

/* the two opt-ins: a line of plain words, never a panel.

   THE SET SHARES ONE COLUMN LINE (2026-08-02). `justify-items: center` shrunk
   each row to its own text and then centred it, so the two checkboxes sat at
   DIFFERENT x: the long row's box at 62px and the short row's at 68px, with
   their words starting 24px apart. Two boxes in a column that do not line up
   read as broken, and the equal-siblings law is explicit that a set is one
   component rendered N times. The rows now share a width, so the group centres
   and the boxes stack on one line. */
.opts {
  display: grid; gap: var(--space-3); margin-top: var(--space-6);
  justify-items: stretch;
  /* the GROUP carries the measure and the centring, so both rows are the same
     width and their boxes cannot drift apart. Capping each row instead let the
     shorter one shrink to its own text. */
  max-width: 38ch; margin-inline: auto; width: 100%;
}
.opt { display: flex; align-items: flex-start; gap: var(--space-3); text-align: left; cursor: pointer; }
.opt input { flex: 0 0 auto; width: 18px; height: 18px; margin-top: 2px; accent-color: var(--ink); }
.opt span { font-size: .88rem; line-height: 1.45; color: var(--ink-soft); }

/* --------------------------- the invitation ------------------------------ */
/* THE SIGNATURE: the card opens on a real sentence with ruled blanks written
   into it, so the four things that decide everything read in one glance and
   the whole thing looks like their stationery instead of an intake screen. */
.wish {
  margin: var(--space-6) auto 0; max-width: 30ch;
  font-family: var(--display); font-size: clamp(1.3rem, 4.2vw, 1.85rem);
  line-height: 1.9; color: var(--ink); text-align: center; text-wrap: balance;
}
.wish__b { display: inline-block; position: relative; }
.wish__b select, .wish__b input {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 78%, transparent);
  border-radius: 0; padding: 0 var(--space-2) 2px;
  font-family: var(--display); font-size: inherit; line-height: 1.3;
  color: var(--ink); text-align: center; cursor: pointer;
  min-height: 0; width: auto; max-width: 100%;
  transition: border-color var(--dur-element) var(--ease-standard);
}
.wish__b--n input { width: 3.4ch; }
.wish__b input[type="date"] { min-width: 8.4ch; cursor: pointer; }
.wish__b select:focus, .wish__b input:focus { outline: none; border-bottom-color: var(--ink); border-bottom-width: 2px; }
.wish__b select:hover, .wish__b input:hover { border-bottom-color: var(--ink); }
.wish__note { margin: var(--space-4) auto 0; max-width: 40ch; font-size: .84rem;
  color: var(--ink-faint); text-align: center; }

/* the two quiet sections beneath it: an engraved band, never a tinted panel */
.enquire__band {
  display: flex; align-items: center; justify-content: center; gap: var(--space-4);
  /* `auto`, not 0: this is a <p>, so it caps at --measure (65ch), and a capped
     box with a zero inline margin sits pinned to its host's left edge while its
     words centre inside it. Measured 5.4px out of true at 1440 before this. */
  margin: var(--space-9) auto var(--space-6);
  font-family: var(--display); font-size: var(--t-label);
  letter-spacing: .3em; text-transform: uppercase; color: var(--ink-faint);
}
.enquire__band i {
  flex: 1 1 auto; max-width: 5.5rem; height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--gold) 70%, transparent), transparent);
}
.enquire__band span { flex: 0 0 auto; }
/* Wesley's line, in the card's own voice. NOT a callout box: a tinted panel
   around a sentence is unearned furniture, and it was killed. */
.enquire__aside {
  margin: var(--space-7) auto var(--space-5); max-width: 44ch;
  font-family: var(--display); font-style: italic; font-size: 1.06rem;
  line-height: 1.6; color: var(--ink-soft); text-align: center;
}
.enquire__call { margin: var(--space-6) auto 0; max-width: 44ch; font-size: .88rem;
  color: var(--ink-faint); text-align: center; }
.enquire__call a { color: var(--ink-soft); text-underline-offset: 4px; }

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

@media (min-width: 52rem) {
  .enquire__fields { grid-template-columns: 1fr 1fr; }
  .enquire .field--wide { grid-column: 1 / -1; }
  .wish { max-width: 40ch; }
}
/* a blank and the punctuation that belongs to it are one unbreakable unit */
.wish__k { display: inline-block; white-space: nowrap; }

/* ==========================================================================
   11. THE TWO DOORS, /join/ and /play/ (2026-07-28)
   A tick list is a RULED ROW, never a pill. Campbell killed the chip cloud
   ("it's just a scattered list of things"), and a cloud of tappable pills is
   an enumeration wearing a costume whether it is skills or dishes.
   The availability grid is a real grid because it is scanned, not read, and
   a grid of days by shifts earns its columns.
   ========================================================================== */
.lab-c {
  margin: var(--space-8) 0 var(--space-3); text-align: center;
  font-size: var(--t-label); font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.ticks {
  display: grid; gap: 0; max-width: 26rem; margin-inline: auto; text-align: left;
  border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
}
.tick-row {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  cursor: pointer; min-height: 46px;
}
.tick-row:hover { background: color-mix(in srgb, var(--rattan) 8%, transparent); }
.tick-row input { flex: 0 0 auto; width: 18px; height: 18px; accent-color: var(--ink); }
.tick-row span { font-size: .98rem; color: var(--ink-soft); }
.tick-row:has(input:checked) span { color: var(--ink); }

/* the grid: days across, the split down */
.avail { max-width: 30rem; margin: 0 auto; }
.avail__t { width: 100%; border-collapse: collapse; }
.avail__t th {
  font-size: .66rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint); padding: var(--space-2) 2px; text-align: center;
}
.avail__t tbody th { text-align: right; padding-right: var(--space-3); white-space: nowrap; }
.avail__t td { padding: 2px; }
.avail__c { position: absolute; opacity: 0; width: 0; height: 0; }
.avail__c + label {
  display: block; aspect-ratio: 1 / 1; min-height: 34px; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  border-radius: 4px; transition: background var(--dur-element) var(--ease-standard);
}
.avail__c + label:hover { border-color: var(--ink); }
.avail__c:checked + label { background: var(--ink); border-color: var(--ink); }
.avail__c:focus-visible + label { outline: 2px solid var(--focus); outline-offset: 2px; }
.avail__note { margin: var(--space-3) 0 0; text-align: center; font-size: .8rem; color: var(--ink-faint); }
/* The grid must FIT the card on a phone, not run out of its right edge: at
   390px the seventh column (Sunday) was being clipped, which reads as a
   missing day rather than a narrow screen. Fixed layout so the columns share
   what there is, a narrow row label, and an overflow container underneath as
   the floor so it can never clip again on a width nobody tested. */
.avail { max-width: 30rem; margin: 0 auto; overflow-x: auto; }
.avail__t { table-layout: fixed; min-width: 17rem; }
.avail__t tbody th {
  width: 4.2rem; padding-right: var(--space-2);
  font-size: .6rem; letter-spacing: .08em;
}
.avail__t thead th { font-size: .58rem; letter-spacing: .04em; padding-inline: 0; }
.avail__t td { padding: 1.5px; }
.avail__c + label { min-height: 30px; }
@media (min-width: 30rem) {
  .avail__t thead th { font-size: .66rem; letter-spacing: .1em; }
  .avail__t tbody th { font-size: .66rem; letter-spacing: .1em; width: 5rem; }
  .avail__c + label { min-height: 34px; }
}
/* A band label longer than a few words ran off both edges of the card at
   390px, because .3em tracking on a full phrase is wider than the phone. The
   label wraps and the rules shrink to whatever is left, so the band holds its
   shape at any width instead of only at the one it was drawn at. */
.enquire__band { flex-wrap: wrap; row-gap: var(--space-2); }
.enquire__band span { max-width: 100%; letter-spacing: .22em; text-wrap: balance; }
.enquire__band i { flex: 1 1 2rem; min-width: 1.5rem; }
@media (max-width: 30rem) {
  .enquire__band { gap: var(--space-3); margin: var(--space-8) auto var(--space-5); }
  .enquire__band span { font-size: .62rem; letter-spacing: .18em; }
}
/* The opt-in rows are a real tap target, not just a checkbox: measured at
   41px, which is under the 44px a thumb wants. The label is the target. */
.opt { min-height: 44px; align-items: center; padding-block: var(--space-1); }
.opt input { margin-top: 0; }

/* THE GRID'S ROW LABELS (2026-07-28, Campbell on /join/: "dinner looked like
   it was inside the box and not properly staged").
   He was right and the cause was mine: `table-layout: fixed` takes its column
   widths from the FIRST ROW ONLY, and the width was declared on `tbody th`,
   which that algorithm never reads. So the label column got an equal 34px
   share while "Dinner" needs 42px, and the word overflowed its own cell and
   sat on top of Monday's box. The width now rides the corner cell in the head
   row, where the fixed algorithm actually looks, and the label keeps a real
   gutter so it can never touch the grid again. */
.avail__t colgroup .avail__col { width: 4.6rem; }
.avail__t thead td.avail__corner { width: 4.6rem; }
.avail__t tbody th {
  width: auto;                     /* the corner cell owns the column now */
  padding-right: var(--space-3);
  white-space: nowrap; overflow: visible;
  font-size: .58rem; letter-spacing: .06em;
}
@media (min-width: 30rem) {
  .avail__t colgroup .avail__col, .avail__t thead td.avail__corner { width: 5.4rem; }
  .avail__t tbody th { width: auto; font-size: .64rem; letter-spacing: .09em; }
}
/* ...and the second half of the same defect: with a rem-width label column,
   the seven day columns each rounded UP by half a pixel and the accumulated
   4px pushed Sunday past the container's right edge, measured scrollWidth 277
   against clientWidth 273, so the last column was clipped even though the box
   "scrolled". Percentages under `table-layout: fixed` resolve against the
   table's own width, so the columns can never sum to more than the table. */
.avail__t { min-width: 0; }
.avail__t colgroup .avail__col, .avail__t thead td.avail__corner { width: 23%; }
@media (min-width: 30rem) { .avail__t colgroup .avail__col, .avail__t thead td.avail__corner { width: 20%; } }
/* ...and the last 1.39px of it: `aspect-ratio: 1/1` together with a
   `min-height` forces a SQUARE at least that tall, so in a column narrower
   than the minimum the box grew sideways and three of Sunday's tap targets
   hung 1.39px past the container. Height is stated outright and the width is
   left to follow the column, which is what a grid cell should do. */
.avail__c + label { aspect-ratio: auto; height: 30px; min-height: 0; width: auto; }
@media (min-width: 30rem) { .avail__c + label { height: 34px; } }

/* CONTRAST, measured by the wall at ship: every field label on the invitation
   cards read 3.65:1 against a 4.5:1 requirement, because --ink-faint is a
   whisper colour meant for asides, not for the word telling somebody what to
   type. Functional text gets --ink-soft, which measures well clear. The
   decorative small print keeps the faint ink. */
.enquire .field > label,
.book__step legend,
.lab-c { color: var(--ink-soft); }
/* the same law for the engraved band labels and the grid's instruction: they
   are read, not decoration, so they carry readable ink */
.enquire__band, .avail__note, .wish__note, .enquire__call { color: var(--ink-soft); }

/* THE DRAWER'S SECOND GROUP (2026-07-28, Campbell: "The 'Work With Us' and
   'Play at Foster's' in the footer work well. Do you think it would be in the
   hamburger menu?").
   Yes, here, and NOT in the desktop top row. The top row is the visit, and a
   stranger arrives hungry. But on a phone the drawer is the ENTIRE navigation
   and the footer is a six-thousand-pixel scroll away, so somebody after a job
   or a Friday night slot would never reach it. Under a gold rule, at secondary
   weight: found by whoever is looking, read past by whoever is not. */
.drawer__rule {
  display: block; height: 1px; width: 4.5rem; margin: var(--space-6) 0 var(--space-5);
  background: linear-gradient(90deg, color-mix(in srgb, var(--gold) 80%, transparent), transparent);
}
.drawer a.drawer__aside {
  font-family: var(--text);
  font-size: 1rem; letter-spacing: .04em;
  color: var(--ink-soft);
  padding-block: var(--space-3); min-height: 48px;
  display: flex; align-items: center;
}
.drawer a.drawer__aside:hover { color: var(--ink); }
.drawer a.drawer__aside[aria-current="page"] { color: var(--ink); text-decoration: underline; text-underline-offset: 5px; }

/* ==========================================================================
   THE CONTROL HOST, ON ITS SPINE AT EVERY WIDTH.

   A paragraph whose entire content is one control is not reading matter: it is
   the control's host, and a lone control centres on the box that holds it.

   Measured before this was written (tools/align-diff.mjs): at 1440 one page
   carried three different answers. "Read their story" sat 188px LEFT of its
   column's centre, "Read the menu" 150px RIGHT of it, "What is pouring" 142px
   left, and on /services/ "Take a table" and "Find us upstairs" sat 258px and
   215px off with ZERO space above them. Nobody authored that. Each was
   inheriting whatever its container happened to say, and it is exactly the
   scatter Campbell photographed ("both buttons seem to be placed right at the
   bottom of the text and that seems wrong").

   :only-child is doing real work here. The looser `p:has(> .link)` that used to
   do this on phones alone also matched a SENTENCE that merely contains a link,
   and centring a sentence because there is a link inside it is a different bug.

   The margin is the other half of his note: a control pressed against the last
   line of the text it follows reads as part of the paragraph, not as the thing
   to press.
   ========================================================================== */
main p:has(> .btn:only-child),
main p:has(> .link:only-child),
main p:has(> .act__go:only-child) {
  text-align: center;
  margin-top: var(--space-6);
}

@media (max-width: 59.99rem) {
  /* THE WHOLE SITE CENTRES ITS COPY ON A PHONE.

     Campbell, 2026-08-03: "the text on paragraphs on the home page that are
     left aligned just look misplaced, seems like centering and properly
     formatting the text would be better on mobile." Scoped to the home page
     on the first pass, because that is the page he named. He then sent
     screenshots of /our-story/ and said: "the images I attached because I
     wanted you to see that they were aligned left and not properly formatted
     on the site." So it is the site, not the route.

     This reverses the phone half of the earned-left reading law for RUNNING
     COPY, on his word, twice given with evidence. What it does NOT touch is
     the structures that are SCANNED rather than read: the menu ledger, the
     ruled points lists, the availability grid, the form fields and tick lists
     all keep their hard left edge, because a centred list with hanging ticks
     reads as broken. Those exemptions are named once, by structure, further
     up this block, and any component that authors its own axis still wins.

     Declared ONCE on the body. A law written as a selector list dies the day
     the markup is rebuilt, and this build has relearned that three times
     (sessions 19, 20, and the catch-all of 20b). */
  body { --copy-align: center; --box-axis: auto; }

  /* Centred copy needs a tighter measure than left-aligned copy, or the ragged
     edge on both sides reads as a block rather than as lines.

     It caps READING COPY ONLY, and the exclusion list is the same one the
     alignment law uses above, because the two must always name the same set.
     Capping everything capped a LABEL: "Behind the bar" on the blank bar card
     is a signpost with `margin: 0` of its own, and a shorthand margin erases
     the margin-inline that would have re-centred the narrowed box, so it sat
     140px off its host at 768px. axis-probe caught it; the eye would not
     have. Do not narrow a box that cannot centre itself. */
  main p:not(:is(.sub, .lead, .fine, .deck, .engraved, .quote__say, .quote__who,
                 .dish__line, .dish__of, .course__note, .blankcard__head)) {
    max-width: 36ch;
  }

  /* THE SCANNED STRUCTURES KEEP THEIR HARD LEFT EDGE.

     This is the other half of the law and it is not optional. Left is earned
     by being SCANNED rather than read, and the menu is the definitive case: a
     dish line is a name, a run of leader dots and a price on a rail, and
     centring it destroys the one page Campbell has named a keeper twice. The
     first pass at his "centre it on mobile" note did exactly that to 18 menu
     rows, caught by align-diff rather than by eye.

     Named ONCE, by structure, never by section: a ledger, a ruled list, an
     availability grid, a form field, a tick list, a table. A new act joins the
     centred law by existing; a new scanned structure joins this one by being
     one of these things. */
  main .points, main .points li,
  main .dish__line, main .dish__of, main .course__note,
  main .avail, main .field, main .ticks, main .opt, main .opts,
  main table, main th, main td { text-align: left; }
}
