/* ==========================================================================
   MARLOWE COFFEE HOUSE
   Concept project by Northline Web Co.

   Two "worlds": a warm cream world that most of the page lives in, and a dark
   espresso world used for the hero, the story and the footer. Components read
   semantic tokens, so the same markup works in either world without a modifier.

   SHAPE RULE (one system, followed everywhere)
     photographs        4px, barely there — an editorial crop, not a card
     buttons and inputs 4px
     pills and tags     full round
   Nothing else is rounded. The previous 12px card radius was the single
   strongest "template" signal on the page: magazines do not round their
   photographs, and neither does anything that wants to look printed.

   MOTION SYSTEM
     --dur-1 160ms   state changes: hover, focus, press
     --dur-2 280ms   small movements: underlines, crossfades, tags
     --dur-3 420ms   panels, drawers, image swaps
     --dur-4 900ms   entrances, reveals, the hero
     --ease          cubic-bezier(.22,1,.36,1) — decelerating, no overshoot
     --ease-inout    cubic-bezier(.65,0,.35,1) — for things that leave as well
   There is no spring and no bounce anywhere. Everything is disabled under
   prefers-reduced-motion in section 24.

   CONTENTS
     01 fonts            09 editorial layout    17 booking drawer
     02 tokens           10 featured strip      18 lightbox
     03 reset            11 story               19 forms
     04 primitives       12 gallery             20 live status
     05 texture          13 menu                21 reveals
     06 nav              14 weekly filter       22 phone
     07 mobile menu      15 place and contact  23 print
     08 hero             16 footer              24 reduced motion
   ========================================================================== */


/* 01  FONTS
   Self hosted. Hotlinking Google Fonts would send every visitor's IP address
   to a third party, which is not something a café needs to do to show a menu.
   Latin subsets only; they cover the Danish characters.

   The fallback stacks are chosen for metric proximity, not for being "a serif"
   and "a sans": with font-display: swap the fallback is what a visitor on a
   slow connection actually reads for the first few hundred milliseconds.
   ========================================================================== */

@font-face {
  font-family: 'Bodoni Moda';
  font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/bodoni-moda-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Bodoni Moda';
  font-style: italic; font-weight: 400 800; font-display: swap;
  src: url('../fonts/bodoni-moda-italic-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Schibsted Grotesk';
  font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/schibsted-grotesk-latin.woff2') format('woff2');
}


/* 02  TOKENS
   ========================================================================== */

:root {
  /* Palette. The six values below are written by tools/build.js from
     site.config.js — edit them there, not here. */
  /* gen:brand */
  --espresso:    #221F1C;
  --cream:       #F7F5F1;
  --oat:         #EFEBE3;
  --caramel:      #3A5A8C;
  --caramel-deep: #2B4570;
  --caramel-lit:  #93B2DE;
  /* /gen:brand */

  --espresso-2:  #33302B;
  --espresso-3:  #161412;
  --sand:        #E3DED4;

  /* The accent is indigo here, not caramel - the token names are inherited
     from the Marlowe template and were left alone on purpose, since renaming
     them would touch hundreds of declarations and change nothing on screen.
     It still needs three values because one cannot do three jobs. --caramel is
     6.4:1 on paper: fills, rules and large type. --caramel-deep is 8.8:1 and is
     what body-sized accent text uses. --caramel-lit is 7.6:1 on ink, for the
     dark world. Indigo rather than red because #9A2F1C is already spoken for
     as the form-error colour. */

  /* semantic tokens — the warm world is the default */
  --bg:          var(--cream);
  --bg-raised:   #FFFFFF;
  --bg-sunken:   var(--oat);
  --fg:          var(--espresso);
  --fg-muted:    #5A5550;
  /* Darkened from #6E6862, which measured 4.14:1 on cream and 3.80:1 on the
     oat sections and so failed AA for body sized text. This carries labels,
     field hints and the footer base line, all real copy rather than
     decoration. Now 5.12:1 on cream and 4.69:1 on oat. */
  --fg-faint:    #6E6862;
  --rule:        rgb(34 31 28 / 0.13);
  --rule-strong: rgb(34 31 28 / 0.26);
  --accent-fg:   var(--caramel-deep);
  --btn-bg:      var(--espresso);
  --btn-fg:      var(--cream);
  --field-bg:    #FFFFFF;
  /* WCAG 1.4.11 wants 3:1 for the boundary of a UI component against its
     surroundings. At 0.22 this border measured 1.58:1 on white, so the edge of
     every input was effectively invisible to anyone with low contrast vision.
     0.48 measures 3.03:1. The hairline --rule above stays light on purpose: it
     is decorative separation, which 1.4.11 does not cover. */
  --field-rule:  rgb(34 31 28 / 0.48);
  --scrim:       rgb(26 17 12 / 0.52);

  /* spacing */
  --sp-1: .25rem; --sp-2: .5rem;  --sp-3: .75rem; --sp-4: 1rem;
  --sp-5: 1.5rem; --sp-6: 2rem;   --sp-7: 3rem;   --sp-8: 4rem;
  --sp-9: 6rem;   --sp-10: 9rem;
  --section-y: clamp(4.5rem, 10vw, 9rem);

  /* layout */
  --wrap: 1240px;
  --wrap-wide: 1560px;
  --wrap-text: 68ch;
  --gutter: clamp(1.25rem, 5vw, 4rem);
  --nav-h: 76px;
  --nav-h-stuck: 62px;

  /* type */
  --font-display: 'Bodoni Moda', 'Didot', 'Bodoni MT', 'Times New Roman', serif;
  --font-sans: 'Schibsted Grotesk', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --fs-xl:    clamp(2.9rem, 8.4vw, 7.25rem);   /* hero only */
  --fs-d1:    clamp(2.05rem, 4.5vw, 3.85rem);
  --fs-d2:    clamp(1.5rem, 2.6vw, 2.15rem);
  --fs-d3:    clamp(1.15rem, 1.7vw, 1.4rem);
  --fs-lead:  clamp(1.0625rem, 1.35vw, 1.28rem);
  --fs-body:  clamp(.9688rem, 1vw, 1.0625rem);
  --fs-small: .875rem;
  --fs-tiny:  .8125rem;
  --fs-label: .6875rem;
  --track-label: .16em;

  /* shape */
  --r-img: 4px;
  --r-ui: 4px;

  /* motion */
  --dur-1: 160ms;
  --dur-2: 280ms;
  --dur-3: 420ms;
  --dur-4: 900ms;
  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-inout: cubic-bezier(.65, 0, .35, 1);

  /* Paper grain. An inline SVG turbulence rather than an image file: about
     300 bytes, no request, and it tiles at 160px so the browser rasterises one
     small square and repeats it. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");

  --z-grain: 3;
  --z-nav: 100;
  --z-bar: 90;
  --z-panel: 200;
}

/* The dark world, applied per section.

   The footer is in it too, and not as a convenience: it contains a form. With
   only its own background and colour set, the newsletter label rendered in
   --fg-muted (a dark brown meant for cream paper) on espresso, and the primary
   button rendered espresso-on-espresso — an invisible label above an invisible
   button. Components read tokens, so the footer has to actually be in the dark
   world rather than merely look like it. */
.world--dark,
.footer {
  --bg:          var(--espresso);
  --bg-raised:   var(--espresso-2);
  --bg-sunken:   var(--espresso-3);
  --fg:          var(--cream);
  --fg-muted:    #C9C3BB;
  --fg-faint:    #A39C94;
  --rule:        rgb(247 245 241 / 0.15);
  --rule-strong: rgb(247 245 241 / 0.3);
  --accent-fg:   var(--caramel-lit);
  --btn-bg:      var(--cream);
  --btn-fg:      var(--espresso);
  --field-bg:    rgb(247 245 241 / 0.06);
  --field-rule:  rgb(247 245 241 / 0.42);

  background: var(--bg);
  color: var(--fg);
}


/* 03  RESET
   ========================================================================== */

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

html {
  /* clip, not hidden: hidden forces overflow-y to auto, makes <body> a scroll
     container and silently breaks every position: sticky on the page. */
  overflow-x: clip;
  scroll-behavior: smooth;
  /* Anchor targets must clear the sticky header. */
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  overflow-x: clip;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: 1.65;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, p, figure, figcaption, blockquote, ul, ol, dl, dd, table, fieldset { margin: 0; }
ul, ol { padding: 0; list-style: none; }
fieldset { border: 0; padding: 0; min-width: 0; }
legend { padding: 0; }
img { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; }
a { color: inherit; text-decoration: none; }
svg { display: block; }
address { font-style: normal; }
:target { scroll-margin-top: calc(var(--nav-h) + 1.5rem); }

:focus-visible { outline: 2px solid var(--accent-fg); outline-offset: 3px; border-radius: 2px; }
input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent-fg); outline-offset: 1px; }
::selection { background: var(--caramel); color: #fff; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: absolute; top: -100%; left: var(--gutter); z-index: 300;
  background: var(--espresso); color: var(--cream);
  padding: var(--sp-3) var(--sp-4); border-radius: var(--r-ui); font-weight: 600;
}
.skip-link:focus { top: var(--sp-3); }


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

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); }
.wrap--wide { max-width: var(--wrap-wide); }

/* Display type. One face, one set of proportions, four sizes.
   font-weight stays low on purpose: a Didone gets its authority from the
   thick-to-thin contrast, and adding weight replaces elegance with volume. */
.display { font-family: var(--font-display); font-weight: 400; line-height: 1.05; letter-spacing: -0.018em; }
.d1 { font-size: var(--fs-d1); max-width: 18ch; }
.d2 { font-size: var(--fs-d2); }
.d3 { font-size: var(--fs-d3); }
.display em { font-style: italic; font-weight: 500; color: var(--accent-fg); }

