/*
  GENERATED by tools/build-css.mjs — do not edit this file.
  Edit the sources in public/css/ and run `npm run build`.
  Concatenated in layer order: fonts.css → base.css → components.css → pages.css
*/
/* ======================================================================
   fonts.css
   ====================================================================== */
/*
  Self-hosted webfaces. Generated by tools/subset-fonts.py — do not hand-edit.

  These used to be one stylesheet from fonts.googleapis.com, which put a
  third-party origin in front of first paint: resolve, connect, TLS, fetch the
  CSS, and only then start on a font. Same files, same origin as the page now.

  font-display: swap throughout. The alternative is a beat of invisible text,
  and on a page whose whole top half is one enormous headline that reads as a
  broken site.

  The two Korean faces are cut to the syllables the site actually uses — see
  the header of the generating script. Their unicode-range keeps a browser
  from fetching them at all for a page with no Korean on it.
*/

/* ------------------------------------------------- display: the poster voice */
@font-face {
  font-family: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/anton.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/anton-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* --------------------------------- body + sub: Archivo, variable on two axes */
/* The wdth axis is what --f-sub narrows to 88; keep the range or it snaps to
   100 and every card title widens. */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 300 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url('/fonts/archivo.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 300 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url('/fonts/archivo-ext.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* -------------------------------------------------------- Hangul: display */
@font-face {
  font-family: 'Black Han Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/black-han-sans.woff2') format('woff2');
  unicode-range: U+0020-007E,U+00B7,U+1100-11FF,U+2013-2014,U+3130-318F,U+AC00-D7A3;
}

/* --------------------------------------------------------- Hangul: running */
@font-face {
  font-family: 'Noto Sans KR';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/noto-sans-kr.woff2') format('woff2-variations');
  unicode-range: U+0020-007E,U+00B7,U+1100-11FF,U+2013-2014,U+3130-318F,U+AC00-D7A3;
}

/* ======================================================================
   base.css
   ====================================================================== */
/* =========================================================================
   OPPA HOUSE — base.css

   Concept: 불타는 밤 — the burning night.
   A Korean BBQ street after dark: black asphalt, a wall of hand-bent neon,
   bilingual signage stacked English over Hangul, and grill light coming up
   off the table. Everything glows or it is out of the way. The page has
   three voices and no more — the logo's red is the house, cyan is the useful
   information, gold is heat. Body copy never glows, because people have to
   read it.
   ========================================================================= */

@layer reset, tokens, base, atmosphere, components, pages, utils;

/* ---------------------------------------------------------------- RESET */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  /*
    The UA's [hidden] rule is a bare `display: none` and loses to any author
    class that sets display — `.btn` is inline-flex, so a hidden button shows
    anyway. Needs the bang to hold against every such class.
  */
  [hidden] { display: none !important; }

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: 6rem;
  }
  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
  }

  body { min-height: 100svh; }

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

  input, button, textarea, select { font: inherit; color: inherit; }
  button { background: none; border: 0; cursor: pointer; }
  a { color: inherit; text-decoration: none; }
  ul, ol { list-style: none; }
  table { border-collapse: collapse; width: 100%; }
  :target { scroll-margin-top: 7rem; }
}

/* --------------------------------------------------------------- TOKENS */
@layer tokens {
  :root {
    /* ---- The street ---- */
    --void:    #08060C;   /* wet asphalt at 1am */
    --char:    #120E1A;   /* raised panels */
    --char-2:  #1B1526;
    --line:    #2C2340;   /* hairlines, unlit tube, fence wire */

    /* ---- Ink ---- */
    --paper:   #F7F1E4;   /* warm cream — all body copy and big headlines */
    --mist:    #B3A9C0;
    /* Carries small caps labels, so 4.5:1 on the raised --char panels is the
       bar, not just on --void. Measured at 5.2:1 there. */
    --mist-2:  #8E8399;

    /* ---- The three voices ----
       Each has a tube colour (the bright core you see glowing) and a fill
       colour (what it looks like as a solid block of paint). A neon tube is
       always brighter than the same colour painted flat, so these differ —
       and the fills are chosen so their ink partner clears AA on top. */
    /* Pulled to the red the logo is printed in. The lockup carries two: the
       wordmark at hue 0 and the plate's stripes at hue 353. The tube sits
       between them at 356 — far enough off pure red not to go orange under
       its own glow, far enough off pink to be the same colour as the sign. */
    --neon:      #FF2E3C;   /* signal red — the house voice */
    --neon-fill: #C7121E;   /* the same red as paint; cream on it = 5.5:1 */
    --neon-soft: #FF7D86;   /* red lifted for small text and error copy */

    --ice:       #2BE8E8;   /* electric cyan — signage, wayfinding, the useful */
    --gold:      #FFC53D;   /* grill gold — heat, the bar, the flame */
    --flame:     #FF7A2F;   /* ember, for the fire under the grill only */

    /* Kept so the love-lock padlocks and petals still have their colour. */
    --sakura:    #F5A9BC;

    /* The live accent. Zones move these three together — never one alone,
       or a gold button ends up with cream ink on it. */
    --accent:      var(--neon);
    --accent-fill: var(--neon-fill);
    --accent-ink:  #FFF4F4;

    /* ---- Type ----
       What each voice is for:

         --f-display  the poster voice. Heavy condensed caps, the lettering
                      screen-printed on a night-market banner. h1/h2, prices,
                      big numbers, every loud label.
         --f-sub      Archivo at width 88 / weight 800 — the poster voice
                      narrowed to a size you can read a sentence in.
         --f-ko-dis   Hangul display. Black Han Sans is the Korean equivalent
                      of the poster face, and it is what neon signage is
                      actually set in.
         --f-ko       running Korean.
         --f-body     the quiet one; everything you actually read.

       Anton and Black Han Sans each ship ONE weight and no italic. Never ask
       them for bold or italic — the browser fakes it and heavy condensed caps
       fake very badly. Emphasis is colour and glow.

       There was a --f-brand here, Alfa Slab One, for the wordmark. The sign
       is a PNG lockup now, so nothing ever set it: the token and the font
       both went. */
    --f-display: "Anton", "Archivo", "Haettenschweiler", Impact, sans-serif;
    --f-sub:     "Archivo", ui-sans-serif, system-ui, sans-serif;
    --f-ko-dis:  "Black Han Sans", "Apple SD Gothic Neo", sans-serif;
    --f-ko:      "Noto Sans KR", "Apple SD Gothic Neo", sans-serif;
    --f-body:    "Archivo", ui-sans-serif, system-ui, sans-serif;

    /* Fluid scale, 320 -> 1440. Anton is condensed, so it buys back the room
       the slab was eating — the top of the scale goes up, not down. */
    --t-xs:   clamp(0.75rem, 0.72rem + 0.15vw, 0.82rem);
    --t-sm:   clamp(0.86rem, 0.82rem + 0.2vw,  0.95rem);
    --t-base: clamp(1rem,    0.96rem + 0.25vw, 1.1rem);
    --t-md:   clamp(1.1rem,  1.02rem + 0.4vw,  1.32rem);
    --t-lg:   clamp(1.35rem, 1.15rem + 0.9vw,  1.95rem);
    --t-xl:   clamp(2rem,    1.5rem + 2.3vw,   3.4rem);
    --t-2xl:  clamp(2.6rem,  1.7rem + 4.2vw,   5.4rem);
    --t-3xl:  clamp(2.55rem, 1.5rem + 5.6vw,   7.4rem);

    /* ---- Space ---- */
    --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;
    --s-4: 1rem;     --s-5: 1.5rem;   --s-6: 2rem;
    --s-7: 3rem;     --s-8: 4rem;     --s-9: 6rem;
    --s-10: 8rem;
    --section-y: clamp(4.5rem, 3rem + 7vw, 9rem);

    /* ---- Layout ---- */
    /* Outer height of the floating nav tube: its own top padding plus the
       4.15rem bar plus clearance so content never tucks under the bloom.
       Every page's top offset reads this — do not hard-code it again. */
    --nav-h: 5.7rem;

    --wrap: 80rem;
    --wrap-narrow: 52rem;
    --gutter: clamp(1.15rem, 0.6rem + 2.6vw, 3.5rem);
    /* Neon is bent, not cut. Nothing on this site has a sharp corner. */
    --radius: 10px;
    --radius-lg: 16px;
    --radius-pill: 999px;

    /* ---- Motion ---- */
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);

    color-scheme: dark;
  }
}

/* ------------------------------------------------------------ THE NEON */
/*
  One recipe for every glowing edge on the site. A real tube reads as a
  white-hot core inside a coloured halo, so the border is the accent mixed
  toward white and the bloom is three shadows at falling opacity — tight,
  mid, wide. Set --tube on any element to relight it.
*/
@layer base {
  .tube,
  .tube-t {
    --tube: var(--accent);
  }

  .tube {
    border: 2px solid color-mix(in srgb, var(--tube) 82%, #fff);
    border-radius: var(--tube-r, var(--radius));
    box-shadow:
      0 0 4px   color-mix(in srgb, var(--tube) 80%, transparent),
      0 0 14px  color-mix(in srgb, var(--tube) 42%, transparent),
      0 0 40px  color-mix(in srgb, var(--tube) 18%, transparent),
      inset 0 0 12px color-mix(in srgb, var(--tube) 16%, transparent);
  }

  /* Glowing type. Deliberately not available to body copy — a halo on a
     paragraph is what makes neon sites unreadable. */
  .tube-t {
    color: var(--tube);
    text-shadow:
      0 0 6px  color-mix(in srgb, var(--tube) 55%, transparent),
      0 0 18px color-mix(in srgb, var(--tube) 32%, transparent);
  }
}

/* ----------------------------------------------------------------- BASE */
@layer base {
  body {
    background: var(--void);
    color: var(--paper);
    font-family: var(--f-body);
    font-size: var(--t-base);
    font-weight: 400;
    line-height: 1.7;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  /*
    Headlines are the banner over the street: condensed caps, set tight
    enough that two lines lock into one block. Weight 400 is not a choice —
    it is the only weight Anton has, and declaring it stops the browser
    synthesising a heavier one.

    On leading: Anton's cap height measures 0.867em — most faces sit near
    0.70. Sub-1 leading is normal for a poster face and disastrous for this
    one: at 0.9 the next row's capitals land 0.025em under the previous
    baseline, which is the two lines touching. Everything below 1.0 collides.
    1.04 is the tightest that still leaves air, and the glow needs some of
    that air too. Measure the cap height before tightening this again.
  */
  h1, h2 {
    font-family: var(--f-display);
    font-weight: 400;
    line-height: 1.04;
    letter-spacing: 0.005em;
    text-transform: uppercase;
    text-wrap: balance;
  }
  h1 { font-size: var(--t-2xl); }
  h2 { font-size: var(--t-xl); }

  /*
    The plain-English line inside an h1.

    Every headline on this site is written to be heard rather than read —
    "Nobody is listening but us" is a good line and a bad description. This
    carries what the page is actually about, in the same heading, so the
    poster voice keeps the top line and nothing has to explain itself twice.

    It steps all the way out of the display face: Archivo at the h3 posture,
    sentence case, mist. Loud line, quiet line, and the size gap between them
    is what keeps it from reading as a second headline.
  */
  .h1__sub {
    display: block;
    margin-top: var(--s-3);
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-size: var(--t-md);
    font-weight: 600;
    line-height: 1.35;
    letter-spacing: 0;
    text-transform: none;
    color: var(--mist);
  }
  /*
    Narrow screens wrap headings onto more rows, so every gap repeats and a
    tight setting compounds. Open it up rather than shrink the type — the
    shout is the point.
  */
  @media (max-width: 48rem) {
    h1, h2 { line-height: 1.08; }
  }

  /*
    h3 steps out of the poster face into Archivo narrowed to 88 — same
    posture, but it can hold a full card title without shouting. Archivo is
    variable on both axes, so this is one file, not a second family.
  */
  h3 {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-size: var(--t-lg);
    font-weight: 800;
    line-height: 1.08;
    letter-spacing: -0.01em;
    text-wrap: balance;
  }

  p { text-wrap: pretty; max-width: 66ch; }
  strong { font-weight: 700; color: #fff; }

  ::selection { background: var(--neon); color: #fff; }

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

  .skip {
    position: absolute;
    left: var(--s-4);
    top: -100%;
    z-index: 999;
    background: var(--neon-fill);
    color: #fff;
    font-family: var(--f-body);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    font-size: var(--t-xs);
    padding: var(--s-3) var(--s-5);
    border-radius: var(--radius-pill);
    transition: top 0.2s var(--ease-out);
  }
  .skip:focus { top: var(--s-4); }

  /*
    Sticky footer.

    body already carries min-height:100svh, so on a short page the viewport
    height was there but nothing claimed it — the footer sat wherever the
    content happened to end and left a band of dead space beneath it. /thanks
    is the worst of them because it is deliberately brief, and /404 has the
    same shape.

    Safe to do on body here because only <main> and <footer> are ever in
    flow: .skip is absolute, the header and the mobile tab bar are fixed, and
    <script> is display:none, so none of them become flex items. Nothing
    inside a page changes — the column only decides where the footer lands.

    main takes the slack and never gives it back (flex-shrink 0), so a page
    taller than the viewport is untouched and the footer simply follows the
    content as before.
  */
  body {
    display: flex;
    flex-direction: column;
  }
  body > main { flex: 1 0 auto; }
  body > .footer { flex: none; }
}

/* ----------------------------------------------------- TYPE PRIMITIVES */
@layer base {
  /*
    The recurring section label, rebuilt as a lit sign rather than a
    letterspaced word with a rule. Small, bent, and glowing — the same
    object as the nav pills and the fact strip, at label scale.
  */
  .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.9ch;
    align-self: start;
    /*
      The pill owns the space beneath itself. It used to inherit whatever its
      parent happened to provide, which produced four different gaps across
      the site — and 0 in the common case of a plain <div>, where the glow
      then sat directly on the capitals below it.
    */
    margin-bottom: var(--s-2);
    /* The pill hugs its own text. Without this a grid or flex parent stretches
       it to the column and it stops reading as a badge — which is what the
       three inline `justify-content` overrides in the markup were patching. */
    width: fit-content;
    font-family: var(--f-display);
    font-size: var(--t-xs);
    font-weight: 400;
    letter-spacing: 0.16em;
    line-height: 1;
    text-transform: uppercase;
    color: var(--accent);
    padding: 0.62rem 1.2rem 0.56rem;
    border: 1px solid color-mix(in srgb, var(--accent) 62%, transparent);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--accent) 7%, transparent);
    box-shadow:
      0 0 12px color-mix(in srgb, var(--accent) 22%, transparent),
      inset 0 0 10px color-mix(in srgb, var(--accent) 10%, transparent);
    text-shadow: 0 0 10px color-mix(in srgb, var(--accent) 45%, transparent);
  }
  /* The lit dot at the head of the sign */
  .eyebrow::before {
    content: "";
    width: 5px; height: 5px;
    flex: none;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 8px currentColor;
  }

  /* Long-form voice. Plain and unlit on purpose — this is the paragraph
     people read before they decide to book. */
  .lede {
    font-family: var(--f-body);
    font-size: var(--t-md);
    font-weight: 400;
    line-height: 1.7;
    color: #E9E2D4;
  }

  /* Running Korean */
  .ko { font-family: var(--f-ko); letter-spacing: 0; }
  /* Korean at sign scale — the neon-tube Hangul */
  .ko-dis { font-family: var(--f-ko-dis); letter-spacing: 0.01em; }

  /*
    The accent line inside a headline. In the poster grammar the second line
    changes colour and lights up — that one move carries every heading on
    the site, so it is worth doing properly.
  */
  .pop {
    color: var(--accent);
    text-shadow:
      0 0 10px color-mix(in srgb, var(--accent) 48%, transparent),
      0 0 34px color-mix(in srgb, var(--accent) 26%, transparent);
  }

  /* Vertical Hangul, the way signage runs down an alley wall */
  .vert {
    writing-mode: vertical-rl;
    font-family: var(--f-ko-dis);
    font-weight: 400;
    letter-spacing: 0.3em;
    text-transform: uppercase;
  }
}

/* --------------------------------------------------------- ATMOSPHERE */
@layer atmosphere {
  /*
    The street itself, fixed behind everything: pools of sign-light, the
    smoke they are hanging in, and grain over the lot. All of it is one
    pseudo-element on <html> so no section has to carry it.

    There was a tiled wall here — two repeating gradients at a 62px period.
    It came out because a repeat is the one thing this page cannot carry.
    At constant opacity it never faded into the dark corners the way a real
    wall does, 62px was tight enough to read as graph paper rather than
    tile, and it drew hard straight lines through tables and behind panels.
    It was also the only rigid element on a site made of glow, bloom, petals
    and bent tube, so it fought everything around it.

    What replaced it is smoke. This is a room where meat cooks on the table,
    so haze under the signs is what the place actually looks like — and,
    more usefully, a gradient with no period can never resolve into a
    pattern however long you stare at it.
  */
  html::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -10;
    pointer-events: none;
    background:
      /* red sign on the left, gold grill light low and right */
      radial-gradient(58% 44% at 6% 8%,
        color-mix(in srgb, var(--neon) 16%, transparent), transparent 68%),
      radial-gradient(52% 40% at 96% 78%,
        color-mix(in srgb, var(--gold) 11%, transparent), transparent 70%),
      radial-gradient(46% 34% at 78% 4%,
        color-mix(in srgb, var(--ice) 8%, transparent), transparent 72%),
      /* the smoke those lights are hanging in — wider and weaker than the
         sources above, so it reads as air rather than a fourth lamp */
      radial-gradient(70% 50% at 20% 30%,
        color-mix(in srgb, var(--neon) 7%, transparent), transparent 70%),
      radial-gradient(60% 45% at 80% 60%,
        color-mix(in srgb, var(--flame) 6%, transparent), transparent 72%),
      radial-gradient(80% 60% at 50% 95%,
        color-mix(in srgb, var(--gold) 5%, transparent), transparent 65%),
      var(--void);
  }

  /* Film grain — wet asphalt is never clean */
  body::after {
    content: "";
    position: fixed;
    inset: -50%;
    z-index: 900;
    pointer-events: none;
    opacity: 0.07;
    mix-blend-mode: screen;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
  }

  /* A pool of sign-light dropped behind content. Kept under the old name
     so every page that places one still works; it is a neon spill now. */
  .lamp-glow {
    position: absolute;
    z-index: -1;
    pointer-events: none;
    width: clamp(20rem, 55vw, 46rem);
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(
      circle,
      color-mix(in srgb, var(--flame) 18%, transparent) 0%,
      transparent 66%
    );
    filter: blur(34px);
  }
  .lamp-glow--red {
    background: radial-gradient(
      circle,
      color-mix(in srgb, var(--neon) 30%, transparent) 0%,
      transparent 66%
    );
  }

  /*
    Sakura. Sparse and slow — the tree is a real detail in the waiting room,
    and it is the one soft thing on a very loud page. Left alone.
  */

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

  /*
    ---- TURNED OFF, 3 Aug 2026 ----------------------------------------
    Falling petals are switched off while we decide whether we want them.

    This one line is the whole switch: delete it and they come back. The
    markup is still in all eleven pages, the fall animation and its keyframes
    are still in the motion block below, and the per-petal --pd / --pdl / --px
    timings are still on the <i> elements. Nothing was removed, so this is not
    a change that has to be rebuilt — only reversed.
    --------------------------------------------------------------------
  */
  .petals { display: none; }
  .petals i {
    position: absolute;
    top: -6%;
    width: 11px;
    height: 11px;
    background: var(--sakura);
    opacity: 0;
    /* a petal: round on one axis, pointed on the other */
    border-radius: 100% 0 100% 0;
  }
}

