/* ==========================================================================
   Atelier Lumen — main.css

   Structure
   00  reset & tokens
   01  type scale
   02  placeholders (gradient + grain)
   03  app shell: view stack, bar
   04  bottom bar
   05  nav sheet
   06  loader
   07  home bands
   08  rings
   09  stories
   10  views
   11  booking
   12  demo dock
   13  motion classes (the "Licht" system)
   14  reduced motion

   Two rules govern the whole file:
   - Only transform and opacity are animated. Never width, height, top, left.
   - 100dvh, never 100vh, and env(safe-area-inset-*) wherever an edge is touched.
   ========================================================================== */

/* ==========================================================================
   00 · RESET & TOKENS
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol {
  margin: 0;
  padding: 0;
}

ul, ol { list-style: none; }

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

button, input, textarea, select {
  font: inherit;
  color: inherit;
  border: 0;
  background: none;
}

button { cursor: pointer; }

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

:root {
  /* Palette — overwritten at runtime from BRAND.palettes[activePalette].
     These defaults are the emerald set, so the page is correct before JS runs. */
  --dark:      #063A38;
  --dark-soft: #0A4B48;
  --light:     #F7F3ED;
  --accent:    #D9B98A;
  --cta:       #F28B82;

  /* Derived — recomputed in JS when the palette changes. */
  --ink:        var(--dark);
  --paper:      var(--light);
  --hairline:   color-mix(in srgb, var(--dark) 14%, transparent);
  --hairline-d: color-mix(in srgb, var(--light) 18%, transparent);
  --muted:      color-mix(in srgb, var(--dark) 60%, transparent);
  --muted-d:    color-mix(in srgb, var(--light) 66%, transparent);

  /* Layout */
  /* Stages — story, sheet, booking — stay this wide at every screen size. The
     photographs are all portrait, so a story stretched across a monitor would
     crop away most of every frame; and a form 1900px wide is not a form. */
  --stage-w:  min(480px, 100vw);
  --content:  1180px;        /* the widest a reading column ever gets */
  /* Where a stage sits, and how far it must shift to land there. Centred at
     every size now that the app fills the screen — kept as tokens because JS
     writes the same shift into its drag transforms and the two must agree. */
  --col-left:  50%;
  --col-shift: -50%;
  /* Where a surface sits once it has finished arriving. Sheets rest against
     the bottom edge (0); a centred desktop dialog rests at its own half-height
     above centre. JS reads this the same way it reads --col-shift, so the
     slide-up animation never has to know which of the two it is driving. */
  --rest-y:    0%;
  --gutter:   clamp(1.25rem, 5vw, 1.75rem);
  --bar-h:    4.25rem;
  --safe-b:   env(safe-area-inset-bottom, 0px);
  --safe-t:   env(safe-area-inset-top, 0px);
  --radius:   14px;
  --radius-l: 22px;

  /* Motion — the four classes. See CONCEPT-APP §3.2. */
  --t-ceremony: 680ms;
  --t-passage:  450ms;
  --t-beat:     300ms;
  --t-tool:     150ms;
  --e-ceremony: cubic-bezier(.16, 1, .3, 1);    /* expo.out */
  --e-passage:  cubic-bezier(.25, 1, .5, 1);    /* quart.out */
  --e-beat:     cubic-bezier(.25, 1, .5, 1);
  --e-tool:     cubic-bezier(.25, .46, .45, .94);

  /* Type */
  --serif: 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --sans:  'General Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --z-view:    1;
  --z-bar:     40;
  --z-sheet:   60;
  --z-story:   70;
  --z-booking: 80;
  --z-loader:  95;
  --z-dock:    50;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--dark);
  color: var(--light);
  font-family: var(--sans);
  font-weight: 400;
  line-height: 1.55;
  overflow-x: clip;            /* clip, not hidden — hidden breaks sticky */
  overscroll-behavior-y: none;
}

/* Focus is always visible. Never remove this. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: .75rem 1rem;
  background: var(--accent);
  color: var(--dark);
}
.skip:focus { left: .5rem; top: .5rem; }

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

/* ==========================================================================
   01 · TYPE
   Fraunces carries the personality — opsz, SOFT and WONK are used with intent,
   which is the whole reason to choose it over the Playfair default.
   ========================================================================== */

.d1, .d2, .d3, .d4 {
  font-family: var(--serif);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -.022em;
  font-variation-settings: 'opsz' 120, 'SOFT' 40, 'WONK' 1;
  text-wrap: balance;
}

.d1 { font-size: clamp(2.85rem, 13vw, 4.4rem); }
.d2 { font-size: clamp(2.1rem, 9.5vw, 3.1rem); }
.d3 { font-size: clamp(1.6rem, 7vw, 2.2rem); line-height: 1.08; }
.d4 { font-size: clamp(1.24rem, 5.2vw, 1.6rem); line-height: 1.16;
      font-variation-settings: 'opsz' 40, 'SOFT' 30, 'WONK' 0; }

.eyebrow {
  font-size: .72rem;
  font-weight: 500;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--accent);
}

.lead  { font-size: 1.06rem; line-height: 1.6; }
.body  { font-size: .975rem; line-height: 1.68; }
.small { font-size: .84rem; line-height: 1.5; }
.tiny  { font-size: .74rem; line-height: 1.45; letter-spacing: .02em; }

.num {
  font-family: var(--serif);
  font-variant-numeric: tabular-nums;
  font-variation-settings: 'opsz' 100, 'SOFT' 20, 'WONK' 0;
}

.on-dark  { color: var(--light); }
.on-light { color: var(--dark); }
.dim      { color: var(--muted-d); }
.dim-l    { color: var(--muted); }

/* ==========================================================================
   02 · PLACEHOLDERS
   Every image is a real <img> over a curated gradient. On error the img hides
   and the gradient shows, so the build looks intentional and complete with
   zero photography — and the client's real photos appear the moment they land
   in the folders. This is the single most important structural trick here.
   ========================================================================== */

.ph {
  position: relative;
  overflow: hidden;
  background: var(--dark-soft);
  isolation: isolate;
}

.ph::after {                       /* grain, so a flat gradient reads as film */
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  opacity: .16;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}