.lead { font-size: var(--fs-lead); color: var(--fg-muted); max-width: 54ch; line-height: 1.55; }
.muted { color: var(--fg-muted); }
.prose { max-width: var(--wrap-text); }
.prose > * + * { margin-top: var(--sp-5); }

/* The small uppercase editorial label. One class, used for eyebrows, column
   headings in the footer, and field group labels, so they cannot drift. */
.label {
  display: block; font-size: var(--fs-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-faint); font-weight: 600; line-height: 1.4;
}
.eyebrow { composes: label; }
.eyebrow, .label { margin: 0; }

/* Every number a visitor compares — prices, times, counts — is lining and
   tabular, so columns align and digits do not jitter as the clock ticks. */
.num, .hours td, .mitem__price, .feat__price, .slot, .weekly__price {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: 'tnum' 1, 'lnum' 1;
}

.section { padding-block: var(--section-y); background: var(--bg); color: var(--fg); }
.section--tight { padding-block: clamp(3rem, 6vw, 5rem); }
.section--oat { background: var(--oat); }

/* The thin divider that separates one editorial movement from the next.
   A rule, not a box: nothing on this site is inside a container. */
.rule { height: 1px; background: var(--rule); border: 0; margin: 0; }

/* Section heading block. Not centred — the whole page hangs off one left
   margin, which is what makes it read as a magazine rather than a landing
   page. `--num` prints an oversized folio behind the heading. */