/* --------------------------------------------------------------- MOTION */
@layer atmosphere {
  @media (prefers-reduced-motion: no-preference) {
    .rise {
      opacity: 0;
      transform: translateY(24px);
      animation: rise 1s var(--ease-out) forwards;
      animation-delay: var(--d, 0s);
    }
    @keyframes rise { to { opacity: 1; transform: none; } }

    .obs {
      opacity: 0;
      transform: translateY(30px);
      transition:
        opacity 0.85s var(--ease-out) var(--d, 0s),
        transform 0.85s var(--ease-out) var(--d, 0s);
    }
    .obs.is-in { opacity: 1; transform: none; }

    /* A slow reveal for full-bleed scenery, like a shutter going up */
    .unveil {
      clip-path: inset(0 0 100% 0);
      transition: clip-path 1.4s var(--ease-out) var(--d, 0s);
    }
    .unveil.is-in { clip-path: inset(0 0 0 0); }

    .petals i {
      animation: fall var(--pd, 17s) linear var(--pdl, 0s) infinite;
    }
    @keyframes fall {
      0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
      8%   { opacity: 0.85; }
      92%  { opacity: 0.5; }
      100% { transform: translate(var(--px, 60px), 108vh) rotate(420deg); opacity: 0; }
    }

    /*
      One tube on the page has a bad transformer. Exactly one — a flicker on
      every sign is a novelty; a flicker on one is a street. It settles
      after four seconds and never runs again.
    */
    .flicker { animation: flicker 3.6s steps(1, end) 1 both; }
    @keyframes flicker {
      0%, 4%, 10%, 15%, 22%, 100% { opacity: 1; }
      2%, 12%, 19%                { opacity: 0.25; }
      7%                          { opacity: 0.6; }
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .rise, .obs { opacity: 1 !important; transform: none !important; }
    .unveil { clip-path: none !important; }
    .petals { display: none; }
    *, *::before, *::after {
      animation-duration: 0.001ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important;
    }
  }
}

/* ---------------------------------------------------------------- UTILS */
@layer utils {
  .wrap {
    width: 100%;
    max-width: var(--wrap);
    margin-inline: auto;
    padding-inline: var(--gutter);
  }
  .wrap--narrow { max-width: var(--wrap-narrow); }

  .section {
    position: relative;
    padding-block: var(--section-y);
    isolation: isolate;
  }
  /* An unlit length of tube between rooms — the same line the signs are
     bent from, just not switched on. */
  .section + .section::before {
    content: "";
    position: absolute;
    top: 0;
    left: var(--gutter);
    right: var(--gutter);
    height: 1px;
    background: linear-gradient(90deg,
      transparent,
      color-mix(in srgb, var(--accent) 45%, var(--line)) 22%,
      color-mix(in srgb, var(--accent) 45%, var(--line)) 78%,
      transparent);
  }

  /*
    Three voices, and a page only ever gets one. Pink is the house — the
    default, the party rooms, the fence. Gold is anywhere heat is the actual
    subject: the grill, the bar. Cyan is where the page stops performing and
    starts being useful — directions, reservations.

    Fill and ink move with the accent, never independently, or a gold button
    ends up with cream type on it.
  */
  .zone-karaoke,
  .zone-events,
  .zone-sakura {
    --accent: var(--neon);
    --accent-fill: var(--neon-fill);
    --accent-ink: #FFF4F4;
  }
  .zone-warm,
  .zone-bar {
    --accent: var(--gold);
    --accent-fill: var(--gold);
    --accent-ink: #170F02;
  }
  .zone-amber,
  .zone-jade {
    --accent: var(--ice);
    --accent-fill: var(--ice);
    --accent-ink: #03181A;
  }

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

  .center { text-align: center; }
  .center p { margin-inline: auto; }
  /* align-self only bites in a flex or grid parent; the pill also has to
     centre inside a plain block, which is what .center usually is. */
  .center .eyebrow { align-self: center; margin-inline: auto; }

  .stack > * + * { margin-top: var(--s-4); }
  .muted { color: var(--mist); }
  .tight { max-width: 44ch; }

  /* Status lines under a form — the fence submit, the admin sign-in. */
  .muted.is-pending { color: var(--mist-2); }
  .muted.is-ok { color: var(--ice); }
  .muted.is-error { color: var(--neon-soft); }

  /* Content the client still owes us */
  [data-tk] { position: relative; }
  [data-tk]::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: -2px;
    height: 1px;
    background: repeating-linear-gradient(
      90deg, var(--gold) 0 5px, transparent 5px 10px
    );
  }
}

/* ======================================================================
   components.css
   ====================================================================== */
/* =========================================================================
   OPPA HOUSE — components.css
   Nav, buttons, the love-lock fence, scenery frames, cards, forms, footer.
   Everything sits on near-black. Red speaks; nothing else raises its voice.
   ========================================================================= */