.ph > img,
.ph > video {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ph > img[hidden] { display: none; }

/* Eight curated gradients. A service references one by name in data.js; the
   audit fails if a name has no rule here. */
.g-champagne { background: linear-gradient(155deg, #E8D9C0 0%, #C9A87C 52%, #8E6E4C 100%); }
.g-moss      { background: linear-gradient(155deg, #1D4A44 0%, #0B3230 55%, #05201F 100%); }
.g-sand      { background: linear-gradient(155deg, #EADFCE 0%, #CDBBA0 50%, #9B8A70 100%); }
.g-clay      { background: linear-gradient(155deg, #C98F6E 0%, #A56348 52%, #6E3A28 100%); }
.g-ink       { background: linear-gradient(155deg, #2C3138 0%, #171B20 55%, #0A0C0F 100%); }
.g-rose      { background: linear-gradient(155deg, #E7C3BC 0%, #C8918B 52%, #8E5B58 100%); }
.g-amber     { background: linear-gradient(155deg, #E4C489 0%, #C79A52 52%, #8A6630 100%); }
.g-slate     { background: linear-gradient(155deg, #7E8B93 0%, #55636C 52%, #2F3A41 100%); }

/* ==========================================================================
   03 · APP SHELL
   ========================================================================== */

#app {
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100%;
  overflow: hidden;
  background: var(--dark);
  display: flex;
  flex-direction: column;
}

/* The view stack. Only the active view and the one beneath it are mounted. */
#views {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

.view {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: clip;
  /* No -webkit-overflow-scrolling:touch. Apple retired it with iOS 13 — every
     overflow scroller has had momentum since — and what it still does is give
     the element a separately created scrolling layer, the thing behind the old
     WebKit habit of a scroller inside a position:fixed parent refusing the
     first swipe until it has been tapped. #app is exactly that parent. */
  overscroll-behavior-y: contain;
  /* No scrollbar inside the column. A native app does not draw one, the wheel
     still works, and a chunky desktop bar down the middle of the screen is the
     one detail that would give the whole thing away. */
  scrollbar-width: none;
  background: var(--dark);
  padding-bottom: calc(var(--bar-h) + var(--safe-b) + 1rem);
  /* A view must be its own stacking context, or its insides climb out of it.
     `position: relative` alone does NOT make one: a hero headline at z-index 2
     then belongs to #app's context, not to the view, and paints above every
     sibling view — which are all at z-index auto. That is why the Home
     headline, its scrim and its buttons floated on top of whatever you
     navigated to, while every measurement said the new view was correct: the
     DOM order was right, the opacity was right, and only the painting was
     wrong. isolate, not z-index, because it says exactly this and nothing
     else — no containing block for fixed children, no layout effect. */
  isolation: isolate;
}

.view::-webkit-scrollbar { width: 0; height: 0; }

.view.is-under {           /* the frozen view beneath the active one */
  pointer-events: none;
}

.view.is-light {
  background: var(--light);
  color: var(--dark);
}

/* Every view opens with its hero, which is what the morph grows into. */
.view-hero {
  position: relative;
  height: 46dvh;
  min-height: 240px;
}
.view-hero .ph { position: absolute; inset: 0; }
.view-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  background: linear-gradient(to top,
    var(--dark) 0%,
    color-mix(in srgb, var(--dark) 45%, transparent) 45%,
    transparent 100%);
}
.view-hero .view-hero-title {
  position: absolute;
  z-index: 4;
  left: var(--gutter);
  right: var(--gutter);
  bottom: 1.25rem;
}

.section {
  padding: clamp(2.5rem, 9vw, 3.75rem) var(--gutter);
}
.section + .section { padding-top: 0; }

.section-head { margin-bottom: 1.5rem; }
.section-head .eyebrow { display: block; margin-bottom: .5rem; }

.hr {
  height: 1px;
  background: var(--hairline-d);
  border: 0;
  margin: 0 var(--gutter);
}
.is-light .hr { background: var(--hairline); }

/* ==========================================================================
   04 · BOTTOM BAR
   Two slots. The left one morphs between nav pill and back — never both,
   because they are never both useful at the same moment.
   ========================================================================== */

#bottombar {
  position: relative;
  z-index: var(--z-bar);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  height: calc(var(--bar-h) + var(--safe-b));
  padding: 0 var(--gutter) var(--safe-b);
  background: color-mix(in srgb, var(--dark) 88%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-top: 1px solid var(--hairline-d);
  transform: translate3d(0, 0, 0);
  transition: transform var(--t-passage) var(--e-passage);
}

#bottombar[data-hidden='true'] {
  transform: translate3d(0, 120%, 0);
}

.bar-left {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  min-width: 0;
  padding: .6rem .95rem .6rem .75rem;
  border-radius: 999px;
  border: 1px solid var(--hairline-d);
  background: color-mix(in srgb, var(--light) 6%, transparent);
  color: var(--light);
  font-size: .875rem;
  font-weight: 500;
  letter-spacing: .01em;
  transition: background var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
.bar-left:active { transform: scale(.96); }
.bar-left .bar-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 9.5rem;
}

/* The morphing glyph: a dot at root level, a chevron one level down. */
.bar-glyph {
  position: relative;
  width: 18px; height: 18px;
  flex: 0 0 18px;
}
.bar-glyph::before,
.bar-glyph::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  transition: opacity var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
/* dot */
.bar-glyph::before {
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 1;
  transform: scale(1);
}
/* chevron */
.bar-glyph::after {
  width: 9px; height: 9px;
  margin: -5px 0 0 -2px;
  border-left: 1.5px solid var(--accent);
  border-bottom: 1.5px solid var(--accent);
  opacity: 0;
  transform: rotate(45deg) scale(.6);
}
#bottombar[data-mode='back'] .bar-glyph::before { opacity: 0; transform: scale(.4); }
#bottombar[data-mode='back'] .bar-glyph::after  { opacity: 1; transform: rotate(45deg) scale(1); }

/* The inline destinations belong to the top bar only — a phone's two-slot bar
   has no room for them. The mark is a different case: see below. */
.bar-nav { display: none; }

/* The mark's insides are the same at both sizes; only how much of it shows
   differs. Keeping these out of the media query is what lets the phone reuse
   the collapsed state instead of needing a second mark. */
.brand-inner { position: relative; height: 100%; }
.brand-inner .gl { position: absolute; }
.brand-inner g { fill: var(--light); }
.brand-inner .gl svg { display: block; width: 100%; height: 100%; overflow: visible; }
.brand-inner .gl {
  transition: opacity var(--t-beat) var(--e-beat),
              transform var(--t-passage) var(--e-passage);
}

/* PHONE: the full name does not fit, but the monogram does — and the monogram
   is this same element in the state the desktop reaches by scrolling. So the
   phone gets the mark for nothing: no second asset, no second code path, and
   it can never drift from the one the desktop shows. */
.bar-brand {
  display: block;
  --brand-h: 1.35rem;
  /* Sized by flex-basis, never width. The bar is a flex container, and a flex
     item's used size comes from its basis — 'width' is a hint the flex
     algorithm may ignore, and with a fixed-width box inside it does exactly
     that: the mark refuses to shrink. min-width:0 for the same reason. */
  flex: 0 0 calc(var(--brand-h) * var(--brand-ar, 5.41));
  min-width: 0;
  width: auto;
  height: var(--brand-h);
  transition: flex-basis var(--t-passage) var(--e-passage);
  padding: 0;
  background: none;
  border: 0;
  overflow: hidden;
}
/* The inner box keeps the FULL coordinate system while the outer one clips.
   Shrinking the coordinate system instead would scale every letter down as the
   name collapsed, and the A — the one thing that must hold still — would drift. */
.bar-brand .brand-inner { width: calc(var(--brand-h) * var(--brand-ar, 5.41)); }

/* The collapse belongs to BOTH sizes, so it lives outside the media query.
   The phone reaches it the same way the desktop does — by scrolling off the
   hero — and additionally on any view that is not the hero, because there the
   left control carries a longer label ("Behandlungen" is 151px wide) and the
   full name has 89px to live in instead of 149. */
#bottombar[data-compact='true'] .bar-brand {
  flex-basis: calc(var(--brand-h) * var(--brand-ar, 5.41) * var(--brand-compact, 1));
}
#bottombar[data-compact='true'] .brand-inner .gl[data-drop] {
  opacity: 0;
  transition-duration: var(--t-beat);
}
#bottombar[data-compact='true'] .brand-inner .gl[data-keep] {
  transform: translateX(var(--dx, 0));
  transition-delay: 60ms;
}
/* One level down the mark stays — as AL, which is 49px and fits beside even
   the longest Back label. Hiding it there would make the brand blink out every
   time somebody opened a treatment, which is the opposite of what a mark on a
   bar is for. */

.bar-book {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .72rem 1.15rem;
  border-radius: 999px;
  background: var(--cta);
  color: #17110F;
  font-size: .875rem;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
  transition: transform var(--t-tool) var(--e-tool),
              filter var(--t-tool) var(--e-tool);
}
.bar-book:active { transform: scale(.96); }
.bar-book[hidden] { display: none; }

/* ==========================================================================
   05 · NAV SHEET
   ========================================================================== */

#sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  display: none;
}
#sheet.is-open { display: block; }

.sheet-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .55);
  opacity: 0;
  transition: opacity var(--t-beat) var(--e-beat);
}
#sheet.is-in .sheet-scrim { opacity: 1; }

.sheet-panel {
  position: absolute;
  left: var(--col-left);
  bottom: 0;
  width: var(--stage-w);
  transform: translate3d(var(--col-shift), 100%, 0);
  transition: transform var(--t-beat) var(--e-beat);
  background: var(--dark-soft);
  border-radius: var(--radius-l) var(--radius-l) 0 0;
  padding: .5rem 0 calc(var(--safe-b) + 1rem);
  max-height: 88dvh;
  overflow-y: auto;
  box-shadow: 0 -20px 60px rgba(0, 0, 0, .45);
}
#sheet.is-in .sheet-panel { transform: translate3d(var(--col-shift), 0, 0); }

.sheet-grab {
  width: 38px; height: 4px;
  border-radius: 2px;
  background: var(--hairline-d);
  margin: .5rem auto 1rem;
}

.sheet-row {
  display: flex;
  align-items: center;
  gap: .9rem;
  width: 100%;
  padding: .5rem var(--gutter);
  text-align: left;
  transition: background var(--t-tool) var(--e-tool);
}
.sheet-row:active { background: color-mix(in srgb, var(--light) 7%, transparent); }
.sheet-row[aria-current='page'] .sheet-name { color: var(--accent); }

/* The thumbnail is the morph source, not decoration. Same file as the view
   hero — a different photograph would swap images mid-flight. */
.sheet-thumb {
  flex: 0 0 auto;
  width: 56px; height: 72px;
  border-radius: 9px;
}
.sheet-name {
  font-family: var(--serif);
  font-size: 1.32rem;
  font-variation-settings: 'opsz' 60, 'SOFT' 30, 'WONK' 0;
  letter-spacing: -.01em;
}

.sheet-util {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: .75rem;
  padding: 1rem var(--gutter) 0;
  border-top: 1px solid var(--hairline-d);
}

/* Hidden on a phone: there the toggle lives in the nav sheet, which is one
   tap away. On a desktop the sheet cannot be opened at all, so the bar keeps
   its own copy. */
.bar-lang { display: none; }

.lang-toggle { display: inline-flex; gap: .25rem; }
.lang-toggle button {
  padding: .35rem .6rem;
  border-radius: 7px;
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .06em;
  color: var(--muted-d);
}
.lang-toggle button[aria-pressed='true'] {
  background: color-mix(in srgb, var(--light) 10%, transparent);
  color: var(--light);
}

/* ==========================================================================
   06 · LOADER — light, at maximum scale
   ========================================================================== */

#loader {
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  background: var(--loader-bg, #000);
  display: grid;
  place-items: center;
  overflow: hidden;
}
#loader[hidden] { display: none; }

.load-stage {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
}

/* ------------------------------------------------------------------------
   The monogram, written.

   One <path> per pen stroke so the nib can change weight mid-word — a single
   path can only carry one stroke-width, and uniform weight is exactly what
   stops a shape from reading as calligraphy.
   ------------------------------------------------------------------------ */
.load-mark {
  position: relative;
  z-index: 2;
  width: clamp(190px, 56vw, 300px);
}
.load-mark svg { display: block; width: 100%; overflow: visible; }
/* var(--accent), not a baked hex: the dock switches palette live in front of a
   salon owner, and a logo that stayed champagne while the room turned
   terracotta would be the one thing on screen that looked pasted on. */
.load-mark .gl { position: absolute; }
.load-mark g { fill: var(--loader-mark, var(--accent)); }
/* ONE sweep across the whole mark, not one per letter.

   Per letter is right for the sentence below — many glyphs, and the reveal
   granularity is what makes it read as a hand. For two joined letters it is
   wrong: each mask only does its revealing during the middle of its own
   window, so between the A finishing and the L starting there is a stretch
   where the sweep is travelling but nothing is appearing. That dead time is
   the pause you see between the A and the L. One mask has no seam to pause
   at, and for a connected script a single left-to-right sweep IS the pen. */
.load-mark {
  -webkit-mask-image: linear-gradient(102deg, #000 0 34%, rgba(0,0,0,.5) 43%, transparent 55%);
          mask-image: linear-gradient(102deg, #000 0 34%, rgba(0,0,0,.5) 43%, transparent 55%);
  -webkit-mask-size: 300% 100%;         mask-size: 300% 100%;
  -webkit-mask-repeat: no-repeat;       mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;        mask-position: 100% 0;
}

/* The house line. Revealed glyph by glyph, each behind a soft mask angled to
   the script's own slant, so the hand is legible without a cursor chasing it.
   A single sweep across the whole line needed one; per glyph it does not. */
.load-line {
  position: absolute;
  z-index: 2;
  top: calc(50% + clamp(60px, 17vw, 92px));
  left: var(--gutter);
  right: var(--gutter);
  text-align: center;
  font-family: 'Great Vibes', 'Snell Roundhand', cursive;
  font-size: clamp(1.35rem, 5.4vw, 1.9rem);
  line-height: 1.3;
  color: var(--loader-ink, var(--light));
}
.load-line .ch {
  display: inline-block;
  white-space: pre;
  -webkit-mask-image: linear-gradient(118deg, #000 0 38%, rgba(0,0,0,.5) 46%, transparent 56%);
          mask-image: linear-gradient(118deg, #000 0 38%, rgba(0,0,0,.5) 46%, transparent 56%);
  -webkit-mask-size: 320% 100%;         mask-size: 320% 100%;
  -webkit-mask-repeat: no-repeat;       mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;        mask-position: 100% 0;
}


/* The three numbers a treatment is actually chosen on, given a shape that
   reads at a glance instead of a sentence that has to be parsed. */
.facts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .25rem 1rem;
  margin-top: 1.5rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline-d);
}
.is-light .facts { border-top-color: var(--hairline); }
.facts dt {
  font-size: .72rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted-d);
}
.is-light .facts dt { color: var(--muted); }
.facts dd {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.15rem;
  font-variation-settings: 'opsz' 40, 'SOFT' 20, 'WONK' 0;
}

/* ==========================================================================
   07 · HOME BANDS
   ========================================================================== */

.hero {
  position: relative;
  height: calc(100dvh - var(--bar-h) - var(--safe-b));
  min-height: 480px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--gutter);
  /* Lifted off the bottom edge: the headline is the thing being read, and it
     was sitting closer to the screen edge than to the button under it. */
  padding-bottom: clamp(2.5rem, 7vw, 3.5rem);
  overflow: hidden;
}
.hero .ph { position: absolute; inset: 0; z-index: 0; }
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
    var(--dark) 2%,
    color-mix(in srgb, var(--dark) 55%, transparent) 42%,
    color-mix(in srgb, var(--dark) 25%, transparent) 100%);
}
.hero > *:not(.ph) { position: relative; z-index: 2; }

.hero-sub {
  max-width: 30ch;
  margin-top: 1.15rem;
  color: var(--muted-d);
}

.hero-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  /* The headline and its line are one block; the buttons are a separate
     decision. The gap is what says so. */
  margin-top: 3rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .85rem 1.35rem;
  border-radius: 999px;
  font-size: .9rem;
  font-weight: 600;
  letter-spacing: .01em;
  transition: transform var(--t-tool) var(--e-tool),
              background var(--t-tool) var(--e-tool),
              border-color var(--t-tool) var(--e-tool);
}
.btn:active { transform: scale(.965); }

.btn-cta   { background: var(--cta); color: #17110F; }
.btn-ghost { border: 1px solid var(--hairline-d); color: var(--light); }
.is-light .btn-ghost { border-color: var(--hairline); color: var(--dark); }
.btn-solid { background: var(--dark); color: var(--light); }
.btn-block { width: 100%; }

/* Philosophy: pure typography, no image competing with it. */
.philosophy {
  padding: clamp(3.5rem, 13vw, 6rem) var(--gutter);
  background: var(--light);
  color: var(--dark);
}
.philosophy .body { max-width: 34ch; margin-top: 1.1rem; color: var(--muted); }
.philosophy .crafts {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--hairline);
  font-family: var(--serif);
  font-size: 1.05rem;
  color: var(--muted);
}

/* Trust band: score plus computed proof chips. A bare score persuades nobody. */
.trust {
  padding: 1.5rem var(--gutter);
  background: var(--light);
  color: var(--dark);
}
.trust-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  width: 100%;
  text-align: left;
}
.trust-score { font-family: var(--serif); font-size: 2rem; line-height: 1; }
.trust-stars { color: var(--accent); letter-spacing: .05em; font-size: .95rem; }
.trust-count { color: var(--muted); font-size: .85rem; }
.trust-chevron { margin-left: auto; color: var(--muted); }

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  margin-top: .9rem;
}
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: .38rem;
  padding: .45rem .8rem;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  font-size: .82rem;
  color: var(--dark);
  transition: background var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
.chip:active { transform: scale(.96); }
.chip b { font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; }
.chip[aria-pressed='true'] { background: var(--dark); color: var(--light); border-color: var(--dark); }
.chip[aria-pressed='true'] b { color: var(--accent); }

/* Horizontal strips. iOS rule: proximity, never mandatory, and entrance
   animations transform the container only. */
.strip {
  display: flex;
  gap: .75rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: 0 var(--gutter) .5rem;
  margin: 0 calc(var(--gutter) * -1);
}
.strip::-webkit-scrollbar { display: none; }
.strip > * { scroll-snap-align: start; flex: 0 0 auto; }

.strip-wrap { position: relative; }
.strip-nav {
  position: absolute;
  top: 38%;
  z-index: 5;
  width: 34px; height: 34px;
  display: none;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--dark) 80%, transparent);
  border: 1px solid var(--hairline-d);
  color: var(--light);
  transition: opacity var(--t-tool) var(--e-tool);
}
.strip-nav[data-dir='prev'] { left: 2px; }
.strip-nav[data-dir='next'] { right: 2px; }
.strip-nav[disabled] { opacity: .25; pointer-events: none; }
.strip-bar {
  height: 2px;
  background: var(--hairline-d);
  margin: .35rem var(--gutter) 0;
  display: none;
}
.strip-bar i {
  display: block; height: 100%;
  background: var(--accent);
  transform-origin: left center;
}

/* Cards */
.card-service {
  width: 68vw; max-width: 260px;
  text-align: left;
}
.card-service .ph { aspect-ratio: 3 / 4; border-radius: var(--radius); }
.card-service .card-body { padding-top: .7rem; }
.card-service .card-meta {
  display: flex; gap: .5rem;
  color: var(--muted-d);
  margin-top: .2rem;
}
.is-light .card-service .card-meta { color: var(--muted); }

.card-artist { width: 44vw; max-width: 168px; text-align: left; }
.card-artist .ph { aspect-ratio: 2 / 3; border-radius: var(--radius); }
.card-artist .card-body { padding-top: .6rem; }

.card-post { width: 74vw; max-width: 300px; text-align: left; }
.card-post .ph { aspect-ratio: 16 / 10; border-radius: var(--radius); }
.card-post .card-body { padding-top: .7rem; }

.card-product { width: 52vw; max-width: 200px; text-align: left; }
.card-product .ph { aspect-ratio: 1; border-radius: var(--radius); }

.badge {
  display: inline-block;
  padding: .2rem .5rem;
  border-radius: 5px;
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent);
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
}