.head { display: grid; gap: var(--sp-4); max-width: 56ch; margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* The folio. Deliberately in normal flow rather than hung in the left margin:
   the margin version looked better on a 1240px container and slid straight off
   the left edge of the 1560px one, because `right: 100%` has nothing to hang
   into once the container fills the viewport. A number that silently leaves
   the page is worse than a number that sits above the label. */
.head__num {
  display: block; font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 3rem); line-height: 1;
  letter-spacing: -.02em; color: var(--rule-strong);
  margin-bottom: var(--sp-3); user-select: none;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .55em;
  padding: .9rem 1.7rem; border: 1px solid transparent; border-radius: var(--r-ui);
  font-size: var(--fs-small); font-weight: 600; cursor: pointer; white-space: nowrap;
  letter-spacing: .01em;
  /* 44px minimum target in both directions, per WCAG 2.2 target size. */
  min-height: 44px;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.btn--primary { background: var(--btn-bg); color: var(--btn-fg); border-color: var(--btn-bg); }
.btn--primary:hover { background: var(--caramel-deep); border-color: var(--caramel-deep); color: #fff; }
.world--dark .btn--primary:hover, .footer .btn--primary:hover { background: var(--caramel-lit); border-color: var(--caramel-lit); color: var(--espresso); }
.btn--ghost { background: transparent; color: var(--fg); border-color: var(--rule-strong); }
.btn--ghost:hover { border-color: currentColor; background: rgb(34 31 28 / .05); }
.world--dark .btn--ghost:hover { background: rgb(247 245 241 / .08); }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn[aria-disabled='true'] { opacity: .5; cursor: not-allowed; transform: none; }

/* The arrow is drawn, not an icon font, and it moves 3px. Enough to feel
   like the button acknowledged you; not enough to be a performance. */
.btn--arrow::after {
  content: ''; width: .5em; height: .5em; flex: none;
  border-right: 1.5px solid currentColor; border-top: 1.5px solid currentColor;
  transform: rotate(45deg); transition: transform var(--dur-2) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .btn--arrow:hover::after { transform: rotate(45deg) translate(3px, -3px); }
}

/* Text link with an underline that wipes out and back rather than blinking. */
.link-line {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1px; background-repeat: no-repeat; background-position: 0 100%;
  padding-bottom: 1px; transition: background-size var(--dur-2) var(--ease);
}
.link-line:hover { background-size: 0% 1px; background-position: 100% 100%; }

/* Tags. Outline only, no fill, no colour except on the one that sells.
   The dietary tags were previously a green and an olive, which pushed the page
   from three hues to five and made the row read like a sticker sheet. The
   information is in the word, which also survives being colourblind or
   printed. */
.tag {
  display: inline-flex; align-items: center; padding: .16rem .55rem;
  border: 1px solid var(--rule-strong); border-radius: 999px;
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--fg-faint); white-space: nowrap;
}
.tag--pop { border-color: var(--caramel); color: var(--accent-fg); }

/* Media frames. One radius, one overflow rule, used everywhere. */
.media { position: relative; display: block; overflow: hidden; border-radius: var(--r-img); background: var(--bg-sunken); }
.media img { width: 100%; height: 100%; object-fit: cover; }
.media--tall  { aspect-ratio: 3 / 4; }
.media--wide  { aspect-ratio: 3 / 2; }
.media--square{ aspect-ratio: 1 / 1; }
.media--pano  { aspect-ratio: 21 / 9; }
/* The slow push in on hover. transform only, so it stays on the compositor. */
.media--zoom img { transition: transform 1.1s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  a:hover .media--zoom img, .feat:hover .media--zoom img { transform: scale(1.04); }
}

figcaption {
  margin-top: var(--sp-3); font-size: var(--fs-tiny); color: var(--fg-faint);
  letter-spacing: .01em; max-width: 40ch;
}


/* 05  PAPER GRAIN
   Sits above the page and below the chrome, so it lands on photographs and
   flat colour alike but never on the nav, a drawer or the lightbox — a modal
   should read as being in front of the paper, not printed on it.

   At this opacity it is felt rather than seen. Turn it up to 0.12 for a
   moment and the difference in how "printed" the page feels is obvious; leave
   it there and it becomes a texture effect, which is a different and worse
   thing.
   ========================================================================== */

body::after {
  content: ''; position: fixed; inset: 0; z-index: var(--z-grain);
  pointer-events: none; background-image: var(--grain); background-size: 160px 160px;
  opacity: .035;
}
/* Nothing is gained by grain on a printout, and it costs ink. */
@media print { body::after { display: none; } }


/* 06  NAV
   ========================================================================== */

#top-sentinel { position: absolute; top: 0; left: 0; width: 1px; height: 1px; }

.nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  height: var(--nav-h); display: flex; align-items: center;
  background: rgb(247 245 241 / .88);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid transparent;
  transition: height var(--dur-2) var(--ease), background-color var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.nav.is-stuck { height: var(--nav-h-stuck); border-bottom-color: var(--rule); }
@media (prefers-reduced-transparency: reduce) {
  .nav { background: var(--cream); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* NAV OVER THE DARK HERO.
   Only pages carrying `.has-dark-hero` on <body>. While the page is at the top
   the nav has nothing to separate itself from and a cream bar would cut the
   hero in half, so it goes transparent and flips its own tokens to the dark
   set. It re-solidifies the moment `.is-stuck` lands, which the sentinel
   observer already handles — no new JavaScript, no scroll listener.

   The `html.js` qualifier is not decoration. This rule paints the nav's text
   cream and its background transparent, which is right over the photograph and
   catastrophic anywhere else: cream on cream is an invisible navigation bar.
   Only JavaScript can ever take the rule off again, by adding .is-stuck, so it
   must not apply when JavaScript is absent — and main.js carries the matching
   circuit breaker for when the script loads but the observer never fires.
   Failing to solid costs one section its transparency; failing to transparent
   costs the site its navigation.

   The brand mark uses currentColor rather than a literal fill so it travels
   with --fg through this swap. */
html.js .has-dark-hero .nav:not(.is-stuck) {
  --fg:          var(--cream);
  --fg-muted:    #D3CEC6;
  --fg-faint:    #B0A9A1;
  --rule-strong: rgb(247 245 241 / 0.34);
  --btn-bg:      var(--cream);
  --btn-fg:      var(--espresso);
  --caramel:     var(--caramel-lit);
  --accent-fg:   var(--caramel-lit);
  background: transparent;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  color: var(--fg);
}

/* Scroll progress. One pixel, no label, no percentage. It is orientation, not
   information. Driven by a scroll-driven CSS animation where supported, which
   runs off the main thread; where it is not, the bar simply never appears. */
.nav__progress { position: absolute; left: 0; right: 0; bottom: -1px; height: 1px; overflow: hidden; }
.nav__progress span { display: block; height: 100%; width: 100%; background: var(--caramel); transform-origin: 0 50%; transform: scaleX(0); }
@supports (animation-timeline: scroll()) {
  .nav__progress span { animation: progress linear both; animation-timeline: scroll(root block); }
}
@keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.nav__inner {
  width: 100%; max-width: var(--wrap-wide); margin-inline: auto; padding-inline: var(--gutter);
  display: flex; align-items: center; gap: clamp(1rem, 3vw, 2.5rem);
}

.brand { display: inline-flex; align-items: center; gap: .6rem; margin-right: auto; }
.brand__mark {
  width: 26px; height: 26px; flex: none; transform-origin: left center;
  transition: transform var(--dur-2) var(--ease);
}
.brand__text { display: flex; align-items: baseline; gap: .45rem; }
.brand__name {
  font-family: var(--font-display); font-size: 1.35rem; font-weight: 500; letter-spacing: .005em;
  transform-origin: left baseline;
  transition: transform var(--dur-2) var(--ease);
}
.brand__sub { font-size: var(--fs-label); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--fg-faint); font-weight: 600; }
/* The logo steps down as the bar shrinks, which is the detail that makes a
   sticky header feel considered rather than merely present. */
.nav.is-stuck .brand__mark { transform: scale(.846); }   /* 22 / 26 */
.nav.is-stuck .brand__name { transform: scale(.852); }   /* 1.15 / 1.35 */
@media (max-width: 560px) { .brand__sub { display: none; } }

.nav__links { display: flex; gap: clamp(1rem, 2.2vw, 2rem); }
.nav__links a {
  position: relative; font-size: var(--fs-small); color: var(--fg-muted);
  padding-block: .35rem; transition: color var(--dur-1) var(--ease);
}
.nav__links a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform var(--dur-2) var(--ease);
}
.nav__links a:hover { color: var(--fg); }
.nav__links a:hover::after, .nav__links a:focus-visible::after { transform: scaleX(1); transform-origin: 0 50%; }
.nav__links a[aria-current='page'] { color: var(--fg); }
.nav__links a[aria-current='page']::after { transform: scaleX(1); background: var(--caramel); height: 2px; }

/* Standalone links — ones that are not a word inside a sentence — need a real
   target. WCAG 2.2 asks for 24×24 CSS pixels and exempts links in flowing
   prose, which is why the address in a <p> is left alone and these are not.
   An audit at four widths found every one of these sitting at 21–23px. */
.nav__status, .hero__rail a, .footer__social a, .now-serving button, .filter__clear {
  display: inline-flex; align-items: center; min-height: 24px;
}

.nav__status { font-size: var(--fs-tiny); color: var(--fg-muted); white-space: nowrap; padding-block: .35rem; }
.nav__status:hover { color: var(--fg); }
@media (max-width: 1120px) { .nav__status { display: none; } }

.nav__cta { padding: .6rem 1.2rem; min-height: 40px; }

.nav__toggle {
  display: none; width: 44px; height: 44px; cursor: pointer;
  border: 1px solid var(--rule-strong); border-radius: var(--r-ui);
  align-items: center; justify-content: center;
  transition: border-color var(--dur-1) var(--ease);
}
.nav__toggle:hover { border-color: currentColor; }
.nav__toggle-bars { display: flex; flex-direction: column; gap: 5px; }
.nav__toggle-bars span { display: block; width: 17px; height: 1.5px; background: currentColor; transition: transform var(--dur-1) var(--ease); }
.nav__toggle[aria-expanded='true'] .nav__toggle-bars span:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav__toggle[aria-expanded='true'] .nav__toggle-bars span:last-child { transform: translateY(-3.25px) rotate(-45deg); }

@media (max-width: 980px) {
  .nav__links, .nav__cta { display: none; }
  .nav__toggle { display: flex; }
  /* With scripting off the toggle cannot open anything, so it is removed and
     the links come back instead of leaving a dead button. */
  html:not(.js) .nav__toggle { display: none; }
  html:not(.js) .nav__links { display: flex; flex-wrap: wrap; gap: var(--sp-4); }
}


/* 07  FULL SCREEN MOBILE MENU
   A native <dialog>. showModal() gives focus trapping, Escape, inertness of
   the page behind and a ::backdrop for free — all of which were previously
   hand written, and one of which (the trap) was not written at all.
   ========================================================================== */

.nav-panel {
  width: 100%; max-width: none; height: 100%; max-height: none;
  margin: 0; padding: 0; border: 0; background: var(--espresso); color: var(--cream);
  overflow: auto; overscroll-behavior: contain;
}
.nav-panel::backdrop { background: rgb(26 17 12 / .6); }
.nav-panel[open] { display: flex; }

/* A photograph behind the menu, dimmed almost to nothing. It is the difference
   between a list of links and a room you are about to walk into. */
.nav-panel__inner {
  position: relative; z-index: 1; flex: 1;
  display: flex; flex-direction: column;
  padding: var(--sp-5) var(--gutter) calc(var(--sp-7) + env(safe-area-inset-bottom, 0px));
  min-height: 100%;
}
.nav-panel::before {
  content: ''; position: absolute; inset: 0;
  background: var(--nav-panel-img, none) center / cover no-repeat;
  opacity: .16; filter: grayscale(.3);
}
.nav-panel__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); }
.nav-panel__eyebrow { font-size: var(--fs-label); letter-spacing: var(--track-label); text-transform: uppercase; color: #A39C94; font-weight: 600; }
.nav-panel__close {
  width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer;
  font-size: 1.8rem; line-height: 1; color: var(--cream);
  border: 1px solid rgb(247 245 241 / .28); border-radius: var(--r-ui);
}
.nav-panel__close:hover { border-color: var(--cream); }

.nav-panel__nav { display: flex; flex-direction: column; gap: .2rem; margin-block: auto; padding-block: var(--sp-7); }
.nav-panel__link {
  font-family: var(--font-display); font-size: clamp(2rem, 10vw, 3.2rem);
  line-height: 1.18; padding-block: .12em;
  opacity: 0; transform: translateY(14px);
  transition: opacity var(--dur-3) var(--ease), transform var(--dur-3) var(--ease), color var(--dur-1) var(--ease);
  transition-delay: calc(var(--i) * 40ms);
}
.nav-panel[open] .nav-panel__link { opacity: 1; transform: none; }
.nav-panel__link:hover { color: var(--caramel-lit); }

.nav-panel__foot { display: grid; gap: var(--sp-3); padding-top: var(--sp-5); border-top: 1px solid rgb(247 245 241 / .16); font-size: var(--fs-small); }
.nav-panel__state { color: var(--cream); }
.nav-panel__addr, .nav-panel__tel { color: #C9C3BB; }
.nav-panel__tel { font-size: var(--fs-lead); }
.nav-panel__social { display: flex; gap: var(--sp-4); margin-top: var(--sp-2); font-size: var(--fs-tiny); color: #A39C94; }


/* 08  CINEMATIC HERO
   One photograph, most of the viewport, and the typography set into it rather
   than beside it.

   Dark is doing real work here rather than decoration. Cheap reads as loud and
   bright; expensive reads as deep and quiet, which is why the ground floor of
   every jewellery shop is dark and every discount shop is lit like a hospital.

   THE HEADLINE went through four typefaces before coming back to the one it
   started on, and the route matters more than the destination. All four were
   set HEAVY, and heavy was the wrong register: weight reads as volume, and a
   thick grotesk shouts, which is right for sportswear and wrong for a room
   whose whole proposition is that nobody will hurry you. Set light, large, and
   on a dark ground with air around it, the original face works. A Didone only
   reads as generic when the treatment is default as well as the face.
   ========================================================================== */

.hero {
  position: relative; isolation: isolate;
  /* svh, not vh: on iOS Safari vh is the height with the address bar hidden,
     so a 100vh hero is taller than the visible viewport on load and the
     scroll cue sits below the fold — under the one thing it exists to
     announce. svh is the smallest viewport, which is what is actually on
     screen. */
  min-height: 100svh;
  display: flex; flex-direction: column;
  /* A sticky nav sits in normal flow, so it would otherwise occupy 76px of
     page background ABOVE the hero and cut a pale strip across the top of it.
     Pulling the hero up and padding it back down puts the photograph behind
     the bar and lets it float. */
  margin-top: calc(var(--nav-h) * -1);
  padding-top: var(--nav-h);
  color: var(--cream);
  background: var(--espresso);
}

.hero__media { position: absolute; inset: 0; z-index: -2; overflow: hidden; }
.hero__media img, .hero__media video { width: 100%; height: 100%; object-fit: cover; object-position: var(--hero-focus, center center); }

/* An extremely slow push in — 24 seconds for 6%, which is roughly a millimetre
   a second at this size. Nobody watches it happen; they notice that the frame
   was not quite the same when they looked back. */
.hero__media img { animation: heroDrift 24s var(--ease-inout) both; }
@keyframes heroDrift { from { transform: scale(1); } to { transform: scale(1.06); } }

/* The video sits ON the photograph rather than instead of it, and stays
   transparent until it is genuinely playing — main.js adds .is-playing on the
   `playing` event, not on `play()`, because play() resolves on a promise that
   says "allowed to start", not "has a frame to show". Fading in on the former
   flashes one black frame on a slow decode.

   Everything that can go wrong here — autoplay refused, decode failure, a 404,
   Save-Data, reduced motion — ends with this element simply never becoming
   visible, and the hero is the photograph. That is the intended failure. */
.hero__video {
  position: absolute; inset: 0;
  opacity: 0; transition: opacity var(--dur-4) var(--ease);
  /* Its own drift would fight the movement already in the footage. */
  animation: none;
}
.hero__video.is-playing { opacity: 1; }

/* The footage is 16:9 and the hero on a phone is roughly 9:19.5, so `cover`
   keeps only the middle quarter of the frame. Every shot in the loop puts its
   subject slightly left of centre — the portafilter, the cup under the spout —
   so a true centre crop lands on the machine body behind them.

   The photograph shifts with it. It is frame one of the same video, so leaving
   the two on different crops would slide the picture sideways underneath the
   crossfade. */
/* Both selectors are written `.hero__media <type>` to match the specificity of
   the rule they override. `.hero__video` alone is one class and loses to
   `.hero__media video`, which is a class AND a type — so the phone crop landed
   on the photograph and not on the video, and the picture jumped sideways the
   moment playback started. A media query adds no specificity of its own. */
@media (max-width: 760px) {
  .hero__media img, .hero__media video { object-position: 42% center; }
}

/* A warm scrim, weighted to the bottom left where the type sits, so the
   photograph keeps its highlights on the right instead of being flattened by
   an even wash. */
.hero__scrim {
  position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(to top, rgb(26 17 12 / .82) 0%, rgb(26 17 12 / .45) 34%, rgb(26 17 12 / .12) 62%, rgb(26 17 12 / .3) 100%),
    linear-gradient(to right, rgb(18 16 14 / .88) 0%, rgb(18 16 14 / .72) 34%, rgb(18 16 14 / .28) 62%, transparent 100%);
}

.hero__inner {
  flex: 1; display: flex; flex-direction: column; justify-content: flex-end;
  width: 100%; max-width: var(--wrap-wide); margin-inline: auto;
  padding: var(--sp-8) var(--gutter) var(--sp-6);
}
.hero__eyebrow { color: #D3CEC6; margin-bottom: var(--sp-5); }

.hero__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 400;              /* the floor: below this the hairlines break up */
  line-height: 1.02;
  letter-spacing: -.02em;
  max-width: 15ch;
  text-shadow: 0 1px 40px rgb(26 17 12 / .35);
}
/* One step of weight and the accent, nothing more. Bodoni thickens fast, so
   400 against 500 is plainly visible at this size, and anything more would put
   the volume straight back into the line the light weight exists to remove. */
.hero__title em { font-style: normal; font-weight: 500; color: var(--caramel-lit); white-space: nowrap; }

.hero__sub { margin-top: var(--sp-5); font-size: var(--fs-lead); color: #DED9D1; max-width: 44ch; }
.hero__actions { margin-top: var(--sp-7); display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.hero .btn--ghost { border-color: rgb(247 245 241 / .45); color: var(--cream); }
.hero .btn--ghost:hover { border-color: var(--cream); background: rgb(247 245 241 / .12); }
.hero .btn--primary { background: var(--cream); color: var(--espresso); border-color: var(--cream); }
.hero .btn--primary:hover { background: var(--caramel-lit); border-color: var(--caramel-lit); color: var(--espresso); }

/* The bottom rail: the four things someone standing outside actually wants. */
.hero__rail {
  margin-top: clamp(2rem, 5vw, 3.5rem); padding-top: var(--sp-4);
  border-top: 1px solid rgb(247 245 241 / .22);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-6);
  font-size: var(--fs-small); color: #D3CEC6;
}
.hero__rail a:hover { color: var(--cream); }
.hero__rail .num { color: var(--cream); }
.hero__scroll {
  margin-left: auto; display: inline-flex; align-items: center; gap: .6rem;
  font-size: var(--fs-tiny); letter-spacing: var(--track-label); text-transform: uppercase; color: #B0A9A1;
}
.hero__scroll::after {
  content: ''; width: 1px; height: 26px; background: currentColor;
  animation: scrollCue 2.4s var(--ease-inout) infinite;
  transform-origin: 50% 0;
}
@keyframes scrollCue {
  0%, 100% { transform: scaleY(.35); opacity: .4; }
  50%      { transform: scaleY(1);   opacity: 1; }
}
@media (max-width: 760px) { .hero__scroll { display: none; } }

/* Entrance. Short, once, and never blocking: the text is readable the moment
   it paints even if the transition never completes. */
.hero__eyebrow, .hero__title, .hero__sub, .hero__actions, .hero__rail {
  opacity: 0; transform: translateY(16px);
  transition: opacity var(--dur-4) var(--ease), transform var(--dur-4) var(--ease);
}
.hero__title  { transition-delay: 60ms; }
.hero__sub    { transition-delay: 140ms; }
.hero__actions{ transition-delay: 210ms; }
.hero__rail   { transition-delay: 280ms; }
.is-ready .hero__eyebrow, .is-ready .hero__title, .is-ready .hero__sub,
.is-ready .hero__actions, .is-ready .hero__rail { opacity: 1; transform: none; }
html:not(.js) .hero__eyebrow, html:not(.js) .hero__title, html:not(.js) .hero__sub,
html:not(.js) .hero__actions, html:not(.js) .hero__rail { opacity: 1; transform: none; }

@media (max-width: 760px) {
  .hero { min-height: 92svh; }
  .hero__title { font-size: clamp(2.6rem, 12vw, 3.6rem); max-width: 13ch; }
  .hero__inner { padding-bottom: var(--sp-7); }
}

/* Compact header for the inner pages. */
.page-head { padding-block: clamp(3.5rem, 8vw, 6.5rem) clamp(2rem, 4vw, 3rem); background: var(--oat); }
.page-head__title { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-d1); line-height: 1.04; letter-spacing: -0.018em; max-width: 18ch; }
.page-head p { margin-top: var(--sp-4); max-width: 54ch; color: var(--fg-muted); font-size: var(--fs-lead); }


/* 09  EDITORIAL LAYOUT
   Alternating, off-centre and deliberately unequal. The previous version put
   every section in the same 1fr 1fr split with the text on the left, which is
   the arrangement that makes a site look generated.
   ========================================================================== */

.ed { display: grid; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
.ed--text-left  { grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr); }
.ed--text-right { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); }
.ed--text-right .ed__text { order: 2; }
.ed--top { align-items: start; }
.ed__text > * + * { margin-top: var(--sp-5); }
.ed__figure { margin: 0; }

/* A photograph that runs out past the text column to the edge of the screen.
   Used once per page at most: the move works because it is rare. */
.ed__figure--bleed-right { margin-right: calc((100vw - min(100vw, var(--wrap))) / -2 - var(--gutter)); }
.ed__figure--bleed-left  { margin-left:  calc((100vw - min(100vw, var(--wrap))) / -2 - var(--gutter)); }

@media (max-width: 900px) {
  .ed--text-left, .ed--text-right { grid-template-columns: 1fr; }
  .ed--text-right .ed__text { order: 0; }
  .ed__figure--bleed-right { margin-right: calc(var(--gutter) * -1); }
  .ed__figure--bleed-left  { margin-left:  calc(var(--gutter) * -1); }
}

/* Parallax, where the browser can do it off the main thread. No scroll
   listener exists on this site; without support the image simply sits still,
   which is a perfectly good version of the page. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .parallax img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes drift { from { transform: translateY(-4%) scale(1.08); } to { transform: translateY(4%) scale(1.08); } }
  }
}

/* Fact lists: label left, value right, hairline between. Used for practical
   information and the weekly filter. */
.facts { display: grid; }
.facts > div { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-5); padding-block: var(--sp-3); border-top: 1px solid var(--rule); }
.facts > div:last-child { border-bottom: 1px solid var(--rule); }
.facts dt { font-size: var(--fs-small); color: var(--fg-faint); flex: none; }
.facts dd { font-size: var(--fs-small); text-align: right; }
.facts--tight > div { padding-block: var(--sp-2); }


/* 10  FEATURED STRIP
   No cards. Photograph, name, price, one line — separated by space rather than
   by a box, which is how a magazine lays out four things.
   ========================================================================== */

.feats { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(1.25rem, 2.5vw, 2.25rem); }
@media (max-width: 900px) { .feats { grid-template-columns: repeat(2, 1fr); gap: var(--sp-6); } }

.feat { display: block; color: inherit; }
.feat__figure { margin: 0; }
.feat__body { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-4); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--rule); transition: border-color var(--dur-2) var(--ease); }
.feat:hover .feat__body { border-color: var(--fg); }
.feat__name { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-d3); line-height: 1.2; transition: color var(--dur-1) var(--ease); }
.feat:hover .feat__name { color: var(--accent-fg); }
.feat__price { font-size: var(--fs-small); color: var(--fg-muted); white-space: nowrap; }
.feat__desc { margin-top: var(--sp-3); font-size: var(--fs-small); color: var(--fg-muted); }