@layer components {

  /* ================================================================ NAV */
  /*
    The sign over the door: one long bent tube with everything mounted
    inside it. It floats off the top edge rather than sitting flush, because
    a neon box needs air around it to read as an object — flush to the
    viewport it just looks like a header with a red line under it.
  */
  .nav {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 100;
    padding: clamp(0.5rem, 1.4vw, 0.95rem) clamp(0.6rem, 2vw, 1.25rem) 0;
  }
  /* The house voice does not drift page to page — the header is always red
     no matter what the zone underneath is lit in. */
  .nav {
    --accent: var(--neon);
    --accent-fill: var(--neon-fill);
    --accent-ink: #FFF4F4;
  }

  .nav__inner {
    position: relative;
    isolation: isolate;
    display: flex;
    align-items: center;
    gap: clamp(0.75rem, 1.8vw, 1.75rem);
    min-height: 4.55rem;   /* the lockup at full size, plus air */
  }
  /*
    The tube itself lives on a pseudo-element, not on .nav__inner. It has to:
    backdrop-filter creates a containing block for fixed descendants, and the
    mobile panel inside here is position:fixed. On the element it would
    resolve the panel's inset against this padded, centred box instead of the
    viewport — the panel would sit low and stop short of both screen edges.
  */
  .nav__inner::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: var(--radius-lg);
    /* Black glass. Still translucent — the tile grid and the hero have to
       read through it — but the tint underneath is the void rather than the
       raised-panel purple, so the tube reads as an unlit sign at night and
       the red on its edge is the only colour in it. */
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--void) 90%, transparent),
        color-mix(in srgb, #000 94%, transparent));
    backdrop-filter: blur(16px) saturate(1.3);
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    border: 1.5px solid color-mix(in srgb, var(--neon) 70%, #fff);
    box-shadow:
      0 0 4px  color-mix(in srgb, var(--neon) 55%, transparent),
      0 0 16px color-mix(in srgb, var(--neon) 28%, transparent),
      0 0 48px color-mix(in srgb, var(--neon) 12%, transparent),
      inset 0 0 20px color-mix(in srgb, var(--neon) 8%, transparent);
    transition: box-shadow 0.35s var(--ease-out);
  }
  /* Once you are past the hero the sign burns a little brighter, the way a
     tube does when the light around it drops. */
  .nav.is-stuck .nav__inner::before {
    box-shadow:
      0 0 5px  color-mix(in srgb, var(--neon) 72%, transparent),
      0 0 22px color-mix(in srgb, var(--neon) 40%, transparent),
      0 0 62px color-mix(in srgb, var(--neon) 18%, transparent),
      inset 0 0 20px color-mix(in srgb, var(--neon) 10%, transparent);
  }

  /*
    The printed lockup, as drawn: plate, steam, name, strapline, crossed
    utensils. It ships as one flat image because the artwork is one drawing —
    the flourishes either side of the plate and the rule under the name only
    read at the proportions the designer set them at.

    The file is the dark-background variant. The original is drawn for light
    stock: the strapline, the flourishes, the rule and the utensils are all
    near-black ink, which is invisible against this header. The variant
    repaints only that neutral ink to --paper and leaves the reds and the
    plate's white stripes exactly as drawn.
  */
  .brand {
    display: flex;
    align-items: center;
    margin-right: auto;
  }
  /*
    A near-square lockup (1140 × 785) in a bar, so height is what's scarce.
    At header scale the name carries and the strapline reads as texture
    rather than words — that is the trade the printed logo asks for.
    The <img> keeps the full name in its alt text, so the accessible name of
    the home link and the first thing a crawler sees are both unchanged.
  */
  .brand__lockup {
    width: auto;
    height: clamp(3.05rem, 2.4rem + 2.4vw, 3.85rem);
    flex: none;
    /* White stock on a lit sign picks up the red around it. */
    filter: drop-shadow(0 0 9px color-mix(in srgb, var(--neon) 45%, transparent));
    transition: transform 0.4s var(--ease-spring), filter 0.35s var(--ease-out);
  }
  .brand:hover .brand__lockup {
    transform: translateY(-2px);
    filter: drop-shadow(0 0 14px color-mix(in srgb, var(--neon) 65%, transparent));
  }

  /* The footer has the vertical room the bar doesn't, so the strapline and
     the utensils get to be legible in at least one place on the page. */
  .brand--footer .brand__lockup {
    height: clamp(5.5rem, 4rem + 5vw, 7.5rem);
  }

  .nav__links {
    display: flex;
    align-items: center;
    gap: clamp(0.85rem, 1.6vw, 1.9rem);
  }
  /*
    Bilingual, stacked: the English the room is booked in, the Hangul the
    room is named in. Two lines is what makes a sign read Korean rather than
    English-with-a-Korean-word-on-it.
  */
  .nav__links a {
    position: relative;
    display: grid;
    justify-items: center;
    gap: 0.1em;
    font-family: var(--f-display);
    font-size: 0.82rem;
    font-weight: 400;
    letter-spacing: 0.11em;
    line-height: 1;
    text-transform: uppercase;
    color: var(--paper);
    padding-block: 0.45rem 0.5rem;
    transition: color 0.25s var(--ease-out),
                text-shadow 0.25s var(--ease-out);
  }
  .nav__links a .ko {
    font-family: var(--f-ko);
    font-size: 0.66em;
    font-weight: 400;
    letter-spacing: 0.02em;
    color: var(--ice);
    opacity: 0.85;
    transition: opacity 0.25s var(--ease-out);
  }
  /* The tube under the current page — drawn short and lit, not a hairline */
  .nav__links a::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 2px;
    border-radius: 2px;
    background: var(--neon);
    box-shadow: 0 0 8px var(--neon);
    transform: scaleX(0);
    transform-origin: center;
    transition: transform 0.35s var(--ease-out);
  }
  .nav__links a:hover {
    color: var(--neon);
    text-shadow: 0 0 12px color-mix(in srgb, var(--neon) 60%, transparent);
  }
  .nav__links a:hover .ko { opacity: 1; }
  .nav__links a:hover::after,
  .nav__links a[aria-current="page"]::after { transform: scaleX(1); }
  .nav__links a[aria-current="page"] {
    color: var(--neon);
    text-shadow: 0 0 12px color-mix(in srgb, var(--neon) 55%, transparent);
  }

  .nav__cta { display: flex; gap: var(--s-4); align-items: center; }

  /*
    On desktop the panel is just a wrapper around the links and the CTAs.
    Without a display it falls back to block and the two stack into a
    second row, which is what made the header twice as tall as it needed.
  */
  /*
    Desktop only. In the mobile panel these same buttons are full-width
    stacked tap targets and must keep their button affordance — stripping
    the ghost's outline there leaves it reading as a floating label.
  */
  @media (min-width: 63.001rem) {
    .nav__panel {
      display: flex;
      align-items: center;
      gap: clamp(1.5rem, 3vw, 2.75rem);
    }

    /* Header buttons are UI, not hero CTAs — the page-level sizing is far
       too heavy up here. One lit red action; "Order Online" steps all the
       way back to a cyan text link so the header has one clear primary. */
    .nav .btn {
      padding: 0.68rem 1.35rem 0.6rem;
      font-size: 0.72rem;
      letter-spacing: 0.11em;
    }
    .nav .btn--ghost {
      padding-inline: 0.2rem;
      color: var(--ice);
      background: none;
      border-color: transparent;
      box-shadow: none;
      text-shadow: 0 0 10px color-mix(in srgb, var(--ice) 40%, transparent);
    }
    .nav .btn--ghost:hover {
      border-color: transparent;
      box-shadow: 0 2px 0 var(--ice);
      text-shadow: 0 0 14px color-mix(in srgb, var(--ice) 65%, transparent);
    }
  }

  .nav__toggle {
    display: none;
    /* 3rem = 48px, Google's mobile tap-target guidance. It was 44px, which
       clears the WCAG floor but not this. */
    width: 3rem; height: 3rem;
    flex: none;
    place-items: center;
    border-radius: var(--radius);
    border: 1.5px solid color-mix(in srgb, var(--neon) 62%, transparent);
    box-shadow: 0 0 12px color-mix(in srgb, var(--neon) 20%, transparent);
    color: var(--neon);
  }
  .nav__toggle span {
    display: block;
    width: 20px; height: 1.5px;
    background: currentColor;
    position: relative;
    transition: background 0.2s;
  }
  .nav__toggle span::before,
  .nav__toggle span::after {
    content: "";
    position: absolute;
    left: 0;
    width: 20px; height: 1.5px;
    background: currentColor;
    transition: transform 0.3s var(--ease-out);
  }
  .nav__toggle span::before { top: -6px; }
  .nav__toggle span::after  { top: 6px; }
  .nav__toggle[aria-expanded="true"] span { background: transparent; }
  .nav__toggle[aria-expanded="true"] span::before { transform: translateY(6px) rotate(45deg); }
  .nav__toggle[aria-expanded="true"] span::after  { transform: translateY(-6px) rotate(-45deg); }

  @media (max-width: 63rem) {
    .nav__toggle { display: grid; }
    .nav__panel {
      position: fixed;
      /*
        .nav sets backdrop-filter, which makes it the containing block for
        fixed descendants. `bottom: 0` would resolve against the var(--nav-h)
        header box and collapse the panel — pin top, set explicit height.
      */
      top: var(--nav-h);
      left: 0; right: 0;
      height: calc(100svh - var(--nav-h));
      background: var(--void);
      padding: var(--s-6) var(--gutter) var(--s-8);
      display: flex;
      flex-direction: column;
      gap: var(--s-6);
      overflow-y: auto;
      overscroll-behavior: contain;
      transform: translateX(100%);
      visibility: hidden;
      transition: transform 0.45s var(--ease-out), visibility 0.45s;
    }
    .nav__panel.is-open { transform: none; visibility: visible; }
    .nav__links { flex-direction: column; align-items: stretch; gap: 0; }
    /* Full-width menu board: English big, Hangul trailing it on the same
       line, one lit rule per row. */
    .nav__links a {
      display: flex;
      align-items: baseline;
      justify-items: stretch;
      gap: 0.8ch;
      font-family: var(--f-display);
      font-size: var(--t-lg);
      letter-spacing: 0.03em;
      color: var(--paper);
      padding-block: var(--s-4);
      border-bottom: 1px solid var(--line);
    }
    .nav__links a .ko { font-size: 0.5em; }
    .nav__links a::after { display: none; }
    .nav__links a[aria-current="page"] {
      color: var(--neon);
      border-bottom-color: color-mix(in srgb, var(--neon) 55%, transparent);
    }
    .nav__cta { flex-direction: column; align-items: stretch; }
    body.nav-open { overflow: hidden; }
  }

  /* ========================================================= MOBILE TABS */
  /*
    A phone-only bar of the four things someone actually wants in the street
    outside: what is on the menu, call, book, order. Fixed to the bottom
    because that is where a thumb already is — the header is a stretch on a
    tall phone, and this is the one place the site asks for a decision.

    Hidden above 52rem: a bottom tab bar on a laptop is a phone affordance in
    the wrong place, and the header already carries these actions there.
  */
  .tabbar { display: none; }

  @media (max-width: 52rem) {
    .tabbar {
      position: fixed;
      z-index: 95;
      inset: auto 0 0 0;
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      /* Below the notch bar on an iPhone, not behind it. */
      padding: 0.5rem 0.4rem calc(0.5rem + env(safe-area-inset-bottom));
      background: color-mix(in srgb, var(--void) 92%, transparent);
      backdrop-filter: blur(16px) saturate(1.2);
      -webkit-backdrop-filter: blur(16px) saturate(1.2);
      border-top: 1px solid color-mix(in srgb, var(--neon) 28%, var(--line));
      box-shadow: 0 -14px 40px color-mix(in srgb, var(--void) 85%, transparent);
    }

    .tabbar__item {
      display: grid;
      justify-items: center;
      align-content: center;
      gap: 0.28rem;
      padding: 0.5rem 0.2rem 0.42rem;
      border-radius: var(--radius);
      /* 44px of thumb, whatever the label does. */
      min-height: 2.9rem;
      font-family: var(--f-display);
      font-size: 0.68rem;
      letter-spacing: 0.1em;
      text-transform: uppercase;
      line-height: 1;
      color: var(--mist);
      transition: color 0.2s var(--ease-out);
    }
    .tabbar__icon { width: 22px; height: 22px; }

    .tabbar__item:active { color: var(--paper); }
    .tabbar__item[aria-current="page"] {
      color: var(--ice);
      text-shadow: 0 0 10px color-mix(in srgb, var(--ice) 45%, transparent);
    }

    /*
      The lit one. Real neon does not blink — it sits at a steady burn with a
      slight breathing shimmer off the transformer, so that is what this is:
      the glow swells and settles, the tube colour never changes. A blinking
      tab would be a distraction sitting permanently over the content.
    */
    .tabbar__item--neon {
      color: var(--neon);
      border: 1.5px solid color-mix(in srgb, var(--neon) 62%, #fff);
      background: color-mix(in srgb, var(--neon) 12%, transparent);
      text-shadow: 0 0 8px color-mix(in srgb, var(--neon) 60%, transparent);
      box-shadow:
        0 0 4px  color-mix(in srgb, var(--neon) 55%, transparent),
        0 0 14px color-mix(in srgb, var(--neon) 30%, transparent),
        inset 0 0 12px color-mix(in srgb, var(--neon) 18%, transparent);
    }
    .tabbar__item--neon:active { color: #fff; }

    @media (prefers-reduced-motion: no-preference) {
      .tabbar__item--neon { animation: tube-hum 2.8s ease-in-out infinite alternate; }
      @keyframes tube-hum {
        from {
          box-shadow:
            0 0 3px  color-mix(in srgb, var(--neon) 45%, transparent),
            0 0 10px color-mix(in srgb, var(--neon) 22%, transparent),
            inset 0 0 10px color-mix(in srgb, var(--neon) 14%, transparent);
        }
        to {
          box-shadow:
            0 0 6px  color-mix(in srgb, var(--neon) 85%, transparent),
            0 0 22px color-mix(in srgb, var(--neon) 50%, transparent),
            0 0 46px color-mix(in srgb, var(--neon) 24%, transparent),
            inset 0 0 16px color-mix(in srgb, var(--neon) 26%, transparent);
        }
      }
    }

    /* The bar must not sit on top of the last thing on the page. */
    body { padding-bottom: calc(4.6rem + env(safe-area-inset-bottom)); }

    /* The full-screen nav panel owns the screen while it is open. */
    body.nav-open .tabbar { display: none; }
  }

  /* ============================================================ BUTTONS */
  /*
    A lit sign you can press. The fill is the accent painted flat; the tube
    around it is the same colour at full brightness, so the edge reads hotter
    than the middle the way real neon does. Ink is --accent-ink, which the
    zone moves in lockstep with the fill — that pairing is what keeps a gold
    button from ending up with cream type on it.
  */
  .btn {
    --btn-bg: var(--accent-fill);
    --btn-fg: var(--accent-ink);
    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.7ch;
    font-family: var(--f-display);
    font-size: 0.82rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 1rem 2rem 0.9rem;
    border-radius: var(--radius-pill);
    background: var(--btn-bg);
    color: var(--btn-fg);
    white-space: nowrap;
    border: 1.5px solid color-mix(in srgb, var(--accent) 80%, #fff);
    box-shadow:
      0 0 4px  color-mix(in srgb, var(--accent) 60%, transparent),
      0 0 16px color-mix(in srgb, var(--accent) 34%, transparent),
      0 0 42px color-mix(in srgb, var(--accent) 15%, transparent);
    transition: box-shadow 0.35s var(--ease-out),
                transform 0.25s var(--ease-spring);
  }
  /* Pressing it turns the transformer up */
  .btn:hover {
    transform: translateY(-2px);
    box-shadow:
      0 0 6px  color-mix(in srgb, var(--accent) 85%, transparent),
      0 0 24px color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 70px color-mix(in srgb, var(--accent) 28%, transparent);
  }
  .btn:active { transform: translateY(0); }

  /*
    The unlit tube: same object, no gas in it. Cream type on the void so it
    still clears AA, with only the outline carrying the accent.
  */
  .btn--ghost {
    --btn-bg: color-mix(in srgb, var(--accent) 8%, transparent);
    --btn-fg: var(--paper);
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    box-shadow:
      0 0 12px color-mix(in srgb, var(--accent) 16%, transparent),
      inset 0 0 14px color-mix(in srgb, var(--accent) 8%, transparent);
  }
  .btn--ghost:hover {
    border-color: color-mix(in srgb, var(--accent) 85%, #fff);
    box-shadow:
      0 0 5px  color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 20px color-mix(in srgb, var(--accent) 32%, transparent),
      inset 0 0 16px color-mix(in srgb, var(--accent) 12%, transparent);
  }

  .btn--lg { padding: 1.2rem 2.5rem 1.1rem; font-size: 1rem; letter-spacing: 0.12em; }
  .btn--sm {
    padding: 0.55rem 1.1rem 0.5rem;
    font-size: 0.7rem;
    letter-spacing: 0.12em;
  }
  .btn--full { width: 100%; }

  .btn-row { display: flex; flex-wrap: wrap; gap: var(--s-4); align-items: center; }

  /*
    The quiet action next to a loud one — cyan, underlined by hand, with the
    arrow doing the moving. Deliberately not a tube: if every action glows,
    none of them is the primary.
  */
  .link-arrow {
    display: inline-flex;
    align-items: center;
    gap: 0.7ch;
    font-family: var(--f-display);
    font-size: 0.86rem;
    font-weight: 400;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ice);
    padding-bottom: 0.45rem;
    text-shadow: 0 0 12px color-mix(in srgb, var(--ice) 40%, transparent);
    background-image: linear-gradient(currentColor, currentColor);
    background-size: 100% 2px;
    background-position: 0 100%;
    background-repeat: no-repeat;
    transition: background-size 0.4s var(--ease-out);
  }
  .link-arrow:hover { background-size: 40% 2px; }
  .link-arrow::after {
    content: "→";
    transition: transform 0.3s var(--ease-spring);
  }
  .link-arrow:hover::after { transform: translateX(6px); }

  /* ====================================================== SCENERY FRAME */
  /*
    The walls of the room are papered with Korean scenery. These are those
    walls: full-bleed, desaturated, with a red wash that lifts on hover.
  */
  .scene,
  .shot {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius);
    border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
    background: var(--char);
    aspect-ratio: var(--ar, 4 / 3);
    display: grid;
    place-items: center;
    isolation: isolate;
  }
  /* Saturated, not desaturated — this street is lit in colour. The old
     grayscale wash belonged to the cinematic version of this site. */
  .scene img,
  .shot img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    filter: saturate(1.15) contrast(1.08) brightness(0.86);
    transition: filter 0.7s var(--ease-out), transform 1.2s var(--ease-out);
  }
  .scene:hover img,
  .shot:hover img { filter: saturate(1.3) brightness(1); transform: scale(1.03); }
  /* the wall itself, when there is no photograph yet */
  .scene::before,
  .shot::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      radial-gradient(80% 60% at 30% 20%,
        color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
      linear-gradient(160deg, var(--char-2), var(--void) 78%);
    z-index: -1;
  }
  .scene__ph,
  .shot__ph {
    display: grid;
    gap: var(--s-3);
    justify-items: center;
    text-align: center;
    padding: var(--s-5);
  }
  /* The Hangul ghost behind an empty frame — an unlit sign waiting for a
     photograph. Set in the display Korean, dim, never competing. */
  .scene__ph b,
  .shot__ph b {
    font-family: var(--f-ko-dis);
    font-weight: 400;
    font-size: clamp(1.8rem, 6vw, 3.2rem);
    color: var(--accent);
    opacity: 0.22;
    line-height: 1;
  }
  .scene__ph span,
  .shot__ph span {
    font-family: var(--f-body);
    font-size: var(--t-xs);
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  /*
    Caption plate, like a gallery label. Needs a near-opaque ground — at 82%
    over a black frame it read as loose floating text rather than a plate.
  */
  .scene__cap {
    position: absolute;
    left: 0; bottom: 0;
    padding: 0.6rem 1rem;
    background: color-mix(in srgb, var(--void) 94%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    font-family: var(--f-body);
    font-size: var(--t-xs);
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--paper);
    border-top: 1px solid var(--accent);
    border-right: 1px solid var(--accent);
    border-radius: 0 var(--radius) 0 0;
  }
  /*
    An empty slot holds the full size of the photograph that is coming.

    This used to cap empty frames at 15rem so a placeholder would not eat a
    screen. The cost was worse than the problem: with aspect-ratio honoured,
    capping the height drags the width in with it — a 16/9 slot in a 558px
    column rendered 427px wide and sat marooned with a band of dead space
    beside it. A placeholder's one job is to describe the page it will become.

    Photographs are set in /t, so these frames are temporary anyway.
  */

  .scene--tall, .shot--tall { --ar: 3 / 4; }
  .scene--wide, .shot--wide { --ar: 16 / 9; }
  .scene--sq, .shot--sq { --ar: 1 / 1; }
  .scene--pano { --ar: 21 / 9; }


  /* ================================================== LOVE LOCK FENCE */
  /*
    The real waiting room has a chainlink fence under a sakura tree, and
    guests buy a padlock to leave on it. This is that fence.
  */
  .fence {
    position: relative;
    border: 1px solid var(--line);
    background:
      /* diamond chainlink mesh */
      repeating-linear-gradient(45deg,
        transparent 0 13px, var(--line) 13px 14px),
      repeating-linear-gradient(-45deg,
        transparent 0 13px, var(--line) 13px 14px),
      linear-gradient(180deg, var(--char), var(--void));
    padding: var(--s-6) var(--s-5) var(--s-7);
    min-height: 20rem;
    /*
      `clip` with a margin, not `hidden`. Each lock's engraved message is a
      tooltip centred above it, so locks at the top and at either end paint
      their message past the panel edge — `hidden` cut it off with no warning.

      The margin is derived, not guessed: a tooltip is at most 20rem wide and
      is centred on a 4.1rem lock, so it reaches 7.95rem beyond that lock, and
      the locks sit 1.5rem inside the panel. 8rem clears the worst case with a
      little to spare. Everything else in here — the sakura branches — only
      ever overhangs by a pixel.
    */
    overflow: clip;
    overflow-clip-margin: 8rem;
  }
  /*
    The label over the fence. It has to be centred here rather than by the
    inline style it used to carry: .eyebrow is a pill now, so it hugs its own
    text and `justify-content` centres the text inside the pill instead of
    the pill inside the panel.
  */
  .fence > .eyebrow {
    display: flex;
    width: fit-content;
    margin-inline: auto;   /* bottom space comes from .eyebrow, like everywhere */
  }

  .fence__locks {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-4) var(--s-3);
    padding-top: 1.6rem;
    justify-content: center;
    /*
      Not the default stretch. The "add yours" slot is taller than a lock, and
      stretch hands its height to every lock sharing its row — so one row of
      the wall renders 19px taller than the others for no visible reason.
    */
    align-items: flex-start;
  }
  /*
    The rail the locks hang from, anchored to the locks themselves. It used to
    be a magic `top: 3.2rem` on .fence, measured from the panel edge — so it
    knew nothing about what sat above it and drove a wire straight through the
    label. Pinned here it cannot collide with anything, whatever the header
    above it grows into.

    Stops short of the panel edges: a rail welded to both walls reads as a
    border, and the locks never reach that far anyway.
  */
  .fence__locks::before {
    content: "";
    position: absolute;
    top: 1.2rem;
    left: 4%; right: 4%;
    height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg,
      transparent,
      color-mix(in srgb, var(--mist) 70%, transparent) 12%,
      color-mix(in srgb, var(--mist) 70%, transparent) 88%,
      transparent);
  }
  /* A rail with nothing on it is just a stray wire — and when the wall fails
     to load, the empty notice is all that is in there. */
  .fence__locks:has(.fence__empty)::before,
  .fence__locks:empty::before { display: none; }

  /*
    The listitem wrapper around the "add yours" button and the empty-state
    message — see listItem() in js/locks.js for why they are wrapped.

    display:flex, not display:contents. Contents would let the child sit in
    the rail's own flex line and need no other change, but an element with
    display:contents has a long history of being dropped from the
    accessibility tree entirely — which would delete the very listitem this
    wrapper exists to provide, and quietly reintroduce the audit failure it
    was added to fix. A flex box shrink-wraps its child just as tightly and
    is exposed.
  */
  .fence__item {
    display: flex;
    align-items: flex-start;
  }
  /*
    The empty message was a width:100% flex item of the rail, which did two
    jobs at once: it spanned the row so "add yours" dropped to the next line,
    and the rail's own justify-content:center centred it. Wrapping it in a
    listitem took both away, so the wrapper has to do both.

    It is never actually as wide as it asks to be — the global `p` measure
    rule caps it at 66ch, about 575px — which is exactly why centring has to
    be explicit here. Left to itself the capped paragraph sits hard against
    the left edge of a rail three times its width.
  */
  .fence__item:has(.fence__empty) {
    width: 100%;
    justify-content: center;
  }

  .lock {
    --lock-face: linear-gradient(165deg, #C9CBD2, #7E828E 60%, #5C606B);
    --lock-shackle: #9BA0AC;
    --lock-ink: #1A1A1F;
    --lock-hole: #3A3D46;
    position: relative;
    display: grid;
    place-items: center;
    width: 4.1rem;
    padding: 0.6rem 0.35rem 0.5rem;
    border-radius: 3px 3px 5px 5px;
    background: var(--lock-face);
    color: var(--lock-ink);
    transform-origin: top center;
    rotate: var(--tilt, 0deg);
    cursor: default;
    transition: rotate 0.4s var(--ease-spring),
                filter 0.3s var(--ease-out);
  }
  /* the shackle, hooked over the fence rail */
  .lock::before {
    content: "";
    position: absolute;
    top: -0.78rem;
    width: 1.5rem;
    height: 1.1rem;
    border: 3px solid var(--lock-shackle);
    border-bottom: 0;
    border-radius: 0.9rem 0.9rem 0 0;
  }
  .lock::after {
    content: "";
    width: 4px; height: 4px;
    border-radius: 50%;
    background: var(--lock-hole);
    position: absolute;
    bottom: 0.55rem;
  }
  /*
    The engraving. Two lines, always — the label box is 54px, and a name is
    capped at NAME_MAX so it fits inside two of them. Reserving both rows
    rather than growing to fit keeps every lock on the wall the same height;
    a ragged rail reads as broken rather than organic.

    A character cap alone cannot guarantee the fit: Korean glyphs run about
    twice the width of Latin, so "우리 가족 사랑해" at 9 characters overflows a
    row that "Christopher" at 11 clears. Wrapping is what actually holds it.

    line-clamp stays as the backstop for locks hung before the cap existed.
  */
  .lock b {
    font-family: var(--f-body);
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1.15;
    width: 100%;
    /*
      A fixed two-row box, centred, rather than one that grows to its content.
      height not min-height: box-sizing is border-box site-wide, so a padded
      min-height gets absorbed by the padding and never binds — and an emoji,
      whose line box is taller than Latin or Hangul, then makes its lock stand
      11px above the rest of the rail.

      The margin (not padding) keeps the keyhole below clear of the text.
    */
    /* Two rows of the BASE size, in rem — em would track .lock--long's
       smaller font and make those locks 3px shorter than the rest. */
    height: calc(2 * 1.15 * 0.58rem);
    margin-bottom: 0.85rem;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /*
      anywhere, not break-word — this is load-bearing. Only `anywhere`
      reduces the intrinsic min-content width, and this box is a flex
      container, so with `break-word` the anonymous text item refuses to
      shrink below the width of its longest word and overflows instead of
      wrapping. A one-word name is then clipped at every size step.

      The cost is that a long word can break mid-word rather than at a
      space. That is what an engraver would do with the same amount of
      brass, and it beats losing the end of the name.
    */
    overflow-wrap: anywhere;
    text-align: center;
  }
  /*
    Two size steps for names that will not fit two rows at full size. Which
    one a lock gets is decided by measuring the string, not counting it — at
    NAME_MAX, sixteen Hangul, sixteen W and "Christopher & Am" want three
    different sizes and the last of those needs no step at all. See
    sizeClass() in locks.js.

    --long clears sixteen Hangul; --xlong clears sixteen capital W, which is
    the widest thing the cap permits. The label height is in rem, so neither
    step changes how tall the padlock stands on the rail.
  */
  .lock--long b  { font-size: 0.48rem; letter-spacing: 0; }
  .lock--xlong b { font-size: 0.40rem; letter-spacing: 0; }
  .lock:hover,
  .lock:focus-visible {
    rotate: 0deg;
    filter: brightness(1.25);
    z-index: 3;
  }
  /* engraved message, revealed on hover */
  .lock__msg {
    position: absolute;
    bottom: calc(100% + 0.85rem);
    left: 50%;
    /* --nudge is set by js/locks.js when a lock sits near either end of a row
       and its centred message would hang outside the panel. */
    translate: calc(-50% + var(--nudge, 0px)) 0;
    padding: 0.5rem 0.8rem;
    background: var(--void);
    border: 1px solid var(--accent);
    color: var(--paper);
    font-family: var(--f-ko);
    font-size: var(--t-xs);
    line-height: 1.4;
    /*
      Messages run to 80 characters. On one line that is ~34rem of tooltip
      centred on a 4.1rem padlock, so a lock near either edge of the fence
      threw half its message outside the panel. Wraps at a readable measure
      instead, and only grows as wide as it needs to.
    */
    width: max-content;
    max-width: min(20rem, 55vw);
    border-radius: var(--radius);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s var(--ease-out), translate 0.25s var(--ease-out);
  }
  .lock:hover .lock__msg,
  .lock:focus-visible .lock__msg {
    opacity: 1;
    translate: calc(-50% + var(--nudge, 0px)) -4px;
  }
  /*
    Four finishes a guest can pick from. Engraving stays dark on every one —
    on a real padlock the text is stamped into the metal, and dark-on-light
    is also the only way these clear AA at this size.
  */
  .lock--brass {
    --lock-face: linear-gradient(165deg, #E8CE93, #B08D45 58%, #7C6026);
    --lock-shackle: #C9AE72;
    --lock-hole: #5A4519;
  }
  .lock--steel {
    --lock-face: linear-gradient(165deg, #C9CBD2, #7E828E 60%, #5C606B);
    --lock-shackle: #9BA0AC;
    --lock-hole: #3A3D46;
  }
  .lock--crimson {
    --lock-face: linear-gradient(165deg, #F09A9F, #D2434A 55%, #8E2026);
    --lock-shackle: #E9909A;
    --lock-hole: #6B171C;
  }
  .lock--sakura {
    --lock-face: linear-gradient(165deg, #FBDCE4, #F5A9BC 58%, #D2778D);
    --lock-shackle: #F8C7D4;
    --lock-hole: #94465A;
  }

  /* the one this visitor just hung */
  .lock--mine {
    box-shadow: 0 0 24px color-mix(in srgb, var(--accent) 55%, transparent);
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }

  /* Remove-your-own — only ever appears on the visitor's own locks */
  .lock__remove {
    position: absolute;
    top: -0.5rem; right: -0.5rem;
    width: 1.35rem; height: 1.35rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: var(--void);
    color: var(--paper);
    border: 1px solid var(--accent);
    font-size: 0.8rem;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.2s var(--ease-out);
  }
  .lock--mine:hover .lock__remove,
  .lock--mine:focus-within .lock__remove,
  .lock__remove:focus-visible { opacity: 1; }

  /*
    The empty hook. This is the discoverability fix — the invitation sits on
    the fence itself rather than only in the form underneath, which visitors
    were reading as just another field.
  */
  .lock--add {
    background: none;
    border: 2px dashed color-mix(in srgb, var(--accent) 70%, transparent);
    color: var(--accent);
    cursor: pointer;
    gap: 0.1rem;
    --tilt: 0deg;
  }
  .lock--add::before { border-color: currentColor; border-style: dashed; }
  .lock--add::after { display: none; }
  /* `.lock b` clips with an ellipsis for engraved names; this label needs to
     wrap instead or it renders as "Add you…" */
  .lock--add b {
    font-size: 0.5rem;
    letter-spacing: 0.06em;
    line-height: 1.15;
    text-transform: uppercase;
    white-space: normal;
    overflow: visible;
    text-align: center;
    padding-bottom: 0.35rem;
  }
  .lock__plus { font-size: 1.05rem; line-height: 1; font-weight: 300; }
  .lock--add:hover,
  .lock--add:focus-visible {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border-style: solid;
    filter: none;
  }

  @media (prefers-reduced-motion: no-preference) {
    .lock--new { animation: hang 0.7s var(--ease-spring) both; }
    @keyframes hang {
      0%   { transform: translateY(-26px); opacity: 0; rotate: -14deg; }
      60%  { rotate: 8deg; }
      100% { transform: none; opacity: 1; }
    }
    /* The empty hook breathes so the eye finds it */
    .lock--add { animation: beckon 3.4s ease-in-out infinite; }
    @keyframes beckon {
      0%, 100% { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
      50%      { border-color: var(--accent); }
    }
  }

  /* A real heading over the form, not just a floating eyebrow */
  .fence__head {
    font-family: var(--f-display);
    font-size: var(--t-lg);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: center;
    color: var(--paper);
    margin-top: var(--s-7);
    margin-bottom: var(--s-2);
  }
  .fence__sub {
    text-align: center;
    font-size: var(--t-xs);
    color: var(--mist);
    margin: 0 auto var(--s-5);
    max-width: 34ch;
  }

  /*
    Grid, not wrapped flex. Under flex the submit landed wherever the wrap
    happened to put it — right-aligned beside the fields at one width, hard
    left on its own row at another — while the status line and the wall link
    directly beneath it are both centred. Two named rows fix it at every
    width: the fields, then the button centred under them.
  */
  .fence__form {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-5) var(--s-3);
    align-items: end;
    margin-top: var(--s-6);
    position: relative;
    z-index: 2;
  }
  .fence__form .btn {
    grid-column: 1 / -1;
    justify-self: center;
  }
  /* One column once two fields stop fitting side by side. */
  @media (max-width: 34rem) {
    .fence__form { grid-template-columns: 1fr; }
  }

  .fence__empty {
    width: 100%;
    text-align: center;
    color: var(--mist-2);
    font-size: var(--t-sm);
    padding-block: var(--s-5);
  }

  /* Padlock picker — swatches shaped like the locks themselves */
  .fence__styles {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    align-items: center;
    margin-top: var(--s-5);
    position: relative;
    z-index: 2;
  }
  .fence__styles > span {
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist);
  }
  .swatch {
    width: 2.1rem;
    height: 2.4rem;
    border-radius: 3px 3px 5px 5px;
    background: var(--lock-face);
    position: relative;
    cursor: pointer;
    opacity: 0.55;
    transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
  }
  .swatch::before {
    content: "";
    position: absolute;
    top: -0.5rem; left: 50%;
    translate: -50% 0;
    width: 1rem; height: 0.72rem;
    border: 2.5px solid var(--lock-shackle);
    border-bottom: 0;
    border-radius: 0.6rem 0.6rem 0 0;
  }
  /* The padlock graphic is 34x38 — smaller than a fingertip. Rather than
     inflate the drawing, extend an invisible hit area out to 44x44. */
  .swatch::after {
    content: "";
    position: absolute;
    inset: 50% 50%;
    width: 2.75rem; height: 2.75rem;
    translate: -50% -50%;
  }
  .swatch:hover { opacity: 0.85; }
  .swatch.is-picked {
    opacity: 1;
    transform: translateY(-2px);
    box-shadow: 0 0 0 2px var(--void), 0 0 0 4px var(--accent);
  }

  /* ---- the full wall at /locks ---- */
  .fence--wall {
    min-height: 26rem;
    padding: var(--s-8) var(--s-6) var(--s-8);
  }

  /* ================================================= STAFF TAKE-DOWN ===
    Only rendered for a signed-in session; a guest never sees any of this.
    See the note in js/locks.js for why it lives on the wall rather than in
    the /t settings screen.
  ===================================================================== */

  /*
    The tab sits on the padlock's shoulder rather than inside it. The lock is
    66px wide and already carries a name at two rows — there is no room in
    there for a control, and one placed over the engraving would hide the
    thing you are using to decide.
  */
  .fence--staff .lock { overflow: visible; }
  .lock__pull {
    position: absolute;
    top: -0.55rem;
    right: -0.55rem;
    z-index: 4;
    display: grid;
    place-items: center;
    width: 1.35rem;
    height: 1.35rem;
    padding: 0;
    border-radius: 50%;
    border: 1px solid color-mix(in srgb, var(--neon) 70%, #fff);
    background: var(--void);
    color: var(--paper);
    font-family: var(--f-body);
    font-size: 0.8rem;
    line-height: 1;
    cursor: pointer;
    /* Faint until wanted: a wall of eighty locks should not read as a wall
       of eighty delete buttons. */
    opacity: 0;
    transition: opacity 0.18s var(--ease-out), background 0.18s var(--ease-out);
  }
  .fence--staff .lock:hover .lock__pull,
  .fence--staff .lock:focus-within .lock__pull,
  .lock__pull:focus-visible { opacity: 1; }
  .lock__pull:hover { background: var(--neon-fill); }
  /* Armed: the next tap removes it. Filled, so the change of state carries
     without depending on the glyph swap alone. */
  .lock__pull.is-armed {
    opacity: 1;
    background: var(--neon-fill);
    color: var(--accent-ink);
    border-color: var(--neon);
  }
  .lock__pull:disabled { opacity: 1; cursor: default; }

  /* A pointer-only reveal strands touch and keyboard users, who have no
     hover — show every tab outright where hover is not available. */
  @media (hover: none) {
    .fence--staff .lock__pull { opacity: 1; }
  }

  .fence__bin {
    margin-top: var(--s-5);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--char);
    padding: var(--s-4) var(--s-5);
  }
  .fence__bin > summary {
    cursor: pointer;
    font-family: var(--f-display);
    font-size: var(--t-xs);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--mist);
  }
  .fence__bin > summary:hover { color: var(--paper); }
  .fence__bin-count {
    display: inline-grid;
    place-items: center;
    min-width: 1.5rem;
    padding: 0.1rem 0.4rem;
    margin-left: 0.6ch;
    border-radius: var(--radius-pill);
    background: var(--line);
    color: var(--paper);
    font-family: var(--f-body);
    font-size: 0.65rem;
    letter-spacing: 0;
  }
  .fence__bin > .muted { margin: var(--s-3) 0 var(--s-4); font-size: var(--t-xs); }
  .fence__bin-list { display: flex; flex-direction: column; gap: var(--s-3); }
  .fence__bin-row {
    display: flex;
    align-items: center;
    gap: var(--s-4);
    padding-block: var(--s-3);
    border-top: 1px solid var(--line);
  }
  .fence__bin-row:first-child { border-top: 0; }
  .fence__bin-row .lock { flex: none; rotate: 0deg; cursor: default; }
  .fence__bin-row p {
    flex: 1;
    min-width: 0;
    font-family: var(--f-sub);
    color: var(--paper);
    word-break: break-word;
  }
  .fence__bin-row .btn { flex: none; min-height: 2.75rem; }

  @media (max-width: 34rem) {
    .fence__bin-row { flex-wrap: wrap; }
    .fence__bin-row .btn { width: 100%; }
  }
  /*
    The wall does not scroll inside itself any more. A scroll container has to
    clip, and these locks carry tooltips that must escape upward — so the
    engraved message was cut off for every lock on the wall, which is the one
    page whose entire job is showing them.

    Losing the max-height is not a consolation prize: a nested scroller inside
    a scrolling page traps the wheel and is genuinely awkward on touch, and
    "every lock on the fence" reading as one long wall is the honest shape for
    it. js/locks.js already caps the wall at 500.
  */
  .fence--wall .fence__locks {
    gap: var(--s-5) var(--s-4);
    padding-top: 2rem;
  }
  /* No rail on the wall. One rail would imply the first row is special and
     the rest float; a rail per row is machinery that drifts the moment lock
     height changes. On the real fence the locks clip straight to the mesh at
     whatever height, so the mesh is the substrate — which is also true here. */
  .fence--wall .fence__locks::before { display: none; }

  /* Sakura branches framing the fence, as they do over the real one */
  .fence__branch {
    position: absolute;
    top: -1px;
    width: clamp(9rem, 26%, 17rem);
    color: var(--sakura);
    opacity: 0.5;
    pointer-events: none;
    z-index: 1;
  }
  .fence__branch--l { left: -1px; }
  .fence__branch--r { right: -1px; transform: scaleX(-1); }
  @media (max-width: 40rem) { .fence__branch { display: none; } }

  .wall-count {
    font-family: var(--f-display);
    font-size: var(--t-2xl);
    line-height: 1;
    color: var(--accent);
    text-shadow:
      0 0 14px color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 44px color-mix(in srgb, var(--accent) 28%, transparent);
  }

  /* ============================================================== CARDS */
  /* A panel bolted to the wall. Unlit until you touch it, then the tube
     down its left edge fires and the whole box picks up a halo. */
  .card {
    position: relative;
    padding: var(--s-6);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    transition: border-color 0.4s var(--ease-out),
                box-shadow 0.4s var(--ease-out),
                background 0.4s var(--ease-out);
  }
  .card::after {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
    background: var(--accent);
    box-shadow: 0 0 14px var(--accent);
    transform: scaleY(0);
    transform-origin: top;
    transition: transform 0.45s var(--ease-out);
  }
  .card:hover {
    background: var(--char-2);
    border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
    box-shadow: 0 0 34px color-mix(in srgb, var(--accent) 12%, transparent);
  }
  .card:hover::after { transform: scaleY(1); }
  .card h3 { margin-bottom: var(--s-3); }
  .card p  { font-size: var(--t-sm); color: var(--mist); }

  .grid {
    display: grid;
    gap: var(--s-4);
    grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  }
  /*
    Two columns, and two only — the name is a promise the old rule did not
    keep. `auto-fit` fitted as many 20rem tracks as the width allowed, so a
    four-card set broke 3 + 1 on a desktop and read as a mistake rather than
    a layout. Explicit tracks give an even 2 x 2.
  */
  .grid--2 { grid-template-columns: 1fr; }
  @media (min-width: 48rem) {
    .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }

  /* Numbered steps */
  .steps { counter-reset: step; display: grid; gap: var(--s-6); }
  .steps li {
    counter-increment: step;
    display: grid;
    grid-template-columns: 3rem 1fr;
    gap: var(--s-4);
    align-items: start;
  }
  .steps li::before {
    content: counter(step, decimal-leading-zero);
    font-family: var(--f-display);
    font-size: var(--t-lg);
    color: var(--accent);
    line-height: 1;
    letter-spacing: 0.02em;
    text-shadow:
      0 0 10px color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 26px color-mix(in srgb, var(--accent) 28%, transparent);
  }
  .steps .h-sub { margin-bottom: var(--s-1); }
  .steps p { font-size: var(--t-sm); color: var(--mist); }

  /* Tags */
  .tags { display: flex; flex-wrap: wrap; gap: var(--s-2); }
  .tag {
    font-family: var(--f-display);
    font-size: 0.72rem;
    font-weight: 400;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    padding: 0.58rem 1.1rem 0.5rem;
    border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--line));
    border-radius: var(--radius-pill);
    color: var(--mist);
    transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
  }
  .tag:hover {
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 70%, transparent);
  }

  /* ============================================================= TABLES */
  /*
    Horizontal only, and say so. Declaring overflow-x alone computes
    overflow-y to `auto`, and the 2px dashed TK marker that hangs under the
    last row was enough to trip a vertical scrollbar on a table with nothing
    to scroll vertically.

    The horizontal one stays: the table holds four columns and genuinely does
    not fit a phone. It is only drawn when it is needed, and thin, so on a
    desktop — where the table fits — there is no scrollbar at all.
  */
  .table-wrap {
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .table { min-width: 34rem; font-size: var(--t-sm); }
  .table th, .table td {
    padding: var(--s-4) var(--s-5);
    text-align: left;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
  }
  .table thead th {
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent);
    background: var(--char);
    white-space: nowrap;
  }
  .table tbody tr:last-child td { border-bottom: 0; }
  .table tbody tr:hover { background: var(--char); }
  .table td strong { color: #fff; }

  /* =============================================================== FAQ */
  .faq { display: grid; }
  .faq details { border-bottom: 1px solid var(--line); }
  .faq details:first-child { border-top: 1px solid var(--line); }
  .faq summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--s-4);
    padding: var(--s-5) 0;
    cursor: pointer;
    font-size: var(--t-md);
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-weight: 700;
    list-style: none;
    transition: color 0.25s var(--ease-out);
  }
  .faq summary:hover { color: var(--accent); }
  .faq summary::-webkit-details-marker { display: none; }
  .faq summary::after {
    content: "";
    width: 12px; height: 12px;
    flex: none;
    border-right: 1px solid var(--accent);
    border-bottom: 1px solid var(--accent);
    rotate: 45deg;
    transition: rotate 0.3s var(--ease-spring);
  }
  .faq details[open] summary::after { rotate: -135deg; }
  .faq div {
    padding: 0 0 var(--s-5);
    font-size: var(--t-sm);
    color: var(--mist);
    max-width: 60ch;
  }

  /* ============================================================== FORMS */
  /*
    Forms sit on a panel of their own.

    The page carries a fixed grid behind everything, and a form with no
    background let it run straight through the fields — lines crossing the
    inputs and the labels, which on the events inquiry is the one thing on
    the site a customer has to fill in. Opaque, so the grid stops at the
    edge; bordered, so it reads as a thing to complete rather than as loose
    text on the page.

    .fence__form is deliberately not included: it sits inside the love-lock
    panel, whose chainlink mesh is the point.
  */
  .form {
    display: grid;
    gap: var(--s-5);
    padding: var(--s-6);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
  }
  /* Nested inside a panel that is already a surface — one card, not two. */
  .t-panel .form,
  .form .form {
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }
  /*
    The line that closes a form. text-align alone was not enough: base.css
    caps every <p> at 66ch, so the block was narrower than the form and its
    centred text sat left of the button it belongs under.
  */
  .form__note {
    max-width: none;
    margin-top: var(--s-4);
    font-size: var(--t-xs);
    color: var(--mist);
    text-align: center;
  }

  .form__row {
    display: grid;
    gap: var(--s-5);
    grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  }
  /*
    align-content: start is what keeps the underlines level across a row.

    A .form__row stretches every .field to the height of the tallest, and a
    field carrying a <small> hint is the tallest. Without this, the spare
    height inside the shorter fields is handed to their auto rows, so their
    input grows and its underline sinks to the bottom of the row while the
    hinted field's underline stays up above its hint — 30px out on the "add
    someone" form in /t. Packing the rows to the top instead means the hint
    hangs below a line that has not moved.
  */
  .field { display: grid; gap: var(--s-2); align-content: start; }
  .field label {
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist);
  }
  .field label .req { color: var(--accent); }
  /* Says what the limit is up front. maxlength just stops accepting keys,
     which reads as the field being broken rather than full. */
  .field label .lim {
    font-family: var(--f-body);
    font-size: 0.66rem;
    font-weight: 400;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--mist-2);
    margin-left: 0.6ch;
  }
  /* underline fields — lighter than boxes on a black ground */
  .field input,
  .field select,
  .field textarea {
    width: 100%;
    padding: 0.75rem 0;
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    color: var(--paper);
    font-size: var(--t-base);
    /* Stated rather than left to `normal`, so an input and a select in the
       same row are built from the same one-line box. */
    line-height: 1.75;
    transition: border-color 0.3s var(--ease-out);
  }
  .field select {
    padding-inline: 0 var(--s-4);
    background-color: transparent;
    /*
      A select sizes itself from its option list and ignores line-height, so
      it comes out ~9px shorter than the inputs beside it and its underline
      rides high. Pin it to the box the inputs compute to: the vertical
      padding, one line of text, and the underline — every element here is
      border-box, so the 1px border counts toward the height.
    */
    min-height: calc(1.5rem + 1.75em + 1px);
  }
  .field select option { background: var(--char); color: var(--paper); }
  /*
    A date field carries Chrome's own picker furniture inside it and comes out
    a couple of pixels taller than a plain input, which is enough to see when
    the two sit side by side on the enquiry form. Same box as the rest —
    height rather than min-height, because this one needs bringing down.
  */
  .field input[type="date"] { height: calc(1.5rem + 1.75em + 1px); }
  /*
    Its own line-height: 1.75 is right for one line in a field, and too airy
    for a paragraph someone is typing. min-height rises with it so the box
    still opens on about five lines rather than the three and a half the
    taller leading would otherwise leave.
  */
  .field textarea { min-height: 9rem; line-height: 1.6; resize: vertical; }
  .field input::placeholder,
  .field textarea::placeholder { color: var(--mist-2); }
  /*
    Focus lights the underline like a tube coming on — and nothing else. The
    glow this used to carry (`box-shadow: 0 2px 12px -4px`) bloomed into a
    soft rectangle behind the field, which on the fence's dark mesh read as a
    background highlight rather than as a lit edge.

    The rule doubles to 2px instead, so the indicator is a change of weight as
    well as of colour rather than colour alone. Padding gives back the pixel
    the border takes so the text does not shift when you tab through.
  */
  .field input:focus,
  .field select:focus,
  .field textarea:focus {
    outline: none;
    border-bottom-color: var(--accent);
    border-bottom-width: 2px;
    padding-bottom: calc(0.75rem - 1px);
  }
  .field small { font-size: var(--t-xs); color: var(--mist-2); }

  .hp { position: absolute; left: -9999px; opacity: 0; }

  /* ======================================================= PRINTED MENU */
  /*
    The uploaded menu sheets, as page images. Filled by js/menus.js.

    Capped and centred rather than run to the full 80rem wrap: these are
    portrait pages of small print, and a menu page stretched to a laptop's
    full width is both unreadable and enormous. 46rem is about the width the
    sheet is held at, which is the size the type was set for.
  */
  .menu-sheet {
    max-width: 46rem;
    margin-inline: auto;
    display: grid;
    gap: var(--s-4);
  }
  .menu-sheet__title {
    max-width: 46rem;
    margin: var(--s-6) auto var(--s-4);
    font-family: var(--f-sub);
    font-size: var(--t-sm);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  /*
    The page itself. A white sheet on a black page needs a border rather than
    a shadow — a glow around a rectangle of paper reads as a mistake, where a
    hairline reads as the edge of the sheet.

    height:auto against the width/height attributes the script sets: those
    are there to reserve the right space before the bytes land, and without
    this they would also fix the rendered size.
  */
  .menu-sheet__page {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-sm, 6px);
    border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line));
    background: var(--char);
  }
  .menu-sheet__foot {
    max-width: 46rem;
    margin: var(--s-4) auto 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    font-size: var(--t-xs);
    color: var(--mist-2);
  }
  .menu-sheet__pdf {
    font-family: var(--f-sub);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ice);
  }
  .menu-sheet__pdf:hover { color: var(--paper); }

  /* ============================================================= FOOTER */
  /*
    The footer is the one opaque band on the site. Everything above it lets
    the fixed tile grid show through, which is right for the street but leaves
    the footer reading as more of the same page rather than the end of it.

    Opaque also means it differs from the header without competing: the header
    is glass over whatever is behind it, this is solid. Black rather than the
    raised-panel purples it used to sit on, and it darkens downward so the
    band recedes into the bottom of the screen rather than sitting as a slab.

    The top stop is lifted off black on purpose. Starting the band at full
    black put a hard line across the page where the section above met it —
    the footer stopped reading as the end of the page and started reading as
    a hole in it. Beginning near the raised-panel tone and falling to black
    over the whole height keeps the black without the seam. Four stops rather
    than two because the eye finds the midpoint of a two-stop ramp across a
    band this tall and reads it as a band of its own.
  */
  /* ============================================================ THE 71ST */
  /*
    Seen once in the life of the wall. Fixed rather than absolute so it stays
    put while the page behind it does whatever it was doing, and the scrim is
    dark enough that the fence underneath stops competing for attention.
  */
  .prize {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: grid;
    place-items: center;
    padding: var(--s-5);
    background: color-mix(in srgb, var(--void) 88%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    overflow-y: auto;
  }
  .prize__card {
    position: relative;
    width: min(34rem, 100%);
    max-height: 92svh;
    overflow-y: auto;
    padding: var(--s-7) var(--s-6) var(--s-6);
    background: linear-gradient(180deg, var(--char-2), var(--char));
    border: 2px solid color-mix(in srgb, var(--accent) 70%, #fff);
    border-radius: var(--radius-lg);
    box-shadow:
      0 0 6px  color-mix(in srgb, var(--accent) 55%, transparent),
      0 0 30px color-mix(in srgb, var(--accent) 30%, transparent),
      0 0 90px color-mix(in srgb, var(--accent) 14%, transparent);
  }
  .prize__card h2 { margin-top: var(--s-4); }
  .prize__lede { margin-top: var(--s-4); color: var(--paper); }
  /* Who is paying, in the quieter voice but before the fields — somebody
     being asked for their address is owed this above the form, not under it. */
  .prize__note {
    margin-top: var(--s-5);
    padding: var(--s-4);
    border-left: 3px solid var(--ice);
    background: color-mix(in srgb, var(--ice) 7%, transparent);
    color: var(--mist);
    font-size: var(--t-sm);
    line-height: 1.65;
  }
  .prize__form { margin-top: var(--s-6); }
  .prize__status.is-error { color: var(--neon-soft); }
  .prize__close {
    position: absolute;
    top: var(--s-3);
    right: var(--s-3);
    width: 2.5rem;
    height: 2.5rem;
    display: grid;
    place-items: center;
    font-size: 1.5rem;
    line-height: 1;
    color: var(--mist-2);
    border-radius: 50%;
  }
  .prize__close:hover { color: var(--paper); }
  @media (prefers-reduced-motion: no-preference) {
    .prize__card { animation: prize-in 0.4s var(--ease-spring) both; }
  }
  @keyframes prize-in {
    from { opacity: 0; transform: translateY(14px) scale(0.97); }
    to   { opacity: 1; transform: none; }
  }

  .footer {
    position: relative;
    background:
      linear-gradient(180deg,
        color-mix(in srgb, var(--char) 62%, var(--void)) 0%,
        var(--void) 30%,
        color-mix(in srgb, #000 70%, var(--void)) 62%,
        #000 100%);
    border-top: 1px solid color-mix(in srgb, var(--neon) 42%, var(--line));
    padding-block: var(--s-8) var(--s-6);
    z-index: 2;
  }
  /*
    The glow off that top edge, as a pseudo-element rather than a box-shadow.
    An outer shadow would be painted outside the footer, over the section
    above it; this keeps the light inside the band where the edge actually is.

    Sitting at z-index 0 against the footer's own stacking context puts it
    over the background and under the text, so no column needs its own
    z-index to stay readable.
  */
  .footer::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    height: 8rem;
    z-index: 0;
    pointer-events: none;
    background:
      radial-gradient(120% 100% at 50% 0%,
        color-mix(in srgb, var(--neon) 16%, transparent),
        transparent 72%);
  }
  /* The content rides above the wash. */
  .footer > * { position: relative; z-index: 1; }
  .footer__grid {
    display: grid;
    gap: var(--s-7);
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  }
  /*
    Column labels in the footer.

    h3 rather than h2: "Explore", "Visit" and "Hours" are not sections of the
    page, they are labels on a nav column, and at h2 they sat level with the
    real content sections on every page — three of them, on all eight. Still
    headings, so the footer is navigable by heading; just no longer competing
    with the page's own outline.

    h3 arrives with the Archivo card-title posture, so this has to unset the
    width axis as well as the family — otherwise the label renders narrowed.
  */
  .footer h3 {
    font-family: var(--f-display);
    font-variation-settings: normal;
    font-size: 0.82rem;
    font-weight: 400;
    line-height: 1.08;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ice);
    text-shadow: 0 0 12px color-mix(in srgb, var(--ice) 38%, transparent);
    margin-bottom: var(--s-4);
  }
  .footer p { color: var(--mist); }

  /*
    The line under the lockup, set as a caption rather than a sentence.

    text-transform, not typed in capitals. A screen reader hitting a literal
    "KOREAN BBQ" can decide it is an initialism and spell it out letter by
    letter; the transform leaves the sentence in the markup and only changes
    what is painted. It also keeps this editable by whoever comes next
    without them having to hold shift.

    Capitals at 0.86rem close up badly — Archivo's caps are wide and the
    counters run together — so the tracking is opened and the weight lifted
    to hold the line together at this size.
  */
  .footer__tag {
    font-size: var(--t-sm);
    text-transform: uppercase;
    letter-spacing: 0.09em;
    font-weight: 600;
    line-height: 1.6;
    color: var(--mist);
  }
  .footer a { color: var(--mist); font-size: var(--t-sm); }
  .footer a:hover { color: var(--paper); }
  .footer li + li { margin-top: var(--s-2); }

  .footer__hours dt { font-weight: 700; color: var(--paper); font-size: var(--t-sm); }
  .footer__hours dd { color: var(--mist); font-size: var(--t-sm); margin-bottom: var(--s-2); }
  .footer__hours .closed { color: var(--neon-soft); font-weight: 700; }

  .socials { display: flex; gap: var(--s-3); }
  .socials a {
    /* 48px, as above. The icon inside is unchanged — this grows the hit area,
       not the artwork. */
    width: 3rem; height: 3rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 1px solid var(--line);
    color: var(--mist);
    transition: all 0.25s var(--ease-out);
  }
  .socials a:hover {
    color: var(--void);
    background: var(--neon);
    border-color: color-mix(in srgb, var(--neon) 80%, #fff);
    box-shadow: 0 0 18px color-mix(in srgb, var(--neon) 55%, transparent);
  }
  .socials svg { width: 17px; height: 17px; fill: currentColor; }

  .footer__base {
    margin-top: var(--s-7);
    padding-top: var(--s-5);
    border-top: 1px solid var(--line);
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-4);
    justify-content: space-between;
    align-items: center;
    font-size: var(--t-xs);
    color: var(--mist-2);
  }

  /* ======================================================== CALLOUT BAR */
  /* The one boxed-in sign per page — a lit frame, not a bordered div. */
  .callout {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-5);
    padding: var(--s-7) var(--s-6);
    border-radius: var(--radius-lg);
    border: 1.5px solid color-mix(in srgb, var(--accent) 55%, transparent);
    box-shadow:
      0 0 22px color-mix(in srgb, var(--accent) 14%, transparent),
      inset 0 0 30px color-mix(in srgb, var(--accent) 7%, transparent);
    background:
      radial-gradient(90% 160% at 0% 0%,
        color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
      var(--char);
  }
  .callout h3 { font-size: var(--t-lg); margin-bottom: var(--s-2); }
  .callout p { font-size: var(--t-sm); color: var(--mist); }

  /* Flags content the client still owes us */
  .notice {
    display: flex;
    gap: var(--s-3);
    padding: var(--s-4) var(--s-5);
    border-left: 2px solid var(--gold);
    border-radius: 0 var(--radius) var(--radius) 0;
    background: color-mix(in srgb, var(--gold) 7%, transparent);
    font-size: var(--t-sm);
    color: var(--mist);
  }
  .notice b {
    color: var(--gold);
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .notice a { color: var(--paper); text-decoration: underline; }
}

/* ======================================================================
   pages.css
   ====================================================================== */
/* =========================================================================
   OPPA HOUSE — pages.css
   Hero, section heads, AYCE tiers, rooms, menu, hours, gallery.
   Cinematic and photographic: the walls of the room carry the scenery.
   ========================================================================= */

@layer pages {

  /* =============================================================== HERO */
  .hero {
    position: relative;
    min-height: 100svh;
    display: grid;
    align-items: end;
    padding-top: var(--nav-h);
    overflow: hidden;
    isolation: isolate;
  }

  /*
    The wall behind the grill: sign-light pooling on wet tile. Three lights
    at three heights so the gradient reads as sources in a room rather than
    as a wash — red high left, gold low and centre where the grill is,
    cyan sliding off the right edge.
  */
  .hero__wall {
    position: absolute;
    inset: 0;
    z-index: -3;
    /*
      The whole wall — photograph, lamps, wash and scrim together — runs out
      before the hero's bottom edge instead of being cut off at it.

      Fading only the photograph was not enough, and the reason is worth
      keeping: the hero's ground is built from lights, and two of them are
      still burning where the section ends. The 155deg wash starts at #17101F,
      which is lighter than the page, and the low warm lamp is centred at 94%
      of the hero's height — both were sliced off mid-glow, so the page
      changed colour along a straight horizontal line whether the photograph
      was there or not.

      Masking the container catches every layer at once, and it has to be
      subtraction rather than a black scrim: the ground below is the fixed
      lamplight on <html>, not a flat fill, so painting the seam out in --void
      would only replace a light edge with a dark one. The last stop sits at
      100% so the fade finishes exactly on the hero's own edge.
    */
    -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 66%, transparent 100%);
    mask-image: linear-gradient(180deg, #000 0%, #000 66%, transparent 100%);
    background:
      /* Darkens the lower half behind the type, then lets go entirely before
         the foot. Held at solid --void it laid an opaque band across the
         bottom of the hero, and the fixed light on <html> carried on either
         side of it — so the page changed colour on a straight line exactly
         where the hero ended. Nothing at the seam, and there is no seam. */
      linear-gradient(180deg,
        transparent 26%,
        color-mix(in srgb, var(--void) 88%, transparent) 78%,
        transparent 100%),
      radial-gradient(58% 46% at 12% 14%,
        color-mix(in srgb, var(--neon) 26%, transparent), transparent 68%),
      radial-gradient(58% 42% at 74% 94%,
        color-mix(in srgb, var(--flame) 15%, transparent), transparent 70%),
      radial-gradient(46% 40% at 96% 26%,
        color-mix(in srgb, var(--ice) 15%, transparent), transparent 70%),
      /* transparent at the foot, not --void: the fixed grid on <html> has to
         read through here or the hero ends on a hard line. See .phero::before. */
      linear-gradient(155deg, #17101F 0%,
        color-mix(in srgb, var(--void) 85%, transparent) 64%,
        transparent 96%);
  }
  /* Colour stays in. The photograph is the grill — it is supposed to look hot.
     Brightness only takes it down far enough that the scrim below can finish
     the job; dimming it to black here kills the food. */
  .hero__wall img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: 58% 42%;   /* hold the grill and the hot pot in frame */
    filter: saturate(1.28) contrast(1.06) brightness(0.66);
    z-index: -1;
  }

  /*
    The scrim, and it has to be a pseudo-element rather than more layers on
    .hero__wall's own background. A negative-z-index child paints ABOVE its
    parent's background — so every gradient set there sits under the
    photograph and does nothing to it. ::after is in normal flow, so it lands
    on top, which is the only place a scrim is any use.

    Narrow screens get the photograph full-bleed and heavily veiled, because
    there is nowhere else for it to go.
  */
  .hero__wall::after {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(95deg,
        var(--void) 0%,
        color-mix(in srgb, var(--void) 88%, transparent) 30%,
        color-mix(in srgb, var(--void) 55%, transparent) 62%,
        color-mix(in srgb, var(--void) 40%, transparent) 100%),
      linear-gradient(180deg,
        color-mix(in srgb, var(--void) 82%, transparent) 0%,
        transparent 26%,
        color-mix(in srgb, var(--void) 55%, transparent) 72%,
        transparent 98%),
      radial-gradient(56% 44% at 10% 12%,
        color-mix(in srgb, var(--neon) 24%, transparent), transparent 68%),
      radial-gradient(44% 38% at 95% 22%,
        color-mix(in srgb, var(--ice) 13%, transparent), transparent 70%);
  }

  /*
    Wide screens split the poster the way the printed one does: the banner on
    clean black to the left, the table as a panel to the right. Veiling a
    full-bleed photograph enough to read 7rem type over it meant throwing away
    the half of the shot with the grill and the shrimp in it — this keeps the
    food legible and the type legible at the same time, which a blanket scrim
    cannot do.
  */
  @media (min-width: 64rem) {
    .hero__wall img {
      left: 36%;
      width: 64%;
      object-position: 62% 46%;
      filter: saturate(1.3) contrast(1.06) brightness(0.78);
    }
    /* Feather the panel's left edge into the black instead of letting it cut,
       and keep the top and bottom tucked into the page. */
    .hero__wall::after {
      background:
        linear-gradient(90deg,
          var(--void) 0%,
          var(--void) 39%,
          color-mix(in srgb, var(--void) 68%, transparent) 50%,
          color-mix(in srgb, var(--void) 20%, transparent) 63%,
          transparent 76%),
        linear-gradient(180deg,
          color-mix(in srgb, var(--void) 78%, transparent) 0%,
          transparent 22%,
          color-mix(in srgb, var(--void) 40%, transparent) 74%,
          transparent 99%),
        radial-gradient(52% 44% at 8% 14%,
          color-mix(in srgb, var(--neon) 22%, transparent), transparent 66%),
        radial-gradient(40% 36% at 96% 20%,
          color-mix(in srgb, var(--ice) 14%, transparent), transparent 70%);
    }
  }

  /*
    The sign-light pools sit inside the wall so they fade with it, and this is
    the one line that keeps them where they were: .lamp-glow ships at
    z-index -1, which inside .hero__wall would put them under the scrim —
    and the scrim is solid --void across the left third on a wide screen,
    which is exactly where the red pool is. They would simply disappear.

    z-index 1 puts them back above the scrim, and .hero__wall's own -3 keeps
    the whole group below the headline regardless.
  */
  .hero__wall .lamp-glow { z-index: 1; }

  .hero__inner {
    position: relative;
    z-index: 2;
    display: grid;
    gap: var(--s-5);
    padding-block: var(--s-7) clamp(2.25rem, 5vh, 3.5rem);
    justify-items: start;
    max-width: 62rem;
    width: 100%;
  }
  .hero__inner > .btn-row { margin-top: var(--s-2); }
  .hero__facts { width: 100%; }

  /*
    The banner. Two lines, two colours: the first cream and flat, the second
    lit red — that colour break is the whole grammar of the poster, and
    every heading on the site is a smaller version of it.

    Leading is 1.02, not the sub-1 a poster face invites. Lack of descenders
    is not what governs it — Anton's caps are 0.867em tall, so anything under
    1.0 puts the red line's capitals above the cream line's baseline. This
    is the largest type on the site, so 1.02 still reads tight.
  */
  .hero__sign {
    position: relative;
    font-family: var(--f-display);
    font-size: var(--t-3xl);
    font-weight: 400;
    line-height: 1.02;
    letter-spacing: 0.005em;
    text-transform: uppercase;
    margin: 0;
    padding-bottom: 0.5rem;
  }
  @media (max-width: 48rem) { .hero__sign { line-height: 1.06; } }
  .hero__sign em {
    display: block;
    /*
      Shrink-wrapped to its own text, so the underline below can simply be
      100% of it. As a full-width block the underline had no way to know how
      long the line it belongs to actually is, and was guessed at 62% — which
      is why it stopped short of "Seoul." Still a block, so the line break
      before it is unchanged; if the phrase ever wraps, fit-content falls
      back to the column width and the rule spans that instead.
    */
    width: fit-content;
    max-width: 100%;
    font-style: normal;
    color: var(--neon);
    text-shadow:
      0 0 16px color-mix(in srgb, var(--neon) 45%, transparent),
      0 0 54px color-mix(in srgb, var(--neon) 24%, transparent);
  }
  /*
    The hand-drawn cyan underline the poster puts beneath its own headline.

    Hung on the neon line rather than the h1 so it stays directly under the
    headline: .h1__sub is the last thing in this heading now, and an ::after
    on the h1 would be pushed below it. em is shrink-wrapped to its text, so
    100% here is the width of the words above and nothing else.

    vector-effect on the path is what makes that safe. The SVG is stretched
    with preserveAspectRatio='none', and under a non-uniform stretch a stroke
    thickens by the geometric mean of the two scales — run this to the full
    width of a poster headline and a 3.5px pen comes out closer to 6px.
    non-scaling-stroke holds the pen at one weight however far it is pulled.
  */
  .hero__sign em::after {
    content: "";
    display: block;
    width: 100%;
    height: 14px;
    margin-top: 0.5rem;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 14' preserveAspectRatio='none'%3E%3Cpath d='M3 9C58 3 118 12 178 6c40-4 82 1 119-2' stroke='%232BE8E8' stroke-width='3.5' fill='none' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
    filter: drop-shadow(0 0 7px color-mix(in srgb, var(--ice) 65%, transparent));
  }

  .hero__tag {
    font-family: var(--f-body);
    font-size: var(--t-md);
    line-height: 1.65;
    color: #E4DCCC;
    max-width: 42ch;
  }

  /*
    The strip along the bottom of the poster: one lit box, three facts, hair
    rules between them. This is the last thing above the fold and the only
    place on the page where cyan runs the whole width.
  */
  /*
    Three equal columns, or one — never two. Under flex-wrap the third fact
    dropped onto a row by itself and the divider rules, written for a single
    row, put a stray rule above the orphan while leaving the two above it
    undivided. A fixed column count cannot wrap raggedly.
  */
  .hero__facts {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0;
    margin-top: var(--s-3);
    border-radius: var(--radius-lg);
    border: 1.5px solid color-mix(in srgb, var(--ice) 45%, transparent);
    background: color-mix(in srgb, var(--void) 55%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    box-shadow:
      0 0 20px color-mix(in srgb, var(--ice) 14%, transparent),
      inset 0 0 26px color-mix(in srgb, var(--ice) 6%, transparent);
  }
  .hero__facts li {
    display: grid;
    gap: 0.3rem;
    align-content: start;
    padding: var(--s-5);
  }
  .hero__facts li + li {
    border-left: 1px solid color-mix(in srgb, var(--ice) 26%, transparent);
  }
  .hero__facts b {
    font-family: var(--f-display);
    font-weight: 400;
    font-size: 0.95rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--paper);
  }
  .hero__facts span { font-size: var(--t-xs); color: var(--mist); }

  /*
    The AYCE price line on the home page.

    Everyone this restaurant competes with puts a number in front of the
    searcher before they click anything. This is that number, and it is set to
    be scanned rather than read: the figure itself steps into the poster face
    at accent colour, the sentence around it stays quiet, and a lit rule down
    the left marks the whole thing as a statement of fact rather than more
    prose. js/menu.js swaps the figure if staff have changed it in /t.
  */
  .ayce-price {
    padding-left: var(--s-4);
    border-left: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
    font-size: var(--t-base);
    line-height: 1.6;
    color: var(--mist);
    max-width: 46ch;
  }
  .ayce-price b {
    font-family: var(--f-display);
    font-weight: 400;
    font-size: 1.35em;
    letter-spacing: 0.02em;
    color: var(--accent);
    /* Sits on the same baseline as the sentence despite the size jump. */
    line-height: 1;
  }
  /*
    One column below this. Three facts side by side need about 52rem before
    "Tuesday to Sunday. Closed Monday." starts wrapping to three lines in a
    column narrower than its own text — at which point stacking reads better
    than squeezing. Stacked, the divider turns to run across.
  */
  @media (max-width: 52rem) {
    .hero__facts { grid-template-columns: 1fr; }
    .hero__facts li { padding-block: var(--s-4); }
    .hero__facts li + li {
      border-left: 0;
      border-top: 1px solid color-mix(in srgb, var(--ice) 26%, transparent);
    }
  }

  /* The address, above the banner. Still a row: it held a second badge
     until that was cut, and could hold one again. */
  .hero__badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-4);
    margin-bottom: var(--s-2);
  }
  /* Pills sit side by side here, not above a heading — the row carries the
     space beneath, so the pills themselves should not also. */
  .hero__badges > .eyebrow { margin-bottom: 0; }

  /*
    The banner's column in the split layout. This has to sit below the rules
    it overrides — .hero__sign's base size is declared further up the file and
    would otherwise win on source order at equal specificity.

    The width is set on .hero__sign rather than .hero__inner because
    .hero__inner also carries .wrap, and .wrap lives in the utils layer, which
    beats anything the pages layer says about max-width no matter how specific.
    The fact strip is deliberately left full-bleed — it is the base of the
    poster, not part of the column.
  */
  @media (min-width: 64rem) {
    .hero__sign {
      font-size: clamp(2.9rem, 4.5vw, 4.7rem);
      line-height: 1.02;
      max-width: 20ch;
    }
    .hero__tag { max-width: 34ch; }
    /* Over a photograph the alley signage needs something to sit on */
    .hero__vert { text-shadow: 0 0 10px var(--void), 0 0 26px var(--void); }
  }

  /* Vertical Hangul running down the right edge, like alley signage */
  .hero__vert {
    position: absolute;
    right: var(--gutter);
    top: 30%;
    z-index: 2;
    font-size: clamp(0.72rem, 1.5vw, 0.95rem);
    color: var(--mist-2);
  }
  @media (max-width: 60rem) { .hero__vert { display: none; } }

  /* Interior page hero */
  .phero {
    position: relative;
    padding-top: calc(var(--nav-h) + var(--s-8));
    padding-bottom: var(--s-8);
    overflow: hidden;
    isolation: isolate;
  }
  /*
    The gradient drops to -2 so a photograph can sit between it and the
    content. With no photo uploaded this is still what you see — it is the
    fallback, not decoration on top of one.
  */
  /*
    The wash has to end in transparent, not in --void.

    The page's grid and lamplight are one fixed layer on <html>, drawn behind
    everything. An opaque wash here covers it, and because .phero clips at its
    own edge the cover stopped dead at the fold: no grid above, grid below,
    a hard line across every interior page. Ending transparent lets the same
    fixed layer read through the bottom of the hero, so the two meet with
    nothing to see. The colour is unchanged — the canvas underneath is --void
    already, which is exactly what this used to fade to.
  */
  .phero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -2;
    background:
      radial-gradient(62% 58% at 6% 0%,
        color-mix(in srgb, var(--accent) 24%, transparent), transparent 68%),
      radial-gradient(46% 44% at 94% 8%,
        color-mix(in srgb, var(--neon) 12%, transparent), transparent 72%),
      linear-gradient(180deg,
        #16101E 0%,
        color-mix(in srgb, #16101E 60%, transparent) 42%,
        transparent 88%);
  }
  /*
    Optional background photograph for an interior page hero, set per page in
    /t. Mirrors the homepage: the image fills the band, and its own scrim sits
    on top — a pseudo-element rather than more layers on .phero::before,
    because a negative-z-index child paints ABOVE its parent's background and
    a scrim underneath the photo would do nothing to it.
  */
  .phero__wall {
    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
  }
  .phero__wall img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 42%;
    filter: saturate(1.15) contrast(1.05) brightness(0.6);
  }
  /* Only drawn once a photograph is actually in place, so an empty wall does
     not darken the gradient it is sitting over. */
  .phero__wall.has-photo::after {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(95deg,
        var(--void) 0%,
        color-mix(in srgb, var(--void) 82%, transparent) 42%,
        color-mix(in srgb, var(--void) 45%, transparent) 78%,
        color-mix(in srgb, var(--void) 30%, transparent) 100%),
      linear-gradient(180deg,
        color-mix(in srgb, var(--void) 70%, transparent) 0%,
        transparent 34%,
        var(--void) 97%);
  }

  .phero__inner {
    position: relative;
    z-index: 2;
    display: grid;
    gap: var(--s-6);
    justify-items: start;
    max-width: 48rem;
  }
  /* Same as .shead: cancel the grid gap under the pill so it clears its
     heading by the same 8px it does everywhere else on the site. */
  .phero__inner > .eyebrow { margin-bottom: calc(var(--s-2) - var(--s-6)); }
  /* Leading is inherited — the base h1 rule already clears Anton's caps. */
  .phero h1 { font-size: var(--t-2xl); }
  .phero .lede { max-width: 46ch; }

  /* Vertical Hangul signage, same device as the homepage hero */
  .phero__vert {
    position: absolute;
    right: var(--gutter);
    top: 50%;
    translate: 0 -50%;
    z-index: 2;
    font-size: clamp(0.7rem, 1.3vw, 0.88rem);
    color: var(--mist-2);
  }
  @media (max-width: 64rem) { .phero__vert { display: none; } }

  /* Namsan Tower behind the love-lock hero — dimmed to near-black so the
     type stays readable over whatever photograph goes in. */
  .locks-hero { padding-bottom: var(--s-9); }
  .locks-hero__wall {
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
      linear-gradient(180deg, transparent 20%, var(--void) 94%),
      radial-gradient(60% 50% at 78% 18%,
        color-mix(in srgb, var(--sakura) 16%, transparent), transparent 70%),
      radial-gradient(70% 55% at 20% 30%,
        color-mix(in srgb, var(--neon) 20%, transparent), transparent 72%),
      linear-gradient(160deg, #16121A 0%, var(--void) 66%);
  }
  .locks-hero__wall img {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
    filter: grayscale(0.35) brightness(0.34) contrast(1.1);
    z-index: -1;
  }

  .crumbs {
    display: flex;
    gap: 0.7ch;
    font-family: var(--f-display);
    font-size: 0.74rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  .crumbs a:hover { color: var(--accent); }

  /* ======================================================= SECTION HEAD */
  .shead {
    display: grid;
    gap: var(--s-5);
    margin-bottom: var(--s-8);
    max-width: 46rem;
  }
  /*
    .shead is a grid with a 24px gap, so a pill sitting directly in it would
    get that gap on top of its own margin and stand further off its heading
    than the same pill anywhere else. Cancel the gap back down so every
    eyebrow on the site clears its heading by the same 8px.

    (The rule this replaces added 8px on top instead, to compensate for the
    old sub-1 heading leading. That leading is gone.)
  */
  .shead > .eyebrow { margin-bottom: calc(var(--s-2) - var(--s-5)); }
  .shead--split {
    max-width: none;
    grid-template-columns: 1fr auto;
    align-items: end;
    gap: var(--s-6);
  }
  .shead p { color: var(--mist); }
  @media (max-width: 48rem) {
    .shead--split { grid-template-columns: 1fr; align-items: start; }
  }

  /* ============================================================== SPLIT */
  .split {
    display: grid;
    gap: clamp(2rem, 5vw, 4.5rem);
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    align-items: center;
  }
  .split--media-first > :first-child { order: -1; }
  @media (min-width: 60rem) {
    .split--offset > :last-child { transform: translateY(2.5rem); }
  }

  /* ======================================================= STALL CARDS */
  /* The four offerings, as gallery plates rather than market stalls */
  /* Four stalls in a row, divided by unlit tube. The whole block is one
     bent frame so the row reads as a single lit board. */
  /*
    Four stalls, so the track count may only ever be 1, 2 or 4 — never 3.
    The divider lines here are the container's background showing through a
    1px gap, which means an EMPTY grid cell shows that same colour at full
    size. auto-fit was landing on three columns anywhere from about 820px to
    1000px wide and painting the two leftover cells as a solid pink block
    beside the last stall.
  */
  .stalls {
    display: grid;
    gap: 1px;
    grid-template-columns: 1fr;
    background: color-mix(in srgb, var(--accent) 26%, var(--line));
    border: 1.5px solid color-mix(in srgb, var(--accent) 45%, transparent);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: 0 0 30px color-mix(in srgb, var(--accent) 10%, transparent);
  }
  /* Two up once a stall has room to breathe, four up once all of them do.
     Each track at four columns is about 19rem inside the 80rem wrap, which
     is comfortably past the 15rem a stall needs. */
  @media (min-width: 40rem) {
    .stalls { grid-template-columns: repeat(2, 1fr); }
  }
  @media (min-width: 72rem) {
    .stalls { grid-template-columns: repeat(4, 1fr); }
  }
  .stall {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--void);
    transition: background 0.45s var(--ease-out);
  }
  .stall:hover { background: var(--char); }
  .stall__body {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-6) var(--s-5) var(--s-5);
    flex: 1;
  }
  /* The Korean name, at sign scale and lit — the stall's actual signboard */
  .stall__ko {
    font-family: var(--f-ko-dis);
    font-weight: 400;
    font-size: var(--t-lg);
    color: var(--accent);
    line-height: 1;
    text-shadow:
      0 0 12px color-mix(in srgb, var(--accent) 50%, transparent),
      0 0 34px color-mix(in srgb, var(--accent) 25%, transparent);
  }
  .stall__title {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-size: var(--t-md);
    font-weight: 800;
    line-height: 1.12;
    letter-spacing: -0.01em;
    color: var(--paper);
  }
  .stall__text {
    font-size: var(--t-sm);
    color: var(--mist);
    margin-top: auto;
  }
  .stall__meta {
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--accent);
  }
  .stall__link::before { content: ""; position: absolute; inset: 0; z-index: 1; }

  /* ========================================================= AYCE TIERS */
  .tiers {
    display: grid;
    gap: 1px;
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
    background: color-mix(in srgb, var(--accent) 26%, var(--line));
    border: 1.5px solid color-mix(in srgb, var(--accent) 45%, transparent);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: 0 0 30px color-mix(in srgb, var(--accent) 10%, transparent);
  }
  .tier {
    --tc: var(--mist);
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    padding: var(--s-7) var(--s-6);
    background: var(--void);
  }
  .tier--gold { --tc: var(--neon); background: var(--char); }
  /* The lit tag on the tier the room actually sells */
  .tier--gold::before {
    content: "Most Popular";
    position: absolute;
    top: var(--s-5); right: var(--s-6);
    font-family: var(--f-display);
    font-size: 0.66rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--neon);
    padding: 0.3rem 0.7rem 0.24rem;
    border: 1px solid color-mix(in srgb, var(--neon) 60%, transparent);
    border-radius: var(--radius-pill);
    text-shadow: 0 0 10px color-mix(in srgb, var(--neon) 55%, transparent);
    box-shadow: 0 0 14px color-mix(in srgb, var(--neon) 20%, transparent);
  }
  .tier__name {
    font-family: var(--f-display);
    font-size: var(--t-lg);
    font-weight: 400;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--tc);
    line-height: 1;
  }
  .tier__price {
    font-family: var(--f-display);
    font-size: var(--t-xl);
    color: var(--paper);
    line-height: 1;
    letter-spacing: 0.01em;
  }
  .tier__price sub {
    display: block;
    font-family: var(--f-display);
    font-size: 0.72rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
    margin-top: var(--s-2);
  }
  .tier__list { display: grid; gap: var(--s-3); font-size: var(--t-sm); }
  .tier__list li {
    display: grid;
    grid-template-columns: 1.2rem 1fr;
    gap: var(--s-2);
    color: var(--mist);
  }
  .tier__list li::before {
    content: "";
    width: 5px; height: 5px;
    margin-top: 0.65em;
    background: var(--tc);
    rotate: 45deg;
  }
  .tier__list li.is-gold { color: var(--paper); }
  .tier .btn { margin-top: auto; }

  /* Price placeholders read as awaiting content, never as real numbers */
  .tier__price > [data-tk],
  .mitem__price[data-tk] {
    display: inline-block;
    font-family: var(--f-body);
    font-weight: 700;
    letter-spacing: 0.18em;
    color: var(--gold);
    border: 1px dashed var(--gold);
    vertical-align: middle;
  }
  .tier__price > [data-tk] { font-size: 0.32em; padding: 0.5em 1em; }
  .mitem__price[data-tk] { font-size: var(--t-xs); padding: 0.25em 0.6em; }
  .tier__price > [data-tk]::after,
  .mitem__price[data-tk]::after { display: none; }

  /* ============================================================= ROOMS */
  .rooms { display: grid; gap: var(--s-8); }
  .room {
    display: grid;
    gap: clamp(1.5rem, 4vw, 3.5rem);
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    align-items: center;
  }
  .room:nth-child(even) > .scene,
  .room:nth-child(even) > .shot { order: 1; }
  @media (max-width: 52rem) {
    .room { grid-template-columns: 1fr; }
    .room:nth-child(even) > .scene,
    .room:nth-child(even) > .shot { order: -1; }
  }

  /* The room number, painted on the door and lit */
  .room__code {
    font-family: var(--f-display);
    font-size: var(--t-xl);
    color: var(--accent);
    line-height: 1;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    text-shadow:
      0 0 14px color-mix(in srgb, var(--accent) 52%, transparent),
      0 0 44px color-mix(in srgb, var(--accent) 26%, transparent);
  }
  .room__name {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-weight: 800;
    font-size: var(--t-lg);
    letter-spacing: -0.01em;
    color: var(--paper);
    margin-block: var(--s-2) var(--s-4);
  }
  .room__stats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-6);
    margin-block: var(--s-5);
    padding-block: var(--s-5);
    border-block: 1px solid var(--line);
  }
  .room__stats div { display: grid; gap: 4px; }
  .room__stats dt {
    font-family: var(--f-display);
    font-size: 0.68rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  .room__stats dd {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-weight: 700;
    font-size: var(--t-md);
    color: var(--paper);
    line-height: 1.1;
  }

  /* ============================================================ EVENTS */
  .occasions {
    display: grid;
    gap: 1px;
    grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
    background: color-mix(in srgb, var(--accent) 22%, var(--line));
    border: 1.5px solid color-mix(in srgb, var(--accent) 40%, transparent);
    border-radius: var(--radius-lg);
    overflow: hidden;
  }
  .occasion {
    display: grid;
    gap: var(--s-2);
    padding: var(--s-5);
    background: var(--void);
    transition: background 0.35s var(--ease-out);
  }
  .occasion:hover { background: var(--char); }
  .occasion b {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 88;
    font-size: var(--t-md);
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--paper);
    line-height: 1.12;
  }
  .occasion span { font-size: var(--t-xs); color: var(--mist); }

  .stats {
    display: grid;
    gap: var(--s-6);
    grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr));
  }
  .stats div { display: grid; gap: var(--s-2); }
  .stats dt {
    font-family: var(--f-display);
    font-size: var(--t-2xl);
    font-weight: 400;
    line-height: 1;
    color: var(--accent);
    text-shadow:
      0 0 18px color-mix(in srgb, var(--accent) 50%, transparent),
      0 0 56px color-mix(in srgb, var(--accent) 26%, transparent);
  }
  .stats dd {
    font-family: var(--f-display);
    font-size: 0.76rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
  }

  /* ============================================================ GALLERY */
  /*
    A bento is a fixed tiling, and a tiling drawn for four frames strands the
    fifth on a row of its own. The home page has four; the visit page has five.
    So the count chooses the tiling, and both of them end flush.

      four                        five
      ┌───────┬───┬───┐           ┌───────────┬─────┐
      │       │ 2 │ 3 │           │           │  2  │
      │   1   ├───┴───┤           │     1     ├─────┤
      │       │   4   │           │           │  3  │
      └───────┴───────┘           ├─────┬─────┴─────┤
                                  │  4  │     5     │
                                  └─────┴───────────┘

    The five-up is deliberately even-tempered: every frame lands near 2:1, so
    a set of 4:3 room photographs crops the same amount wherever it is placed.
  */
  .gallery {
    display: grid;
    gap: var(--s-3);
    grid-template-columns: 1fr;
  }
  /*
    Two up before the bento. Fixed at two rather than auto-fit: auto-fit picks
    its own column count as the window moves, and an odd number of frames
    leaves a different hole at every width. Two columns has exactly one odd
    case, and it is handled below.
  */
  @media (min-width: 34rem) {
    .gallery { grid-template-columns: repeat(2, 1fr); }
  }
  @media (min-width: 34rem) and (max-width: 54.99rem) {
    /* the odd one out takes the whole row instead of sitting beside a gap */
    .gallery > :nth-child(odd):last-child { grid-column: 1 / -1; --ar: 2 / 1; }
  }
  @media (min-width: 55rem) {
    .gallery { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 12rem; }
    .gallery > :nth-child(1) { grid-column: span 2; grid-row: span 2; }
    .gallery > :nth-child(4) { grid-column: span 2; }
    .gallery .scene, .gallery .shot { aspect-ratio: auto; height: 100%; }

    /* Exactly five — :last-child so a sixth frame added later cannot land
       here and quietly break the packing. */
    .gallery:has(> :nth-child(5):last-child) {
      grid-template-columns: repeat(6, 1fr);
      grid-template-rows: 12rem 12rem 14rem;
    }
    .gallery:has(> :nth-child(5):last-child) > :nth-child(1) {
      grid-column: span 4;
      grid-row: span 2;
    }
    .gallery:has(> :nth-child(5):last-child) > :nth-child(2),
    .gallery:has(> :nth-child(5):last-child) > :nth-child(3) { grid-column: span 2; }
    .gallery:has(> :nth-child(5):last-child) > :nth-child(4),
    .gallery:has(> :nth-child(5):last-child) > :nth-child(5) { grid-column: span 3; }
  }

  /* ======================================================== 3-D TOUR */
  /*
    The walkthrough sits in a lit frame like everything else on this street.
    The embed script replaces our placeholder div with its own iframe and puts
    inline width/height on it, so the frame is sized here and the iframe is
    pinned to fill it — whatever it arrives as.
  */
  .tour__frame {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-lg);
    border: 1.5px solid color-mix(in srgb, var(--accent) 55%, transparent);
    background: var(--char);
    box-shadow:
      0 0 22px color-mix(in srgb, var(--accent) 16%, transparent),
      inset 0 0 40px color-mix(in srgb, var(--accent) 6%, transparent);
  }
  /* Whatever the embed inserts, it fills the frame. */
  .tour__frame > iframe,
  .tour__frame > [data-teliportme] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
  }

  /*
    16:9 on a phone is a letterbox slot barely 200px tall — useless for
    looking around a room. Portrait-ish and capped to the screen instead.
  */
  @media (max-width: 46rem) {
    .tour__frame {
      aspect-ratio: auto;
      height: min(70svh, 30rem);
    }
  }

  .tour__status {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.8ch;
    margin: 0;
    max-width: none;
    font-family: var(--f-display);
    font-size: 0.86rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  /* Idle means the script has not been asked for yet — saying "loading"
     then would be a lie, and a spinner that never resolves is worse. */
  .tour[data-tour-state="idle"] .tour__status { visibility: hidden; }
  .tour[data-tour-state="failed"] .tour__status::after {
    content: "Tour unavailable — open it full screen below.";
  }
  .tour[data-tour-state="failed"] .tour__status {
    font-size: var(--t-sm);
    letter-spacing: 0.04em;
    text-transform: none;
    color: var(--mist);
  }
  .tour[data-tour-state="failed"] .tour__status > *,
  .tour[data-tour-state="ready"] .tour__status { display: none; }

  .tour__spinner {
    width: 0.9rem; height: 0.9rem;
    border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
    border-top-color: var(--accent);
    border-radius: 50%;
  }
  @media (prefers-reduced-motion: no-preference) {
    .tour[data-tour-state="loading"] .tour__spinner {
      animation: tour-spin 0.9s linear infinite;
    }
    @keyframes tour-spin { to { rotate: 360deg; } }
  }

  .tour__note {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2) var(--s-4);
    margin-top: var(--s-4);
    max-width: none;
    font-size: var(--t-sm);
    color: var(--mist);
  }

  /* ======================================================= HOURS / MAP */
  .hours { display: grid; border-top: 1px solid var(--line); }
  .hours > div {
    display: flex;
    justify-content: space-between;
    gap: var(--s-4);
    padding: var(--s-4) 0;
    font-size: var(--t-sm);
    border-bottom: 1px solid var(--line);
  }
  .hours dt { color: var(--paper); }
  .hours dd { color: var(--mist); font-variant-numeric: tabular-nums; }
  .hours .is-closed dd { color: var(--neon); }
  .hours .is-today {
    padding-inline: var(--s-4);
    margin-inline: calc(var(--s-4) * -1);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--gold) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--gold);
  }

  .map-frame {
    border: 1px solid var(--line);
    overflow: hidden;
    background: var(--char);
    min-height: 22rem;
    height: 100%;
  }
  .map-frame iframe {
    width: 100%;
    height: 100%;
    min-height: 22rem;
    border: 0;
    display: block;
    /* the map, dimmed into the room */
    filter: invert(0.92) hue-rotate(180deg) saturate(0.55) contrast(0.92) brightness(0.95);
  }

  .nap { display: grid; gap: var(--s-5); font-size: var(--t-sm); }
  .nap a { color: var(--paper); }
  .nap a:hover { color: var(--accent); }
  .nap dt {
    font-family: var(--f-display);
    font-size: 0.7rem;
    font-weight: 400;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--mist-2);
    margin-bottom: 6px;
  }

  /* =============================================================== MENU */
  /*
    Twenty sections, so this scrolls sideways — and a strip that scrolls has
    to say so. It reads as a lit rail rather than a tint on the page: its own
    panel, its own border, its own glow, so it is obviously a control and not
    a caption. A fade at each end shows there is more past the edge, and the
    ends only appear when there is actually something to scroll to, set by
    js/nav.js — a permanent fade against an unscrollable rail is a lie.
  */
  /*
    Sticky lives on the wrapper, not the rail. A sticky element rides over
    whatever follows it, so a scroll cue placed after the rail is covered the
    moment it sticks — inside the wrapper, the cue travels with it.
  */
  .menu-tabs-rail {
    position: sticky;
    top: calc(var(--nav-h) + var(--s-2));
    z-index: 50;
    margin-bottom: var(--s-7);
  }
  .menu-tabs {
    position: relative;
    display: flex;
    gap: var(--s-5);
    overflow-x: auto;
    /* Padding inside the rail, so the first and last tab are not jammed
       against the edge, and scroll-padding so a scrolled-to tab is not
       either. */
    padding: var(--s-4) var(--s-5);
    scroll-padding-inline: var(--s-5);
    border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--char) 92%, transparent);
    backdrop-filter: blur(14px) saturate(1.2);
    -webkit-backdrop-filter: blur(14px) saturate(1.2);
    box-shadow:
      0 0 18px color-mix(in srgb, var(--accent) 16%, transparent),
      0 6px 24px color-mix(in srgb, var(--void) 70%, transparent);
    scrollbar-width: none;
    scroll-behavior: smooth;
  }
  .menu-tabs::-webkit-scrollbar { display: none; }

  /*
    The fades. mask rather than a gradient overlay: the rail is translucent
    and sits over a moving page, so an overlay would have to match a
    background that is not fixed. A mask takes the tabs out instead.
  */
  .menu-tabs-rail.can-scroll-right .menu-tabs {
    mask-image: linear-gradient(90deg, #000 calc(100% - 4rem), transparent);
  }
  .menu-tabs-rail.can-scroll-left .menu-tabs {
    mask-image: linear-gradient(90deg, transparent, #000 4rem);
  }
  .menu-tabs-rail.can-scroll-left.can-scroll-right .menu-tabs {
    mask-image: linear-gradient(90deg,
      transparent, #000 4rem, #000 calc(100% - 4rem), transparent);
  }

  .menu-tabs a {
    flex: none;
    /* Anchors the hit area below. */
    position: relative;
    font-family: var(--f-display);
    font-size: 0.82rem;
    font-weight: 400;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    padding-bottom: 0.4rem;
    /* Was --mist-2, which on this panel reads as disabled rather than as a
       list of twenty places you can go. */
    color: var(--paper);
    border-bottom: 2px solid transparent;
    transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
  }
  /*
    The visible tab is one line of Anton — about 31px tall, under the 44px
    minimum for a touch target. Padding the anchor would work and would also
    push the text down and drag the active underline away from it, so the
    extra height goes on an invisible ::after centred over the link instead.
    Nothing visible moves.

    Vertical only, deliberately. The rail's own padding is --s-4 (16px), so
    ~7px each way stays inside it and clears nothing else; growing sideways
    would eat into the --s-5 (24px) gap and let neighbouring tabs overlap,
    which trades one target-size failure for another.

    height + min-height rather than a flat 44px: the target tracks the link
    if the type scale ever puts it above 44, and never drops below.
  */
  .menu-tabs a::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 50%;
    translate: 0 -50%;
    height: 100%;
    min-height: 44px;
  }

  .menu-tabs a:hover { color: var(--accent); }
  .menu-tabs a.is-active {
    color: var(--accent);
    border-bottom-color: var(--accent);
    text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 50%, transparent);
  }

  /* The hint under the rail, until the guest scrolls it once. */
  /*
    The cue: a chevron pinned to whichever edge has more past it, sitting on
    the rail rather than after it. Pointer-events off, because it overlaps
    tabs that must stay clickable underneath.
  */
  /* The row the pill sits in. The chevrons hang off this rather than off the
     rail, so that the printed-menu line below cannot drag them off centre. */
  .menu-tabs-bar { position: relative; }

  .menu-tabs-bar::after,
  .menu-tabs-bar::before {
    content: "";
    position: absolute;
    top: 50%;
    translate: 0 -50%;
    width: 2.1rem;
    height: 2.1rem;
    z-index: 2;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.25s var(--ease-out);
    background: no-repeat center / 0.85rem;
  }
  .menu-tabs-bar::after {
    right: 0.5rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 20'%3E%3Cpath d='M2 2l8 8-8 8' stroke='%23FFC53D' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
  .menu-tabs-bar::before {
    left: 0.5rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 20'%3E%3Cpath d='M10 2l-8 8 8 8' stroke='%23FFC53D' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
  .menu-tabs-rail.can-scroll-right .menu-tabs-bar::after,
  .menu-tabs-rail.can-scroll-left .menu-tabs-bar::before { opacity: 1; }

  /* A nudge on the chevron that still has somewhere to go, until the guest
     scrolls the rail once. */
  @media (prefers-reduced-motion: no-preference) {
    .menu-tabs-rail.is-untouched.can-scroll-right .menu-tabs-bar::after {
      animation: nudge 1.8s var(--ease-out) infinite;
    }
    @keyframes nudge {
      0%, 100% { translate: 0 -50%; }
      50%      { translate: 5px -50%; }
    }
  }

  /*
    The line under the tabs that offers the printed sheet.

    Deliberately quiet. The live list is the menu — it is the version that
    reads on a phone, that search engines index, and that is always current.
    This points at the designed page with the dish photographs on it, which
    is what somebody who cannot picture "japchae" actually wants, so it has
    to be findable without competing with the thing it sits on top of. One
    line, house cyan, no box, no button.

    Tucked under the right-hand end of the tabs and inside the sticky rail,
    so it stays reachable for the length of a 262-item list rather than
    scrolling away in the first second.

    A bare link, no plate under it. It does sit in a sticky element, so the
    page slides beneath it — the only concession to that is a dark halo in
    the text shadow, which holds the letters together over moving menu rows
    without drawing a box around them.

    max-width because base.css measures a paragraph at 66ch, and this is a
    label, not prose.
  */
  .menu-cue {
    width: fit-content;
    max-width: 100%;
    margin: var(--s-3) 0 0 auto;   /* auto on the left = right-aligned */
  }
  /* A step down from the tabs beside it. This is the alternative, not the
     navigation. */
  .menu-cue .link-arrow {
    font-size: 0.72rem;
    padding-bottom: 0.3rem;
    text-shadow:
      0 0 12px color-mix(in srgb, var(--ice) 40%, transparent),
      0 1px 5px var(--void),
      0 0 14px var(--void);
  }
  @media (max-width: 40rem) {
    .menu-cue { margin-top: var(--s-2); }
    .menu-cue .link-arrow { font-size: 0.68rem; }
  }
  /* The arrow points where the link goes. Same component, turned through 90
     degrees — including the nudge it makes on hover. */
  .link-arrow--down::after { content: "↓"; }
  .link-arrow--down:hover::after { transform: translateY(5px); }

  .mgroup { margin-bottom: var(--s-9); scroll-margin-top: 9rem; }
  .mgroup__head {
    display: flex;
    align-items: baseline;
    gap: var(--s-4);
    margin-bottom: var(--s-5);
    padding-bottom: var(--s-4);
    border-bottom: 1px solid var(--line);
  }
  .mgroup__head .ko {
    font-family: var(--f-ko-dis);
    color: var(--accent);
    font-size: var(--t-md);
    text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 45%, transparent);
  }
  .mgroup__note { font-size: var(--t-sm); color: var(--mist); margin-bottom: var(--s-6); }

  .mlist {
    display: grid;
    gap: var(--s-5) var(--s-8);
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
  }
  .mitem {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0 var(--s-4);
    align-items: baseline;
    padding-bottom: var(--s-3);
    border-bottom: 1px solid color-mix(in srgb, var(--line) 65%, transparent);
  }
  .mitem__name {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 90;
    font-size: var(--t-base);
    font-weight: 700;
    color: var(--paper);
    display: flex;
    align-items: center;
    gap: 0.6ch;
    flex-wrap: wrap;
  }
  .mitem__ko {
    font-family: var(--f-ko);
    font-size: 0.82em;
    color: var(--mist-2);
    font-weight: 400;
  }
  .mitem__price {
    font-family: var(--f-display);
    font-size: var(--t-base);
    font-weight: 400;
    letter-spacing: 0.02em;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  .mitem__desc {
    grid-column: 1 / -1;
    font-size: var(--t-xs);
    color: var(--mist);
    margin-top: 4px;
  }
  .mitem__flags { display: inline-flex; gap: 0.35ch; }
  .flag {
    font-family: var(--f-display);
    font-size: 0.64rem;
    line-height: 1;
    padding: 0.28rem 0.45rem 0.22rem;
    font-weight: 400;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    border: 1px solid currentColor;
    border-radius: var(--radius-pill);
  }
  .flag--spicy { color: var(--neon); }
  .flag--veg,
  .flag--gf    { color: #6FE8A8; }
  .flag--gold  { color: var(--gold); }
  .flag--new   { color: var(--ice); }

  /* A row with no price gives the name the whole width rather than leaving
     a gap where a number would be. */
  .mitem--noprice { grid-template-columns: 1fr; }

  /*
    The AYCE house rules. Two columns on desktop because it is a list of
    thirteen short lines — one column reads as fine print nobody finishes.
  */
  .rules {
    display: grid;
    gap: var(--s-3) var(--s-7);
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    padding: var(--s-6);
    border-radius: var(--radius-lg);
    border: 1.5px solid color-mix(in srgb, var(--accent) 45%, transparent);
    box-shadow: inset 0 0 30px color-mix(in srgb, var(--accent) 7%, transparent);
    background: var(--char);
  }
  .rules li {
    display: grid;
    grid-template-columns: 1.1rem 1fr;
    gap: var(--s-2);
    font-size: var(--t-sm);
    color: var(--mist);
  }
  .rules li::before {
    content: "";
    width: 5px; height: 5px;
    margin-top: 0.62em;
    background: var(--accent);
    rotate: 45deg;
  }

  /* ============================================================== QUOTE */
  /*
    One review at a time, on a scroll-snap track.

    A grid of three or four columns worked when the quotes were a sentence
    each; real Google reviews run to a paragraph, and four paragraphs side by
    side is a wall nobody reads. One card, full width, paged by swipe.

    Every review is still in the DOM and in order — the snap track only
    decides what is on screen — so the crawlers this section exists for read
    the lot. See netlify/lib/reviews-html.mjs.
  */
  .quotes {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 100%;
    gap: var(--s-6);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    /* The dots are the position indicator; a scrollbar under one card is
       just a second one that looks like a mistake. */
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .quotes::-webkit-scrollbar { display: none; }
  .quotes:focus-visible { outline: 2px solid var(--ice); outline-offset: 6px; }
  /* A single review is not a carousel — no track, no controls, no swipe. */
  .quotes.is-single { display: block; overflow: visible; }

  .quote {
    position: relative;
    scroll-snap-align: start;
    padding-top: var(--s-5);
    border-top: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
    /* Long reviews read as a column, not a paragraph across a laptop. */
    max-width: 44rem;
  }
  /* A guest talking, not the house — so it stays in the body face and does
     not glow. The lit rule above it is the house's frame around them.

     A step down from --t-md: these are now paragraphs rather than one-liners,
     and at the old size a full Google review filled the screen. */
  .quote p {
    font-family: var(--f-body);
    font-size: var(--t-sm);
    line-height: 1.7;
    color: var(--paper);
  }
  /* The rating, drawn not marked up — see the note in reviews-html.mjs.

     Scoped through .quote rather than left as a bare class: the stars are a
     <p>, so `.quote p` above matches them too and at equal weight the colour
     there wins on source order. This needs the extra specificity or the
     stars come out cream. */
  .quote .quote__stars {
    margin-bottom: var(--s-3);
    font-size: 0.95rem;
    letter-spacing: 0.18em;
    line-height: 1;
    color: var(--gold);
  }

  /* --------------------------------------------------- carousel controls */
  /* Written by js/reviews.js, so they are absent until it runs — which is
     also what keeps the track usable when it does not. */
  .quotes-nav {
    display: flex;
    align-items: center;
    gap: var(--s-4);
    margin-top: var(--s-5);
  }
  .quotes-nav__btn {
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--line));
    background: var(--char);
    color: var(--paper);
    cursor: pointer;
    transition: border-color 0.25s var(--ease-out), opacity 0.25s var(--ease-out);
  }
  .quotes-nav__btn:hover { border-color: var(--accent); }
  .quotes-nav__btn[disabled] { opacity: 0.35; cursor: default; }
  .quotes-nav__dots { display: flex; gap: var(--s-3); margin-inline: auto; }
  .quotes-nav__dot {
    width: 7px; height: 7px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--mist-2) 55%, transparent);
    cursor: pointer;
    transition: background 0.25s var(--ease-out), transform 0.25s var(--ease-out);
  }
  .quotes-nav__dot.is-on {
    background: var(--accent);
    transform: scale(1.5);
  }
  .quotes-nav__count {
    font-family: var(--f-display);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    color: var(--mist-2);
  }
  /* figcaption as well as cite: the quotes are figure > blockquote >
     figcaption, which is the markup for an attributed quotation. cite is for
     the title of a work, not the name of the person who said it. */
  .quote cite,
  .quote figcaption {
    display: block;
    margin-top: var(--s-4);
    font-family: var(--f-display);
    font-size: 0.78rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-style: normal;
    color: var(--mist-2);
  }
  /* Recency is most of what a review is worth, so the date is shown, but it
     is the guest's name that should read first. */
  .quote__date { color: color-mix(in srgb, var(--mist-2) 70%, transparent); }

  /*
    The reviews section runs down the middle.

    One quote is on screen at a time inside a band the full width of the
    wrap, and the quote is capped at 44rem so a long review reads as a column
    rather than a line across a laptop. Left-aligned that left about 475px of
    empty band beside every review, which read as a layout mistake rather
    than as breathing room.

    The paragraph itself stays ragged-right. These are real reviews and they
    run long — the first one on the site is about five hundred characters —
    and centring a block that size gives every line a different starting
    point, which is exactly what makes centred body copy hard to read. Only
    the short things go centred: the heading, the stars, and the name.
  */
  .reviews .shead {
    margin-inline: auto;
    justify-items: center;
    text-align: center;
  }
  /*
    Centred, and it has to be the snap that says so.

    The auto margin alone only holds at scrollLeft 0. .quote snaps with
    scroll-snap-align:start, so the moment the track scrolls at all the
    browser pulls the card's left edge onto the scrollport's left edge and
    cancels the 302px the margin just added — first and second review flush
    left, third centred only because it cannot scroll any further. Centring
    the snap point instead keeps all three in the middle, and it agrees with
    what js/reviews.js already scrolls to for a given card.
  */
  .reviews .quote {
    margin-inline: auto;
    scroll-snap-align: center;
  }
  .reviews .quote .quote__stars,
  .reviews .quote cite,
  .reviews .quote figcaption { text-align: center; }
  /* Line the controls up with the quote instead of the whole band, or the
     arrows sit far out to either side of what they are paging. */
  .reviews .quotes-nav {
    max-width: 44rem;
    margin-inline: auto;
  }

  /* ================================================== SETTINGS (/t) */
  /*
    A back-office screen wearing the same clothes as the site. It gets the
    neon vocabulary so it does not feel like a different product, but nothing
    here glows for its own sake — the job is reading numbers and changing them.
  */
  /* ---- Inbox ---- */
  /*
    Two columns on a desktop, one on a phone. A restaurant reads this on
    whatever is behind the counter, so the list has to stay usable narrow.
  */
  .t-inbox {
    display: grid;
    gap: var(--s-5);
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
    align-items: start;
  }
  @media (max-width: 56rem) {
    .t-inbox { grid-template-columns: 1fr; }
  }

  /*
    A real gap, not a hairline. These rows are separate cards — own border,
    own radius, own background — so a 1px gap read as one block with seams
    scored across it rather than as a list of messages. The admin__row lists
    elsewhere use 1px on purpose because those rows are borderless bands of
    one panel; this is not that.
  */
  /*
    Panel head: the blurb on the left, the figures on the right. One column
    on a narrow screen, because a four-up stat block squeezed beside a
    paragraph is unreadable long before it is unattractive.
  */
  /* The per-person level picker sits in the row's button strip, so it has
     to read as one of the controls rather than as a form field. */
  .admin__rolepick select {
    padding: 0.45rem 0.7rem;
    background: var(--char-2);
    color: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    font-family: var(--f-display);
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }
  .admin__rolepick select:hover { border-color: var(--accent); }

  /* ---- How to ---- */
  /*
    Reading matter, so it is set narrow. The rest of /t is a control panel
    and wants the width; this is the one screen somebody actually reads a
    paragraph on, and a 1200px line is unreadable.
  */
  #t-help { display: grid; gap: var(--s-6); max-width: 46rem; }
  .t-help__item {
    display: grid;
    gap: var(--s-3);
    padding: var(--s-5) var(--s-6);
    background: var(--char);
    border: 1px solid var(--line);
    border-left: 3px solid var(--ice);
    border-radius: var(--radius);
  }
  .t-help__item h3 {
    font-family: var(--f-display);
    font-size: 1.05rem;
    font-weight: 400;
    letter-spacing: 0.02em;
    color: var(--paper);
  }
  .t-help__item p {
    font-size: var(--t-sm);
    line-height: 1.7;
    color: var(--mist);
    max-width: none;
  }
  /* The "you cannot change these" note is information, not instruction. */
  .t-help__item--muted { border-left-color: var(--mist-2); }

  /* The role ladder on the People panel — a definition list, because that
     is what it is: four names and what each one means. */
  .t-roles {
    display: grid;
    gap: 1px;
    margin-top: var(--s-5);
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
  }
  .t-roles > div {
    display: grid;
    gap: 0.2rem;
    padding: var(--s-3) var(--s-4);
    background: var(--char);
  }
  @media (min-width: 46rem) {
    .t-roles > div {
      grid-template-columns: 9rem minmax(0, 1fr);
      gap: var(--s-4);
      align-items: baseline;
    }
  }
  .t-roles dt {
    font-family: var(--f-display);
    font-size: 0.72rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ice);
  }
  .t-roles dd { color: var(--mist); font-size: var(--t-sm); }

  .t-panel__head { display: grid; gap: var(--s-5); }
  @media (min-width: 62rem) {
    .t-panel__head {
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: start;
      gap: var(--s-8);
    }
  }

  .t-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(7.5rem, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
  }
  .t-stat {
    display: grid;
    gap: 0.2rem;
    padding: var(--s-4) var(--s-5);
    background: var(--char);
  }
  .t-stat dt {
    font-family: var(--f-display);
    font-size: 0.66rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  .t-stat dd {
    font-family: var(--f-display);
    font-size: 1.6rem;
    line-height: 1;
    color: var(--paper);
  }
  /*
    The only one that lights up, and only when it is not zero. A dashboard
    where every number glows tells you nothing; an empty inbox should look
    calm, and one with people waiting in it should not.
  */
  .t-stat.is-live dd { color: var(--accent); }

  .t-inbox__list { display: grid; gap: var(--s-3); }

  /*
    Delete, and delete-about-to-happen.

    Neither class had a rule anywhere, so the destructive button rendered
    identically to Archive and the two-tap arm changed nothing but the word
    on it — while the comment in js/t.js justified the pattern on the grounds
    that the second tap "lands on a button that has visibly changed colour".
    It now does.

    Red reads as danger in here specifically because /t is a cyan zone: every
    other control on the screen is --ice, so the one red thing is the one
    that destroys something. Armed goes solid rather than louder-outlined —
    a filled button under a cursor that is already moving is much harder to
    press by accident than one that only changed its border.
  */
  .t-danger {
    border-color: color-mix(in srgb, var(--neon) 45%, var(--line));
    color: var(--neon-soft);
  }
  .t-danger:hover {
    border-color: var(--neon);
    color: var(--neon);
  }
  /* .t-danger.is-armed went with the two-tap confirm on the inbox delete —
     the love-lock wall and the people list still arm their own controls, so
     the class is not dead everywhere, just here. */
  .t-thread-row {
    display: grid;
    gap: 0.15rem;
    width: 100%;
    text-align: left;
    padding: var(--s-4);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
  }
  .t-thread-row:hover { background: var(--char-2); }
  .t-thread-row.is-open { border-color: var(--accent); background: var(--char-2); }
  /* Unread carries a lit edge — the one thing worth spotting across a room. */
  .t-thread-row.is-new { border-left: 3px solid var(--accent); }
  .t-thread-row.is-archived { opacity: 0.55; }

  .t-thread-row__top {
    display: flex;
    justify-content: space-between;
    gap: var(--s-3);
    align-items: baseline;
  }
  .t-thread-row__top b { font-family: var(--f-sub); color: var(--paper); }
  .t-thread-row__top time { font-size: var(--t-xs); color: var(--mist-2); flex: none; }
  .t-thread-row__subject {
    font-family: var(--f-display);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent);
  }
  .t-thread-row__preview {
    font-size: var(--t-xs);
    color: var(--mist);
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }

  .t-inbox__thread {
    padding: var(--s-5);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    min-height: 12rem;
  }
  .t-thread__head h3 { font-size: var(--t-md); }
  .t-thread__who { font-size: var(--t-xs); color: var(--mist); margin-top: 0.2rem; }
  .t-thread__who a { color: var(--accent); }

  .t-thread__facts {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.3rem var(--s-4);
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--line);
    font-size: var(--t-xs);
  }
  .t-thread__facts dt { color: var(--mist-2); text-transform: uppercase; letter-spacing: 0.08em; }
  .t-thread__facts dd { color: var(--paper); }

  .t-thread__messages { display: grid; gap: var(--s-4); margin-block: var(--s-5); }
  .t-msg {
    padding: var(--s-4);
    border-radius: var(--radius);
    background: var(--void);
    border: 1px solid var(--line);
  }
  /* The house's own words sit on the house's colour, so a thread reads at a
     glance as a conversation rather than as a log. */
  .t-msg--staff {
    background: color-mix(in srgb, var(--accent) 10%, var(--void));
    border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
  }
  .t-msg__meta {
    font-size: 0.66rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mist-2);
    margin-bottom: 0.35rem;
  }

  .t-thread__actions { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-top: var(--s-4); }
  .t-thread__actions .btn { min-height: 2.75rem; }

  /* Unread count on the tab. */
  .t-badge {
    display: inline-grid;
    place-items: center;
    min-width: 1.35rem;
    padding: 0 0.35rem;
    margin-left: 0.6ch;
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--accent-ink);
    font-family: var(--f-body);
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0;
  }

  /* ---- Photos panel ---- */
  .t-photos { display: grid; gap: var(--s-3); margin-top: var(--s-4); }
  .t-photo {
    display: flex;
    /* Top, not centre. These rows carry a paragraph of shooting notes now, so
       a centred thumbnail and button float in the middle of a tall row with
       nothing beside them; aligned to the top they sit level with the name of
       the frame they belong to. */
    align-items: flex-start;
    gap: var(--s-4);
    padding: var(--s-3) var(--s-4);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  /* 16/9 so the thumbnail reads as the frame it will fill, not as an avatar */
  .t-photo__thumb {
    flex: none;
    /* A variable because the narrow layout below has to reclaim exactly this
       much width back, and two hard-coded 7rems would eventually disagree. */
    width: var(--t-photo-thumb, 7rem);
    aspect-ratio: 16 / 9;
    display: grid;
    place-items: center;
    overflow: hidden;
    border-radius: var(--radius-sm, 6px);
    background: var(--void);
    border: 1px solid var(--line);
    font-family: var(--f-body);
    font-size: 0.62rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mist-2);
  }
  .t-photo__thumb img { width: 100%; height: 100%; object-fit: cover; }
  .t-photo__body { flex: 1; min-width: 0; }
  .t-photo__label {
    font-family: var(--f-sub);
    color: var(--paper);
    word-break: break-word;
  }
  .t-photo__note {
    margin-top: 0.2rem;
    font-size: var(--t-xs);
    color: var(--mist-2);
  }
  .t-photo__actions { flex: none; display: flex; gap: var(--s-3); }
  /* The file input is visually hidden; its <label> is the button. Keeping the
     input focusable means the keyboard still reaches the picker. */
  .t-photo__actions .btn { min-height: 2.75rem; cursor: pointer; }
  .t-photo__actions .btn.is-busy { opacity: 0.6; pointer-events: none; }
  .t-photo__actions input:focus-visible + .btn,
  .t-photo__actions .btn:focus-visible { outline: 2px solid var(--ice); outline-offset: 3px; }

  @media (max-width: 40rem) {
    .t-photo { flex-wrap: wrap; }
    .t-photo__body { flex-basis: calc(100% - 8.5rem); }
    .t-photo__actions { width: 100%; }
    .t-photo__actions .btn { flex: 1; }
    /*
      The brief takes the whole row back, sliding under the thumbnail.

      The name of the frame still wants to sit beside its thumbnail — that
      pairing is what makes the list scannable. A paragraph does not: in the
      column left over on a phone it sets to about thirty characters a line,
      which is a newspaper column, and this is the one thing on the page
      somebody actually has to read. So only the brief reclaims the width.
    */
    .t-photo__brief {
      margin-left: calc(-1 * (var(--t-photo-thumb, 7rem) + var(--s-4)));
    }
  }

  /* ------------------------------------------------------------- reviews */
  .t-review {
    padding: var(--s-4);
    background: var(--char);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .t-review + .t-review { margin-top: var(--s-3); }
  /* Hidden means hidden from the site, not from the person managing it —
     dimmed enough to scan past, legible enough to read and put back. */
  .t-review.is-hidden { opacity: 0.55; }
  .t-review__quote {
    font-family: var(--f-sub);
    color: var(--paper);
    max-width: 60ch;
  }
  .t-review__by {
    margin-top: 0.35rem;
    font-size: var(--t-xs);
    color: var(--mist-2);
  }
  .t-review .btn-row { margin-top: var(--s-3); flex-wrap: wrap; }
  .t-review__form {
    margin-top: var(--s-4);
    padding-top: var(--s-4);
    border-top: 1px solid var(--line);
  }

  .t-panel { padding-block: var(--s-7) var(--s-8); }
  .t-panel + .t-panel { border-top: 1px solid var(--line); }
  .t-panel h2 { font-size: var(--t-lg); }

  .t-grid {
    display: grid;
    gap: var(--s-5);
    grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
    margin-top: var(--s-5);
  }

  /*
    Opaque, not transparent. The page carries a faint grid, and without a
    background of its own the card let that grid run straight through the
    prices — the one thing on this screen that has to read as solid.
    --char-2 rather than --char: on a near-black page the darker one lifts
    by 1.06:1, which is close enough to nothing that the card still reads as
    a hole rather than a surface.
  */
  .t-card {
    border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
    border-radius: var(--radius);
    padding: var(--s-5);
    display: grid;
    gap: var(--s-4);
    background: var(--char-2);
  }
  /* The legend sits in the gap it punches in the border, so it needs the
     card's own background behind it rather than the page's. */
  .t-card legend {
    background: var(--char-2);
    font-family: var(--f-display);
    font-size: 0.86rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent);
    padding-inline: var(--s-2);
  }

  /* A checkbox big enough to hit on a phone behind the counter. */
  .t-switch {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    font-size: var(--t-sm);
    color: var(--paper);
    cursor: pointer;
  }
  .t-switch input {
    width: 1.15rem;
    height: 1.15rem;
    accent-color: var(--accent);
    flex: none;
  }

  /* The recommended dimensions under each upload. Quiet, but always there —
     it is cheaper to read than to re-crop a photo that came out wrong. */
  .t-photo__guide {
    margin-top: var(--s-2);
    max-width: 46ch;
    font-size: var(--t-xs);
    line-height: 1.5;
    color: var(--ice);
    opacity: 0.85;
  }

  /*
    The house rules above the photo slots. Folded away by default: it is read
    once, before the first photograph, and after that it is eight paragraphs
    standing between staff and the button they came here to press. Open it and
    it sets as an ordinary column of prose — a panel of body copy is easier to
    read than the same words dressed up as an interface.
  */
  .t-help {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--char);
  }
  .t-help > summary {
    cursor: pointer;
    padding: var(--s-4) var(--s-5);
    font-family: var(--f-display);
    font-size: 0.8rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ice);
  }
  .t-help > summary:hover { color: var(--paper); }
  .t-help[open] > summary { border-bottom: 1px solid var(--line); }
  .t-help > p {
    margin: 0;
    padding: var(--s-4) var(--s-5) 0;
    max-width: 68ch;
    font-size: var(--t-sm);
    line-height: 1.7;
    color: var(--mist);
  }
  .t-help > p:last-child { padding-bottom: var(--s-5); }
  .t-help b { color: var(--paper); font-weight: 700; }

  /*
    The shooting brief under a photo slot: which way up, what to shoot, and
    the one thing that ruins it.

    Set as body copy rather than as a caption, because it is a paragraph and
    not a measurement — full measure, comfortable leading, and only the shape
    line coloured. Colouring all three would read as three warnings, when in
    fact two of them are just instructions and one is the warning.
  */
  .t-photo__brief {
    margin-top: var(--s-3);
    max-width: 64ch;
    display: grid;
    gap: var(--s-2);
  }
  .t-photo__shape {
    font-family: var(--f-display);
    font-size: 0.8rem;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    line-height: 1.3;
    color: var(--ice);
  }
  /* Long enough to be read, so it gets the leading of something meant to be
     read. The cyan belongs to the line above it, not to this. */
  .t-photo__brief .t-photo__guide {
    margin-top: 0;
    max-width: none;
    font-size: var(--t-sm);
    line-height: 1.65;
    color: var(--mist);
    opacity: 1;
  }
  .t-photo__avoid {
    font-size: var(--t-sm);
    line-height: 1.6;
    color: var(--mist-2);
  }
  .t-photo__avoid b {
    font-weight: 700;
    color: var(--gold);
  }

  .t-readout {
    margin-top: var(--s-5);
    padding: var(--s-5);
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: var(--char);
  }
  .t-readout .hours { border-top: 0; margin-top: var(--s-4); }

  .t-flag {
    font-family: var(--f-display);
    font-size: 0.78rem;
    letter-spacing: 0.13em;
    text-transform: uppercase;
  }
  .t-flag--ok { color: var(--ice); }
  .t-flag--warn { color: var(--gold); }

  /* ============================================== ADMIN (/admin, noindex) */
  /*
    A utility screen, not a page of the site — no hero, no scenery, no
    reveals. Whoever opens this is looking for one lock and wants it gone.
  */
  .admin {
    padding-top: calc(var(--nav-h) + var(--s-7));
    padding-bottom: var(--s-8);
    min-height: 80svh;
  }
  .admin > .wrap > .h-sub { margin-bottom: var(--s-5); }

  .admin__gate { max-width: 26rem; }
  .admin__login { margin-top: var(--s-6); }
  .admin__login .btn { margin-top: var(--s-4); }

  .admin__head {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-5);
    align-items: flex-end;
    justify-content: space-between;
    padding-bottom: var(--s-5);
    border-bottom: 1px solid var(--line);
  }
  /* Square, so it reads as an icon rather than as a button whose label
     failed to load. The 44px floor is the tap target — the icon inside stays
     small. */
  .admin__home {
    padding: 0;
    width: 2.75rem;
    height: 2.75rem;
    display: inline-grid;
    place-items: center;
    flex: none;
  }
  .admin__home svg { width: 1.15rem; height: 1.15rem; }

  .admin__search { min-width: min(20rem, 100%); margin: 0; }
  .admin__head .link-arrow { margin-top: var(--s-3); }

  .admin__note {
    margin: var(--s-5) 0;
    padding: var(--s-4) var(--s-5);
    border-left: 2px solid var(--accent);
    background: var(--char);
    color: var(--mist);
    font-size: var(--t-sm);
  }

  .admin__rows { display: flex; flex-direction: column; gap: 1px; }

  .admin__row {
    display: flex;
    align-items: center;
    gap: var(--s-5);
    padding: var(--s-4) var(--s-5);
    background: var(--char);
    transition: background 0.2s var(--ease-out);
  }
  .admin__row:hover { background: var(--char-2); }

  /* The padlock itself, so staff match what they see on the wall rather than
     reading a row of near-identical names. */
  .admin__chip {
    flex: none;
    rotate: 0deg;
    cursor: default;
    scale: 0.85;
  }
  .admin__chip::after { display: none; }

  .admin__body { flex: 1; min-width: 0; }
  .admin__name {
    font-family: var(--f-sub);
    font-variation-settings: "wdth" 90;
    font-weight: 700;
    font-size: var(--t-md);
    color: var(--paper);
    word-break: break-word;
  }
  .admin__msg {
    font-family: var(--f-ko);
    color: var(--mist);
    word-break: break-word;
  }
  .admin__meta {
    margin-top: 0.25rem;
    font-size: var(--t-xs);
    letter-spacing: 0.06em;
    color: var(--mist-2);
  }
  .admin__err { margin-top: 0.4rem; font-size: var(--t-xs); color: var(--neon-soft); }

  /* btn--sm lands at 39px tall — under the touch minimum, and on mobile this
     is the only control in the row. */
  .admin__act { flex: none; min-height: 2.75rem; }
  /* Armed: the next tap removes it. Red is the only place on this screen it
     appears, so the change of state is unmissable. */
  .admin__act.is-armed {
    background: var(--neon);
    color: var(--accent-ink);
    box-shadow: none;
  }
  /* The ghost button's hover wipe is a white ::before. The pointer is by
     definition still over the button at the moment it arms, so without this
     the wipe paints straight over the red and the warning reads as cream. */
  .admin__act.is-armed::before { display: none; }
  .admin__act.is-armed:hover { color: var(--accent-ink); }

  .admin__bin { margin-top: var(--s-7); }
  .admin__bin > summary {
    cursor: pointer;
    font-family: var(--f-display);
    font-size: 0.78rem;
    font-weight: 400;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--mist);
    padding: var(--s-4) 0;
    border-top: 1px solid var(--line);
  }
  .admin__bin > summary:hover { color: var(--paper); }
  .admin__bin > .muted { margin-bottom: var(--s-4); font-size: var(--t-xs); }
  .admin__bin .admin__row { opacity: 0.65; }
  .admin__bin .admin__row:hover { opacity: 1; }

  .admin__pill {
    display: inline-grid;
    place-items: center;
    min-width: 1.6rem;
    padding: 0.1rem 0.4rem;
    margin-left: 0.5ch;
    border-radius: 999px;
    background: var(--line);
    color: var(--paper);
    font-size: 0.65rem;
    letter-spacing: 0;
  }

  @media (max-width: 40rem) {
    .admin__row { flex-wrap: wrap; gap: var(--s-4); }
    .admin__body { flex-basis: calc(100% - 6rem); }
    .admin__act { width: 100%; }
  }
}