/* Newsletter */
.news-form { display: flex; gap: .5rem; margin-top: 1rem; }
.news-form input {
  flex: 1 1 auto;
  min-width: 0;
  padding: .8rem 1rem;
  border-radius: 999px;
  border: 1px solid var(--hairline-d);
  background: color-mix(in srgb, var(--light) 5%, transparent);
  color: var(--light);
}
.is-light .news-form input { border-color: var(--hairline); background: transparent; color: var(--dark); }

/* ---------------------------------------------------------------------------
   Footer.

   It used to be five identical ghost buttons in a row — Anrufen · Schreiben ·
   Route · Instagram · Facebook — with the telephone number living only inside
   href="tel:". On a phone that is a link; on a desktop it is a number nobody
   can read attached to a link that does nothing. The two things a visitor
   actually came for are the number and the door, so those two are now the
   largest things here and both are the text itself, not a control beside it.
   --------------------------------------------------------------------------- */

.foot {
  display: block;
  padding: clamp(2.75rem, 9vw, 4rem) var(--gutter) calc(var(--gutter) + .5rem);
  border-top: 1px solid var(--hairline-d);
}
.is-light .foot { border-color: var(--hairline); }

.foot-mark {
  font-family: var(--serif);
  font-size: clamp(1.5rem, 6.5vw, 2rem);
  line-height: 1.05;
  letter-spacing: -.022em;
  font-variation-settings: 'opsz' 90, 'SOFT' 40, 'WONK' 1;
}
.foot-tag { margin-top: .4rem; color: var(--muted-d); }
.is-light .foot-tag { color: var(--muted); }