/* 11  STORY
   Two photographs that overlap on a wide screen and stack cleanly on a narrow
   one, with the founding year set large enough to be a graphic element.
   ========================================================================== */

.story { position: relative; }
.story__year {
  font-family: var(--font-display); font-size: clamp(4rem, 13vw, 11rem); line-height: .82;
  color: var(--accent-fg); opacity: .22; letter-spacing: -.03em; user-select: none;
}
.story__stack { position: relative; }
.story__detail {
  position: absolute; right: -6%; bottom: -12%; width: 46%; margin: 0;
  box-shadow: 0 24px 60px -30px rgb(0 0 0 / .55);
}
@media (max-width: 900px) {
  .story__stack { display: grid; gap: var(--sp-5); }
  .story__detail { position: static; width: 100%; box-shadow: none; }
}


/* 12  GALLERY
   Deliberately uneven. A four-up row of identical squares is the default that
   makes a café site look like a template.
   ========================================================================== */

.gallery { display: grid; grid-template-columns: repeat(6, 1fr); gap: clamp(1rem, 2.4vw, 2rem); align-items: start; }
.gitem { margin: 0; }
.gitem--wide   { grid-column: span 4; }
.gitem--tall   { grid-column: span 2; }
.gitem--normal { grid-column: span 2; }
.gitem--full   { grid-column: 1 / -1; }
.gitem--tall .media { aspect-ratio: 3 / 4; }
.gitem--normal .media { aspect-ratio: 1 / 1; }
.gitem--wide .media { aspect-ratio: 3 / 2; }
.gitem--full .media { aspect-ratio: 21 / 9; }
.gitem__link { display: block; }
.gitem__link:hover figcaption { color: var(--fg); }

/* A configured slot with no photograph. Neutral, labelled, obviously
   intentional — not a broken image icon. */
.gitem__missing {
  display: grid; place-items: center; aspect-ratio: 1 / 1;
  border: 1px dashed var(--rule-strong); border-radius: var(--r-img);
  background: var(--bg-sunken); color: var(--fg-faint);
  font-size: var(--fs-tiny); letter-spacing: var(--track-label); text-transform: uppercase;
  text-align: center; padding: var(--sp-4);
}

@media (max-width: 760px) {
  .gallery { grid-template-columns: repeat(2, 1fr); }
  .gitem--wide, .gitem--full { grid-column: 1 / -1; }
  .gitem--tall, .gitem--normal { grid-column: span 1; }
  .gitem--full .media { aspect-ratio: 3 / 2; }
}


/* 13  MENU
   An editorial price list. Names large, prices aligned right, hairline between
   rows, and — on a wide screen — a photograph that follows whatever the
   visitor is reading.
   ========================================================================== */

.menu-layout { display: grid; grid-template-columns: minmax(0, 1fr) clamp(260px, 26vw, 400px); gap: clamp(2rem, 5vw, 5rem); align-items: start; }
/* A grid item defaults to min-width: auto, so it refuses to shrink below its
   content. The category tabs are a nowrap flex row that scrolls sideways, and
   their intrinsic width is the sum of every tab — around 855px. Without this,
   that number becomes the column's minimum and the whole menu page overflowed
   a 360px phone. minmax(0, 1fr) does the same job in the two-column case above;
   this covers the single-column one and anything nested. */
.menu-layout > * { min-width: 0; }
@media (max-width: 980px) { .menu-layout { grid-template-columns: minmax(0, 1fr); } }

.menu-figure { position: sticky; top: calc(var(--nav-h) + 2rem); }
.menu-figure__frame { position: relative; aspect-ratio: 3 / 4; overflow: hidden; border-radius: var(--r-img); background: var(--bg-sunken); }
.menu-figure__frame img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity var(--dur-3) var(--ease);
}
.menu-figure__frame img.is-shown { opacity: 1; }
.menu-figure__cap { margin-top: var(--sp-3); font-size: var(--fs-tiny); color: var(--fg-faint); min-height: 1.5em; }
/* The photograph is an enhancement on top of a complete price list. It is
   removed rather than stacked on a narrow screen, where it would push the
   prices below the fold to show one drink. */
@media (max-width: 980px) { .menu-figure { display: none; } }

.menu-tabs { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.menu-tab {
  padding: .55rem 1.05rem; min-height: 40px; cursor: pointer;
  border: 1px solid var(--rule-strong); border-radius: 999px;
  color: var(--fg-muted); font-size: var(--fs-small);
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.menu-tab:hover { border-color: var(--fg); color: var(--fg); }
.menu-tab[aria-selected='true'] { background: var(--espresso); border-color: var(--espresso); color: var(--cream); }

.mgroup { margin-bottom: clamp(3rem, 6vw, 5rem); scroll-margin-top: calc(var(--nav-h) + 2rem); }
.mgroup[hidden] { display: none; }
.mgroup__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4); padding-bottom: var(--sp-3); border-bottom: 1px solid var(--fg); margin-bottom: var(--sp-2); }
.mgroup__note { font-size: var(--fs-tiny); color: var(--fg-faint); text-align: right; }

.mlist { display: grid; }

/* Each row is a <details>. Native disclosure: focusable, keyboard operable,
   works with scripting off, and needs no ARIA. The essential information —
   name, price, description, dietary labels — is in the <summary> and is never
   hidden. Only allergens and availability are behind the disclosure. */
.mitem { border-bottom: 1px solid var(--rule); }
.mitem__sum {
  display: block; cursor: pointer; padding-block: var(--sp-4);
  list-style: none; position: relative;
  transition: background-color var(--dur-2) var(--ease);
}
.mitem__sum::-webkit-details-marker { display: none; }
.mitem__sum::marker { content: ''; }
.mitem:hover .mitem__sum, .mitem__sum:focus-visible { background: rgb(34 31 28 / .025); }
.world--dark .mitem:hover .mitem__sum { background: rgb(247 245 241 / .04); }
.mitem__sum:focus-visible { outline: 2px solid var(--accent-fg); outline-offset: -2px; }

.mitem__row { display: flex; align-items: baseline; gap: var(--sp-3); }
.mitem__name { font-family: var(--font-display); font-weight: 400; font-size: clamp(1.15rem, 1.9vw, 1.5rem); line-height: 1.25; transition: color var(--dur-1) var(--ease); }
.mitem:hover .mitem__name { color: var(--accent-fg); }
/* The leader between name and price, drawn rather than typed. */
.mitem__rule { flex: 1 1 auto; min-width: 1.5rem; border-bottom: 1px dotted var(--rule-strong); transform: translateY(-.35em); }
.mitem__price { font-size: var(--fs-lead); white-space: nowrap; }
.mitem__price::after { content: ' kr'; font-size: var(--fs-tiny); color: var(--fg-faint); }
.mitem__chev {
  flex: none; width: .5em; height: .5em; margin-left: .35rem; align-self: center;
  border-right: 1.5px solid var(--fg-faint); border-bottom: 1.5px solid var(--fg-faint);
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--dur-2) var(--ease);
}
.mitem[open] .mitem__chev { transform: rotate(-135deg) translateY(-2px); }

.mitem__desc { display: block; margin-top: var(--sp-2); font-size: var(--fs-small); color: var(--fg-muted); max-width: 56ch; }
.mitem__tags { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-3); }
.mitem__detail {
  padding: 0 0 var(--sp-5); font-size: var(--fs-small); color: var(--fg-muted);
  display: grid; gap: var(--sp-2); max-width: 56ch;
}
.mitem__detail .label { display: inline; margin-right: .4rem; }
.mitem[hidden] { display: none; }

.menu-empty { padding: var(--sp-8); text-align: center; border: 1px dashed var(--rule-strong); border-radius: var(--r-ui); color: var(--fg-muted); }

.filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.filters__label { font-size: var(--fs-tiny); color: var(--fg-faint); margin-right: var(--sp-2); }
.filter {
  padding: .4rem .85rem; min-height: 36px; cursor: pointer;
  border: 1px solid var(--rule-strong); border-radius: 999px; color: var(--fg-muted);
  font-size: var(--fs-tiny); font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.filter:hover { border-color: var(--fg); color: var(--fg); }
.filter[aria-pressed='true'] { background: var(--espresso); border-color: var(--espresso); color: var(--cream); }
.filter__clear { margin-left: auto; font-size: var(--fs-tiny); color: var(--fg-muted); cursor: pointer; text-decoration: underline; }
.filter__clear[hidden] { display: none; }
.menu-count { font-size: var(--fs-tiny); color: var(--fg-faint); }

.now-serving {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding-block: var(--sp-3); border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  font-size: var(--fs-small);
}
.now-serving button { margin-left: auto; cursor: pointer; font-size: var(--fs-tiny); color: var(--fg-muted); text-decoration: underline; }


/* 14  UGENS FILTER
   ========================================================================== */

.weekly { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .8fr); gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
@media (max-width: 860px) { .weekly { grid-template-columns: 1fr; } }
.weekly__name { margin-top: var(--sp-3); max-width: 14ch; }
.weekly__price { margin-block: var(--sp-4) var(--sp-5); font-size: var(--fs-lead); color: var(--fg-muted); }
.weekly__price .num { font-family: var(--font-display); font-size: clamp(2rem, 4vw, 3rem); color: var(--fg); margin-right: .3rem; }
.weekly__note { margin-top: var(--sp-4); font-size: var(--fs-tiny); color: var(--fg-faint); }
.weekly__figure { margin: 0; }


/* 15  HOURS, PLACE AND CONTACT
   ========================================================================== */

.hours { width: 100%; border-collapse: collapse; }
.hours tr { border-top: 1px solid var(--rule); }
.hours tr:last-child { border-bottom: 1px solid var(--rule); }
.hours th, .hours td { padding-block: var(--sp-3); font-size: var(--fs-small); font-weight: 400; }
.hours th { text-align: left; color: var(--fg-muted); }
.hours td { text-align: right; }
.hours tr[data-today] th, .hours tr[data-today] td { color: var(--accent-fg); font-weight: 600; }
/* Today is marked with a word as well as a colour, injected by main.js, so the
   row is still identifiable without colour vision. */
.hours tr[data-today] th::after { content: ' · i dag'; font-size: var(--fs-tiny); letter-spacing: .04em; }
.hours__note { margin-top: var(--sp-3); font-size: var(--fs-tiny); color: var(--fg-faint); }

.practical__transport { margin-top: var(--sp-6); }
.practical__transport ul { margin-top: var(--sp-3); display: grid; gap: var(--sp-2); font-size: var(--fs-small); color: var(--fg-muted); }


/* THE WAYS IN — one cell per entry in contact.channels.

   No boxes. The shape rule at the top of this file says separation is
   hairlines and space, so a cell is a top rule and a lot of padding, exactly
   like a row of .facts or .hours. Boxing these would have put eight rounded
   cards on a page that has none anywhere else.

   auto-fit rather than a fixed column count: the grid has to survive a café
   deleting four of the eight rows from the config, and a fixed
   `repeat(3, 1fr)` would leave a hole. */
.chan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16.5rem, 1fr));
  column-gap: var(--sp-7);
}

/* The whole cell is the target, not just the address. `position: relative`
   here is what the ::after below stretches against. */
.chan { position: relative; border-top: 1px solid var(--rule); padding-block: var(--sp-5) var(--sp-6); }

.chan__link { display: block; text-decoration: none; color: inherit; }
/* Stretched hit area. It sits under nothing else, so the note stays
   selectable text rather than becoming part of a link's accessible name. */
.chan__link::after { content: ''; position: absolute; inset: 0; }

.chan__icon {
  display: block; width: 22px; height: 22px;
  color: var(--fg-faint);
  margin-bottom: var(--sp-4);
  transition: color var(--dur-1) var(--ease), transform var(--dur-2) var(--ease);
}
.chan__label {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-d3);
  line-height: 1.2;
  margin-bottom: var(--sp-2);
}
/* The address itself carries the underline, so the cell reads as a link even
   though the whole cell is clickable. Wraps anywhere because a long mail
   address on a narrow column has no spaces to break at. */