/* One column on a phone, three where there is room. Not a media query of its
   own: the grid decides from the space it is given, so the footer is right
   inside the 480px booking stage and inside a 1180px page without being told
   which one it is in. */
.foot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: clamp(1.75rem, 5vw, 2.75rem);
  margin-top: clamp(2rem, 7vw, 2.75rem);
}
.foot-col > .eyebrow { display: block; margin-bottom: .85rem; }

/* The number and the address. Serif, large, and the link is the text. */
.foot-line {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  margin-top: .55rem;
}
.foot-line .ico { flex: 0 0 auto; color: var(--accent); transform: translateY(.18em); }
.foot-big {
  font-family: var(--serif);
  font-size: clamp(1.15rem, 4.6vw, 1.35rem);
  line-height: 1.25;
  font-variation-settings: 'opsz' 40, 'SOFT' 30, 'WONK' 0;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  border-bottom: 1px solid transparent;
  transition: border-color var(--t-tool) var(--e-tool),
              color var(--t-tool) var(--e-tool);
}
.foot-mail { font-family: var(--sans); font-size: .95rem; letter-spacing: 0; }
.foot-addr { font-style: normal; }
.foot-addr .foot-big { display: block; }
.foot-addr .foot-sub { display: block; margin-top: .1rem; color: var(--muted-d); font-size: .95rem; }
.is-light .foot-addr .foot-sub { color: var(--muted); }
.foot-route {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  margin-top: .7rem;
  font-size: .84rem;
  color: var(--accent);
}

/* Open or shut, computed in the salon's own timezone. A dot, because a status
   that has to be read twice is not a status. */
.foot-state {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-size: .88rem;
  font-weight: 500;
}
.foot-state::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--muted-d);
  flex: 0 0 auto;
}
.is-light .foot-state::before { background: var(--muted); }
.foot-state[data-open='true'] { color: var(--accent); }
.foot-state[data-open='true']::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent);
}

.foot-hours { margin-top: 1rem; }
.foot-hours li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .42rem 0;
  border-bottom: 1px solid var(--hairline-d);
  font-size: .9rem;
}
.is-light .foot-hours li { border-color: var(--hairline); }
.foot-hours li.is-today { color: var(--accent); }
.foot-hours li:last-child { border-bottom: 0; }

/* Socials get their own room and their own shape. As ghost buttons carrying
   the word "Instagram" they were visually indistinguishable from "Anrufen",
   which made five equal-weight controls out of two very unequal jobs. */
.foot-social {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  margin-top: .25rem;
}
.foot-social a {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 50%;
  border: 1px solid var(--hairline-d);
  color: var(--light);
  transition: background var(--t-tool) var(--e-tool),
              border-color var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
.is-light .foot-social a { border-color: var(--hairline); color: var(--dark); }
.foot-social a:active { transform: scale(.94); }

.foot-legal {
  margin-top: clamp(2rem, 7vw, 2.75rem);
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline-d);
  display: flex;
  flex-wrap: wrap;
  gap: .35rem 1rem;
  font-size: .74rem;
  color: var(--muted-d);
}
.is-light .foot-legal { border-color: var(--hairline); color: var(--muted); }

/* The agency credit, as a signature: "Made by", the mark, the tagline —
   stacked, and all three inside one link that dims and lights as a single
   thing. At rest the whole block sits at .66, which is exactly the alpha of
   the copyright line beside it, so it reads as part of the legal row rather
   than as an advert on the salon's own site. */
.foot-legal { line-height: 1.4; }
.foot-credit {
  /* A whole row on a phone, under the copyright line. As a shrinkable flex
     item it was once measured before the remote logo had loaded and broke
     into pieces around the image. */
  flex: 1 1 100%;
  margin-top: .6rem;
  font-size: inherit;
  line-height: 1.4;
}
.foot-credit a {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .3rem;
  color: var(--light);
  opacity: .66;
  transition: opacity var(--t-tool) var(--e-tool);
}
.foot-credit a:focus-visible { opacity: 1; }
.foot-credit img {
  /* Explicit height, not max-height: the file is 211x25, and 15px is where
     the wordmark reads without shouting. */
  display: block;
  height: 15px;
  width: auto;
}
.foot-credit-name { font-weight: 600; letter-spacing: .02em; }
.is-light .foot-credit a { color: var(--dark); }
.is-light .foot-credit img { filter: invert(1); }   /* the supplied mark is white */
/* Hover only where there is a real pointer: on a phone, :hover sticks after
   a tap and would leave the signature lit for the rest of the visit. */
@media (hover: hover) and (pointer: fine) {
  .foot-credit a:hover { opacity: 1; }
}

@media (min-width: 900px) {
  /* Right edge, and standing on the same line as the copyright: flex-end
     aligns the bottom edges, and one shared line-height turns that into a
     shared baseline. A three-line block next to a one-line row otherwise
     looks like it slipped. */
  .foot-legal { align-items: flex-end; }
  .foot-credit { flex: 0 0 auto; margin-left: auto; margin-top: 0; }
}

.ico { display: inline-flex; line-height: 0; }

/* --------------------------------------------------------------------------
   Light tone. A palette with tone:'light' swaps ground and ink in the token
   values, and almost everything follows on its own. These are the few rules
   that were written assuming the ground is dark.
   -------------------------------------------------------------------------- */

/* (The loader's ground, ink and mark colour are chosen in Palette.apply —
   --loader-bg, --loader-ink, --loader-mark — by contrast, for every tone.) */

/* Paper surfaces (.is-light) are the dark band on a light palette, and a
   form field hard-coded to #fff under --dark text became cream on white. */
[data-tone='light'] .is-light .form-field input,
[data-tone='light'] .is-light .form-field textarea {
  background: color-mix(in srgb, var(--dark) 9%, var(--light));
  color: var(--dark);
}

/* A booking sheet pinned to its own colour (palette bookingBg). Every token
   the sheet reads is redeclared HERE, not just the two base colours: the
   derived ones are computed where they are declared, on :root, and inherit
   as finished colours — redefining --dark alone would leave every hairline
   and muted label still mixed from the page's colours. */
[data-bk='pinned'] #booking .bk-shell {
  --light:      var(--bk-bg);
  --dark:       var(--bk-ink);
  --ink:        var(--dark);
  --paper:      var(--light);
  --hairline:   color-mix(in srgb, var(--dark) 14%, transparent);
  --hairline-d: color-mix(in srgb, var(--light) 18%, transparent);
  --muted:      color-mix(in srgb, var(--dark) 60%, transparent);
  --muted-d:    color-mix(in srgb, var(--light) 66%, transparent);
}
[data-bk='pinned'] #booking .is-light .form-field input,
[data-bk='pinned'] #booking .is-light .form-field textarea {
  background: color-mix(in srgb, var(--dark) 9%, var(--light));
  color: var(--dark);
}
/* Gold chips carry whichever colour reads on this palette's gold. */
[data-bk='pinned'] #booking .tab[aria-selected='true'] { color: var(--bk-on-accent); }

/* The agency mark is supplied white. */
[data-tone='light'] .foot-credit img { filter: invert(1); }

/* ==========================================================================
   08 · RINGS
   ========================================================================== */

.rings {
  display: flex;
  gap: 1.1rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: .25rem var(--gutter) .5rem;
  margin: 0 calc(var(--gutter) * -1);
}
.rings::-webkit-scrollbar { display: none; }

/* The label column is wider than the circle it sits under. Sizing the two
   together truncated the longest German word in the row — and the label is the
   entire reason the ring is legible, so it is the last thing that should be
   asked to give way. */
.ring {
  flex: 0 0 auto;
  scroll-snap-align: start;
  width: 96px;
  text-align: center;
}
.ring-circle {
  position: relative;
  margin: 0 auto;
  width: 78px; height: 78px;
  border-radius: 50%;
  padding: 3px;
  background: conic-gradient(from 210deg,
    var(--accent), color-mix(in srgb, var(--cta) 70%, var(--accent)), var(--accent));
  transition: transform var(--t-tool) var(--e-tool);
}
.ring:active .ring-circle { transform: scale(.94); }
.ring-circle .ph {
  width: 100%; height: 100%;
  border-radius: 50%;
  border: 2px solid var(--dark);
}
.is-light .ring-circle .ph { border-color: var(--light); }
.ring-label {
  display: block;
  margin-top: .45rem;
  font-size: .74rem;
  letter-spacing: .01em;
  color: var(--muted-d);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.is-light .ring-label { color: var(--muted); }
.ring.is-seen .ring-circle { background: var(--hairline-d); }

/* ==========================================================================
   09 · STORIES
   ========================================================================== */

#story {
  position: fixed;
  inset: 0;
  z-index: var(--z-story);
  display: none;
  background: #000;
  overscroll-behavior: none;
}
#story.is-open { display: block; }