.chan__value {
  display: block;
  font-size: var(--fs-small);
  color: var(--accent-fg);
  overflow-wrap: anywhere;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 0% 1px;
  background-position: 0 100%;
  transition: background-size var(--dur-2) var(--ease);
}
.chan__note { margin-top: var(--sp-3); font-size: var(--fs-small); color: var(--fg-muted); max-width: 34ch; }

.chan:focus-within .chan__icon { color: var(--accent-fg); transform: translateY(-2px); }
@media (hover: hover) and (pointer: fine) {
  .chan:hover .chan__icon { color: var(--accent-fg); transform: translateY(-2px); }
}
.chan:hover .chan__value, .chan:focus-within .chan__value { background-size: 100% 1px; }
/* focus-within rather than :focus on the anchor: the visible ring belongs on
   the cell, because the cell is what the pointer gets. */
.chan:focus-within { outline: 2px solid var(--accent-fg); outline-offset: 6px; }
.chan__link:focus-visible { outline: none; }


/* PROFILES — pills, which is the one shape besides 4px that this site allows. */
.soc-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.soc {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .6rem 1.1rem;
  border: 1px solid var(--rule-strong); border-radius: 999px;
  font-size: var(--fs-small); font-weight: 600; text-decoration: none;
  color: var(--fg); background: transparent;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.soc:hover { background: var(--btn-bg); border-color: var(--btn-bg); color: var(--btn-fg); }
.soc .chan__icon { width: 18px; height: 18px; margin: 0; color: currentColor; transition: none; }
.soc-note { margin-top: var(--sp-4); font-size: var(--fs-tiny); color: var(--fg-faint); max-width: 62ch; line-height: 1.6; }


/* THE MAP — nothing is fetched until the button is pressed.

   Two rules here are load-bearing and not decoration:

   1. .mapblock__frame is `contain: paint` + a fixed aspect-ratio, because the
      injected <iframe> is Google's markup and will happily paint outside a
      rounded corner otherwise.
   2. The veil is REMOVED from the flow by main.js, not just faded. Left
      behind at opacity 0 it still swallows every click and drag, and a map
      you cannot pan is worse than no map.

   The placeholder behind the veil is drawn in CSS — two sets of crossing
   lines on the oat ground. It is deliberately abstract: a photograph of the
   room would imply the picture IS the location, and any real street pattern
   would be a claim about a place. It reads as "a map goes here", which is
   exactly what is true before the click. */
.mapblock { margin: 0; }

.mapblock__frame {
  position: relative;
  /* Letterbox on a wide screen. At 16/9 this column is over 800px tall, which
     is most of a laptop viewport spent on a placeholder. */
  aspect-ratio: 21 / 9;
  border-radius: var(--r-img);
  overflow: hidden;
  contain: paint;
  background-color: var(--oat);
  background-image:
    repeating-linear-gradient(90deg, transparent 0 79px, rgb(34 31 28 / .07) 79px 80px),
    repeating-linear-gradient(0deg,  transparent 0 79px, rgb(34 31 28 / .07) 79px 80px),
    repeating-linear-gradient(58deg, transparent 0 219px, rgb(34 31 28 / .09) 219px 222px),
    linear-gradient(160deg, rgb(184 115 51 / .10), transparent 60%);
}

.mapblock__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }

.mapblock__veil {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-4); padding: var(--sp-6);
  text-align: center;
  background: linear-gradient(180deg, rgb(247 245 241 / .35), rgb(247 245 241 / .78));
}

/* A pin, not a photograph: a ring with a dot, the same two shapes as the
   brand mark. The shadow underneath is what stops it floating. */
.mapblock__pin {
  width: 18px; height: 18px; border-radius: 999px;
  background: var(--caramel-deep);
  box-shadow: 0 0 0 6px rgb(138 79 30 / .18), 0 0 0 14px rgb(138 79 30 / .08);
}

.mapblock__where {
  font-family: var(--font-display); font-size: var(--fs-d2); line-height: 1.15;
  margin-top: var(--sp-3);
}
.mapblock__privacy { font-size: var(--fs-tiny); color: var(--fg-muted); max-width: 44ch; line-height: 1.5; }

/* The global figcaption rule caps captions at 40ch, which is right under a
   photograph and far too narrow under a full-width map — it left the demo
   note as a thin column of text beside a metre of empty page. Overridden
   here, and laid out in two columns so the note sits beside the caption
   rather than under it. */
.mapblock__cap { display: grid; gap: var(--sp-3); margin-top: var(--sp-5); max-width: none; }
.mapblock__caption-text { font-size: var(--fs-small); color: var(--fg-muted); }
/* The demo line is the only part of this block that must never be missed, so
   it gets the accent rule down its left edge rather than being one more grey
   line under a picture. */
.mapblock__demo {
  font-size: var(--fs-tiny); color: var(--fg-muted); line-height: 1.6;
  border-left: 2px solid var(--caramel); padding-left: var(--sp-4);
  max-width: 66ch;
}
.mapblock__out { font-size: var(--fs-small); justify-self: start; }

@media (min-width: 900px) {
  .mapblock__cap {
    grid-template-columns: minmax(0, 32ch) minmax(0, 60ch);
    grid-template-areas: 'cap demo' 'out demo';
    gap: var(--sp-3) var(--sp-8);
    align-items: start;
  }
  .mapblock__caption-text { grid-area: cap; }
  .mapblock__demo { grid-area: demo; }
  .mapblock__out { grid-area: out; align-self: end; }
}

@media (max-width: 900px) { .mapblock__frame { aspect-ratio: 4 / 3; } }
@media (max-width: 640px) { .mapblock__frame { aspect-ratio: 4 / 5; } }


/* 16  FOOTER
   ========================================================================== */

.footer { background: var(--espresso); color: var(--cream); padding-top: var(--sp-9); overflow: hidden; }
.footer .label { color: #A39C94; margin-bottom: var(--sp-4); }
.footer a { color: #E2DDD5; transition: color var(--dur-1) var(--ease); }
.footer a:hover { color: var(--cream); }
.footer .btn--ghost { border-color: rgb(247 245 241 / .3); color: var(--cream); }
.footer .btn--ghost:hover { border-color: var(--cream); background: rgb(247 245 241 / .1); }

.footer__top { display: grid; grid-template-columns: 1.15fr .8fr .8fr 1.3fr; gap: clamp(2rem, 4vw, 3.5rem); }
@media (max-width: 1024px) { .footer__top { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer__top { grid-template-columns: 1fr; } }
.footer__addr { font-size: var(--fs-lead); line-height: 1.5; }
.footer__links { margin-top: var(--sp-4); font-size: var(--fs-small); line-height: 1.9; }
.footer__col > p + p, .footer__col > * + .btn, .footer__col p + ul { margin-top: var(--sp-4); }
.footer__col--say > p:last-of-type { margin-top: var(--sp-5); }
.footer__hours { display: grid; gap: var(--sp-2); font-size: var(--fs-small); }
.footer__hours li { display: flex; justify-content: space-between; gap: var(--sp-4); color: #C9C3BB; }
.footer__hours li span:last-child { color: var(--cream); font-variant-numeric: tabular-nums; }
.footer__clock { margin-top: var(--sp-4); font-size: var(--fs-tiny); color: #A39C94; }
.footer__clock[hidden] { display: none; }
.footer__nav { display: grid; gap: var(--sp-2); font-size: var(--fs-small); }
.footer__blurb { color: #C9C3BB; font-size: var(--fs-small); max-width: 34ch; }
.footer__fine { margin-top: var(--sp-3); font-size: var(--fs-tiny); color: #A39C94; max-width: 40ch; }
.footer__social { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin-top: var(--sp-5); font-size: var(--fs-tiny); letter-spacing: .04em; text-transform: uppercase; }

/* The closing composition: the name at the largest size it appears anywhere,
   cropped by the bottom edge of the page. It is a full stop, not a logo. */
.footer__mark {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(5rem, 21vw, 19rem); line-height: .74; letter-spacing: -.035em;
  color: rgb(247 245 241 / .07);
  margin-top: clamp(3rem, 7vw, 6rem);
  margin-bottom: -.18em;   /* the crop */
  user-select: none; white-space: nowrap;
}

.footer__base {
  position: relative; z-index: 1;
  margin-top: var(--sp-6); padding-block: var(--sp-5);
  border-top: 1px solid rgb(247 245 241 / .14);
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6); justify-content: space-between;
  font-size: var(--fs-tiny); color: #A39C94;
}


/* 17  BOOKING DRAWER
   A native <dialog>, injected once by main.js and shared by every page. Every
   trigger is a real link to /book, so with scripting off the visitor gets the
   full booking page instead of a button that does nothing.

   showModal() supplies the focus trap, Escape, the inert background and focus
   return. Nothing here reimplements any of that.
   ========================================================================== */

/* SCROLL LOCK, DERIVED RATHER THAN TOGGLED.

   The page must not scroll behind an open overlay, and the action bar must get
   out of the way. Both used to be done in JavaScript, incrementing a counter on
   open and decrementing it on the dialog's `close` event — which broke, because
   `close` is not guaranteed to arrive when you want it. Testing found it not
   firing at all in one environment, and the result was a page that could never
   be scrolled again after the drawer had been opened once.

   `:has(dialog:modal)` asks the question directly: is a modal open right now?
   It cannot desynchronise, it needs no events, it survives Escape and backdrop
   clicks and a closed tab, and it deletes the counter entirely. main.js keeps a
   small fallback for browsers without :has(). */
html:has(dialog:modal) { overflow: hidden; }
html:has(dialog:modal) .actionbar { transform: translateY(110%); }

.drawer {
  border: 0; padding: 0; margin: 0 0 0 auto; max-width: min(560px, 100%);
  width: 100%; height: 100%; max-height: 100%;
  background: var(--bg); color: var(--fg);
  overflow: hidden;
}
.drawer::backdrop { background: rgb(26 17 12 / .5); }
.drawer[open] { display: flex; flex-direction: column; }

/* Transitions on a dialog need the element to stay rendered while it leaves,
   which is what allow-discrete does. Where it is unsupported the drawer simply
   appears and disappears — correct, just less smooth. */
@media (prefers-reduced-motion: no-preference) {
  .drawer, .drawer::backdrop {
    transition: opacity var(--dur-3) var(--ease), transform var(--dur-3) var(--ease),
                overlay var(--dur-3) allow-discrete, display var(--dur-3) allow-discrete;
  }
  .drawer { opacity: 0; transform: translateX(24px); }
  .drawer[open] { opacity: 1; transform: none; }
  @starting-style { .drawer[open] { opacity: 0; transform: translateX(24px); } }
  .drawer::backdrop { opacity: 0; }
  .drawer[open]::backdrop { opacity: 1; }
  @starting-style { .drawer[open]::backdrop { opacity: 0; } }

  @media (max-width: 640px) {
    .drawer { transform: translateY(24px); }
    @starting-style { .drawer[open] { transform: translateY(24px); } }
  }
}

@media (max-width: 640px) {
  .drawer { margin: auto 0 0 0; max-width: 100%; height: 94svh; border-radius: 12px 12px 0 0; }
}

.drawer__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-6) var(--sp-4); border-bottom: 1px solid var(--rule); flex: none;
}
.drawer__title { font-family: var(--font-display); font-weight: 400; font-size: var(--fs-d2); }
.drawer__sub { margin-top: var(--sp-2); font-size: var(--fs-tiny); color: var(--fg-faint); max-width: 38ch; }
.drawer__close {
  flex: none; width: 44px; height: 44px; display: grid; place-items: center; cursor: pointer;
  font-size: 1.6rem; line-height: 1; border: 1px solid var(--rule-strong); border-radius: var(--r-ui);
  transition: border-color var(--dur-1) var(--ease);
}
.drawer__close:hover { border-color: var(--fg); }
.drawer__body { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-6); }
.drawer__foot {
  flex: none; padding: var(--sp-4) var(--sp-6) calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--rule); background: var(--bg-raised);
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.drawer__foot .btn { flex: 1 1 auto; }
.drawer__note { font-size: var(--fs-tiny); color: var(--fg-faint); }


/* 18  LIGHTBOX
   Also a native dialog, also injected, and also progressive: every gallery
   item is a real link to the image file.
   ========================================================================== */

.lightbox {
  border: 0; padding: 0; margin: auto; width: 100%; max-width: 100%;
  height: 100%; max-height: 100%; background: transparent; overflow: hidden;
}
.lightbox::backdrop { background: rgb(20 13 9 / .93); }
.lightbox[open] { display: grid; grid-template-rows: 1fr auto; }
.lightbox__stage { position: relative; display: grid; place-items: center; padding: clamp(1rem, 4vw, 3rem); min-height: 0; }
.lightbox__img {
  max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain;
  opacity: 0; transition: opacity var(--dur-3) var(--ease);
}
.lightbox__img.is-shown { opacity: 1; }
.lightbox__bar {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-4) clamp(1rem, 4vw, 3rem) calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  color: #D3CEC6; font-size: var(--fs-tiny);
}
.lightbox__cap { flex: 1; min-width: 0; }
.lightbox__count { font-variant-numeric: tabular-nums; color: #A39C94; }
.lightbox__btn {
  width: 44px; height: 44px; flex: none; display: grid; place-items: center; cursor: pointer;
  border: 1px solid rgb(247 245 241 / .3); border-radius: var(--r-ui); color: var(--cream);
  transition: border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
.lightbox__btn:hover { border-color: var(--cream); background: rgb(247 245 241 / .12); }
.lightbox__btn svg { width: 16px; height: 16px; }
.lightbox__close { position: absolute; top: clamp(.75rem, 2vw, 1.5rem); right: clamp(.75rem, 2vw, 1.5rem); z-index: 2; }


/* 19  FORMS
   ========================================================================== */

.form { display: grid; gap: var(--sp-6); }
.form__row { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-5); }
.form__row--3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 760px) { .form__row, .form__row--3 { grid-template-columns: 1fr; } }

.field { display: grid; gap: var(--sp-2); min-width: 0; }
.field--wide { grid-column: 1 / -1; }
.field label, .field-block__label { font-size: var(--fs-small); font-weight: 600; color: var(--fg-muted); }
.field label .opt { font-weight: 400; color: var(--fg-faint); }
.field input, .field select, .field textarea {
  width: 100%; padding: .85rem .95rem; min-height: 46px;
  background: var(--field-bg); border: 1px solid var(--field-rule);
  border-radius: var(--r-ui); color: var(--fg);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--fg-faint); }
.field input:focus, .field select:focus, .field textarea:focus { box-shadow: 0 0 0 3px rgb(184 115 51 / .18); }
.field textarea { resize: vertical; min-height: 96px; }
.field__hint { font-size: var(--fs-tiny); color: var(--fg-faint); }
.field__error { font-size: var(--fs-tiny); font-weight: 600; color: #9A2F1C; }
.world--dark .field__error, .footer .field__error { color: #F0A08C; }
.field__error:empty { display: none; }
/* The invalid state is a red border AND the message AND the word in the
   message. Never colour alone. */
.field[data-invalid='true'] input,
.field[data-invalid='true'] select,
.field[data-invalid='true'] textarea { border-color: #9A2F1C; }

.field-block { display: grid; gap: var(--sp-3); }

/* Radio groups rendered as pressable chips. */
.choice { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.choice__item { position: relative; display: block; }
.choice__item input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; margin: 0; cursor: pointer; }
.choice__item span {
  display: grid; place-items: center; min-width: 48px; min-height: 44px; padding: .6rem .95rem;
  border: 1px solid var(--field-rule); border-radius: var(--r-ui);
  font-size: var(--fs-small); color: var(--fg-muted);
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
.choice__item input:hover + span { border-color: var(--fg); color: var(--fg); }
.choice__item input:checked + span { background: var(--espresso); border-color: var(--espresso); color: var(--cream); }
.world--dark .choice__item input:checked + span { background: var(--cream); border-color: var(--cream); color: var(--espresso); }
.choice__item input:focus-visible + span { outline: 2px solid var(--accent-fg); outline-offset: 2px; }
.choice__item input:disabled + span { opacity: .4; cursor: not-allowed; }
.choice__item input:active + span { transform: translateY(1px); }

/* Time slots. */
.slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: var(--sp-2); min-height: 92px; }
.slot {
  padding: .7rem .4rem; min-height: 44px; cursor: pointer;
  border: 1px solid var(--field-rule); border-radius: var(--r-ui);
  color: var(--fg-muted); font-size: var(--fs-small);
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
              background-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.slot:hover:not([disabled]) { border-color: var(--fg); color: var(--fg); }
.slot:active:not([disabled]) { transform: translateY(1px); }
.slot[aria-pressed='true'] { background: var(--espresso); border-color: var(--espresso); color: var(--cream); }
.world--dark .slot[aria-pressed='true'] { background: var(--cream); border-color: var(--cream); color: var(--espresso); }
.slot[disabled] { opacity: .45; cursor: not-allowed; text-decoration: line-through; }
.slot--skeleton {
  height: 44px; border-color: var(--rule); cursor: default;
  background: linear-gradient(90deg, var(--bg-sunken) 0%, var(--bg-raised) 50%, var(--bg-sunken) 100%);
  background-size: 220% 100%; animation: shimmer 1.15s linear infinite;
}
@keyframes shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
.slots__empty { grid-column: 1 / -1; padding: var(--sp-5); border: 1px dashed var(--rule-strong); border-radius: var(--r-ui); font-size: var(--fs-small); color: var(--fg-muted); }
.slots__empty strong { display: block; color: var(--fg); margin-bottom: var(--sp-1); }

/* Status panels: booking confirmation, contact result, newsletter result. */
.status { padding: var(--sp-5); border: 1px solid var(--rule-strong); border-left: 3px solid var(--caramel); border-radius: var(--r-ui); background: var(--bg-raised); font-size: var(--fs-small); }
.status:empty { display: none; }
.status h3 { font-family: var(--font-display); font-weight: 400; font-size: 1.35rem; margin-bottom: var(--sp-3); }
.status dl { display: grid; gap: var(--sp-2); margin-block: var(--sp-4); }
.status dl > div { display: flex; justify-content: space-between; gap: var(--sp-4); }
.status dt { color: var(--fg-faint); }
.status dd { text-align: right; }
.status[data-state='error'] { border-left-color: #9A2F1C; }
.status[data-state='ok'] { border-left-color: #4E7A46; }
.status__note { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--rule); color: var(--fg-muted); }
/* The newsletter result sits in a tight column and does not want a panel. */
.status--inline { padding: 0; border: 0; background: none; margin-top: var(--sp-3); font-size: var(--fs-tiny); }
.status--inline:empty { display: none; }
.world--dark .status--inline, .footer .status--inline { color: #E2DDD5; }

.spinner {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }


/* 20  LIVE OPENING STATUS
   ========================================================================== */

.status-dot {
  position: relative; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: .45rem; background: var(--fg-faint);
}
[data-open='true'] .status-dot { background: #5C8A52; }
[data-open='true'] .status-dot::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: #5C8A52; animation: ping 2.8s var(--ease) infinite;
}
@keyframes ping {
  0%   { transform: scale(1); opacity: .5; }
  70%, 100% { transform: scale(2.6); opacity: 0; }
}
[data-open='false'] .status-dot { background: var(--fg-faint); }
/* The word carries the state; the dot and its colour only reinforce it. */
.closing-soon { color: var(--accent-fg); font-weight: 600; }


/* 21  SCROLL REVEALS
   Four motions, not one, each picked for what it is revealing.

     rv-wipe   photographs   uncover from the bottom edge while the frame
                             settles out of a slight overscale, like a print
                             coming up in developer
     rv-line   display type  rises from behind its own baseline, masked
     rv-soft   body copy     opacity only, no travel. Text that moves while you
                             are trying to read it is an irritation
     rv-rise   small units   a short 10px lift, used only where the stagger
                             itself is the information

   Menu rows, price lists, hours tables and facts get nothing at all. They are
   reference content and a visitor scanning for a price should never wait.
   ========================================================================== */

.rv-wipe { clip-path: inset(0 0 100% 0); }
.rv-wipe > img, .rv-wipe .media > img { transform: scale(1.06); }
.rv-wipe.is-in { clip-path: inset(0 0 0 0); transition: clip-path 1.05s var(--ease); transition-delay: var(--d, 0ms); }
.rv-wipe.is-in > img, .rv-wipe.is-in .media > img { transform: none; transition: transform 1.4s var(--ease); transition-delay: var(--d, 0ms); }

/* The inner span is inserted by main.js so the mask has something to slide
   behind. padding-bottom keeps descenders off the clip. */
.rv-line { display: block; overflow: hidden; padding-bottom: .12em; }
.rv-line__i { display: block; transform: translateY(105%); }
.rv-line.is-in .rv-line__i { transform: none; transition: transform 1s var(--ease); transition-delay: var(--d, 0ms); }

.rv-soft { opacity: 0; }
.rv-soft.is-in { opacity: 1; transition: opacity .7s var(--ease); transition-delay: var(--d, 0ms); }

.rv-rise { opacity: 0; transform: translateY(10px); }
.rv-rise.is-in { opacity: 1; transform: none; transition: opacity .55s var(--ease), transform .55s var(--ease); transition-delay: var(--d, 0ms); }

/* Captions arrive after the photograph they belong to. */
.rv-cap { opacity: 0; }
.rv-cap.is-in { opacity: 1; transition: opacity .6s var(--ease); transition-delay: calc(var(--d, 0ms) + 320ms); }

html:not(.js) .rv-wipe, html:not(.js) .rv-soft,
html:not(.js) .rv-rise, html:not(.js) .rv-cap { clip-path: none; opacity: 1; transform: none; }
html:not(.js) .rv-line { overflow: visible; }


/* 22  PHONE
   Not the desktop layout squeezed. These are different decisions, made because
   a phone is held in one hand by someone standing outside deciding whether to
   come in.
   ========================================================================== */

@media (max-width: 760px) {

  /* Featured items become a swipeable rail rather than four stacked blocks
     that bury the rest of the page. Same content, thumb sized. */
  .feats {
    display: grid; grid-auto-flow: column; grid-auto-columns: 72%;
    grid-template-columns: none;
    overflow-x: auto; scroll-snap-type: x mandatory;
    scroll-padding-left: var(--gutter);
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter); padding-bottom: var(--sp-4);
    scrollbar-width: none;
  }
  .feats::-webkit-scrollbar { display: none; }
  .feats > * { scroll-snap-align: start; }

  /* Menu categories scroll sideways instead of wrapping into four ragged rows
     that push the actual menu off the screen. */
  .menu-tabs {
    flex-wrap: nowrap; overflow-x: auto;
    scroll-snap-type: x proximity; scrollbar-width: none;
    margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter);
  }
  .menu-tabs::-webkit-scrollbar { display: none; }
  .menu-tab { flex: none; scroll-snap-align: start; }

  /* Leader dots do not work in a narrow column: the name wraps and the dotted
     line ends up floating on its own line. */
  .mitem__rule { display: none; }
  .mitem__row { justify-content: space-between; }
  .mgroup__head { flex-direction: column; align-items: flex-start; gap: var(--sp-1); }
  .mgroup__note { text-align: left; }

  /* Three slot columns is the widest that keeps a 44px target without the
     times wrapping. */
  .slots { grid-template-columns: repeat(3, 1fr); }

  .facts > div { flex-direction: column; gap: var(--sp-1); }
  .facts dd { text-align: left; }
}

/* THE MOBILE ACTION BAR
   The three things someone on a phone outside the café wants, in thumb reach.
   Injected by main.js so there is one copy of it rather than one per page.
   ========================================================================== */

.actionbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-bar);
  display: none; align-items: stretch;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: rgb(247 245 241 / .95);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-top: 1px solid var(--rule);
  transform: translateY(0);
  transition: transform var(--dur-3) var(--ease);
}
.actionbar[data-hidden='true'] { transform: translateY(110%); }
.actionbar a, .actionbar button {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .15rem;
  min-height: 56px; padding: .5rem .25rem; cursor: pointer;
  font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-muted);
  border-right: 1px solid var(--rule);
  transition: background-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.actionbar > :last-child { border-right: 0; }
.actionbar a:hover, .actionbar button:hover { color: var(--fg); }
.actionbar a:active, .actionbar button:active { background: rgb(34 31 28 / .06); }
.actionbar [aria-current='page'] { color: var(--fg); }
.actionbar [aria-current='page']::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--caramel); }
.actionbar a, .actionbar button { position: relative; }
.actionbar__primary { color: var(--espresso) !important; background: var(--sand); }
.actionbar svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.6; }