.story-stage {
  position: absolute;
  left: var(--col-left);
  top: 0;
  width: var(--stage-w);
  height: 100%;
  transform: translateX(var(--col-shift));
  overflow: hidden;
  background: #000;
}

/* One frame per service. Only current ±1 are mounted. */
/* Every gesture inside a frame is handled by hand, so the browser must not
   claim any of them first. Without this the chrome layer defaults to
   touch-action:auto, a vertical drag starting on a tap zone is read as a page
   scroll, and the pointer stream is cancelled before swipe-up can ever fire —
   which is why the panel only opened from its button. */
.story-frame {
  position: absolute;
  inset: 0;
  will-change: transform, opacity;
  background: #000;
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
.story-frame * { touch-action: none; }

.story-media {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* Two image layers so the light sweep has something to reveal. */
.story-img {
  position: absolute;
  inset: 0;
  will-change: transform, clip-path;
  transform-origin: center center;
}
.story-img .ph { position: absolute; inset: 0; }
.story-img.is-next { z-index: 2; }
.story-img.is-cur  { z-index: 3; }

/* The accent line riding the leading edge of the sweep. */
.story-beam {
  position: absolute;
  top: 0; bottom: 0;
  width: 2px;
  z-index: 4;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
  filter: blur(.4px);
}

.story-scrim-top,
.story-scrim-bot {
  position: absolute;
  left: 0; right: 0;
  z-index: 5;
  pointer-events: none;
  transition: opacity var(--t-beat) var(--e-beat);
}
.story-scrim-top {
  top: 0; height: 34%;
  background: linear-gradient(to bottom, rgba(0,0,0,.72), transparent);
}
.story-scrim-bot {
  bottom: 0; height: 42%;
  background: linear-gradient(to top, rgba(0,0,0,.82), transparent);
}

.story-chrome {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  pointer-events: none;
  transition: opacity var(--t-beat) var(--e-beat);
}
#story.is-peeking .story-chrome,
#story.is-peeking .story-scrim-top,
#story.is-peeking .story-scrim-bot { opacity: 0; }

.story-progress {
  display: flex;
  gap: 3px;
  padding: calc(var(--safe-t) + .6rem) .6rem .5rem;
}
.story-progress i {
  flex: 1 1 0;
  height: 2.5px;
  border-radius: 2px;
  background: rgba(255,255,255,.28);
  overflow: hidden;
}
.story-progress i.is-seen { background: rgba(255,255,255,.85); }
.story-progress i.is-cur  { background: var(--accent); }

.story-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .1rem .85rem;
  pointer-events: auto;
}
.story-head .ph { width: 30px; height: 30px; border-radius: 50%; flex: 0 0 30px; }
.story-head-txt { min-width: 0; }
.story-head-name {
  font-size: .84rem; font-weight: 600; color: #fff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.story-head-n { font-size: .72rem; color: rgba(255,255,255,.6); }
.story-close {
  margin-left: auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  color: #fff; font-size: 1.35rem; line-height: 1;
}

.story-tapzone { flex: 1 1 auto; display: flex; pointer-events: auto; }
.story-tap-prev { flex: 0 0 33%; }
.story-tap-next { flex: 1 1 auto; }

/* Persistent bottom bar: name, duration, from-price, and the way up. */
.story-foot {
  pointer-events: auto;
  padding: .75rem .95rem calc(var(--safe-b) + .95rem);
}
.story-foot-btn {
  display: flex;
  align-items: center;
  gap: .75rem;
  width: 100%;
  text-align: left;
  color: #fff;
}
.story-foot-meta { min-width: 0; flex: 1 1 auto; }
.story-foot-name { font-family: var(--serif); font-size: 1.16rem; line-height: 1.15; }
.story-foot-sub  { font-size: .78rem; color: rgba(255,255,255,.72); margin-top: .1rem; }
.story-foot-up {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.3);
  /* Three nudges, then it rests. An affordance teaches a gesture once; a
     perpetual bounce is noise, and it holds a compositor slot open on every
     mounted frame for as long as the story is on screen. */
  animation: nudge 2.6s var(--e-beat) 3;
}
@keyframes nudge {
  0%, 72%, 100% { transform: translateY(0); }
  80%           { transform: translateY(-4px); }
  88%           { transform: translateY(0); }
}

/* The swipe-up panel — this replaces the old service-detail overlay. */
.story-panel {
  position: absolute;
  left: var(--col-left);
  bottom: 0;
  z-index: 8;
  width: var(--stage-w);
  max-height: 65%;
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translate3d(var(--col-shift), 100%, 0);
  transition: transform var(--t-beat) var(--e-beat);
  background: var(--dark);
  border-radius: var(--radius-l) var(--radius-l) 0 0;
  /* No padding-top. A sticky child sticks to the PADDING box, so 8px of
     padding here parked the bar 8px below the panel's own top edge and left a
     window in the rounded shoulder — scrolling text rose through it and sat
     over the photograph, clipped in half. The bar supplies its own height
     instead, and the panel's top edge and the bar's top edge are the same
     line. */
  padding: 0 var(--gutter) calc(var(--safe-b) + 1.1rem);
  /* touch-action:none by default, because pan-y hands every downward drag to
     the browser as a scroll — it fires pointercancel mid-gesture and the
     swipe-to-dismiss never reaches its threshold. Most of these panels have
     nothing to scroll anyway; the .is-scrollable class, set from the measured
     content in fillPanel(), gives pan-y back only to the ones that do. */
  touch-action: none;
}

.story-panel.is-scrollable { touch-action: pan-y; }

/* The drag handle, and on a scrollable panel the ONLY place the gesture is
   reliably ours.

   touch-action is resolved by the compositor at the moment the finger lands,
   before a line of JS runs. A panel that overflows has to keep pan-y so its
   own content can scroll — which means a drag starting anywhere in the body
   belongs to the browser and arrives here as pointercancel, halfway through.
   Only a subtree that declares touch-action:none can be counted on.

   That makes the size of this strip a correctness question rather than a
   cosmetic one: at 24px it was a target most thumbs miss. It is 44px now —
   the ordinary sheet-header height — full width, and sticky, so it stays
   under the thumb after the panel has been scrolled. The visible pill is
   still 38x4; the ground is opaque so scrolled text cannot run under it. */
/* The panel's top strip: the drag handle and, beside it, a control that works
   whatever the compositor decides about the gesture. Sticky, so both stay
   under the thumb after the content has been scrolled. */
.panel-bar {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  height: 52px;
  /* Full bleed inside the panel: the ground has to reach both edges, or text
     passing underneath shows in the gutters. */
  margin: 0 calc(var(--gutter) * -1) .4rem;
  padding: 0 var(--gutter);
  background: var(--dark);
  touch-action: none;
}
.panel-close {
  position: absolute;
  right: calc(var(--gutter) - 6px);
  top: 4px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--muted-d);
  touch-action: manipulation;
}
.panel-close:active { color: var(--light); }

.story-panel > .panel-bar > .sheet-grab {
  position: relative;
  flex: 1 1 auto;
  /* An explicit box, not padding around a 4px line: box-sizing is border-box
     here, so padding alone left the strip 4px short of the 44 it wants. */
  height: 44px;
  padding: 0;
  margin: 0 0 .4rem;
  background: none;
  border-radius: 0;
  touch-action: none;
  cursor: grab;
}
.story-panel > .panel-bar > .sheet-grab:active { cursor: grabbing; }
.story-panel > .panel-bar > .sheet-grab::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 38px;
  height: 4px;
  border-radius: 2px;
  background: var(--hairline-d);
}
#story.is-panel .story-panel { transform: translate3d(var(--col-shift), 0, 0); }

/* A scaled, dimmed copy of the photograph — not a live blur, which is
   expensive on a mid-range Android. */
.story-panel-bg {
  position: absolute;
  inset: 0;
  z-index: 7;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-beat) var(--e-beat);
  background: rgba(0, 0, 0, .55);
}
#story.is-panel .story-panel-bg { opacity: 1; pointer-events: auto; }

.panel-artists { display: flex; gap: .6rem; margin-top: .9rem; }
.panel-artist { text-align: center; width: 60px; flex: 0 0 60px; }
.panel-artist .ph { width: 54px; height: 54px; border-radius: 50%; margin: 0 auto; }
.panel-artist span {
  display: block; margin-top: .3rem;
  font-size: .68rem; color: var(--muted-d);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* One review, immediately above the CTA. This is the point of hesitation. */
.panel-review {
  margin-top: 1.1rem;
  padding: .9rem 1rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--light) 7%, transparent);
  border-left: 2px solid var(--accent);
}
.panel-review .stars { color: var(--accent); font-size: .8rem; }
.panel-review p { margin-top: .3rem; font-size: .88rem; line-height: 1.55; }
.panel-review cite { display: block; margin-top: .45rem; font-style: normal; font-size: .76rem; color: var(--muted-d); }

/* Zoom affordance */
.story-zoomhint {
  position: absolute;
  right: .85rem;
  bottom: 50%;
  z-index: 6;
  padding: .3rem .55rem;
  border-radius: 999px;
  background: rgba(0,0,0,.45);
  font-size: .68rem;
  color: rgba(255,255,255,.75);
  opacity: 0;
  transition: opacity var(--t-beat) var(--e-beat);
  pointer-events: none;
}
#story.is-zoomed .story-zoomhint { opacity: 1; }

/* ==========================================================================
   10 · VIEWS
   ========================================================================== */

.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: .9rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; }

.tabs {
  display: flex;
  gap: .4rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: .25rem;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  flex: 0 0 auto;
  padding: .5rem .9rem;
  border-radius: 999px;
  border: 1px solid var(--hairline-d);
  font-size: .85rem;
  color: var(--muted-d);
  transition: background var(--t-tool) var(--e-tool), color var(--t-tool) var(--e-tool);
}
.is-light .tab { border-color: var(--hairline); color: var(--muted); }
.tab[aria-selected='true'] { background: var(--accent); color: var(--dark); border-color: var(--accent); }

.list-row {
  display: flex;
  align-items: center;
  gap: .9rem;
  width: 100%;
  padding: .8rem 0;
  text-align: left;
  border-bottom: 1px solid var(--hairline-d);
}
.is-light .list-row { border-color: var(--hairline); }
.list-row .ph { width: 66px; height: 88px; border-radius: 10px; flex: 0 0 66px; }
.list-row-body { min-width: 0; flex: 1 1 auto; }
.list-row-price { flex: 0 0 auto; text-align: right; font-size: .85rem; color: var(--muted-d); }
.is-light .list-row-price { color: var(--muted); }

.profile-video { aspect-ratio: 9 / 16; border-radius: var(--radius-l); max-height: 52dvh; }

.review-card {
  padding: 1rem 0;
  border-bottom: 1px solid var(--hairline-d);
}
.is-light .review-card { border-color: var(--hairline); }
.review-card .stars { color: var(--accent); font-size: .82rem; }
.review-card p { margin-top: .35rem; }
.review-card footer { margin-top: .5rem; font-size: .78rem; color: var(--muted-d); }

/* --------------------------------------------------------------------------
   Das Haus.

   The one view in the build with no photograph at the top, and that is the
   point rather than a gap. There is not a single frame of the room on disk —
   so this is built on words and on what clients said about the room, which is
   the better material anyway: a photograph of an empty salon is the most
   generic asset in the trade, and "no television, no canned music" cannot be
   bought from a stock library.

   When the seven studio frames arrive, "Der Ort" appears inside here on its
   own — same self-removing rule as everywhere else. Nothing below has to
   change for that to happen.
   -------------------------------------------------------------------------- */

.haus-hero {
  padding: calc(var(--safe-t) + 2.75rem) var(--gutter) clamp(2.25rem, 8vw, 3.25rem);
  border-bottom: 1px solid var(--hairline-d);
}
.haus-hero .eyebrow { display: block; margin-bottom: .9rem; }
.haus-hero .d1 { max-width: 15ch; }
.haus-lede {
  margin-top: 1.25rem;
  max-width: 34ch;
  color: var(--muted-d);
}

/* Three statements, hairline-separated. Not big numerals: the facts here are
   sentences, and a 72px "4" with "appointments per artist per day" under it
   turns a house rule into an infographic. */
.haus-facts { margin-top: 1.5rem; }
.haus-facts li {
  display: flex;
  gap: 1rem;
  padding: 1.1rem 0;
  border-bottom: 1px solid var(--hairline-d);
}
.haus-facts li:last-child { border-bottom: 0; }
.haus-facts .haus-idx {
  flex: 0 0 auto;
  font-family: var(--serif);
  font-size: .95rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  padding-top: .1rem;
}
.haus-facts p { max-width: 46ch; }

/* The founder, beside her own portrait. */
.haus-founder {
  display: flex;
  gap: 1.1rem;
  align-items: flex-start;
  width: 100%;
  text-align: left;
  margin-top: 1.25rem;
}
.haus-founder .ph {
  flex: 0 0 84px;
  width: 84px;
  aspect-ratio: 2 / 3;
  border-radius: 10px;
}
.haus-founder-body { min-width: 0; flex: 1 1 auto; }
.haus-founder-body .tiny { color: var(--muted-d); margin-top: .15rem; }
.haus-founder-body .body { margin-top: .6rem; }

.haus-more {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin-top: 1.25rem;
  font-size: .84rem;
  color: var(--accent);
}

/* The door on the home page. The band already says "Das Haus" — it becomes
   the way in rather than growing a menu row it has no photograph for. */
.phil-door {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  margin-top: 1.75rem;
  font-size: .86rem;
  font-weight: 600;
  color: var(--dark);
  border-bottom: 1px solid var(--hairline);
  padding-bottom: .2rem;
}

/* --------------------------------------------------------------------------
   Journal. The writing is the best thing in this build and it was presented
   as four thumbnails in a sideways strip: a title, a read time, and no way to
   reach a fifth post. It reads as an article now — dateline, lead, measure,
   end mark — and the index exists, so the strip is a window onto something
   rather than the whole of it.

   Deliberately still no per-post URL. That is the real-build job, and writing
   twenty posts before it exists would be twenty posts nobody can link to.
   -------------------------------------------------------------------------- */

.jr-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  font-size: .72rem;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--muted-d);
}
.is-light .jr-meta { color: var(--muted); }
.jr-meta .jr-dot { opacity: .5; }
.jr-meta time { font-variant-numeric: tabular-nums; }

.card-post .jr-meta { margin-bottom: .35rem; }
.card-post .jr-excerpt {
  margin-top: .45rem;
  color: var(--muted-d);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  overflow: hidden;
}
.is-light .card-post .jr-excerpt { color: var(--muted); }

/* The index: one column on a phone, filling out wherever there is room. The
   cards stop being strip-width because they are no longer in a strip. */
.jr-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: clamp(1.5rem, 5vw, 2rem);
}
.jr-list .card-post { width: auto; max-width: none; }

/* The article. A measure, because 34ch is a caption and the full width of a
   monitor is not a paragraph. */
.jr-article { max-width: 68ch; }
.jr-article .jr-lead {
  padding-left: 1.1rem;
  border-left: 2px solid var(--accent);
  font-family: var(--serif);
  font-size: clamp(1.15rem, 4.8vw, 1.4rem);
  line-height: 1.42;
  font-variation-settings: 'opsz' 60, 'SOFT' 40, 'WONK' 1;
  text-wrap: pretty;
}
.jr-article .body { text-wrap: pretty; }
/* Subheads inside a post. Serif at d4 weight but smaller — a signpost, not a
   second headline competing with the one in the hero. */
.jr-article .jr-sub {
  margin-top: 2rem;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.12rem, 4.4vw, 1.3rem);
  line-height: 1.25;
  letter-spacing: -.01em;
  font-variation-settings: 'opsz' 40, 'SOFT' 30, 'WONK' 0;
  color: var(--light);
  text-wrap: balance;
}
.is-light .jr-article .jr-sub { color: var(--dark); }

.jr-end {
  width: 34px;
  height: 1px;
  background: var(--accent);
  border: 0;
  margin: clamp(2rem, 7vw, 2.75rem) 0 0;
}

.jr-cta {
  margin-top: 1.25rem;
  padding: 1.4rem 0 0;
}
.jr-cta p { max-width: 40ch; color: var(--muted-d); }
.is-light .jr-cta p { color: var(--muted); }
.jr-cta .btn { margin-top: 1.1rem; }

/* Reviews view: each review over the photograph it praises. */
.rv-frame {
  position: absolute;
  inset: 0;
  will-change: transform, opacity;
  background: #000;
}
.rv-frame .ph { position: absolute; inset: 0; }
.rv-frame::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,.88) 8%, rgba(0,0,0,.25) 50%, rgba(0,0,0,.55) 100%);
}
.rv-body {
  position: absolute;
  z-index: 2;
  left: 0; right: 0; bottom: 0;
  padding: var(--gutter);
  padding-bottom: calc(var(--safe-b) + var(--gutter));
  color: #fff;
}
.rv-quote {
  font-family: var(--serif);
  font-size: clamp(1.3rem, 5.6vw, 1.7rem);
  line-height: 1.3;
  font-variation-settings: 'opsz' 60, 'SOFT' 40, 'WONK' 1;
  text-wrap: pretty;
}

.form-field { margin-bottom: 1rem; }
.form-field label { display: block; font-size: .82rem; color: var(--muted-d); margin-bottom: .35rem; }
.is-light .form-field label { color: var(--muted); }
.form-field input,
.form-field textarea {
  width: 100%;
  padding: .8rem .95rem;
  border-radius: var(--radius);
  border: 1px solid var(--hairline-d);
  background: color-mix(in srgb, var(--light) 5%, transparent);
  color: var(--light);
  transition: border-color var(--t-tool) var(--e-tool);
}
.is-light .form-field input,
.is-light .form-field textarea { border-color: var(--hairline); background: #fff; color: var(--dark); }
.form-field textarea { min-height: 92px; resize: vertical; }
.form-field.has-error input,
.form-field.has-error textarea { border-color: var(--cta); }
.field-error { display: block; margin-top: .35rem; font-size: .78rem; color: var(--cta); }

.check {
  display: flex;
  gap: .65rem;
  align-items: flex-start;
  font-size: .85rem;
  line-height: 1.45;
}
.check input { flex: 0 0 auto; width: 20px; height: 20px; margin-top: .12rem; accent-color: var(--accent); }

/* ==========================================================================
   11 · BOOKING
   ========================================================================== */

#booking {
  position: fixed;
  inset: 0;
  z-index: var(--z-booking);
  display: none;
}
#booking.is-open { display: block; }