@media (max-width: 980px) {
  .actionbar { display: flex; }
  /* Room so the footer's last line is never trapped behind the bar. */
  body { padding-bottom: 64px; }
}
@media (prefers-reduced-transparency: reduce) {
  .actionbar { background: var(--cream); backdrop-filter: none; -webkit-backdrop-filter: none; }
}


/* 23  PRINT
   A café prints its menu. This makes Ctrl+P produce a clean sheet for the
   counter instead of a screenshot of a website: no navigation, no photographs,
   no interface, every category shown regardless of what is filtered on screen,
   and the address and hours added at the top where a printed menu needs them.
   ========================================================================== */

@media print {
  @page { margin: 14mm; }

  .nav, .nav-panel, .footer, .actionbar, .skip-link, .drawer, .lightbox,
  .hero, .menu-tabs, .filters, .now-serving, .menu-figure, .page-head p,
  .feats, .media, .gallery, .signup, .status, .weekly__figure { display: none !important; }

  html, body { background: #fff !important; color: #000 !important; font-size: 10.5pt; }
  body { padding: 0 !important; }
  .section { padding-block: 0 !important; background: #fff !important; }
  .wrap { max-width: none; padding: 0; }

  /* Filters and tabs must not hide anything on paper, and every disclosure is
     open: an allergen list is exactly what a printed menu is for. */
  .mgroup[hidden], .mitem[hidden] { display: block !important; }
  .mitem__detail { display: block !important; }
  .mitem__chev { display: none; }

  .page-head { padding: 0 0 6mm; background: #fff !important; }
  .page-head__title { font-size: 26pt; }
  /* gen:printfoot */
  .page-head::after {
    content: 'Cafe Lilly · Holte Midtpunkt 23, 2840 Holte · 60 88 33 36 · Åbent alle dage 10 til 21';
    display: block; margin-top: 3mm; font-size: 8.5pt; line-height: 1.5; color: #333;
  }
  /* /gen:printfoot */

  .mgroup { margin-bottom: 7mm; break-inside: avoid; }
  .mgroup__head { border-bottom: .5pt solid #000; margin-bottom: 2.5mm; padding-bottom: 1.5mm; }
  .mgroup__head h2 { font-size: 13pt; }
  .mlist { columns: 2; column-gap: 8mm; }
  .mitem { padding-block: 1.2mm; break-inside: avoid; border-bottom: none; }
  .mitem__sum { padding-block: 1mm; }
  .mitem__desc { font-size: 8.5pt; color: #444; }
  .mitem__rule { display: block !important; border-bottom: .5pt dotted #999; }
  .tag { border-color: #999; color: #444; font-size: 6.5pt; }

  a[href]::after { content: ''; }   /* no printed URLs cluttering the sheet */
}


/* 24  REDUCED MOTION
   Everything decorative stops. Nothing that carries information is hidden as a
   side effect, which is the usual mistake: a reveal that is disabled must
   settle to its visible state, not its hidden one.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
    /* Movement goes. Colour and opacity stay, so the interface still answers
       when it is touched - that feedback is comprehension, not decoration. */
    transition-property: color, background-color, border-color, opacity, box-shadow, fill, stroke !important;
    transition-duration: var(--dur-1) !important;
  }
  .hero__eyebrow, .hero__title, .hero__sub, .hero__actions, .hero__rail { opacity: 1; transform: none; }
  .hero__media img { animation: none; transform: none; }
  .hero__scroll::after { animation: none; transform: scaleY(1); opacity: .7; }
  .rv-wipe { clip-path: none !important; }
  .rv-wipe > img, .rv-wipe .media > img { transform: none !important; }
  .rv-line { overflow: visible; }
  .rv-line__i { transform: none !important; }
  .rv-soft, .rv-rise, .rv-cap { opacity: 1 !important; transform: none !important; }
  .nav-panel__link { opacity: 1 !important; transform: none !important; }
  .media--zoom img { transition: none; }
  .slot--skeleton { animation: none; }
  [data-open='true'] .status-dot::after { animation: none; opacity: 0; }
  .drawer, .drawer::backdrop { transition: none; opacity: 1; transform: none; }
  .parallax img { animation: none !important; }
}