.bk-shell {
  position: absolute;
  left: var(--col-left);
  top: 0;
  width: var(--stage-w);
  height: 100%;
  transform: translate(var(--col-shift), 100%);
  display: flex;
  flex-direction: column;
  background: var(--light);
  color: var(--dark);
  will-change: transform;
}

/* The scrim only exists where the sheet does not cover the whole screen. */
#booking::before {
  content: '';
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, #0B0A09 64%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-passage) var(--e-passage);
}

.bk-head {
  flex: 0 0 auto;
  padding: calc(var(--safe-t) + .9rem) var(--gutter) .75rem;
  border-bottom: 1px solid var(--hairline);
}
.bk-head-top { display: flex; align-items: center; gap: .75rem; }
.bk-steps { display: flex; gap: 4px; margin-top: .7rem; }
.bk-steps i {
  flex: 1 1 0; height: 3px; border-radius: 2px;
  background: var(--hairline);
  transition: background var(--t-tool) var(--e-tool);
}
.bk-steps i.is-done { background: var(--accent); }
.bk-steps i.is-cur  { background: var(--dark); }

.bk-body {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  overflow: hidden;
}
.bk-step {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 1.25rem var(--gutter) 1.5rem;
  will-change: transform, opacity;
}

.bk-foot {
  flex: 0 0 auto;
  padding: .8rem var(--gutter) calc(var(--safe-b) + .9rem);
  border-top: 1px solid var(--hairline);
  background: var(--light);
}
.bk-sum {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: .7rem;
  font-size: .85rem;
}
.bk-sum strong { font-family: var(--serif); font-size: 1.1rem; }

.pick {
  display: flex;
  align-items: center;
  gap: .8rem;
  width: 100%;
  padding: .7rem;
  margin-bottom: .5rem;
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  text-align: left;
  transition: border-color var(--t-tool) var(--e-tool),
              background var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
.pick:active { transform: scale(.99); }
.pick[aria-pressed='true'] {
  border-color: var(--dark);
  background: color-mix(in srgb, var(--dark) 5%, transparent);
}
.pick .ph { width: 56px; height: 72px; border-radius: 9px; flex: 0 0 56px; }
.pick-body { flex: 1 1 auto; min-width: 0; }
.pick-meta { font-size: .78rem; color: var(--muted); margin-top: .1rem; }
.pick-mark {
  flex: 0 0 22px;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--hairline);
  display: grid; place-items: center;
  font-size: .75rem;
  color: transparent;
}
.pick[aria-pressed='true'] .pick-mark {
  background: var(--dark); border-color: var(--dark); color: var(--light);
}

.pick-artist { flex-direction: column; width: 88px; flex: 0 0 88px; text-align: center; padding: .6rem .35rem; }
.pick-artist .ph { width: 56px; height: 56px; border-radius: 50%; flex: 0 0 56px; }
.pick-artist .pick-body { text-align: center; }

.daystrip {
  display: flex;
  gap: .4rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-bottom: .35rem;
}
.daystrip::-webkit-scrollbar { display: none; }
.day {
  flex: 0 0 auto;
  scroll-snap-align: start;
  width: 54px;
  padding: .55rem .25rem;
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  text-align: center;
  transition: background var(--t-tool) var(--e-tool),
              border-color var(--t-tool) var(--e-tool);
}
.day-dow { font-size: .68rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.day-num { font-family: var(--serif); font-size: 1.2rem; line-height: 1.2; }
.day[aria-pressed='true'] { background: var(--dark); border-color: var(--dark); color: var(--light); }
.day[aria-pressed='true'] .day-dow { color: var(--accent); }
.day.is-full { opacity: .38; }

.slot-group { margin-top: 1.1rem; }
.slot-group h4 {
  font-size: .74rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); margin-bottom: .5rem; font-weight: 500;
}
.slots { display: flex; flex-wrap: wrap; gap: .4rem; }
.slot {
  padding: .55rem .85rem;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  font-size: .88rem;
  font-variant-numeric: tabular-nums;
  transition: background var(--t-tool) var(--e-tool),
              border-color var(--t-tool) var(--e-tool),
              transform var(--t-tool) var(--e-tool);
}
.slot:active { transform: scale(.95); }
.slot[aria-pressed='true'] { background: var(--dark); border-color: var(--dark); color: var(--light); }
/* Taken slots are shown, not hidden. Scarcity is honest and it drives action. */
.slot[disabled] {
  opacity: .34;
  text-decoration: line-through;
  cursor: not-allowed;
}

.bk-done { text-align: center; padding-top: 2rem; }
.bk-check {
  width: 76px; height: 76px;
  margin: 0 auto 1.25rem;
  border-radius: 50%;
  border: 2px solid var(--accent);
  display: grid; place-items: center;
}
.bk-check svg { width: 34px; height: 34px; }
.bk-check path {
  fill: none; stroke: var(--accent); stroke-width: 3;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 40; stroke-dashoffset: 40;
}

.bk-recap {
  margin: 1.5rem 0;
  padding: 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  text-align: left;
}
.bk-recap dt { font-size: .74rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.bk-recap dd { margin: .15rem 0 .8rem; font-size: .95rem; }
.bk-recap dd:last-child { margin-bottom: 0; }

.bk-actions { display: grid; gap: .5rem; }

.install-prompt {
  margin-top: 1.5rem;
  padding: 1rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--dark) 6%, transparent);
  text-align: left;
}

/* How to arrive. Set apart from the buttons around it by a rule rather than a
   filled box — it is something to read, not something to press, and three
   tinted panels stacked on one screen stop reading as emphasis. */
.bk-note {
  margin-top: 1.5rem;
  padding: 1.1rem 0 0;
  border-top: 1px solid var(--hairline);
  text-align: left;
}
.bk-note .eyebrow { color: var(--dark); opacity: .55; }

/* ==========================================================================
   12 · DEMO DOCK
   Not part of the real site chrome. demoMode:false removes it entirely.
   ========================================================================== */

/* Top-left, not bottom-left: the bottom-left corner is where the hero CTAs and
   the Book button live, and a demo control must never sit on the one button
   the pitch is trying to demonstrate. */
#dock {
  position: fixed;
  left: .75rem;
  top: calc(var(--safe-t) + .75rem);
  z-index: var(--z-dock);
  display: flex;
  gap: .4rem;
  padding: .4rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, .45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
#dock[hidden] { display: none; }
.dock-dot {
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 1.5px solid rgba(255,255,255,.35);
  transition: transform var(--t-tool) var(--e-tool), border-color var(--t-tool) var(--e-tool);
}
.dock-dot[aria-pressed='true'] { border-color: #fff; transform: scale(1.14); }

/* ==========================================================================
   13 · MOTION CLASSES — the "Licht" system
   Light marks a change of level. Depth marks movement between siblings.
   ========================================================================== */

/* The light that travels with an Open/Close. */
#lightlayer {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle at var(--lx, 50%) var(--ly, 50%),
    rgba(255,255,255,.92) 0%,
    color-mix(in srgb, var(--accent) 70%, transparent) 24%,
    transparent 62%);
  will-change: opacity, transform;
}

/* The morph clone: a real element flying from source rect to destination. */
.morph-ghost {
  position: fixed;
  z-index: 88;
  pointer-events: none;
  overflow: hidden;
  will-change: transform, border-radius;
  transform-origin: top left;
  backface-visibility: hidden;
}
/* The photograph inside the travelling crop. It counter-scales whatever the
   crop imposes, so it is only ever scaled uniformly — the crop stretches, the
   picture never does. Origin at the centre, so the cover crop is centred. */
.morph-inner {
  width: 100%;
  height: 100%;
  will-change: transform;
  transform-origin: 50% 50%;
  backface-visibility: hidden;
}

/* Entrance for content inside a freshly pushed view. */
.reveal {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
}
.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--t-passage) var(--e-passage),
              transform var(--t-passage) var(--e-passage);
}

/* Strips fade their children and translate the container only — the iOS rule. */
.strip .reveal { transform: none; }

/* ==========================================================================
   14 · DESKTOP — app column plus art-directed surround
   One layout, not two. A drawn phone frame reads as a mockup; a designed
   column reads as a product.
   ========================================================================== */

@media (min-width: 900px) {
  :root {
    --bar-h: 4.5rem;
    --gutter: clamp(1.75rem, 4vw, 3rem);
  }

  /* The app IS the screen. It used to be a 480px column with three quarters of
     the monitor given over to a scrim-covered photograph — which meant the
     better the display, the smaller the product looked. */

  /* --- the bar moves to the top and grows --- */
  #bottombar {
    order: -1;                       /* first in the column flex, so: a top bar */
    height: var(--bar-h);
    padding: 0 var(--gutter);
    gap: 1.5rem;
    border-top: 0;
    border-bottom: 1px solid var(--hairline-d);
  }
  #bottombar[data-hidden='true'] { transform: translate3d(0, -120%, 0); }

  .view { padding-bottom: 0; }

  .bar-brand {
    /* Bigger here than on a phone, and sitting on the same line as the
       destinations beside it — a script's own box carries more air above the
       letters than a sans does, so it is nudged down to put the two on one
       optical baseline rather than one box-centre. */
    --brand-h: 1.95rem;
    flex: 0 0 calc(var(--brand-h) * var(--brand-ar, 5.41));
    transform: translateY(var(--brand-nudge, 0));
    transition: flex-basis var(--t-passage) var(--e-passage);
  }
  /* Nothing else changes here: the collapse itself is shared, and only the
     size and the optical nudge are the wide screen's own. */
  /* At a root view the inline destinations do the navigating, so the pill is
     redundant; one level down the same control is the back button and earns
     its place again. */
  #bottombar[data-mode='pill'] .bar-left { display: none; }
  /* ...which is why the language toggle cannot live only in the sheet here. */
  .bar-lang { display: inline-flex; margin-left: auto; }
  #bottombar[data-mode='back'] .bar-brand { display: none; }

  .bar-nav {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin-right: auto;
  }
  .bar-nav-item {
    padding: .5rem .8rem;
    border-radius: 999px;
    font-size: .88rem;
    color: var(--muted-d);
    white-space: nowrap;
    transition: color var(--t-tool) var(--e-tool),
                background var(--t-tool) var(--e-tool);
  }
  .bar-nav-item[aria-current='page'] {
    color: var(--light);
    background: color-mix(in srgb, var(--light) 8%, transparent);
  }

  /* --- content gets a reading width, not the whole monitor --- */
  .section,
  .foot,
  .haus-hero,
  .trust,
  .philosophy > .reveal {
    max-width: var(--content);
    margin-left: auto;
    margin-right: auto;
  }
  .philosophy { padding-left: var(--gutter); padding-right: var(--gutter); }

  /* --- strips stop scrolling sideways and become grids --- */
  /* Scoped to views on purpose. The booking dialog uses the same .strip for
     its artist row, and unscoped this rule blew five 88px avatars out into a
     three-column grid 306px tall — pushing the calendar it was meant to sit
     beside back off the bottom of the sheet. A layout rule written for a
     1180px page does not belong inside a 940px dialog. */
  .view .strip {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 1.25rem;
    overflow: visible;
    margin: 0;
    padding: 0;
  }
  .view .strip > * { scroll-snap-align: none; }
  .view .card-service, .view .card-artist,
  .view .card-post, .view .card-product {
    width: auto;
    max-width: none;
  }
  .view .strip-nav, .view .strip-bar { display: none; }

  .grid-2 { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1.25rem; }
  .grid-3 { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .75rem; }

  /* --- the rings sit centred and larger --- */
  .rings {
    justify-content: center;
    gap: 1.75rem;
    margin: 0;
    padding: .25rem 0 .5rem;
  }
  .ring { width: 128px; }
  .ring-circle { width: 104px; height: 104px; }
  .ring-label { font-size: .82rem; }

  /* --- type at desktop scale --- */
  .d1 { font-size: clamp(4rem, 6vw, 6.5rem); }
  .d2 { font-size: clamp(2.6rem, 3.4vw, 3.6rem); }
  .d3 { font-size: clamp(1.9rem, 2.4vw, 2.6rem); }

  /* --- the hero plays the landscape footage, full bleed --- */
  .hero {
    height: calc(100dvh - var(--bar-h));
    padding: var(--gutter);
    padding-bottom: clamp(3rem, 8vh, 5rem);
  }
  /* margin-left/right, never the margin shorthand: `margin: 0 auto` also
     writes margin-top:0, which silently flattened the whole hero stack on a
     wide screen — the sub and the buttons had no gap at all up here, and the
     spacing you set on the phone appeared to do nothing. Centre horizontally,
     leave the vertical rhythm alone. */
  .hero > *:not(.ph) {
    max-width: var(--content);
    width: 100%;
    margin-left: auto;
    margin-right: auto;
  }
  .hero-sub { max-width: 44ch; font-size: 1.1rem; }

  .view-hero { height: 56dvh; }
  .view-hero .view-hero-title { max-width: var(--content); margin: 0 auto; left: 0; right: 0;
                                padding: 0 var(--gutter); }

  .foot-hours li { font-size: .95rem; }

  /* --- booking becomes a dialog, not a phone glued to the middle --- */
  /* A 480px column of form on a 1440 screen is not intimacy, it is a scroll:
     step 2 alone made you scroll past the artists to reach the calendar and
     past the calendar to reach the times, on a screen with room for all three
     at once. The flow, the steps and the gesture contract are unchanged —
     only the room it is given. */
  :root { --rest-y: -50%; }

  .bk-shell {
    top: 50%;
    width: min(940px, 92vw);
    height: min(720px, 88vh);
    border-radius: 22px;
    overflow: hidden;
    box-shadow: 0 40px 120px rgba(0, 0, 0, .45);
  }
  #booking.is-open::before  { opacity: 1; }
  #booking.is-closing::before { opacity: 0; }

  .bk-head { padding: 1.25rem 2rem .9rem; }
  .bk-step { padding: 1.75rem 2rem 2rem; }
  .bk-foot { padding: 1rem 2rem 1.25rem; }

  /* Two columns wherever the list was only ever one because of the phone. */
  .bk-picks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: .6rem;
  }
  .bk-picks .pick { margin-bottom: 0; }

  /* Step 2 stops being a vertical queue: who, then when, side by side. */
  .bk-when {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2rem;
    align-items: start;
  }
  .bk-when > * { min-width: 0; }
  .slot-group:first-child { margin-top: 0; }

  /* The contact form reads better in two columns than as one long ribbon. */
  .bk-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 1.25rem;
  }
  /* Anything that is not one of the paired fields runs the full width — the
     submit button included, which is the one control in the form that must
     never look like it belongs to the left-hand column. */
  .bk-fields > .form-field.is-wide,
  .bk-fields > *:not(.form-field) { grid-column: 1 / -1; }

  .bk-recap { columns: 2; column-gap: 2rem; }

  /* The dock has a whole margin to sit in now. */
  #dock { top: calc(var(--bar-h) + .75rem); }
}

/* Fine pointers get hover states; touch devices never see them. */
@media (hover: hover) and (pointer: fine) {
  .btn-cta:hover   { filter: brightness(1.06); }
  .btn-ghost:hover { background: color-mix(in srgb, var(--light) 8%, transparent); }
  .chip:hover      { background: color-mix(in srgb, var(--dark) 6%, transparent); }
  .sheet-row:hover { background: color-mix(in srgb, var(--light) 6%, transparent); }
  .slot:not([disabled]):hover { border-color: var(--dark); }
  .strip-nav:hover { background: var(--dark); }
  .grabbing { cursor: grabbing; }
  .strip.is-draggable { cursor: grab; }
}

/* ==========================================================================
   15 · REDUCED MOTION — a complete kill switch
   Decorative motion stops. The app stays entirely usable: every state change
   still happens, it just happens as a short crossfade.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  :root {
    --t-ceremony: 120ms;
    --t-passage:  120ms;
    --t-beat:     100ms;
    --t-tool:     80ms;
  }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  .reveal, .reveal.is-in { opacity: 1; transform: none; transition: none; }
  .story-foot-up { animation: none; }
  #lightlayer { display: none; }
  .morph-ghost { display: none; }
}

/* Print gets the essentials: who, where, when. Nothing else survives. */
@media print {
  #bottombar, #sheet, #story, #booking, #dock, #loader, #surround { display: none !important; }
  .view { position: static; overflow: visible; }
  body { background: #fff; color: #000; }
}

/* A story that has handed off to a deeper view steps aside rather than closing,
   so backing out of that view lands back in the story. visibility, not display:
   the morph still needs to measure the rect it is flying from. */
#story.is-suspended {
  visibility: hidden;
  pointer-events: none;
}

/* Sheets own their vertical drag once there is nothing left to scroll up. */
.sheet-panel {
  touch-action: pan-y;
  overscroll-behavior: contain;
}

/* Reviews advance on a clock, so they get filling progress bars — the one
   place the Instagram timer belongs, because a testimonial is read once while
   a photograph is for lingering over. The bar IS the timer: it is a single
   animation object, so it can never disagree with what happens next. */
.rv-progress {
  display: flex;
  gap: 3px;
  margin-bottom: .75rem;
}
.rv-progress i {
  flex: 1 1 0;
  height: 2.5px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .26);
  overflow: hidden;
}
.rv-progress i > b {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
}
.rv-progress i.is-seen > b { transform: scaleX(1); background: rgba(255, 255, 255, .85); }

/* Review tap zones: elements, so the prev/next split never depends on a
   measurement. Sits above the photograph and below the body, so the quote and
   its button stay clickable. */
.rv-tapzone {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
}
.rv-tap-prev { flex: 0 0 33%; }
.rv-tap-next { flex: 1 1 auto; }
.rv-tapzone button { background: none; }
