/* ============================================================
   One of One Barbershop — Surrey, BC
   Titanium on true black. Corner navigation, slide transitions.
   Fonts: Archivo (200/400/600) + Cormorant Garamond italic (500)
   ============================================================ */

:root {
  /* Pinned brand roles */
  --bg: #000000;
  --titanium: #c4c4c4;
  --accent: #8a8a8a;

  /* Derived roles (cool, titanium-adjacent) */
  --bright: #e6e8eb;
  --text: #c9cbce;
  --text-2: #9da1a7;
  --panel: #0e0f12;
  --panel-2: #131418;
  --line: #26282c;
  --line-soft: #1a1c1f;
  --error: #e2acac;
  /* Hero title fill: titanium at the top, dimming to the accent grey at the baseline. */
  --title-ramp: linear-gradient(to top, var(--accent) 0%, var(--titanium) 100%);

  --font-sans: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-serif: "Cormorant Garamond", Georgia, serif;

  --space-section: clamp(84px, 13vh, 168px);
  --pad-x: clamp(20px, 5vw, 64px);
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);

  color-scheme: dark;
}

/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: 96px;
  /* Off-screen reveal transforms must not widen the page (clip keeps the
     viewport as the scroller, unlike hidden). */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 1.0625rem;
  line-height: 1.7;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
  /* hidden on x quietly turns body into a scroll container on y as well, which
     kills position: sticky anywhere inside it. clip holds the same off-screen
     transforms back without that side effect; browsers that do not know the
     keyword drop the line and keep the hidden above it. */
  overflow-x: hidden;
  overflow-x: clip;
}

::selection { background: var(--titanium); color: #000; }
:focus-visible { outline: 2px solid var(--titanium); outline-offset: 3px; border-radius: 2px; }

html { scrollbar-color: #3a3d42 #000; scrollbar-width: thin; }
body::-webkit-scrollbar { width: 10px; }
body::-webkit-scrollbar-track { background: #000; }
body::-webkit-scrollbar-thumb { background: #3a3d42; border-radius: 8px; border: 2px solid #000; }

input, textarea, select, button { font: inherit; caret-color: var(--titanium); }

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

/* Any class that sets a display value outranks the browser's [hidden] rule, so
   hidden has to win outright or scripts that toggle it quietly do nothing. */
[hidden] { display: none !important; }

p { max-width: 62ch; margin: 0 0 1.2em; }
p:last-child { margin-bottom: 0; }

a {
  color: var(--titanium);
  text-decoration-color: var(--accent);
  text-underline-offset: 0.28em;
  text-decoration-thickness: 1px;
  transition: color 0.2s var(--ease-out), text-decoration-color 0.2s var(--ease-out);
}
a:hover { color: var(--bright); text-decoration-color: var(--bright); }

.skip {
  position: absolute;
  left: 16px; top: -60px;
  z-index: 200;
  background: var(--titanium); color: #000;
  padding: 10px 18px; border-radius: 999px;
  font-weight: 600; text-decoration: none;
  transition: top 0.2s var(--ease-out);
}
.skip:focus-visible { top: 16px; color: #000; }

/* ---------- Type ---------- */
h1, h2, h3 { margin: 0; color: var(--bright); }

.display {
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.025em;
  line-height: 0.98;
}

h1.display { font-size: clamp(2.7rem, 8.6vw, 7.6rem); }

h2 {
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1.05;
  font-size: clamp(1.9rem, 4.6vw, 3.4rem);
  margin: 0 0 0.6em;
}

h3 {
  font-weight: 600;
  font-size: 1.2rem;
  letter-spacing: 0.01em;
  margin: 0 0 0.5em;
}

.serif {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  line-height: 1.4;
  color: var(--titanium);
}

/* Falls back to a solid titanium fill where background-clip: text is unsupported. */
.sheen-text { color: var(--titanium); }
@supports (-webkit-background-clip: text) or (background-clip: text) {
  .sheen-text {
    background-image: var(--title-ramp);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

.lede { font-size: 1.15rem; color: var(--text); }
.muted { color: var(--text-2); }
.num { font-variant-numeric: tabular-nums; }

/* ---------- Layout ---------- */
.wrap { max-width: 1240px; margin-inline: auto; padding-inline: var(--pad-x); }
.wrap-narrow { max-width: 880px; margin-inline: auto; padding-inline: var(--pad-x); }

section { padding-block: var(--space-section); }
section + section { padding-top: 0; }

.rule-top { border-top: 1px solid var(--line-soft); padding-top: var(--space-section); }

[data-mb="book-services"] { padding-bottom: clamp(32px, 5vh, 64px); }

.section-head { margin-bottom: clamp(36px, 6vh, 64px); }
.section-head .serif { display: block; margin-top: 0.9em; }
/* A booking step's supporting line: what the step does, and how the choice
   carries into the rest of the form. Body face rather than the display one
   the heads use, so it reads as instruction and not as a second heading. */
.section-head .step-note {
  display: block;
  margin: 0.9em 0 0;
  max-width: 56ch;
  color: var(--text-2);
}

/* ---------- Corner navigation ---------- */
.corner {
  position: fixed;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--titanium);
}
.corner-tl { top: 22px; left: clamp(18px, 3vw, 40px); }
.corner-tr { top: 22px; right: clamp(18px, 3vw, 40px); }
.corner-bl { bottom: 20px; left: clamp(18px, 3vw, 40px); }
.corner-br { bottom: 20px; right: clamp(18px, 3vw, 40px); }

/* The client's supplied logo, inverted to read light on the black page. */
.brandmark {
  width: auto;
  height: 46px;
  flex: none;
  transition: opacity 0.25s var(--ease-out);
}
.corner-tl { opacity: 0.92; transition: opacity 0.25s var(--ease-out); }
.corner-tl:hover, .corner-tl:focus-visible { opacity: 1; }

.corner-bl, .corner-br {
  padding: 9px 18px;
  line-height: 1.25;
}
.corner-bl::after, .corner-br::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s var(--ease-out), background 0.2s var(--ease-out);
}
.corner-bl:hover::after, .corner-br:hover::after,
.corner-bl:focus-visible::after, .corner-br:focus-visible::after {
  transform: scaleX(1);
  background: var(--bright);
}

.corner .arrow { width: 15px; height: 15px; color: var(--accent); flex: none; transition: transform 0.25s var(--ease-out), color 0.2s var(--ease-out); }
.corner-bl:hover .arrow { transform: translateX(-3px); color: var(--bright); }
.corner-br:hover .arrow { transform: translateX(3px); color: var(--bright); }

/* A corner marked as a jump goes to a section on this page instead of paging
   away, so its chevron turns to point down the page. The hover nudge rides in
   the turned frame, which sends it down rather than sideways. Both hover rules
   have to name their corner: the plain .corner-bl/.corner-br hover above is
   more specific than .pager-jump alone and would straighten the arrow out. */
.pager-jump .arrow { transform: rotate(-90deg); }
.corner-bl.pager-jump:hover .arrow,
.corner-bl.pager-jump:focus-visible .arrow,
.corner-br.pager-jump:hover .arrow,
.corner-br.pager-jump:focus-visible .arrow { transform: rotate(-90deg) translateX(-3px); }

/* ---------- Book now: the circle between the two pagers ----------
   A dim track marks the target at rest and the white ring draws over it,
   clockwise from the bottom. An svg <circle> starts at 3 o'clock, so the ring
   is turned a quarter turn to bring that start point down to 6 o'clock; the
   draw itself is a dashoffset running to zero. A fine pointer draws it on
   hover, a coarse one on tap (script.js adds .is-drawing and holds the
   navigation until the ring closes). With no script the link still books a
   chair, it just goes straight there. */
.corner-bc {
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  text-align: center;
}
.book-dot-ring {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(90deg);
  color: var(--line);
}
.book-dot-draw {
  /* 2πr at r=36, rounded up so offset 0 closes the ring with no seam */
  stroke-dasharray: 227;
  stroke-dashoffset: 227;
  transition: stroke-dashoffset 0.72s var(--ease-out);
}
.book-dot:hover .book-dot-track,
.book-dot:focus-visible .book-dot-track { color: #2f3237; }
.book-dot.is-drawing .book-dot-draw { stroke-dashoffset: 0; transition-duration: 0.42s; }
.book-dot-label {
  position: relative;
  max-width: 4.6em;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 1.25;
  text-transform: uppercase;
  color: var(--titanium);
  transition: color 0.25s var(--ease-out);
}
.book-dot:hover .book-dot-label,
.book-dot:focus-visible .book-dot-label { color: var(--bright); }
/* Tabbing to the button draws it on any device; hover is the trigger only
   where hovering is a thing the pointer can actually do. */
.book-dot:focus-visible .book-dot-draw { stroke-dashoffset: 0; }
@media (hover: hover) and (pointer: fine) {
  .book-dot:hover .book-dot-draw { stroke-dashoffset: 0; }
}

.corner-stack { display: flex; flex-direction: column; }
.corner-role { font-size: 0.7rem; letter-spacing: 0.08em; color: var(--text-2); }
.corner-name { font-weight: 600; font-size: 0.95rem; color: var(--bright); }

/* ---------- Corner menu button: two bars that fold into an x ----------
   Same spring the FAQ plus rides on, so the two icons read as one family.
   Without JS the panel stays shut and the footer nav carries the same links. */
.menu-btn {
  padding: 14px 10px;
  margin: -14px -10px;         /* a 44px hit area without moving the bars */
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--titanium);
}
.menu-bars {
  position: relative;
  display: block;
  width: 26px;
  height: 16px;
}
.menu-bar {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(50% - 0.8px);
  height: 1.6px;
  background: currentColor;
  transition: transform 0.45s cubic-bezier(0.34, 1.45, 0.5, 1);
}
.menu-bar:first-child { transform: translateY(-4.5px); }
.menu-bar:last-child { transform: translateY(4.5px); }
.menu-btn:hover, .menu-btn:focus-visible { color: var(--bright); }
.menu-btn[aria-expanded="true"] .menu-bar:first-child { transform: rotate(45deg); }
.menu-btn[aria-expanded="true"] .menu-bar:last-child { transform: rotate(-45deg); }
/* The editor's saved box for the old "Book now" pill (a 152x63 slab, nudged
   20px right) would stretch the bars and knock this corner out of line with
   the other pages. Higher specificity beats it without touching that file. */
header .corner.menu-btn {
  width: auto !important;
  min-height: 0 !important;
  translate: none !important;
}

/* Open, the menu owns the screen: the page dissolves behind it (see the
   .menu-open rules further down) and what is left is white rules with the
   page names sitting on them — the same 200-weight uppercase as the H2s. */
.menu-panel {
  position: fixed;
  inset: 0;
  z-index: 99;
  display: flex;
  align-items: center;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease-out), visibility 0s linear 0.4s;
}
.menu-panel.is-open {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.menu-nav {
  width: 100%;
  max-width: 1240px;
  margin-inline: auto;
  padding-inline: var(--pad-x);
}
.menu-nav ul { list-style: none; margin: 0; padding: 0; }
.menu-nav li {
  border-top: 1px solid rgba(255, 255, 255, 0.68);
  /* Each row lands on the FAQ spring, one after the next. */
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.3s var(--ease-out),
              transform 0.5s cubic-bezier(0.34, 1.45, 0.5, 1);
}
.menu-nav li:last-child { border-bottom: 1px solid rgba(255, 255, 255, 0.68); }
.menu-panel.is-open li { opacity: 1; transform: none; }
.menu-panel.is-open li:nth-child(1) { transition-delay: 0.06s; }
.menu-panel.is-open li:nth-child(2) { transition-delay: 0.11s; }
.menu-panel.is-open li:nth-child(3) { transition-delay: 0.16s; }
.menu-panel.is-open li:nth-child(4) { transition-delay: 0.21s; }
.menu-panel.is-open li:nth-child(5) { transition-delay: 0.26s; }
.menu-panel.is-open li:nth-child(6) { transition-delay: 0.31s; }
.menu-panel.is-open li:nth-child(7) { transition-delay: 0.36s; }

.menu-nav a {
  display: block;
  padding: clamp(9px, 1.7vh, 20px) 2px;
  color: rgba(255, 255, 255, 0.88);
  text-decoration: none;
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1.05;
  font-size: clamp(1.35rem, 3.6vw, 2.7rem);
  transition: color 0.2s var(--ease-out), transform 0.3s var(--ease-out);
}
.menu-nav a:hover, .menu-nav a:focus-visible { color: #fff; transform: translateX(10px); }
/* The page you are already on stays listed, marked rather than dressed as a link. */
.menu-nav a[aria-current="page"] { color: var(--accent); }
.menu-nav a[aria-current="page"]::before { content: "— "; }

/* Everything but the header — the page itself and the corner pager — fades
   out under the open menu. The logo and the x stay lit on top of it. */
.page, body > nav { transition: opacity 0.4s var(--ease-out); }
html.menu-open .page, html.menu-open body > nav {
  opacity: 0;
  pointer-events: none;
  /* An arrival animation still running (or still filled) would hold the page at
     opacity 1 — animated values beat plain rules — and leave the menu sitting on
     top of readable copy. Ending it here means the fade always wins. */
  animation: none !important;
}
html.menu-open { overflow: hidden; }

/* ---------- Buttons ---------- */
.btn:not(.corner) { position: relative; }
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: none;
  color: var(--titanium);
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 15px 34px;
  border: none;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.2s var(--ease-out), transform 0.15s var(--ease-out);
}
.btn::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s var(--ease-out);
}
.btn:hover, .btn:focus-visible { color: var(--bright); }
.btn:hover::after, .btn:focus-visible::after { transform: scaleX(1); }
.btn:active { transform: translateY(1px); }
.btn:disabled { color: var(--text-2); cursor: not-allowed; }
.btn:disabled::after { transform: scaleX(0) !important; }

.btn-small { padding: 10px 22px; font-size: 0.92rem; }

.btn-ghost { color: var(--titanium); }
.btn-ghost:hover, .btn-ghost:focus-visible { color: var(--bright); }

/* Full-width booking band. Same uppercase weight as a service name, so it
   reads as the last row of the list rather than a widget dropped under it.
   Hover swaps the thin underline every other button gets for a titanium fill
   that wipes across from the left. */
.btn-block {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2vw, 26px);
  padding: clamp(24px, 4vh, 38px) 28px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--bright);
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: clamp(1.5rem, 3.2vw, 2.4rem);
  line-height: 1.05;
  overflow: hidden;
  transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out);
}
.btn-block::after {
  inset: 0;
  height: auto;
  background: var(--titanium);
  transition: transform 0.45s var(--ease-out);
}
.btn-block > * { position: relative; z-index: 1; }
.btn-block:hover, .btn-block:focus-visible { color: var(--bg); border-color: var(--titanium); }
.btn-block:active { transform: none; }
.btn-block-arrow { font-size: 0.7em; transition: transform 0.3s var(--ease-out); }
.btn-block:hover .btn-block-arrow, .btn-block:focus-visible .btn-block-arrow,
.btn-box:hover .btn-block-arrow, .btn-box:focus-visible .btn-block-arrow { transform: translateX(8px); }

/* The booking band shrunk to an inline control: same border, same panel, same
   titanium wipe from the left. Every measurement is in em, so the box grows
   and shrinks with whatever font size the button inherits. Weight sits at 300
   rather than the band's 200 — extra-light loses its strokes at 1rem. */
.btn-box {
  padding: 0.85em 1.9em;
  gap: 0.6em;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--bright);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  overflow: hidden;
  transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out);
}
.btn-box::after {
  inset: 0;
  height: auto;
  background: var(--titanium);
  transition: transform 0.45s var(--ease-out);
}
.btn-box > * { position: relative; z-index: 1; }
.btn-box:hover, .btn-box:focus-visible { color: var(--bg); border-color: var(--titanium); }
.btn-box:active { transform: none; }
/* A phone gives a tracked-out uppercase label about 300px to work with, and the
   longer ones ("book any available appointment") wrap to two ragged lines at
   full size. Tightening the type and the side padding keeps every one of these
   boxes a single line the width of a thumb. */
@media (max-width: 767px) {
  .btn-box { padding: 0.9em 1.15em; font-size: 0.8rem; }
}

.cta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 26px; margin-top: 34px; }
/* A row holding a single closing button — the privacy page's way back — sits
   centred under its paragraph rather than ranged left with the text. */
.cta-row-center { justify-content: center; }

/* One size up from the standard box button. Every measurement inside .btn-box
   is in em, so lifting the font size grows the padding and the gap with it. */
.btn-lg { font-size: 1.15rem; }
@media (max-width: 767px) {
  .btn-lg { font-size: 0.95rem; }
}
/* An arrow pointing back travels the other way on hover. */
.btn-block-arrow-back { transform: none; }
.btn-box:hover .btn-block-arrow-back, .btn-box:focus-visible .btn-block-arrow-back,
.btn-block:hover .btn-block-arrow-back, .btn-block:focus-visible .btn-block-arrow-back { transform: translateX(-8px); }

/* The line under a greyed-out button, naming what the form is still waiting on.
   It takes the full row so it always breaks below the button rather than
   sitting beside it; on wider screens it drops under the whole row instead. */
.cta-note {
  flex: 0 0 100%;
  margin: -6px 0 0;
  max-width: 52ch;
  color: var(--text-2);
  font-size: 0.92rem;
  line-height: 1.5;
}
.cta-note[hidden] { display: none; }
/* Wide enough for the button and the phone link to share a line: the note goes
   last so it sits under both rather than splitting them. */
@media (min-width: 768px) {
  .cta-note { order: 3; margin-top: 0; }
}

/* An empty status line should not hold a gap open under the button. */
.form-status:empty { margin-top: 0; }

.trust-note {
  display: block;
  margin-top: 26px;
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 500;
  font-size: 1.15rem;
  color: var(--text-2);
  max-width: 46ch;
}
.trust-note cite { font-style: normal; font-family: var(--font-sans); font-size: 0.85rem; color: var(--accent); display: block; margin-top: 6px; }

/* ---------- Hero ---------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: end;
  padding: 0 0 clamp(96px, 14vh, 150px);
  overflow: hidden;
  /* The backdrop below is position: fixed, so it answers the viewport and
     overflow: hidden cannot reach it — a fixed box is not in this element's
     overflow. clip-path is: it cuts every descendant whatever its positioning,
     and unlike a transform or contain: paint it does not make this element the
     containing block for fixed children, which would pin the backdrop back
     onto the hero and cancel the hold outright. */
  clip-path: inset(0);
  /* The hero owns its own stacking context, so the z-index:-1 backdrop below
     resolves against this section rather than the document root. Without it
     the backdrop sits behind body's background box and any opaque body colour
     hides the photo outright. */
  isolation: isolate;
}
.hero-media {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  /* Black and white, dimmed, sitting low behind the logo. The strength lives
     here so the editor and admin can tune it in one place; the img below
     reads it. */
  --hero-base: 0.5;
}
/* The photo holds still on screen while the hero scrolls over it, and the
   browser does that itself. Moving a layer down by exactly the distance the
   page moved up is the definition of position: fixed, so the viewport holds it
   on the thread that owns the scroll and there is nothing left to fall behind:
   no scroll handler writing a transform a frame late, no keyframe to keep in
   step. .hero clips it, so it still ends on the hero's bottom edge.

   Everything a scroll handler used to chase went with it. A phone scrolls on
   the compositor while script waits its turn on the main thread, which is why
   the photo stepped against a page that was gliding; the same lag put the
   relit ceiling on a different step again and crossed the two sets of beams.
   Both layers are fixed to the same box now, so they cannot come apart.

   The wash and the dimming sit on the photo rather than on the box around it:
   a filter or a part-opacity on .hero-media turns that parent into an
   offscreen surface the compositor re-blends at full screen size. The picture
   is identical either way — the gradient over it is black over a black page. */
.hero-media img,
.hero-media video {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  /* The large viewport, deliberately. A phone's address bar sliding away
     shortens the small viewport the hero is sized in, and a photo sized to
     that would re-crop itself mid-scroll; sized to the large one it never
     changes. The hero is 100svh, never taller, so the clip edge always lands
     on photo rather than past the bottom of it. */
  height: 100vh;
  object-fit: cover;
  opacity: var(--hero-base);
  filter: grayscale(1) brightness(0.7) contrast(1.06);
}
/* The reel plays over the still at the same strength and wash, so the swap is
   only the room starting to move. Hidden until it is actually playing; the
   still goes after the fade so the two never show through each other. If the
   reel never starts (reduced motion, data saver, Low Power Mode) the still
   simply stays. */
.hero-video {
  opacity: 0;
  transition: opacity 0.8s ease;
}
.hero-media.is-playing .hero-video { opacity: var(--hero-base); }
.hero-media.is-playing img { visibility: hidden; transition: visibility 0s 0.8s; }
.hero-media::after {
  content: "";
  position: absolute; inset: 0;
  z-index: 2;
  background: linear-gradient(180deg, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.25) 40%, rgba(0,0,0,0.9) 88%, #000 100%);
}

/* The shop photo fades up once on load. The box fades the photo and its
   gradient in together, then sits at full opacity and costs nothing; the
   photo is then held against the viewport at its own strength for the rest
   of the hero: the logo is what leaves, the room stays.
   Filled backwards only: the end of the fade is the box's own opacity, so
   there is nothing to hold, and a forwards fill left an opacity effect on the
   box for good, which kept it an offscreen surface the compositor re-blended
   on every scroll frame. */
html.js .hero-media { animation: hero-photo-in 1.4s ease backwards; }
@keyframes hero-photo-in { from { opacity: 0; } }
.hero .serif { display: block; margin-top: 1.1em; max-width: 34ch; }

/* ---------- Hero ceiling lights (phone and tablet) ---------- */
/* The shop photo's ceiling carries five strip lights that all run back to one
   vanishing point behind the room. This layer relights them on load: it is the
   same photo again, crushed so only the strips survive, added back over the
   original with screen blending. A circular front grows out of that vanishing
   point (script.js sets --vpx/--vpy and drives --lr), and because every strip
   is a straight ray from that point, the front runs along all five at once and
   dead straight, wall to ceiling edge — no beam is drawn or tweened by hand.

   It sits above .hero-media rather than inside it, so the media's own dimming
   (opacity .5, brightness .7) and its dark top gradient do not swallow the
   glow. It is fixed to the same box as the photo, so the two hold the viewport
   as one: an overlay that answered the scroll even a frame later than the
   photo slid off the strips it was lighting.

   The work is split over two boxes because each can carry only one mask. The
   outer one fades the glow out along the wall, so the beams dissolve into the
   photo instead of ending on a cut line; the inner one carries the photo, the
   crush and the travelling front. Desktop never builds it: script.js only
   paints here below the desktop breakpoint, and only on the known photo. */
.hero-lights {
  /* Same box as the photo it relights, down to the 100vh (see .hero-media img
     above), so script.js can measure the geometry once off the photo and have
     it land on this layer's pixels too. */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  /* Only in the tree while it is lit. A masked, blended, full-screen layer is
     paid for on every scroll frame whether or not it has anything to show,
     and this one is dark for all but four seconds after load. script.js puts
     is-lit on for the intro and takes it off when the room has settled. */
  display: none;
  mix-blend-mode: screen;
  /* The soft floor. Everything above --lsolid is untouched; between there and
     --lfade the glow thins to nothing, which lands just past the end of the
     strips and hides the join. */
  -webkit-mask-image: linear-gradient(180deg, #000 0, #000 var(--lsolid, 60%), transparent var(--lfade, 70%));
  mask-image: linear-gradient(180deg, #000 0, #000 var(--lsolid, 60%), transparent var(--lfade, 70%));
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.hero-lights.is-lit { display: block; }
.hero-lights::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--lphoto, none);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  /* No filter. The crush, bloom and lift that used to run here per frame are
     baked into the image itself (tools/build-ceiling-glow.py), which is white
     with the glow in its alpha. Asking a phone to grayscale, crush at
     contrast(34), blur and lift a full-size photo under a blend mode is enough
     work that iOS rasterises the layer at a fraction of its size, and that is
     what made the beams blurry and blotchy on a real handset while a desktop
     GPU kept up. It is one ordinary image draw now. */
  /* The travelling front. Its solid core reaches 80% of --lr, and script.js
     sizes the opening radius against that same 80%, so the light is at full
     strength the moment it touches the end of the strips rather than crawling
     up to them through the soft part of the gradient. */
  -webkit-mask-image: radial-gradient(circle var(--lr, 0px) at var(--vpx, 50%) var(--vpy, 50%),
    #000 80%, rgba(0, 0, 0, 0.5) 93%, transparent 100%);
  mask-image: radial-gradient(circle var(--lr, 0px) at var(--vpx, 50%) var(--vpy, 50%),
    #000 80%, rgba(0, 0, 0, 0.5) 93%, transparent 100%);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.hero-inner { position: relative; }

/* Sub-page hero: shorter, type-led */
.hero-sub {
  padding-top: clamp(150px, 22vh, 240px);
  padding-bottom: clamp(48px, 8vh, 96px);
}
.hero-sub .serif { display: block; margin-top: 1em; max-width: 40ch; }

/* ---------- Service rows ---------- */
.service-list { border-top: 1px solid var(--line); list-style: none; margin: 0; padding: 0; }
.service-row {
  display: grid;
  grid-template-columns: 120px 1fr auto;
  gap: 12px 32px;
  align-items: baseline;
  border-bottom: 1px solid var(--line);
  padding: clamp(22px, 3.5vh, 34px) 0;
}
.service-time {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 500;
  font-size: 1.2rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.service-name {
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: 0.025em;
  line-height: 1.05;
  font-size: clamp(1.5rem, 3.2vw, 2.4rem);
  color: var(--bright);
  margin: 0;
}
.service-desc { grid-column: 2; margin: 0; color: var(--text-2); max-width: 56ch; }
.service-price { grid-column: 1; font-size: 0.85rem; color: var(--text-2); }
.service-book {
  font-weight: 600;
  font-size: 0.95rem;
  white-space: nowrap;
  align-self: center;
}

/* Home page: the book link reads as its own right-hand column — centred in
   that column and against the full height of the row, so it sits opposite the
   name and the description instead of floating up beside the name alone. */
.service-list[data-mb="home-services-list"] .service-row {
  grid-template-columns: 120px 1fr 180px;
}
.service-list[data-mb="home-services-list"] .service-book {
  grid-column: 3;
  grid-row: 1 / span 2;
  align-self: center;
  justify-self: stretch;
  text-align: center;
  font-weight: 700;
  font-size: 1.02rem;
}

/* The slot reel on those book links — script.js builds the spans and drives
   the roll. One window per word, one line tall, with the second face parked
   directly underneath it.

   The line under the label is drawn here rather than left to the browser's
   own underline. A browser that paints a link's underline inside each word's
   box hands every word its own piece of the line, and the piece then rides up
   with the word — the line splits and springs back instead of holding still.
   One element, outside the clipped window, cannot be split or carried. */
.service-book.is-reel { text-decoration: none; }
.service-book .reel {
  /* Where the line sits, down from the top of the line box: half the leading
     ((1.7 - 1.042) / 2), plus Archivo's ascent (0.858em), plus the
     text-underline-offset the rest of the site's links use (0.28em). The
     Helvetica and Arial fallbacks land within a fifth of a pixel of it. */
  --pay: 1.467em;
  display: inline-block;
  position: relative;
}
/* The reel window. Cut off at the top edge of the line, so a word rises out
   from behind it rather than across it — and at rest the cut still clears the
   deepest descender by about a tenth of an em.
   Clipped with clip-path rather than overflow, which would align the box's
   bottom edge to the baseline and lift the whole label off its line. */
.service-book .reel-words {
  display: inline-block;
  /* Only the bottom edge does any work; the sides are held open so a glyph
     that leans past its own advance width is not shaved. */
  clip-path: inset(0 -0.2em calc(100% - var(--pay)) -0.2em);
}
.service-book .reel-word {
  display: inline-block;
  position: relative;
}
.service-book .reel-face {
  position: absolute;
  left: 0;
  top: 100%;
}
.service-book .reel-face::before { content: attr(data-word); }
/* The payline: still, whole, and in front of the words. */
.service-book .reel::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: var(--pay);
  height: 1px;
  background: var(--accent);
  transition: background-color 0.2s var(--ease-out);
}
.service-book:hover .reel::after,
.service-book:focus-visible .reel::after {
  background: var(--bright);
}

/* Services page: the menu column. One column at every width.
   On desktop it keeps the measure it had when the jump rail sat beside it —
   the page minus that old 240px column and its gap — but sits centred on the
   page rather than indented to one side. Tablet and phone run full width. */
.services-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(32px, 5vw, 80px);
  align-items: start;
}

@media (min-width: 1024px) {
  .services-layout {
    grid-template-columns: calc(100% - 240px - clamp(32px, 5vw, 80px));
    justify-content: center;
  }
}
.service-group { padding-block: 0 clamp(40px, 7vh, 72px); }
.service-group h2 { font-size: clamp(1.6rem, 3vw, 2.2rem); }

/* ---------- Privacy policy: the clause column ----------
   The page borrows the services layout's sticky rail, so only the reading
   column needs rules of its own. A privacy policy is read straight through
   rather than scanned, so the clauses run at a legal-document measure with
   more air between clauses than inside one, and the headings stay small
   enough that nine of them in a row do not shout. */
/* The collection notice under the booking fields: quiet, but not so quiet it
   reads as decoration, since it is the point-of-collection disclosure. */
.form-privacy {
  margin-top: clamp(20px, 3vh, 30px);
  font-size: 0.92rem;
  max-width: 62ch;
}

.legal-doc { max-width: 68ch; }
.legal-clause { scroll-margin-top: 110px; }
.legal-clause + .legal-clause { margin-top: clamp(40px, 6vh, 64px); }
.legal-clause h2 {
  font-size: clamp(1.35rem, 2.5vw, 1.95rem);
  margin-bottom: 0.65em;
}
.legal-clause p { max-width: 68ch; }
.legal-doc cite { font-style: italic; color: var(--titanium); }

.legal-list {
  list-style: none;
  margin: 0 0 1.2em;
  padding: 0;
  display: grid;
  gap: 12px;
  max-width: 68ch;
}
.legal-list:last-child { margin-bottom: 0; }
/* A drawn rule rather than a bullet glyph, aligned to the first line's
   x-height so it reads as part of the column's ruling. */
.legal-list li { position: relative; padding-left: 24px; }
.legal-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.78em;
  width: 10px;
  height: 1px;
  background: var(--accent);
}
.legal-list strong { color: var(--bright); font-weight: 600; }

/* ---------- Photo strip ----------
   The track scrolls on its own with the browser's scrollbar. Once script.js
   takes over it sets .has-rail on the wrapper, which swaps that scrollbar for
   the titanium rail below: --grab lifts it while a pointer holds it, --v
   carries scroll speed so the thumb stretches the way the track is running. */
.strip-scroller {
  --grab: 0;
  --v: 0;
  position: relative;
  margin-top: clamp(24px, 4vh, 40px);
}

.strip {
  display: flex;
  gap: clamp(14px, 2vw, 24px);
  overflow-x: auto;
  padding-bottom: 18px;
  scroll-snap-type: x mandatory;
  scrollbar-color: #3a3d42 transparent;
  scrollbar-width: thin;
}
.strip::-webkit-scrollbar { height: 8px; }
.strip::-webkit-scrollbar-thumb { background: #3a3d42; border-radius: 8px; }
/* The strip runs on one height and lets each tile take the width its picture
   needs, so the row keeps a straight top and bottom edge whether the photo is
   portrait, landscape or square. */
.strip { --strip-h: clamp(300px, 34vw, 450px); }
.strip figure {
  flex: 0 0 auto;
  margin: 0;
  scroll-snap-align: start;
}
.strip-frame {
  position: relative;
  overflow: hidden;
  height: var(--strip-h);
  /* Height over width. A landscape photograph takes a tile half as wide again
     as it is tall and stops there; a portrait stops at 9:16. Past either end
     the tile crops rather than stretching the row. Square is the fallback
     because a tile can crop a picture taller than itself, but nothing fills
     one shorter. */
  width: calc(var(--strip-h) / clamp(0.6667, var(--shot-aspect, var(--ig-aspect, 1)), 1.7778));
  border-radius: 6px;
  border: 1px solid var(--line-soft);
  background: #0a0a0a;
}
.ig-still,
.shot-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Instagram's embed puts the media 54px down its own page, full width, and
   never taller than 4:5 — so a frame this wide renders the picture exactly
   --strip-h tall, and pulling it up by that 54px leaves the picture alone in
   the tile. Anything past the tile (the button bar, the like count, the black
   a reel is letterboxed on) is clipped. */
.ig-live {
  position: absolute;
  left: 50%;
  top: -54px;
  width: max(calc(var(--strip-h) / min(var(--ig-aspect, 1), 1.25)), 100%);
  height: calc(var(--strip-h) + 260px);
  transform: translateX(-50%);
  border: 0;
  /* Every click and tab stop belongs to .ig-hit over the top, and a drag has
     to reach the rail underneath. */
  pointer-events: none;
  color-scheme: light;
}
/* Instagram paints its own white page before the post lands, so the frame
   stays hidden behind the still until script.js has seen it load. A frame
   script.js never started — no JavaScript, or nobody scrolled this far — has
   no address yet and takes no room at all. */
.ig-live:not([src]) { display: none; }
.ig-live { opacity: 0; transition: opacity 500ms var(--ease-out); }
.ig-live.is-live { opacity: 1; }
/* The whole tile is the link out to the post. */
.ig-hit {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: block;
}
.ig-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Enhanced state: the browser scrollbar goes, the rail arrives. */
.has-rail .strip {
  padding-bottom: 0;
  scrollbar-width: none;
  cursor: grab;
  overscroll-behavior-x: contain;
  /* Mandatory snapping overrules the rail: Chrome drags a scrollTo back to the
     snapped frame, so the last photos are unreachable from the bar. Proximity
     still tidies a swipe up to the nearest frame. */
  scroll-snap-type: x proximity;
}
.has-rail .strip::-webkit-scrollbar { display: none; }
.has-rail.is-grabbed .strip { cursor: grabbing; }
/* Dragging and its momentum have to run without snap points yanking back. */
.has-rail.is-free .strip { scroll-snap-type: none; }
/* The lift on grab rides the frame, not the photo: the photo's own transform
   belongs to the scroll pan and must not carry a transition. */
.has-rail .strip-frame {
  transition: filter 420ms var(--ease-out), transform 420ms var(--ease-out);
  filter: brightness(calc(0.92 + var(--grab) * 0.12)) contrast(calc(1 + var(--grab) * 0.04));
  transform: scale(calc(1 + var(--grab) * 0.008));
}
.has-rail .strip figure { user-select: none; -webkit-user-select: none; }
.has-rail .strip img { -webkit-user-drag: none; user-drag: none; }

.strip-rail {
  display: none;
  position: relative;
  height: 30px;
  margin-top: clamp(18px, 2.4vh, 28px);
  cursor: pointer;
  touch-action: none;
}
.has-rail .strip-rail { display: block; }
/* Hairline the thumb rides along. */
.strip-rail::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--line);
  transform: translateY(-50%) scaleY(calc(1 + var(--grab)));
  transition: background 300ms var(--ease-out), transform 300ms var(--ease-out);
}
.strip-rail:hover::before, .is-grabbed .strip-rail::before { background: #2f3237; }

.strip-rail-thumb {
  position: absolute;
  top: 50%;
  left: 0;
  width: 80px;
  height: 3px;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--accent), var(--titanium) 50%, var(--accent));
  transform: translate3d(var(--thumb-x, 0px), -50%, 0) scaleX(calc(1 + var(--v) * 0.06)) scaleY(calc(1 + var(--grab) * 1.6));
  transform-origin: center;
  transition: background 300ms var(--ease-out), box-shadow 300ms var(--ease-out);
  will-change: transform;
}
.strip-rail:hover .strip-rail-thumb,
.is-grabbed .strip-rail-thumb {
  background: linear-gradient(90deg, var(--titanium), var(--bright) 50%, var(--titanium));
  box-shadow: 0 0 18px rgba(196, 196, 196, 0.32);
}
/* End caps: a tick at each end so the run has visible limits. */
.strip-rail::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 9px;
  transform: translateY(-50%);
  background:
    linear-gradient(var(--line), var(--line)) left center / 1px 100% no-repeat,
    linear-gradient(var(--line), var(--line)) right center / 1px 100% no-repeat;
  opacity: calc(0.5 + var(--grab) * 0.5);
  transition: opacity 300ms var(--ease-out);
  pointer-events: none;
}

/* ---------- Cards-free grids ---------- */
.portrait-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(16px, 2.5vw, 32px);
  margin-top: clamp(32px, 5vh, 56px);
}
.portrait-grid figure { margin: 0; }
.portrait-grid img { border-radius: 6px; border: 1px solid var(--line-soft); }
.portrait-grid figcaption { margin-top: 12px; color: var(--text-2); font-size: 0.92rem; }
.portrait-grid figcaption strong { color: var(--bright); font-weight: 600; display: block; }

/* ---------- Barber rail ----------
   A hover-steered roster that repeats forever. How far the pointer sits from
   the middle sets the speed, and a card parked under it offers its booking
   link. Script.js clones the roster and adds .is-hot / .at-start / .at-end —
   the rail scrolls and reads fine with none of that, and holds still under
   prefers-reduced-motion. */
.rail-hint { font-size: 0.9rem; margin-top: -0.5em; }
/* The instruction has to match the input the visitor actually has. */
.hint-touch { display: none; }

.barber-rail {
  --barber-card-w: clamp(192px, 20.5vw, 270px);
  /* The card frame reads against black on a desk and disappears on a phone held
     outdoors, so the roster draws its own edge rather than the site hairline. */
  --barber-edge: rgba(196, 196, 196, 0.26);
  position: relative;
  margin-top: clamp(28px, 4.5vh, 48px);
  /* Span the wrap's full border-box so the track can run to the page edge. */
  margin-inline: calc(var(--pad-x) * -1);
}

.barber-track {
  display: flex;
  gap: clamp(11px, 1.5vw, 22px);
  padding: 4px var(--pad-x);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.barber-track::-webkit-scrollbar { display: none; }

.barber-card {
  position: relative;
  flex: 0 0 var(--barber-card-w);
  text-decoration: none;
  color: inherit;
  scroll-snap-align: center;
}
.barber-card:hover { color: inherit; }

/* The card is a div so the caption can hold its own Instagram link — two
   anchors cannot nest. This one wraps the photo and then stretches over the
   whole card, so the name and the role still book the chair; the Instagram
   icon sits above it on z-index and keeps its own click. */
.barber-hit {
  display: block;
  text-decoration: none;
  color: inherit;
}
.barber-hit::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.barber-shot {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 6px;
  border: 1px solid var(--barber-edge);
  background: var(--panel);
}
.barber-shot img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  /* Studio whites cooled into the titanium range until the card is picked out. */
  filter: grayscale(0.6) brightness(0.66) contrast(1.06);
  /* Quick enough that a face sliding past the pointer still registers. */
  transition: filter 0.28s var(--ease-out), transform 0.4s var(--ease-out);
}
/* Lift the hovered card clear of the edge fades, so a face running along the
   side reads at full strength instead of through the black. */
.barber-card.is-hot { position: relative; z-index: 2; }
.barber-card.is-hot .barber-shot { border-color: var(--accent); }
.barber-card.is-hot .barber-shot img {
  filter: grayscale(0) brightness(0.94) contrast(1);
  transform: scale(1.04);
}

.barber-book {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 32px 15px 14px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.94), rgba(0, 0, 0, 0.6) 55%, rgba(0, 0, 0, 0));
  color: var(--bright);
  font-size: 0.885rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.18s var(--ease-out), transform 0.28s var(--ease-out);
}
.barber-card.is-hot .barber-book { opacity: 1; transform: none; }
.barber-card:focus-within .barber-book { opacity: 1; transform: none; }

.barber-cap {
  position: relative;
  display: block;
  /* room on the right for the Instagram icon */
  padding-right: 32px;
  margin-top: 13px;
  /* The role sits under a name at --bright; at --text-2 it dropped out of the
     card on a phone screen, so it carries the body colour instead. */
  color: var(--text);
  font-size: 0.972rem;
  line-height: 1.5;
}
.barber-cap strong {
  display: block;
  color: var(--bright);
  font-weight: 600;
  letter-spacing: 0.01em;
}
.barber-card.is-hot .barber-cap strong { color: var(--titanium); }

/* Sits against the right edge of the caption, level with the name and the
   role, and rides above the card's booking link so it opens Instagram. */
.barber-ig {
  position: absolute;
  right: 0;
  top: 50%;
  z-index: 2;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin-right: -5px;
  border-radius: 999px;
  /* The glyph carries Instagram's own gradient, painted on its strokes by the
     linearGradient each icon holds — so this one does not inherit a colour. */
  transition: background-color 0.2s var(--ease-out), filter 0.2s var(--ease-out);
}
.barber-ig:hover,
.barber-ig:focus-visible {
  background: rgba(255, 255, 255, 0.07);
  filter: brightness(1.18) saturate(1.1);
}
/* The width/height attributes on the glyph hold the no-CSS size; the rule
   scales it with the rest of the caption. */
.barber-ig svg { display: block; width: 18px; height: 18px; }

/* Edge fades. Decoration only: hovering an edge is what moves the roster, so
   these must never swallow a click meant for the card running underneath. */
.barber-rail::before,
.barber-rail::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 1;
  width: clamp(40px, 7%, 104px);
  pointer-events: none;
  transition: opacity 0.4s var(--ease-out);
}
.barber-rail::before { left: 0; background: linear-gradient(to right, var(--bg) 10%, rgba(0, 0, 0, 0)); }
.barber-rail::after { right: 0; background: linear-gradient(to left, var(--bg) 10%, rgba(0, 0, 0, 0)); }
/* Nothing more that way, so the fade lifts rather than implying a card is cut off. */
.barber-rail.at-start::before,
.barber-rail.at-end::after { opacity: 0; }

/* Phone: one big card held in the middle with its neighbours peeking in. The
   side padding is what lets the first and last card reach the centre, and
   without it a centred snap drags card one off the left edge at rest. */
@media (max-width: 767px) {
  .barber-rail {
    --barber-card-w: min(71.3vw, 302px);
    /* Held at arm's length in daylight a phone loses the faintest greys first,
       so the frame carries a little more weight here than on a desk. */
    --barber-edge: rgba(196, 196, 196, 0.36);
  }
  .barber-track { padding-inline: max(var(--pad-x), calc(50% - var(--barber-card-w) / 2)); }
  .barber-book { font-size: 0.929rem; padding: 37px 17px 16px; }
  .barber-cap { font-size: 1.026rem; }
}

/* No hover to scrub with: swipe the track, and the centred card is the live one.
   Snapping only ever turns on here, because on a pointer it would fight the
   hover scrub for control of the same scroll position. */
@media (hover: none), (pointer: coarse) {
  .barber-track { scroll-snap-type: x proximity; }
  .barber-shot img { filter: grayscale(0.45) brightness(0.78); }
  .hint-hover { display: none; }
  .hint-touch { display: inline; }
}
/* The switch laid over a barber's photo. A tap on it plays the Taptic Engine
   and then hands the tap to the card's own link; a drag passes straight
   through to the track, which is what touch-action buys. Sized off the photo's
   own 3:4 frame rather than measured, so it follows the card at every width
   and never reaches the caption or the Instagram link under it. */
.card-haptic {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  aspect-ratio: 3 / 4;
  margin: 0;
  opacity: 0;
  z-index: 3;
  touch-action: manipulation;
}

/* The switch that gives an iPhone its detent tick. Script builds it, script
   flips it, and Safari answers with the haptic; it has to stay in the viewport
   and keep its native look for that, so it hides on opacity alone. A finger
   never reaches it and a screen reader never meets it. */
.haptic-tick {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

/* On a phone every swipe should land on a face, not between two. */
@media (hover: none) and (max-width: 767px), (pointer: coarse) and (max-width: 767px) {
  .barber-track { scroll-snap-type: x mandatory; }
}

/* Names the link for a screen reader without repeating anything on screen —
   the tile's picture carries the description, this carries the destination. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Each tile carries the platform glyph, so it reads as a post rather than as a
   picture of one. */
.ig-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  color: #fff;
  opacity: 0.7;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.6));
  transition: opacity 0.25s var(--ease-out), transform 0.25s var(--ease-out);
}
.ig-badge svg { width: 100%; height: 100%; }
.ig-hit:hover .ig-badge, .ig-hit:focus-visible .ig-badge { opacity: 1; transform: scale(1.08); }

@media (prefers-reduced-motion: reduce) {
  .ig-badge { transition: none; }
  .ig-hit:hover .ig-badge, .ig-hit:focus-visible .ig-badge { transform: none; }
  .ig-live { transition: none; }
}

/* ---------- Quotes ---------- */
blockquote { margin: 0; }
.quote {
  border-top: 1px solid var(--line);
  padding: clamp(36px, 6vh, 64px) 0;
}
.quote p {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(1.5rem, 3vw, 2.2rem);
  line-height: 1.35;
  color: var(--titanium);
  max-width: 26em;
  margin: 0 0 0.7em;
}
.quote--feature p { font-size: clamp(1.9rem, 4vw, 3rem); }
.quote--right { display: flex; flex-direction: column; align-items: flex-end; text-align: right; }
.quote cite {
  font-style: normal;
  font-size: 0.92rem;
  color: var(--text-2);
  letter-spacing: 0.02em;
}
.quote cite::before { content: ""; display: inline-block; width: 28px; height: 1px; background: var(--accent); vertical-align: middle; margin-right: 12px; }
/* A quote lifted off the shop's Google profile: Google's G, in its own
   colours, after the name. The one spot of colour on the page is the point. */
.g-mark { display: inline-block; width: 16px; height: 16px; margin-left: 10px; vertical-align: -0.2em; line-height: 0; }
.g-mark svg { display: block; width: 100%; height: 100%; }

/* ---------- FAQ ---------- */
.faq { border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 22px 4px;
  font-weight: 600;
  font-size: 1.08rem;
  color: var(--bright);
  transition: color 0.2s var(--ease-out);
}
.faq summary:hover { color: #fff; }
.faq summary::-webkit-details-marker { display: none; }
/* Plus that rotates into an x: two bars drawn as gradients, spun 135deg. */
.faq summary::after {
  content: "";
  flex: none;
  width: 16px; height: 16px;
  margin-right: 4px;
  background:
    linear-gradient(var(--accent), var(--accent)) center / 100% 1.6px no-repeat,
    linear-gradient(var(--accent), var(--accent)) center / 1.6px 100% no-repeat;
  transform: rotate(0deg);
  transition: transform 0.45s cubic-bezier(0.34, 1.45, 0.5, 1);
}
.faq details[open] summary::after { transform: rotate(135deg); }
/* While a panel animates shut it is still [open], so the x has to follow the
   click rather than the attribute. Same specificity, declared later, so it
   wins; without JS nothing sets it and [open] alone drives the icon. */
.faq details.is-shut summary::after { transform: rotate(0deg); }
.faq .faq-a { padding: 0 4px 26px; color: var(--text-2); max-width: 68ch; overflow: hidden; }
.faq .faq-a p { margin-bottom: 0.8em; }

/* ---------- Split media sections ---------- */
.split {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(32px, 5vw, 80px);
  align-items: center;
}
.split img { border-radius: 6px; border: 1px solid var(--line-soft); }

/* ---------- The room, photo by photo ----------
   One photo today; the shop files more under Room in admin.html and each one
   lands here as another slide. The first slide stays in the flow and gives the
   frame its height — including the height the phone view sets on that picture
   — and the rest lie over it at inset 0, so every photo after it shows in the
   same box whatever shape it arrives in. Nothing here hides the first photo:
   with no JavaScript the slides stand one under the other and the chevrons,
   which would have nothing to drive, never appear. */
/* The margin is the one a bare <figure> carried here before the carousel, kept
   so the picture keeps the size and the inset it has always had in all three
   views. */
.room-carousel { position: relative; margin: 1em 40px; }
.room-frame {
  position: relative;
  overflow: hidden;
  border-radius: 6px;
  border: 1px solid var(--line-soft);
}
.room-slide {
  transform: translateX(calc((var(--i, 0) - var(--active, 0)) * 100%));
  transition: transform 0.62s var(--ease-out);
}
.room-slide + .room-slide { position: absolute; inset: 0; }
html:not(.js) .room-slide + .room-slide { position: static; transform: none; margin-top: 14px; }
.split .room-slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
  border-radius: 0;
  user-select: none;
  -webkit-user-select: none;
}

/* A thin titanium button at each edge of the frame: a hairline box in the
   brand grey over a dark backing, with the chevron drawn thin inside it. The
   backing is what keeps the stroke readable over a bright photograph. */
.room-nav {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  display: none;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 78px;
  padding: 0;
  border: 1px solid rgba(196, 196, 196, 0.5);
  border-radius: 2px;
  background: rgba(0, 0, 0, 0.55);
  color: var(--titanium);
  cursor: pointer;
  transition: color 0.25s var(--ease-out), border-color 0.25s var(--ease-out),
    background-color 0.25s var(--ease-out);
}
.js .room-nav { display: flex; }
.room-nav--prev { left: 14px; }
.room-nav--next { right: 14px; }
.room-nav svg {
  width: 22px;
  height: 22px;
  transition: transform 0.25s var(--ease-out);
}
.room-nav:hover {
  color: var(--bright);
  border-color: var(--titanium);
  background: rgba(0, 0, 0, 0.78);
}
.room-nav--prev:hover svg { transform: translateX(-2px); }
.room-nav--next:hover svg { transform: translateX(2px); }
.room-nav:active svg { transform: scale(0.92); }
.room-nav:focus-visible { outline: 1px solid var(--titanium); outline-offset: 3px; }

/* Pressing at the end of a single-photo reel. The picture leans the way the
   press asked and comes back, so the button answers instead of sitting dead.
   `translate` is its own property here, so the paging transform above is left
   alone and the two never fight. */
.room-frame.is-bump-prev .room-slide { animation: room-bump-prev 0.36s var(--ease-out); }
.room-frame.is-bump-next .room-slide { animation: room-bump-next 0.36s var(--ease-out); }
@keyframes room-bump-prev {
  40% { translate: 14px 0; }
  to { translate: 0 0; }
}
@keyframes room-bump-next {
  40% { translate: -14px 0; }
  to { translate: 0 0; }
}

@media (max-width: 767px) {
  .room-nav { width: 38px; height: 70px; }
  .room-nav--prev { left: 10px; }
  .room-nav--next { right: 10px; }
  .room-nav svg { width: 19px; height: 19px; }
}

@media (prefers-reduced-motion: reduce) {
  .room-slide { transition: none; }
  .room-nav svg { transition: none; }
  .room-nav:active svg { transform: none; }
  .room-nav--prev:hover svg, .room-nav--next:hover svg { transform: none; }
  .room-frame.is-bump-prev .room-slide,
  .room-frame.is-bump-next .room-slide { animation: none; }
}

/* ---------- Forms ---------- */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px 24px; margin-top: 36px; }
.field { display: flex; flex-direction: column; gap: 8px; }
.field-wide { grid-column: 1 / -1; }
.field label { font-weight: 600; font-size: 0.95rem; color: var(--bright); }
.field .hint { font-size: 0.85rem; color: var(--text-2); }
.field input, .field select, .field textarea {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--bright);
  padding: 14px 16px;
  transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
  width: 100%;
}
.field textarea { min-height: 130px; resize: vertical; }
.field input::placeholder, .field textarea::placeholder { color: var(--text-2); }
.field input:hover, .field select:hover, .field textarea:hover { border-color: #3a3d42; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--titanium); outline: none; background: var(--panel-2); }
.field select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%238a8a8a' stroke-width='1.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 16px center; padding-right: 42px; }

.field.invalid input, .field.invalid select, .field.invalid textarea { border-color: var(--error); }
.field .error-msg { display: none; font-size: 0.88rem; color: var(--error); }
.field.invalid .error-msg { display: block; }

/* Line fields: no boxes, just a rule under each control in the same 1px
   weight as the button underline. Rows sit close so the block scrolls less. */
.form-lines { gap: 0 44px; margin-top: 24px; }
.form-lines .field { gap: 4px; padding-bottom: 18px; }
.form-lines .field label { font-size: 0.9rem; }
.form-lines .field input,
.form-lines .field select,
.form-lines .field textarea {
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  line-height: 1.5;
  padding: 5px 0 9px;
  transition: border-color 0.25s var(--ease-out);
}
.form-lines .field select { background-position: right 2px center; padding-right: 26px; }
.form-lines .field textarea { min-height: 86px; }
.form-lines .field input:hover,
.form-lines .field select:hover,
.form-lines .field textarea:hover { border-bottom-color: #3a3d42; }
.form-lines .field input:focus,
.form-lines .field select:focus,
.form-lines .field textarea:focus { background: none; border-bottom-color: var(--titanium); }
.form-lines .field.invalid input,
.form-lines .field.invalid select,
.form-lines .field.invalid textarea { border-bottom-color: var(--error); }
.form-lines .field .error-msg { margin-top: 2px; }

.form-status { margin-top: 22px; font-size: 1rem; color: var(--titanium); }

/* ---------- Book a barber (book.html) ----------
   Native radios and checkboxes carry the state; the art sits on top of them.
   Every control works with the stylesheet alone, so the page books a chair
   whether or not script.js runs. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.pick-set, .opt-set { border: 0; margin: 0; padding: 0; min-width: 0; }

/* Pick a chair and the heading and the prices change one character at a time.
   Characters the wave has not reached sit back in the titanium, the one
   turning over sits back further, and each comes up to full strength as it
   lands, so the change reads left to right. The spans stay inline, so the name
   still wraps at its space on a narrow screen. */
.roll-ch { transition: opacity 0.14s var(--ease-out); }
.roll-ch.is-waiting { opacity: 0.55; }
.roll-ch.is-rolling { opacity: 0.32; }

/* The chair picker: a swipeable row of faces. */
.pick-rail {
  display: flex;
  gap: clamp(10px, 1.4vw, 18px);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding: 4px var(--pad-x) 6px;
  margin-inline: calc(var(--pad-x) * -1);
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.pick-rail::-webkit-scrollbar { display: none; }
/* Looping: the rail is nudged back by one copy of the row at the seam, and
   snapping would drag that nudge somewhere the swipe never asked for. */
.pick-rail.is-ring { scroll-snap-type: none; }

.pick {
  flex: 0 0 clamp(92px, 11vw, 132px);
  position: relative;
  display: block;
  cursor: pointer;
  scroll-snap-align: center;
}
.pick input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.pick .pick-face {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--line-soft);
  background: var(--panel);
  filter: grayscale(0.6) brightness(0.62);
  transition: filter 0.3s var(--ease-out), border-color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
}
/* Flex, not grid: grid blockifies "1", "of" and "1" into three rows and the
   tile grows past its square. */
.pick-face--any {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.16em;
  overflow: hidden;
  color: var(--accent);
  font-size: clamp(1.05rem, 2.4vw, 1.45rem);
  font-weight: 600;
  letter-spacing: 0.02em;
  filter: none;
  transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
}
.pick-face--any span { font-size: 0.62em; font-weight: 400; }
.pick-name {
  display: block;
  margin-top: 9px;
  font-size: 0.86rem;
  color: var(--text-2);
  text-align: center;
  transition: color 0.3s var(--ease-out);
}
/* Hover only where a pointer can hover. On a touch screen a row that restyles
   itself under the finger spends the first tap showing that state instead of
   choosing, so the chair and the service have to pick on the first go. */
@media (hover: hover) and (pointer: fine) {
  .pick:hover .pick-face { filter: grayscale(0.25) brightness(0.85); }
}
/* Script.js lights the chair under the pointer while the rail is running: the
   tiles are the thing moving then, and the browser only re-tests hover when
   the pointer moves. Same treatment as the hover above, and the chosen chair
   still outranks both. */
.pick.is-hot .pick-face { filter: grayscale(0.25) brightness(0.85); }
.pick.is-hot .pick-name { color: var(--text); }
.pick input:checked ~ .pick-face {
  filter: grayscale(0) brightness(0.95);
  border-color: var(--titanium);
  box-shadow: 0 0 0 1px var(--titanium);
}
.pick input:checked ~ .pick-face--any { filter: none; color: var(--titanium); }
.pick input:checked ~ .pick-name { color: var(--bright); font-weight: 600; }
.pick input:focus-visible ~ .pick-face { outline: 2px solid var(--titanium); outline-offset: 3px; }

/* The picker under the booking title. The rail keeps the carousel it is at
   every width; the hero only gives it room under the heading. */
.hero-picker { margin-top: clamp(26px, 4.5vh, 48px); }

/* ---------- The day and time on a desktop ----------
   The chair picker sits under the page title, so this band holds the day and
   time alone: one column on the left of the page, the width the roster used
   to take. Under 1024px the field is a row and the wheels open on a tap. */
@media (min-width: 1024px) {
  .chair-grid {
    display: grid;
    grid-template-columns: minmax(0, 430px);
    gap: clamp(30px, 3.6vw, 58px);
  }
  /* In the grid the band is a column, so the spacing it carries between two
     sections comes off and the gap is the grid's. */
  .chair-grid .when-band { margin: 0; padding: 0; }
  .chair-grid .when-block { max-width: none; }
  /* Nine rows of 42px is the open calendar's height on a desktop: the row and
     the panel's own chrome take 101px on top of it. */
  .chair-grid .wheel { --wheel-rows: 9; --wheel-item: 42px; }
  /* Nothing to open or shut here, so the row is a readout with no affordance
     on it. */
  .chair-grid .slot-row.is-locked { cursor: default; }
  .chair-grid .slot-row.is-locked .slot-chev { display: none; }
}

/* The service basket. */
.opt-list { list-style: none; margin: 0; padding: 0; }
.opt-list li + li { border-top: 1px solid var(--line-soft); }

.opt {
  position: relative;
  display: grid;
  grid-template-columns: 26px 1fr auto;
  align-items: start;
  gap: 4px 18px;
  padding: 20px 4px;
  cursor: pointer;
  transition: background 0.2s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .opt:hover { background: var(--panel); }
}
/* Touch gets its feedback from the press itself, which costs no tap. */
.opt:active { background: var(--panel); }
/* The native box is stretched invisibly over the whole row — the label already
   toggles on a click anywhere, so nothing is lost — and .opt-box takes its
   place in the first column, centred against the row rather than pinned to the
   top line. Pseudo-elements do not render on an input; they do on the span,
   which is what lets the mark be drawn rather than swapped in. */
.opt input {
  position: absolute;
  inset: 0;
  appearance: none;
  width: auto; height: auto;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.opt-box {
  position: relative;
  align-self: center;
  width: 20px; height: 20px;
  border: 1px solid var(--accent);
  border-radius: 3px;
  background: transparent;
  transition: border-color 0.25s var(--ease-out), background 0.25s var(--ease-out);
}
/* Two strokes, each anchored at the corner it starts from and grown along its
   own length: the X draws itself in, top-left arm first. */
.opt-box::before,
.opt-box::after {
  content: "";
  position: absolute;
  top: calc(50% - 4.2px);
  width: 12px;
  height: 2px;
  margin-top: -1px;
  border-radius: 1px;
  background: #fff;
  transform-origin: 0 50%;
  opacity: 0;
  transition: transform 0.34s var(--ease-out), opacity 0.16s linear;
}
.opt-box::before { left: calc(50% - 4.2px); transform: rotate(45deg) scaleX(0); }
.opt-box::after { left: calc(50% + 4.2px); transform: rotate(135deg) scaleX(0); transition-delay: 0.07s; }

.opt input:checked ~ .opt-box { border-color: var(--titanium); }
.opt input:checked ~ .opt-box::before { opacity: 1; transform: rotate(45deg) scaleX(1); }
.opt input:checked ~ .opt-box::after { opacity: 1; transform: rotate(135deg) scaleX(1); }
/* The hidden input takes the keyboard focus, so the box has to show it. */
.opt input:focus-visible ~ .opt-box { outline: 2px solid var(--titanium); outline-offset: 3px; }

/* A picked service sits on a faint lift of its own, a shade past the hover
   state so a chosen row still reads as chosen once the pointer moves off. The
   drawn X and the lift carry that on their own — no rule down the left edge. */
.opt:has(input:checked) {
  background: var(--panel-2);
}
.opt-body { display: block; }
.opt-name { display: block; color: var(--bright); font-weight: 600; }
.opt input:checked ~ .opt-body .opt-name { color: var(--titanium); }
.opt-desc { display: block; margin-top: 4px; color: var(--text-2); font-size: 0.92rem; max-width: 52ch; }
.opt-meta { display: block; text-align: right; white-space: nowrap; }
.opt-meta .num { display: block; color: var(--bright); font-weight: 600; font-size: 1.05rem; }
.opt-mins { display: block; margin-top: 2px; color: var(--text-2); font-size: 0.84rem; }
/* What a tick moved: a service that cannot share the appointment coming off,
   or two parts booking as their combination. */
.opt-rule-note { margin: 14px 4px 0; color: var(--titanium); font-size: 0.92rem; max-width: 60ch; }

/* ---------- Pick a day and a time ----------
   script.js builds this under the timing field and turns that field into the
   readout for it: one row carrying the month, the day and the time, which
   opens a pair of scroll wheels. With the script off the block never appears
   and the field is the plain text input it has always been, so nothing here is
   load-bearing. */
/* The day and time sit in the gap between the chair rail and the service menu,
   in a column the width of the calendar rather than the width of the wrap: the
   readout row puts the month at one end and the time at the other, and
   stretched across a desktop page those two land a hand apart. 430px is also
   where the day cells stop growing, so the month keeps reading as a month. */
.when-block {
  margin-top: clamp(26px, 3.4vw, 42px);
  max-width: 430px;
}

/* Under a desktop the band still reads as its own beat between the rail and
   the service menu: it takes the chair section's bottom padding for the space
   above it and gives the same padding back underneath, so the field lands
   halfway between the last face and the "What are we doing today?" heading
   with the service rule clear of the calendar. The inner wrap is a second one
   inside the section's, so its gutter comes off. */
.when-band {
  margin-top: calc(var(--space-section) * 0.82);
  margin-bottom: calc(var(--space-section) * -1);
  padding-bottom: clamp(18px, 2.2vw, 28px);
}
.when-band > .wrap { max-width: none; padding-inline: 0; }
.when-band .when-block { margin-top: 0; }

/* The day-and-time field carries no label text, so its empty label is kept
   out of the column rather than opening a gap above the line. */
.when-block .field > label:empty { display: none; }

/* With the picker up, the field is a value carrier and nothing else: it is
   taken out of the flow so its underline is not left drawing a rule above the
   calendar, while staying in the form for the submit and the required check.
   If the times ever fail to load the class comes off and the line is typable
   again. */
.when-block .field { position: relative; }
.when-block .field input.is-slot-only {
  position: absolute;
  top: 0;
  left: 0;
  /* No size at all: it is never tapped, so it should not read as a target. */
  width: 0;
  height: 0;
  min-height: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.slotpicker { margin-top: 0; }

.slot-row {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 13px 16px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.slot-row:hover { border-color: var(--titanium); }
/* Open, the row is the lid of the panel: one box, one border, no seam. */
.slot-row.is-open {
  border-color: var(--titanium);
  border-bottom-color: var(--line);
  border-radius: 6px 6px 0 0;
}
.slot-row-date {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.slot-row-month {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--titanium);
}
.slot-row-day {
  font-size: 1.45rem;
  line-height: 1.1;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--bright);
}
.slot-row-time {
  flex: none;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--bright);
}
/* Nothing picked yet: the row shows the day it would take, greyed, the way a
   placeholder reads in the fields above it. */
.slot-row.is-empty .slot-row-month { color: var(--accent); }
.slot-row.is-empty .slot-row-day,
.slot-row-time.is-empty { color: var(--text-2); font-weight: 400; }

.slot-chev {
  flex: none;
  display: inline-flex;
  color: var(--text-2);
  /* The same overshoot the panel lands on, so the arrow turns with it rather
     than beating it there. */
  transition: transform 0.42s cubic-bezier(0.34, 1.45, 0.5, 1), color 0.2s var(--ease-out);
}
.slot-chev svg { width: 15px; height: 15px; }
.slot-row:hover .slot-chev { color: var(--titanium); }
.slot-row.is-open .slot-chev { transform: rotate(180deg); color: var(--titanium); }

.slot-panel {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  border: 1px solid var(--titanium);
  border-top: 0;
  border-radius: 0 0 6px 6px;
  background: var(--panel);
  overflow: hidden;
}
.slot-col { padding: 10px 0 12px; min-width: 0; }
.slot-col + .slot-col { border-left: 1px solid var(--line); }
.slot-cap {
  margin: 0 0 4px;
  max-width: none;
  text-align: center;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-2);
}

/* The wheel: a snapping scroller with a lit band across the middle. What sits
   in the band is what is booked, so the band is drawn over the scroller and
   never moves with it. */
.wheel {
  --wheel-item: 40px;
  --wheel-rows: 5;
  position: relative;
  height: calc(var(--wheel-item) * var(--wheel-rows));
}
.wheel-band {
  position: absolute;
  left: 10px;
  right: 10px;
  top: 50%;
  height: var(--wheel-item);
  transform: translateY(-50%);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: rgba(196, 196, 196, 0.05);
  pointer-events: none;
}
.wheel-scroll {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-snap-type: y mandatory;
  /* Half the wheel above the first row and below the last, so either end can
     still reach the band. */
  padding-block: calc((var(--wheel-rows) - 1) * var(--wheel-item) / 2);
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* The rows dim into the edges rather than being clipped by them. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
}
.wheel-scroll::-webkit-scrollbar { width: 0; height: 0; }
.wheel-item {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--wheel-item);
  padding: 0 6px;
  border: 0;
  background: none;
  scroll-snap-align: center;
  color: var(--text-2);
  font-family: inherit;
  font-size: 0.92rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0.55;
  cursor: pointer;
  transition: color 0.18s var(--ease-out), opacity 0.18s var(--ease-out);
}
.wheel-item:hover { color: var(--text); opacity: 0.85; }
/* The row in the band is the answer, so it reads like one whatever the
   pointer is doing. */
.wheel-item.is-on,
.wheel-item.is-on:hover {
  color: var(--bright);
  font-weight: 600;
  letter-spacing: 0.02em;
  opacity: 1;
}
.wheel-item:focus-visible { outline: 1px solid var(--titanium); outline-offset: -3px; }
/* A time the calendar is not offering, or a day it has booked out: still in
   the wheel so the day reads as full rather than short, but greyed, struck
   and inert. In the lit band it stays grey — landing on it picks nothing. */
.wheel-item.is-off,
.wheel-item.is-off:hover,
.wheel-item.is-off.is-on,
.wheel-item.is-off.is-on:hover {
  color: var(--text-2);
  opacity: 0.28;
  font-weight: 400;
  letter-spacing: normal;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  cursor: default;
}
/* Nothing to offer — shut for the day, or still loading. */
.wheel-msg {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--wheel-item);
  margin: 0;
  max-width: none;
  color: var(--text-2);
  font-size: 0.85rem;
  text-align: center;
}

.slot-note {
  margin: 10px 0 0;
  max-width: none;
  color: var(--text-2);
  font-size: 0.85rem;
}

/* The readout is no longer typed into once the picker is up. */
.field input[readonly] { cursor: default; }
.form-lines .field input[readonly]:hover { border-bottom-color: var(--line); }

/* The running tally. */
.tally {
  --tally-pad: clamp(20px, 3vw, 30px);
  margin-top: 30px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
  padding: var(--tally-pad);
  /* The frame the haptic pad below is measured against. */
  position: relative;
}

/* A native switch laid invisibly over the booking band on a touch screen, so
   that the tap which books the chair is a real touch on a real control and an
   iPhone answers it with the Taptic Engine. script.js builds it, measures it
   onto the band and submits the form from its change event; without that
   script the band is the plain button it has always been. */
.submit-haptic {
  position: absolute;
  left: var(--hx, 0px);
  top: var(--hy, 0px);
  width: var(--hw, 0px);
  height: var(--hh, 0px);
  margin: 0;
  opacity: 0;
  z-index: 2;
}
/* These paragraphs are table rows, so the 62ch measure on <p> has to go. */
.tally p { max-width: none; }
.tally-line { margin: 0 0 14px; color: var(--text-2); font-size: 0.95rem; }
.tally-line strong { color: var(--bright); font-weight: 600; }
.tally-list { list-style: none; margin: 0; padding: 0; }
.tally-list li {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 9px 0;
  border-top: 1px solid var(--line-soft);
  font-size: 0.96rem;
}
.tally-list li span:last-child { color: var(--bright); font-variant-numeric: tabular-nums; }
.tally-empty { margin: 0; padding-top: 14px; border-top: 1px solid var(--line-soft); color: var(--text-2); font-size: 0.95rem; }
.tally-sum {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  align-items: baseline;
  margin: 0;
  padding-top: 16px;
  margin-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--bright);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 0.88rem;
}
.tally-sum .num { font-size: 1.7rem; letter-spacing: 0; text-transform: none; color: var(--titanium); }
.tally-mins { margin: 10px 0 0; font-size: 0.9rem; }

/* The booking band that closes the tally. Same band as "Book now" under the
   home page service list, pulled out to the panel's own edges so it reads as
   the last row of the total rather than a button parked inside it: only a top
   rule, and the panel's bottom corners. */
.tally-book {
  margin: var(--tally-pad) calc(var(--tally-pad) * -1) calc(var(--tally-pad) * -1);
  /* A button sizes to its label on width:auto, so the band is given the panel's
     inner width plus the padding it is pulled out over. */
  width: calc(100% + var(--tally-pad) * 2);
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0 0 5px 5px;
  padding: clamp(18px, 3vh, 26px) 22px;
  font-size: clamp(1.15rem, 2.1vw, 1.6rem);
}
.tally-book:hover, .tally-book:focus-visible { border-color: var(--titanium); }
/* Greyed out, the band stays a rule and a label: no fill, no lift. */
.tally-book:disabled { color: var(--text-2); border-color: var(--line); }
.tally-book:disabled::after { transform: scaleX(0) !important; }
.tally-book:disabled .btn-block-arrow { opacity: 0.5; }

/* Booked: the chair is settled, so stop inviting edits to it. */
.is-held .opt, .is-held .pick { pointer-events: none; opacity: 0.5; }
.is-held .form-status { color: var(--bright); font-weight: 600; }

/* ---------- Steps ---------- */
.steps { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(24px, 4vw, 56px); margin-top: 40px; }
.steps h3 { color: var(--bright); }
.steps .serif { font-size: 1.2rem; color: var(--accent); display: block; margin-bottom: 10px; }

/* ---------- Footer ---------- */
footer {
  border-top: 1px solid var(--line);
  padding: clamp(56px, 9vh, 96px) 0 130px;
  background: var(--panel);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: clamp(32px, 5vw, 72px);
}
.footer-brand .wordmark {
  display: inline-flex; align-items: center;
  text-decoration: none;
}
.brandmark-footer { height: 54px; opacity: 0.88; }
.footer-brand .wordmark:hover .brandmark-footer,
.footer-brand .wordmark:focus-visible .brandmark-footer { opacity: 1; }
.footer-brand p { margin-top: 18px; color: var(--text-2); font-size: 0.98rem; max-width: 34ch; }
footer h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--accent); font-weight: 600; margin-bottom: 18px; }
.footer-links ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.footer-links a { text-decoration: none; color: var(--text); }
.footer-links a:hover { color: var(--bright); text-decoration: underline; text-decoration-color: var(--accent); }
.footer-contact ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; color: var(--text-2); }
/* The bottom row keeps the copyright and the privacy link together as one
   block, with the photo note opposite. Placement is by data-mb rather than by
   source order, so reordering the row in the visual editor cannot strand the
   privacy link on its own in the far column. */
.footer-bottom {
  margin-top: clamp(40px, 7vh, 64px);
  padding-top: 26px;
  border-top: 1px solid var(--line-soft);
  display: grid;
  grid-template-columns: auto auto;
  justify-content: space-between;
  align-items: start;
  gap: 10px 32px;
  font-size: 0.88rem;
  color: var(--text-2);
}
.footer-bottom > p { margin-bottom: 0; }
.footer-bottom > [data-mb$="-footer-copyright"] { grid-column: 1; grid-row: 1; }
.footer-bottom > .footer-privacy { grid-column: 1; grid-row: 2; justify-self: start; }
.footer-bottom > [data-mb$="-footer-note"] { grid-column: 2; grid-row: 1; justify-self: end; text-align: right; }

.footer-privacy {
  color: var(--text-2);
  text-decoration: none;
  border-bottom: 1px solid var(--line);
  padding-bottom: 3px;
  transition: color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.footer-privacy:hover { color: var(--bright); border-bottom-color: var(--accent); }
.footer-privacy[aria-current="page"] { color: var(--titanium); border-bottom-color: var(--accent); }

/* Narrow enough that two columns would crush the note: stack the whole row. */
@media (max-width: 700px) {
  .footer-bottom { grid-template-columns: 1fr; justify-content: start; }
  .footer-bottom > [data-mb$="-footer-copyright"] { grid-column: 1; grid-row: 1; }
  .footer-bottom > .footer-privacy { grid-column: 1; grid-row: 2; }
  .footer-bottom > [data-mb$="-footer-note"] { grid-column: 1; grid-row: 3; justify-self: start; text-align: left; }
}

/* ---------- Motion: page slide + scroll reveals ---------- */
.page { display: block; }

html.js[data-slide="fwd"] .page { animation: page-in-fwd 0.5s var(--ease-out) both; }
html.js[data-slide="back"] .page { animation: page-in-back 0.5s var(--ease-out) both; }
@keyframes page-in-fwd { from { opacity: 0; transform: translateX(5%); } to { opacity: 1; transform: none; } }
@keyframes page-in-back { from { opacity: 0; transform: translateX(-5%); } to { opacity: 1; transform: none; } }

.page.exit-fwd, .page.exit-back { transition: opacity 0.32s ease, transform 0.32s var(--ease-out); }
.page.exit-fwd { opacity: 0; transform: translateX(-5%); }
.page.exit-back { opacity: 0; transform: translateX(5%); }

/* ---------- Book now: the ring opens onto the booking page ----------
   Clicking the circle between the pagers grows its white ring out from the
   button until it has passed every corner of the screen, and book.html is
   already standing inside it. The browser's cross-document view transition
   holds the old page's last frame and the new page's first, so there is never
   an empty frame between them. Both pages opt in, but script.js skips every
   navigation except this one, and a page that is not flagged .oo-ring swaps
   with no animation, so nothing else on the site changes.
   One registered length drives the lot: the group paints the ring, the new
   page is clipped just inside it and the old page is masked just outside it,
   so the three can never drift apart. script.js measures the button's centre
   and the distance to the farthest corner; book.html's head bootstrap writes
   them to --ring-x, --ring-y and --ring-end before its first frame. */
@view-transition { navigation: auto; }
@property --ring-r { syntax: "<length>"; inherits: true; initial-value: 0px; }
@keyframes ring-open {
  from { --ring-r: 36.75px; } /* the outer edge of the drawn ring, r=36 + half its 1.5px stroke */
  to   { --ring-r: var(--ring-end, 150vmax); }
}
html:not(.oo-ring)::view-transition-old(root),
html:not(.oo-ring)::view-transition-new(root),
html:not(.oo-ring)::view-transition-group(root) { animation: none; }
html.oo-ring::view-transition-group(root) {
  animation: ring-open 0.82s cubic-bezier(0.7, 0, 0.2, 1) both;
  background: radial-gradient(circle at var(--ring-x) var(--ring-y),
    transparent calc(var(--ring-r) - 2.5px), #fff calc(var(--ring-r) - 2px),
    #fff calc(var(--ring-r) + 0.5px), transparent calc(var(--ring-r) + 1px));
}
html.oo-ring::view-transition-image-pair(root) { isolation: auto; }
html.oo-ring::view-transition-old(root),
html.oo-ring::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
html.oo-ring::view-transition-new(root) {
  clip-path: circle(calc(var(--ring-r) - 1.5px) at var(--ring-x) var(--ring-y));
}
html.oo-ring::view-transition-old(root) {
  -webkit-mask: radial-gradient(circle at var(--ring-x) var(--ring-y), transparent var(--ring-r), #000 calc(var(--ring-r) + 0.5px));
  mask: radial-gradient(circle at var(--ring-x) var(--ring-y), transparent var(--ring-r), #000 calc(var(--ring-r) + 0.5px));
}
/* Browsers without cross-document view transitions: the same ring opens over
   this page, black inside it, and book.html comes up out of the black. */
.ring-cover {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  pointer-events: none;
  background: radial-gradient(circle at var(--ring-x) var(--ring-y),
    #000 calc(var(--ring-r) - 2px), #fff calc(var(--ring-r) - 1.5px),
    #fff var(--ring-r), transparent calc(var(--ring-r) + 0.5px));
  animation: ring-open 0.82s cubic-bezier(0.7, 0, 0.2, 1) both;
}
html.js[data-slide="ring"] .page { animation: page-in-fade 0.4s var(--ease-out) both; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  .ring-cover { display: none; }
}

/* ---------- Picking a chair: the background collapses, the face holds ----------
   Clicking a barber on the home roster collapses black down over the page from
   the top edge while the rest of it — header, sections and footer — falls away
   underneath, leaving that barber's photo alone on black, standing exactly where
   it was clicked. book.html puts it back in that same spot and fades it out as
   the booking page rises under it.
   script.js builds the veil and the photo clone from the card's own <img>, so
   with no JS the card is an ordinary link. */
.oo-drop {
  transition: opacity 0.42s ease, transform 0.5s cubic-bezier(0.5, 0, 0.75, 0);
  transition-delay: var(--drop-delay, 0s);
}
html.oo-leaving .oo-drop { opacity: 0; transform: translateY(56px); }

/* Black collapsing down over the cleared page: it drops from the top edge and
   closes the whole viewport in one move, so the page behind it is gone before
   the navigation. The last frame before the navigation and the first frame
   after it are then the same black. */
.handoff-veil {
  position: fixed;
  inset: 0;
  /* Over the fixed corner pager (z-index 100) as well as the page. */
  z-index: 150;
  background: var(--bg);
  pointer-events: none;
  transform: scaleY(0);
  transform-origin: top;
  will-change: transform;
  transition: transform 0.52s cubic-bezier(0.65, 0, 0.35, 1);
}
.handoff-veil.is-lit { transform: scaleY(1); }

/* The far side of the handoff is pure CSS, switched on by book.html's head
   bootstrap, which hands it the photo and the rect it was clicked in through
   custom properties. That matters: script.js is deferred and can land well
   after the click, long after this needs to be on screen. Done in the head,
   the black and the photo are part of the very first frame the page paints,
   and they clear themselves whether or not script.js ever runs. */
html.js.oo-arrive body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--bg);
  pointer-events: none;
  animation: veil-clear 0.9s ease both;
}
@keyframes veil-clear {
  0%, 18% { opacity: 1; }
  80% { opacity: 0; }
  100% { opacity: 0; visibility: hidden; }
}

/* The photo, standing in the exact box it was clicked in on the home page. */
html.js.oo-arrive body::before {
  content: "";
  position: fixed;
  z-index: 151;
  left: var(--oo-face-x);
  top: var(--oo-face-y);
  width: var(--oo-face-w);
  height: var(--oo-face-h);
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: var(--oo-face) center / cover no-repeat, var(--panel);
  pointer-events: none;
  animation: face-clear 0.9s ease both;
}
/* No hold on this side: the black has finished collapsing, so the photo starts
   fading on the first frame of the booking page and goes as that page rises. */
@keyframes face-clear {
  0% { opacity: 1; }
  62% { opacity: 0; }
  100% { opacity: 0; visibility: hidden; }
}

/* The held photo. Pinned to the rect of the card image it was lifted out of and
   left there — nothing about its box animates, only the colour coming up in it
   and, on the far side, the fade out. */
.handoff-face {
  position: fixed;
  z-index: 151;
  overflow: hidden;
  border-radius: 6px;
  border: 1px solid var(--accent);
  background: var(--panel);
  pointer-events: none;
  will-change: opacity;
}
.handoff-face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Chosen, so it comes out of the roster's titanium wash into full colour. */
  filter: none;
  transition: filter 0.55s var(--ease-out);
}

html.js.oo-arrive .page { animation: page-arrive 0.9s var(--ease-out) both; }
@keyframes page-arrive {
  0%, 22% { opacity: 0; transform: translateY(22px); }
  100% { opacity: 1; transform: none; }
}

/* Sections slide in from alternating sides (brief-pinned) */
html.js .reveal { opacity: 0; transition: opacity 0.7s ease, transform 0.7s var(--ease-out); }
html.js .reveal[data-side="left"] { transform: translateX(-44px); }
html.js .reveal[data-side="right"] { transform: translateX(44px); }
html.js .reveal.in { opacity: 1; transform: none; }

/* The hero h1 is the shop's logo: artwork rather than type. Sized against
   the viewport the way the h1 clamp is. */
.title-line { display: block; }
.title-logo { margin-top: 0.12em; margin-bottom: 0.2em; }
.hero-logo { width: clamp(250px, 50vw, 620px); height: auto; }

/* ---------- Hero logo: fade in ---------- */
/* The logo rises a few pixels into place as it fades up, a beat behind the
   shop photo it sits on. Gated on html.js so the logo is simply there at full
   strength when the script never runs.

   The whole 3D object carries this, not the face: the extruded sides are
   separate elements standing in their final spot, so animating only the face
   slid it up through a stack that had already arrived and read as two logos,
   one landing on the other. Riding .logo-scene, face and sides travel
   together. The scene has no other transform on it — the tilt lives on
   .logo-stage inside, the editor's offsets on the img — so nothing here
   fights anything. */
html.js .logo-scene { animation: hero-logo-in 1.5s var(--ease-out) 0.25s both; }
@keyframes hero-logo-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- Hero logo: the scroll exit ---------- */
/* Scroll down from the hero and the logo goes away from you: it holds its
   place on screen, shrinks back into the room and thins out, while the shop
   photo behind it carries on exactly as it did. Scrolling back up walks the
   logo home along the same curve instead of replaying an animation, and a
   page without the script shows it at full size where it has always sat.

   The hold is position: sticky, so the browser pins the logo in the same step
   as it moves the page, on the thread that owns the scroll — the way
   position: fixed already holds the photo behind it. It used to be a
   translate equal to the scroll offset, written per frame by script.js on
   desktop and by a scroll-linked keyframe on phones. Either way the page moved
   first and the logo caught up a frame later, so it stepped against a
   background that glided: the choppy, frame-by-frame look. Sticky has no
   "later" to fall into.

   For that, .hero-inner fills the hero's height and centres the logo itself
   (the same spot the hero's own centring gave it), so the h1 has room to stay
   put while the page scrolls under it. It lets go when the hero's bottom edge
   reaches it, by which point it has faded out. The flex column stops the
   logo's bottom margin collapsing into the CTA row's, as it did in block flow,
   so it is dropped here and the row's own 34px keeps the gap it always had.
   script.js measures where the h1 rests into --logo-hold-top and only then
   adds .logo-hold. The hero clips with overflow: clip rather than hidden:
   hidden makes it a scroll container, and sticky would pin to that box, which
   never scrolls. A browser without clip keeps hidden, and the logo simply
   rides up with the page as it fades. */
html.js .hero.logo-hold { overflow: clip; }
html.js .hero.logo-hold .hero-inner {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
html.js .hero.logo-hold .hero-title {
  position: sticky;
  top: var(--logo-hold-top, 0px);
}
html.js .hero.logo-hold .title-logo { margin-bottom: 0; }

/* The fade and the shrink ride two different elements on purpose. .logo-scene
   is spoken for: its entrance animation fills forwards and so pins opacity at
   1 for good. .logo-stage cannot take the fade either — an opacity below 1
   there is a grouping property, which would flatten the extrusion's
   preserve-3d and collapse the sixteen depth clones onto the face. So the fade
   sits on .title-logo, above both, and only the shrink sits on .logo-stage,
   whose transform-origin sync() already moves to wherever the editor has
   translated the artwork, so the logo shrinks into its own centre in all
   three views rather than drifting off the spot. The custom properties are
   the fallback for a browser that cannot tie keyframes to the scroll:
   script.js writes them there, and nothing else. */
html.js .title-logo {
  opacity: var(--logo-exit-o, 1);
  will-change: opacity;
}
html.js .logo-stage { scale: var(--logo-exit-s, 1); }

/* Wherever the browser can, the fade and the shrink are keyframes tied to the
   scroll position itself, at every width, so no script runs on the scroll at
   all. script.js measures the run once into --logo-exit-span. */
@supports (animation-timeline: scroll()) {
  html.js .title-logo {
    animation: logo-exit-fade linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 var(--logo-exit-span, 60vh);
  }
  html.js .logo-stage {
    animation: logo-exit-shrink linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 var(--logo-exit-span, 60vh);
  }
}

/* ---------- The backdrop, on the way out ----------
   A page leaving slides .page sideways, and a transform makes that element the
   containing block for anything fixed inside it. The photo would snap from the
   viewport to the top of the document the instant the exit class lands — click
   a corner button halfway down the hero and that is a jump of however far you
   had scrolled. So the exit hands the backdrop back to the hero at the exact
   spot it is standing: absolute, offset by the scroll the hold was carrying.
   script.js writes --hero-leave-y once, as it goes. */
html.oo-unpin .hero-media img,
html.oo-unpin .hero-media video,
html.oo-unpin .hero-lights {
  position: absolute;
  transform: translateY(var(--hero-leave-y, 0px));
}

/* Both curves are eased, and sampled here at the points script.js computes:
   the fade is done 85% of the way through the run, and the shrink lags it so
   the logo leaves at a readable size rather than shrinking to a speck. */
@keyframes logo-exit-fade {
  0%   { opacity: 1; }
  10%  { opacity: 0.979; }
  20%  { opacity: 0.926; }
  30%  { opacity: 0.847; }
  40%  { opacity: 0.743; }
  50%  { opacity: 0.615; }
  60%  { opacity: 0.466; }
  70%  { opacity: 0.295; }
  80%  { opacity: 0.103; }
  85%, 100% { opacity: 0; }
}
@keyframes logo-exit-shrink {
  0%   { scale: 1; }
  10%  { scale: 0.977; }
  20%  { scale: 0.944; }
  30%  { scale: 0.906; }
  40%  { scale: 0.863; }
  50%  { scale: 0.817; }
  60%  { scale: 0.768; }
  70%  { scale: 0.717; }
  80%  { scale: 0.663; }
  90%  { scale: 0.608; }
  100% { scale: 0.55; }
}

/* ---------- Hero logo: intro light sweep ---------- */
/* An SVG point light (index.html, #logo-sheen) crosses the logo's own alpha
   shape once as the intro settles, so the glint sits on the lettering rather
   than sweeping a rectangle over it. The filter lives on the img itself and
   so tracks the editor's per-view translates. script.js drives the light —
   SMIL changes never repaint a CSS-referenced filter on an HTML element — so
   the filter is gated on html.js, and the script drops it (.sheen-done) once
   the sweep lands. The filter ships with its light off: browsers that skip
   CSS filter url() references, or a script that never finishes, both leave
   the logo untouched. */
@media (prefers-reduced-motion: no-preference) {
  html.js .hero-logo { filter: url("#logo-sheen"); }
  html.js .hero-logo.sheen-done { filter: none; }
}

/* ---------- Hero logo: 3D tilt ---------- */
/* The logo is an extruded metal object, the lessestudio.com way: script.js
   stacks darkened clones of the logo PNG behind the real img on the z-axis,
   so the visible face stays the original artwork at full quality and the
   clones read as its milled sides. The stage rotates a few degrees towards
   the cursor (or the device tilt on touch screens); a radial glint, masked
   to the logo's own alpha, slides across the face like light on chrome.
   Both wrappers are inert inline boxes until script.js populates them, so
   the no-JS page renders the flat logo exactly as before. */
/* Block wrappers shrunk to the img keep the h1's own fit-content centring,
   and add no text line box (the img was a plain block child before). Only
   the real img sizes the stage: the clones and the glint sit absolutely over
   its box, so they never widen it. */
/* The vanishing point sits close enough that the near end of the word grows
   and the far end shortens, so the turn reads as an object rotating rather
   than a flat picture being skewed — eased back with the swing itself. */
.logo-scene { display: block; width: fit-content; perspective: 760px; }
.logo-stage { position: relative; display: block; width: fit-content; transform-style: preserve-3d; }
html.js .logo-stage { will-change: transform, scale; }
/* Below Desktop the logo never turns, so script.js builds no extrusion and
   there is no depth to keep. Dropping the 3D context and the layer promotion
   leaves the phone compositing a plain image: the glare and the shine still
   stack above the artwork on document order alone. Both were costing a real
   handset a re-raster on every frame of the hero's scroll exit, which is what
   made the logo shimmer there and nowhere else. */
@media (max-width: 1023px) {
  .logo-stage { transform-style: flat; }
  html.js .logo-stage { will-change: auto; }
  /* A phone never builds these. The editor can still be carrying a set from
     the Desktop view when it switches, and flattened they would all collapse
     onto one another behind the artwork and darken its edges. */
  .logo-depth { display: none; }
}
.logo-depth, .logo-glint { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Click-through, so a drag in the editor picks up the logo itself rather than
   one of the copies stacked over it. */
.logo-depth, .logo-glint { pointer-events: none; }
/* A short fade of their own, so a clone inserted after the scene's entrance
   has already begun still eases in rather than snapping on. */
html.js .logo-depth { animation: logo-depth-in 0.3s linear both; }
@keyframes logo-depth-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* ---------- Hero logo: the glare ---------- */
/* In the reference the letter does not merely pick up a highlight as it
   turns: its whole face swings between blown white and near-dark, because a
   fixed light is crawling across curved metal. Two washes reproduce that, both
   masked to the logo's own alpha so neither ever paints outside the
   letterforms. A hot spot marks where the light lands, and a broad sheen band
   carries shadow at one end and highlight at the other — so as script.js
   slides them with the turn (--gx/--gy), one side of the word lights up while
   the other falls into shade. The band is drawn on an oversized box and moved
   by background-position, which the compositor handles without repainting. */
.logo-glint {
  pointer-events: none;
  background-image:
    radial-gradient(42% 76% at var(--gx, 50%) var(--gy, 40%),
      rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.26) 44%, rgba(255, 255, 255, 0) 70%),
    linear-gradient(104deg,
      rgba(0, 0, 0, 0.42) 0%,
      rgba(0, 0, 0, 0.14) 26%,
      rgba(255, 255, 255, 0.32) 43%,
      rgba(255, 255, 255, 0.85) 53%,
      rgba(255, 255, 255, 0.24) 66%,
      rgba(0, 0, 0, 0.18) 85%,
      rgba(0, 0, 0, 0.38) 100%);
  background-size: 100% 100%, 300% 300%;
  background-position: 0 0, var(--gx, 50%) var(--gy, 40%);
  background-repeat: no-repeat;
  -webkit-mask-image: url("images/logo-one-of-one.png");
  -webkit-mask-size: 100% 100%;
  mask-image: url("images/logo-one-of-one.png");
  mask-size: 100% 100%;
  opacity: 0.88;
  transform: translateZ(1px);
  animation: logo-depth-in 0.3s linear both;
}

/* ---------- Hero logo: the intro shine (phone and tablet) ---------- */
/* One broad blaze of light crossing the lettering once on load, the way a lamp
   crosses a polished badge. The artwork is near-white, #f0f0f0 over most of
   the letter faces, so a white bar laid on top of it has almost nothing left
   to brighten and the light has to register somewhere else. It registers in
   two places. The crisp copy blows out the bevels and the shading between the
   glyphs as it passes, which is where the artwork still holds mid-greys; the
   blurred copy under it throws a halo onto the black around the letterforms.

   An earlier version shaded the band dark on both flanks to give the white
   core something to beat. That put two black bars across the logo and read as
   a shadow crossing it. Nothing here paints dark.

   script.js builds both copies, drives --sx in px from left of the box to
   right of it below the desktop breakpoint, and copies the editor's per-view
   translate off the logo img so the light tracks the artwork wherever the
   Phone and Tablet views have moved it. The layer is dropped once the blaze
   has left. */
.logo-sweep {
  --bloom: 9px;
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform: translateZ(1.6px);
}
/* The blur belongs on this wrapper, outside the mask below it: blurring the
   masked layer itself would soften the light inside the glyph edges and none
   of it would reach the black the halo needs. */
.logo-sweep-bloom {
  position: absolute;
  inset: 0;
  filter: blur(var(--bloom));
  opacity: 0.9;
}
/* Nothing is painted off the letterforms: the artwork's own alpha is the mask. */
.logo-sweep-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;
  -webkit-mask-image: url("images/logo-one-of-one.png");
  -webkit-mask-size: 100% 100%;
  mask-image: url("images/logo-one-of-one.png");
  mask-size: 100% 100%;
}
/* Hot in the middle, fading to nothing at both ends, wide enough to read as
   one pass of light rather than a streak. It leans, so it crosses the tall
   glyphs and the long word at the same moment rather than wiping down the line
   like a progress bar. Both copies read --sx off .logo-sweep, so one write
   moves them together. */
.logo-sweep-band {
  position: absolute;
  top: -30%;
  bottom: -30%;
  left: 0;
  width: 42%;
  background-image: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.10) 18%,
    rgba(255, 255, 255, 0.42) 36%,
    rgba(255, 255, 255, 0.92) 47%,
    rgba(255, 255, 255, 1) 50%,
    rgba(255, 255, 255, 0.92) 53%,
    rgba(255, 255, 255, 0.42) 64%,
    rgba(255, 255, 255, 0.10) 82%,
    rgba(255, 255, 255, 0) 100%);
  transform: translateX(var(--sx, -200%)) skewX(-14deg);
  will-change: transform;
}
@media (max-width: 767px) {
  /* The logo stands about 80px tall on a phone against 130 on a tablet, so the
     halo comes down with it or it swallows the letterforms. */
  .logo-sweep { --bloom: 6px; }
}

/* Hide the inline SVG that only carries filter defs. */
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ---------- Booking confirmed ----------
   The site slides between pages. This one arrival does not: finishing a booking
   is the end of a job rather than a step along a row, so book.html dims out and
   the confirmation rises through a long fade, the slowest movement anywhere on
   the site. Everything here is visible with no JavaScript and with motion
   turned down — the rise only ever runs on top of a page that already reads. */
.page.exit-done { transition: opacity 0.62s ease; opacity: 0; }

html.js[data-slide="fade"] .page { animation: page-in-fade 1.15s cubic-bezier(0.22, 0.61, 0.36, 1) both; }
@keyframes page-in-fade { from { opacity: 0; } to { opacity: 1; } }

/* Staged by script.js, one band after another, so the ticket arrives after the
   headline rather than with it. */
html.js .confirm-rise { animation: confirm-rise 1.05s cubic-bezier(0.22, 0.61, 0.36, 1) both; animation-delay: var(--rise-delay, 0s); }
@keyframes confirm-rise {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: none; }
}

.confirm-head { padding-bottom: clamp(18px, 3vh, 34px); }
.confirm-said {
  font-size: clamp(1.05rem, 2.4vw, 1.3rem);
  color: var(--titanium);
  margin: clamp(14px, 2.5vh, 26px) 0 0;
  max-width: 34ch;
}
.confirm-lede { color: var(--text-2); max-width: 62ch; margin-top: 0.7em; }
.confirm-block { padding-bottom: var(--space-section); }

/* The ticket: one plate, hairline framed, the reference set large enough to
   read out over the phone without zooming. */
.ticket {
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  padding: clamp(22px, 4vw, 38px);
  background: linear-gradient(180deg, rgba(196, 196, 196, 0.045), rgba(196, 196, 196, 0.012));
}
.ticket-ref-label {
  margin: 0;
  font-size: 0.82rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-2);
}
.ticket-ref {
  margin: 6px 0 0;
  font-size: clamp(1.7rem, 6vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: 0.04em;
  color: var(--bright);
  overflow-wrap: anywhere;
}
.ticket-ref-note { margin: 10px 0 0; font-size: 0.9rem; color: var(--text-2); max-width: 56ch; }

.ticket-rows { margin: clamp(22px, 4vh, 34px) 0 0; display: grid; gap: 0; }
.ticket-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 20px;
  justify-content: space-between;
  align-items: baseline;
  padding: 13px 0;
  border-top: 1px solid var(--line-soft);
}
.ticket-row dt { color: var(--text-2); font-size: 0.92rem; }
.ticket-row dd { margin: 0; text-align: right; flex: 1 1 auto; min-width: 0; }
.ticket-row-total dd { font-size: 1.15rem; color: var(--bright); }
.ticket-notes {
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
  color: var(--text-2);
  font-size: 0.92rem;
}

.cal-row { margin-top: clamp(26px, 4.5vh, 40px); }
.cal-title { font-size: 1.05rem; margin: 0 0 14px; }
.cal-buttons { display: flex; flex-wrap: wrap; gap: 12px 18px; }
.cal-note { margin: 14px 0 0; font-size: 0.86rem; color: var(--text-2); max-width: 56ch; }

.confirm-empty { border: 1px solid var(--line-soft); border-radius: 8px; padding: clamp(22px, 4vw, 34px); }
.confirm-empty h3 { margin: 0 0 10px; font-size: 1.15rem; }
.confirm-empty p { color: var(--text-2); max-width: 62ch; }

/* A phone holds the ticket rows as label over value: side by side, a long
   service list would crush the label into a column of single letters. */
@media (max-width: 560px) {
  .ticket-row { display: block; }
  .ticket-row dd { text-align: left; margin-top: 3px; }
  .cal-buttons .btn { width: 100%; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* The logo is plain white from the first frame: no fade, no rise, and the
     sheen filter above never applies, so its light never moves. */
  html.js .logo-scene { animation: none; }
  /* The 3D tilt never runs (script.js skips it), and if the stage was ever
     rotated it snaps flat; the glint and depth clones only exist when the
     script built them, so nothing else is left to hold still. */
  .logo-stage { transform: none !important; }
  /* No scroll exit either: script.js skips it, and this holds the logo at full
     size and full strength even if the setting changed mid-scroll. */
  html.js .title-logo { opacity: 1; will-change: auto; }
  html.js .hero.logo-hold .hero-title { position: static; }
  html.js .logo-stage { scale: 1; }
  /* Including the scroll-driven copy of the exit. A scroll-linked animation
     takes its progress from the scroll position, not from a clock, so the
     blanket duration override further down does not stop it — left alone it
     would go on holding and fading the logo here, which is the one place it
     must not. */
  html.js .title-logo, html.js .logo-stage { animation: none; }
  .logo-glint, .logo-sweep { display: none; }
  /* The ceiling never relights: script.js skips the intro outright, and this
     keeps the layer dark even if it was mid-run when the setting changed. */
  .hero-lights, .hero-lights.is-lit { display: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    transition-duration: 0.01ms !important;
  }
  html.js .reveal { opacity: 1; transform: none; }
  html.js[data-slide] .page { animation: none; }
  /* The book ring is simply closed from the start: nothing to draw. */
  .book-dot-draw { stroke-dashoffset: 0; }
  /* The backdrop holds still at full strength: no fade-up, no scroll fade
     (script.js skips its scroll handler under reduced motion). */
  html.js .hero-media { animation: none; transform: none; }
  /* No hold here at all. Holding the backdrop against the viewport is motion
     whoever is asking for less of it can see — the room slides under the page
     — and position: fixed does it without an animation to switch off. So the
     photo and the relit ceiling go back into the hero and scroll away with it,
     which is also what a page with no script has always shown. */
  html.js .hero-media img, html.js .hero-lights {
    animation: none;
    transform: none;
    position: absolute;
    height: 100%;
  }
  /* The reel never plays: the still holds the hero. */
  .hero-video { display: none !important; }
  .hero-media.is-playing img { visibility: visible; }
  /* The photo rail still drags the strip, it just stops reacting: no stretch
     on the thumb, no lift on the photos, no momentum (script.js skips that). */
  .strip-scroller { --v: 0; }
  .has-rail .strip-frame { transform: none; }
  .strip-rail-thumb { transform: translate3d(var(--thumb-x, 0px), -50%, 0); }
  /* The roster stays where you left it: no hover scrub, no push-in on the shot. */
  .barber-track { scroll-behavior: auto; }
  /* The date and time wheels still snap — they just jump to the row rather
     than gliding to it (script.js skips the smooth scroll as well). */
  .wheel-scroll { scroll-behavior: auto; }
  .barber-card.is-hot .barber-shot img { transform: none; }
  /* Picking a chair is a plain navigation: nothing drops, nothing travels. */
  html.js.oo-arrive .page { animation: none; }
  html.js.oo-arrive body::after { display: none; }
  html.oo-leaving .oo-drop { opacity: 1; transform: none; }
  .handoff-veil, .handoff-face { display: none; }
}

/* ---------- Responsive ---------- */
/* Tablet only. The home hero stands its content on the bottom edge — the grid
   aligns to the end and a tall bottom padding holds it clear of the pagers —
   and the Desktop and Phone views both pull the logo back up off that edge
   with an offset the shop set in the editor. The Tablet view carries no such
   offset, so the logo sat low here and nowhere else. Centring the hero's own
   content on this breakpoint answers it in the layout rather than with a
   number: the logo lands in the middle of the hero at any tablet height,
   portrait or landscape, and it keeps the slight lift above dead centre that
   the other two views read with (the empty CTA row below the logo carries a
   little height, so the artwork sits a touch high inside the centred box).
   .hero is the home hero alone; sub-pages use .hero-sub, untouched.
   The Desktop and Phone offsets have since gone (the Desktop drag landed on
   the extrusion clones rather than the logo), so every view centres the same
   way now instead of only the tablet. */
.hero { align-items: center; padding-bottom: 0; }

@media (max-width: 1023px) {
  .split { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 767px) {
  body { font-size: 1rem; }
  .corner-tl { top: 16px; left: 16px; }
  .corner-tr { top: 14px; right: 16px; }
  .corner-bl { bottom: 14px; left: 14px; }
  .corner-br { bottom: 14px; right: 14px; }
  .brandmark { height: 34px; }
  .brandmark-footer { height: 44px; }
  .corner-bl, .corner-br { padding: 8px 14px; }
  /* Full screen either way; the rows just sit a little tighter. */
  .menu-nav a { padding-block: 11px; }
  .corner-role { display: none; }
  .corner-name { font-size: 0.88rem; }
  /* Smaller circle so the two pagers keep their room on a narrow screen. The
     bottom offset lands the circle on the same centre line as the two text
     pagers, which the editor's phone view lifts clear of the edge. */
  .corner-bc { width: 64px; height: 64px; bottom: 32px; }
  .book-dot-label { font-size: 0.66rem; letter-spacing: 0.06em; }

  .service-row,
  .service-list[data-mb="home-services-list"] .service-row { grid-template-columns: 1fr; gap: 6px; }
  .service-desc { grid-column: 1; }
  .service-book { justify-self: start; margin-top: 6px; order: 4; }
  /* Stacked: the link goes back under the description, left-aligned. */
  .service-list[data-mb="home-services-list"] .service-book {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
    text-align: left;
  }

  .portrait-grid { grid-template-columns: 1fr; max-width: 420px; }
  .steps { grid-template-columns: 1fr; }
  .form-grid { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
  .quote--right { align-items: flex-start; text-align: left; }

  footer { padding-bottom: 110px; }

  /* The editor's phone view had the tally pinned to a fixed 504px box and
     lifted out of its section, which left roughly 400px of empty panel between
     the total and the button. Sized to its own content, the button sits back
     directly under the booking. Higher specificity than the override, which is
     deliberate — this is the layout the page needs on a phone. */
  .book-form [data-mb="book-tally"] { min-height: 0 !important; translate: none !important; }
  /* The note under the tally, level with the panel edge above it. */
  .book-form .cta-row { margin-top: 20px; }

  /* Tap targets at 48px. Each hit area grows around the thing it holds and
     gives the room back with a negative margin, so nothing moves on screen. */
  .corner-tl { top: 9px; padding-block: 7px; }
  .barber-ig { width: 48px; height: 48px; margin-right: -13px; }
  .service-book,
  .service-list[data-mb="home-services-list"] .service-book {
    display: inline-flex;
    align-items: center;
    min-height: 48px;
    margin-block: -4px -10px;
  }

  /* Footer: Pages and Visit side by side, one 48px row per item, so the
     taller rows cost no height; the brand runs across both. The top padding
     clears the fixed corner logo (57px deep), the bottom the Book now circle. */
  footer { padding-top: 64px; padding-bottom: 102px; }
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 14px 16px; }
  .footer-brand { grid-column: 1 / -1; }
  .footer-brand p { margin-top: 8px; }
  footer h3 { margin-bottom: 2px; }
  .footer-links ul, .footer-contact ul { gap: 0; }
  .footer-links li, .footer-contact li { display: flex; align-items: center; min-height: 48px; }
  .footer-links a, .footer-contact a { display: inline-flex; align-items: center; min-height: 48px; }
  .footer-bottom { margin-top: 12px; padding-top: 12px; gap: 6px; }
  .footer-privacy { display: inline-flex; align-items: flex-end; min-height: 48px; margin-top: -14px; }

  /* Links inside running text: the hit area reaches 16px above and below
     the line and the negative margin hands it back, so the paragraph keeps
     its own line spacing. */
  main p a:not([class]),
  main li a:not([class]),
  footer p a:not([class]) {
    display: inline-block;
    padding-block: 16px;
    margin-block: -16px;
  }
  /* The privacy page's "On this page" list: two columns of 48px rows, the
     same height as the single column of short rows it replaces. */
  .services-rail ul { display: grid; grid-template-columns: 1fr 1fr; gap: 0 24px; padding-left: 18px; }
  .services-rail li a:not([class]) { display: flex; align-items: center; min-height: 48px; padding-block: 0; margin-block: 0; }
  .room-nav { width: 48px; }
  .form-lines .field input,
  .form-lines .field select { min-height: 48px; }
}

/* Scroll reveals rise rather than slide in from the side on phone and
   tablet: calmer, and a section parked 44px past the edge reads as
   overflow on a narrow screen. */
@media (max-width: 1023px) and (prefers-reduced-motion: no-preference) {
  html.js .reveal[data-side]:not(.in) { transform: translateY(24px); }
}

/* The last two steps are the same form as the service list above them, not a new
   section of the page: the rule across their top read as a break mid-fill, and a
   full section gap under it left a stretch of empty panel between the last
   service and "Your details". No rule, and they sit up close to the list. */
.book-form > [data-mb="book-you"],
.book-form > [data-mb="book-total"] {
  border-top: 0;
  /* Pulls back most of the section gap the service list leaves under itself,
     without touching that section's own rhythm. */
  margin-top: clamp(-56px, -5vh, -32px);
  padding-top: clamp(8px, 1.5vh, 20px);
}

/* Stacked, the details run straight into the tally — on a phone the total is
   simply the next thing under the fields, with no gap of its own. */
@media (max-width: 1023px) {
  .book-form > [data-mb="book-you"] { padding-bottom: clamp(26px, 4vh, 44px); }
  .book-form > [data-mb="book-total"] { margin-top: 0; padding-top: 0; }
}

/* ---------- Booking page: your details and your sitting share a row ----------
   From 1024px up, the last two steps stand side by side: who you are and when
   you want the chair on the left, the running total and the card on the right.
   The form itself becomes the grid, so the columns land on the same content
   edges every other section uses — the two inner tracks cap at half the .wrap
   measure each, and the outer tracks hold the page gutter and then centre what
   is left. Both sections still run to the viewport edge.
   Under 1024px none of this applies and the steps stack as they did, and so
   does anything without subgrid, which is what carries the alignment. */
@supports (grid-template-columns: subgrid) {
@media (min-width: 1024px) {
  .book-form {
    /* The channel between the two columns. Each side carries half of it. */
    --book-gut: clamp(28px, 3.2vw, 48px);
    display: grid;
    grid-template-columns:
      [full-start] minmax(var(--pad-x), 1fr)
      [content-start] minmax(0, calc(620px - var(--pad-x)))
      [mid] minmax(0, calc(620px - var(--pad-x))) [content-end]
      minmax(var(--pad-x), 1fr) [full-end];
  }
  .book-form > * { grid-column: full-start / full-end; }

  .book-form > [data-mb="book-you"] { grid-column: full-start / mid; }
  .book-form > [data-mb="book-total"] { grid-column: mid / full-end; }
  /* subgrid hands the inner wrap the parent's own column lines, so the copy
     inside each half sits on the page's content edge without re-deriving it. */
  .book-form > [data-mb="book-you"],
  .book-form > [data-mb="book-total"] {
    display: grid;
    grid-template-columns: subgrid;
  }
  /* Both halves start at the rule, and the shorter one keeps its own height
     instead of stretching down to meet the taller one. */
  .book-form [data-mb="book-you-inner"],
  .book-form [data-mb="book-total-inner"] { max-width: none; margin-inline: 0; align-self: start; }
  .book-form [data-mb="book-you-inner"] {
    grid-column: content-start / mid;
    padding-inline: 0 var(--book-gut);
  }
  .book-form [data-mb="book-total-inner"] {
    grid-column: mid / content-end;
    padding-inline: var(--book-gut) 0;
  }

  /* Left half: name and email share the top line, and the phone number takes
     the whole column under them. */
  .book-form [data-mb="book-form-grid"] { column-gap: var(--book-gut); }
  .book-form [data-mb="book-field-phone"] { grid-column: 1 / -1; }

  /* Right half: the panel is half as wide as it was, so it trades padding for
     room. The total stays the biggest thing in the column. */
  .book-form [data-mb="book-tally"] { --tally-pad: clamp(18px, 1.9vw, 26px); }
  /* Half a column is not wide enough for the band at full size: the label wraps
     mid-word near 1024. A step down keeps it on one line. */
  .book-form [data-mb="book-submit"] { font-size: clamp(1.05rem, 1.5vw, 1.45rem); }
}

/* Screens with the height to spare keep the sitting alongside the details: the
   total and the booking button ride down past the calendar rather than
   scrolling away from it. Anything shorter than the panel itself stays put. */
@media (min-width: 1024px) and (min-height: 780px) {
  .book-form [data-mb="book-total-inner"] { position: sticky; top: 104px; }
}
}

/* ============================================================
   Shop editor (admin.html) and the draft badge it leaves on the
   public pages. Same titanium-on-black language as the site, laid
   out as a working surface: denser, quieter, no motion.
   ============================================================ */

/* The site pins its corners to the viewport and hides body overflow for the
   slide transition. The editor slides nowhere and needs a sticky toolbar, so
   both come off here: the mark scrolls away with the page and the scroll goes
   back to the viewport, where position:sticky can see it. */
body.admin { overflow: visible; overflow-x: clip; }
body.admin .corner-tl { position: absolute; }

.admin-intro { padding: clamp(120px, 16vh, 180px) 0 clamp(22px, 4vh, 40px); }
.admin-intro h1.display { font-size: clamp(2.3rem, 6vw, 4.2rem); }
.admin-intro .serif { font-size: clamp(1.1rem, 2vw, 1.35rem); margin-top: 14px; }
.admin-lead { max-width: 54ch; margin-top: 18px; color: var(--text); font-size: 1.02rem; }

.admin-lock { padding: 0 0 clamp(80px, 14vh, 160px); }
.admin-lock-form { display: flex; flex-direction: column; align-items: flex-start; gap: 18px; max-width: 360px; margin: 24px 0 20px; }
.admin-lock-form .field { width: 100%; }

.admin-bar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.admin-bar-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  padding-block: 14px;
}
.admin-state { margin: 0; max-width: 58ch; font-size: 0.92rem; color: var(--text-2); }
.admin-state.is-bad { color: var(--error); }
.admin-bar-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
/* Preview and discard stay as plain text in the bar, so the solid save button
   is the only thing in it asking to be clicked. */
.admin-bar-actions .link-btn { background: none; border: none; border-radius: 0; padding: 9px 2px; }
.admin-bar-actions .link-btn:hover:not(:disabled) { background: none; color: var(--bright); }

/* The only solid button in the editor, so the shop never hunts for the way to
   put a change live. With nothing to save it empties out and sits quiet
   beside the other links rather than disappearing. */
.btn-save {
  order: -1;
  border: 1px solid var(--titanium);
  border-radius: 999px;
  background: var(--titanium);
  color: var(--bg);
  transition: background 0.2s var(--ease-out), color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.btn-save::after { content: none; }
.btn-save:hover, .btn-save:focus-visible { background: var(--bright); border-color: var(--bright); color: var(--bg); }
.btn-save:disabled {
  border-color: var(--line);
  background: none;
  color: var(--text-2);
  cursor: default;
}

.admin-body { padding-block: clamp(28px, 5vh, 52px) clamp(80px, 12vh, 140px); }

.admin-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 26px;
  border-bottom: 1px solid var(--line);
  margin-bottom: clamp(26px, 4vh, 44px);
}
.admin-tab {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  margin-bottom: -1px;
  padding: 12px 2px;
  color: var(--text-2);
  font-size: 0.98rem;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.admin-tab:hover { color: var(--bright); }
.admin-tab[aria-selected="true"] { color: var(--bright); border-bottom-color: var(--titanium); }

.admin-panel > h2 { font-size: clamp(1.5rem, 3vw, 2.1rem); }
.admin-panel > p { color: var(--text-2); margin-bottom: 28px; }

/* Where a panel's instructions enumerate something, the list carries it so
   the shop can scan the panel instead of reading it through. Same drawn rule
   as the legal pages use, pulled up under the line that introduces it. */
.admin-notes {
  list-style: none;
  margin: -14px 0 28px;
  padding: 0;
  display: grid;
  gap: 10px;
  max-width: 62ch;
  color: var(--text-2);
}
.admin-notes li { position: relative; padding-left: 24px; }
.admin-notes li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.78em;
  width: 10px;
  height: 1px;
  background: var(--accent);
}
.admin-subhead {
  margin: 34px 0 14px;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-2);
  border-bottom: 1px solid var(--line-soft);
  padding-bottom: 8px;
}
.admin-subhead:first-child { margin-top: 0; }

.admin-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 28px; }

.admin-row {
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
}
.admin-row-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 18px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line-soft);
}
.admin-row-head h3 { font-size: 1.05rem; margin-right: auto; }
.admin-row-meta { margin: 0; color: var(--text-2); font-size: 0.92rem; }
/* Booking rules on a service: ticked lists of the rest of the menu. */
.admin-rules .field-label { font-weight: 600; font-size: 0.95rem; color: var(--bright); }
.admin-rule-options { display: flex; flex-wrap: wrap; gap: 10px 22px; padding: 4px 0; }
.admin-rule-option { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; color: var(--text); }
.admin-rule-option input { width: 18px; height: 18px; margin: 0; accent-color: var(--titanium); cursor: pointer; }
.admin-rule-option input:focus-visible { outline: 2px solid var(--titanium); outline-offset: 2px; }
.admin-row-body {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: clamp(18px, 3vw, 32px);
  padding: 20px 18px 24px;
}
.admin-row-body--plain { grid-template-columns: minmax(0, 1fr); }

.admin-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 18px 20px;
  align-content: start;
}
/* The booking form's textareas are for writing paragraphs into; these hold a
   line or two, so the rows attribute decides the height. */
.admin-row textarea, .photo-cell textarea, .admin-grid textarea { min-height: 0; }

/* What one barber charges: a block of short money boxes under their details,
   one per service, so the whole chair reads as a single price list. */
.admin-prices { padding-top: 6px; }
.admin-prices .admin-subhead { margin-top: 0; }
.admin-prices > .hint { font-size: 0.85rem; color: var(--text-2); margin: -6px 0 16px; }
.price-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px 16px; }
.price-grid .field label { font-weight: 400; color: var(--text-2); }

/* Roster cards fold away: shut, the head is the whole row, so it loses its rule
   and the chevron points down at what opening would show. */
.admin-row-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Off the move / remove cluster, so the card's own control reads apart. */
  margin-left: 4px;
}
.admin-row-toggle-text { min-width: 4ch; text-align: left; }
.admin-row-toggle svg { transition: transform 0.2s var(--ease-out); }
.admin-row-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  /* The chevron still turns, it just turns at once. */
  .admin-row-toggle svg { transition: none; }
  /* The save button still fills on hover, it fills at once. */
  .btn-save, .admin-dialog-actions .btn { transition: none; }
}
.admin-row:has(> .admin-row-head .admin-row-toggle):not(.is-open) .admin-row-head { border-bottom: 0; }

/* Pages tab: one row per page, nothing to open, so the head is the whole row. */
.admin-row--flat .admin-row-head { border-bottom: 0; }
.admin-row.is-off { background: var(--panel-2); }

/* Square tab: the sync report and the customer table. */
[data-panel="square"] .admin-row-body h3 { font-size: 1.05rem; margin-bottom: 4px; }
[data-panel="square"] .btn { margin-top: 14px; justify-self: start; }
[data-panel="square"] .admin-row-meta { margin-top: 10px; }
.admin-row-meta.is-bad { color: var(--error); }
.square-report-list {
  list-style: none;
  padding: 0;
  margin: 14px 0 0;
  display: grid;
  gap: 10px;
}
.square-report-list li { font-size: 0.92rem; color: var(--text-2); }
.square-report-list li strong { color: var(--text); font-weight: 600; }
.square-report-list li.is-todo { border-left: 2px solid var(--error); padding-left: 10px; }
.square-report .hint { margin-top: 14px; }

/* The sync passcode sits read-only behind a warning, because publishing and the
   customer list both stop working without it. Locked, the box reads as a fact
   rather than a form control; unlocked, the border picks up so it is obvious
   which state you are in. */
.field input.locked-input[readonly] {
  background: var(--panel-2);
  color: var(--text-2);
  border-style: dashed;
  cursor: default;
}
.field input.locked-input[readonly]:hover { border-color: var(--line); }
.field input.locked-input:not([readonly]) { border-color: var(--titanium); }

.key-lock { display: flex; flex-direction: column; gap: 10px; }
.key-lock-state { margin: 0; font-size: 0.85rem; color: var(--text-2); }
.key-lock-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

.key-lock-warn {
  border: 1px solid rgba(226, 172, 172, 0.4);
  border-radius: 10px;
  background: rgba(226, 172, 172, 0.06);
  padding: 16px 18px 18px;
}
.key-lock-warn h4 { margin: 0 0 10px; font-size: 0.95rem; color: var(--error); }
.key-lock-warn ul { margin: 0; padding-left: 1.2em; display: grid; gap: 8px; }
.key-lock-warn li { font-size: 0.88rem; color: var(--text); line-height: 1.5; }
.key-lock-warn .publish-check { margin: 16px 0 14px; font-size: 0.9rem; }

.admin-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 12px;
  font-size: 0.92rem;
}
.admin-table th, .admin-table td {
  text-align: left;
  padding: 8px 12px 8px 0;
  border-bottom: 1px solid var(--line-soft);
  overflow-wrap: anywhere;
}
.admin-table th { color: var(--text-2); font-weight: 400; }

/* The Barbers tab reads Square back rather than editing it: a few facts per
   barber and a price list, laid out like the fields around them. */
.admin-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px 20px;
  margin: 0;
}
.admin-fact dt { font-size: 0.82rem; color: var(--text-2); margin-bottom: 4px; }
.admin-fact dd { margin: 0; font-size: 1rem; color: var(--bright); overflow-wrap: anywhere; }
.admin-price-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 8px 20px;
}
.admin-price-list li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 0.95rem;
}
.admin-price-list li span { color: var(--text-2); }
.admin-price-list li strong { font-weight: 600; color: var(--bright); white-space: nowrap; }
.admin-row.is-off .admin-row-head h3 { color: var(--text-2); }

.admin-switch { position: relative; display: flex; align-items: center; gap: 10px; cursor: pointer; }
.admin-switch input {
  /* Out of the flow and see-through, laid exactly over the track it drives, so
     a tap anywhere on it lands and the keyboard still reaches it. */
  position: absolute;
  top: 50%;
  left: 0;
  width: 44px;
  height: 24px;
  margin: 0;
  transform: translateY(-50%);
  opacity: 0;
  cursor: pointer;
}
.admin-switch-track {
  position: relative;
  flex: none;
  width: 44px;
  height: 24px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-2);
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.admin-switch-dot {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text-2);
  transition: transform 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.admin-switch input:checked ~ .admin-switch-track { background: rgba(255, 255, 255, 0.14); border-color: var(--titanium); }
.admin-switch input:checked ~ .admin-switch-track .admin-switch-dot { transform: translateX(20px); background: var(--bright); }
.admin-switch input:focus-visible ~ .admin-switch-track { outline: 2px solid var(--titanium); outline-offset: 2px; }
.admin-switch input:disabled { cursor: not-allowed; }
.admin-switch:has(input:disabled) { cursor: not-allowed; opacity: 0.55; }
.admin-switch-text { font-size: 0.92rem; color: var(--text-2); min-width: 8ch; }
.admin-switch input:checked ~ .admin-switch-text { color: var(--bright); }

.row-tools { display: flex; align-items: center; gap: 10px; }
.icon-btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--panel-2);
  color: var(--text-2);
  cursor: pointer;
  transition: color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.icon-btn:hover:not(:disabled) { color: var(--bright); border-color: #3a3d42; }
.icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }

.link-btn {
  appearance: none;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 9px 14px;
  color: var(--titanium);
  font-size: 0.92rem;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out), color 0.2s var(--ease-out);
}
.link-btn:hover:not(:disabled) { background: var(--panel); border-color: #3a3d42; color: var(--bright); }
.link-btn:disabled { color: var(--text-2); background: var(--panel-2); border-color: var(--line); cursor: not-allowed; opacity: 0.5; }
.link-btn-warn { color: var(--error); border-color: rgba(226, 172, 172, 0.4); background: rgba(226, 172, 172, 0.08); }
.link-btn-warn:hover:not(:disabled) { color: #f0cccc; border-color: rgba(226, 172, 172, 0.6); background: rgba(226, 172, 172, 0.12); }

/* Photo slots */
.photo-picker { display: flex; flex-direction: column; gap: 12px; }
.shot-frame {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  background: var(--panel-2);
  aspect-ratio: 3 / 4;
}
.shot-frame img { width: 100%; height: 100%; object-fit: cover; }
.shot-frame.is-empty {
  display: grid;
  place-items: center;
  color: var(--text-2);
  font-size: 0.86rem;
  border-style: dashed;
}
.photo-picker-controls { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.photo-picker-controls .hint { margin: 0; font-size: 0.82rem; color: var(--text-2); }
.file-input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.file-label { cursor: pointer; }
/* The real file input is off-screen, so its focus ring has to land on the
   label standing in for it. */
.file-input:focus-visible + .file-label { outline: 2px solid var(--titanium); outline-offset: 3px; border-radius: 2px; }

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 22px;
}
.photo-cell {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
}
.photo-cell .shot-frame { aspect-ratio: 4 / 3; }
.photo-slot { margin: 0; font-size: 0.88rem; color: var(--text-2); }
.photo-cell .link-btn { align-self: flex-start; }

/* Testimonials tab: the quotes on the page, then the shop's Google reviews to
   pick from. Google's G keeps its own colours wherever it marks a review. */
.admin-row-head h4 { font-size: 1.05rem; margin: 0 auto 0 0; overflow-wrap: anywhere; }
.t-source { flex-basis: 100%; max-width: none; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.t-place { color: var(--text-2); }
.t-place::before { content: "\00B7"; margin-right: 10px; }
.admin-row .hint.is-bad { color: var(--error); }
.g-icon { flex: none; width: 16px; height: 16px; display: block; }
.g-line { display: flex; align-items: flex-start; gap: 10px; margin: 0; color: var(--bright); }
.g-line .g-icon { margin-top: 0.3em; margin-top: calc((1lh - 16px) / 2); }
.g-stars { display: inline-flex; gap: 1px; font-size: 0.95rem; line-height: 1; letter-spacing: 0; }
.g-stars .on { color: var(--bright); }
.g-stars .off { color: #3a3d42; }
.admin-subhead--onpage { margin-top: 56px; }
.google-reviews { display: flex; flex-direction: column; gap: 22px; }
.g-setup .admin-row-body { gap: 12px; }
.g-setup .hint { margin: 0; color: var(--text-2); max-width: 66ch; }
.g-steps { margin: 0; padding-left: 1.3em; display: grid; gap: 8px; color: var(--text); max-width: 66ch; }
.g-connect { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
.g-connect .admin-row-meta { max-width: 60ch; }
.g-connect-btn { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding-inline: 16px 18px; color: var(--bright); }
.g-pick { display: grid; gap: 14px; justify-items: start; max-width: 520px; }
.g-pick .field { width: 100%; }
.g-browser { display: flex; flex-direction: column; gap: 20px; }
.g-summary { color: var(--text); }
.g-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 260px)); gap: 14px 20px; }
.g-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 14px; align-items: start; }
.g-list > .hint { grid-column: 1 / -1; margin: 0; }
.g-card .admin-row-body { padding-top: 16px; gap: 0; }
.g-card.is-picked { border-color: var(--titanium); }
.g-when { display: inline-flex; align-items: center; gap: 10px; }
.g-text { margin: 0; color: var(--text); white-space: pre-line; overflow-wrap: anywhere; }
.g-text.is-empty { color: var(--text-2); font-style: italic; }
.g-card-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin-top: 16px; }
.g-card-foot .hint { margin: 0; font-size: 0.88rem; color: var(--text-2); }
.g-on { flex-basis: 100%; max-width: none; }
.g-more { align-self: flex-start; }

/* Publish sheet */
.admin-dialog {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.78);
}
.admin-dialog-card {
  width: min(620px, 100%);
  max-height: 86vh;
  overflow-y: auto;
  padding: clamp(22px, 4vw, 34px);
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7);
}
.admin-dialog-card h2 { font-size: clamp(1.4rem, 3vw, 2rem); }
.publish-steps { color: var(--text); padding-left: 1.2em; margin: 0 0 22px; }
.publish-steps li { margin-bottom: 8px; }
.publish-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.publish-files { list-style: none; margin: 0 0 20px; padding: 0; }
.publish-files li {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 0;
  border-top: 1px solid var(--line-soft);
}
.publish-files img { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; }
.publish-file-name { flex: 1; font-size: 0.9rem; color: var(--text-2); }
.publish-summary { color: var(--text); padding-left: 1.2em; margin: 0 0 20px; }
.publish-summary li { margin-bottom: 6px; }
.publish-check {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 22px 0 18px;
  color: var(--bright);
  cursor: pointer;
}
.publish-check input { flex: none; width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--titanium); cursor: pointer; }
.publish-check input:focus-visible { outline: 2px solid var(--titanium); outline-offset: 2px; }
.publish-progress { min-height: 1.4em; margin-top: 14px; }
.publish-progress:empty { min-height: 0; margin-top: 0; }
.publish-progress.is-good { color: var(--bright); }
.publish-manual { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.publish-manual summary { cursor: pointer; color: var(--text-2); margin-bottom: 14px; }
/* Cancel on the left, the action that changes the live site on the right,
   where a thumb and a mouse both expect to find it. */
.admin-dialog-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 24px;
  padding-top: 18px;
  border-top: 1px solid var(--line-soft);
}
.admin-dialog-actions .btn {
  order: 2;
  border: 1px solid var(--titanium);
  border-radius: 999px;
  background: var(--titanium);
  color: var(--bg);
  transition: background 0.2s var(--ease-out), color 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.admin-dialog-actions .btn::after { content: none; }
.admin-dialog-actions .btn:hover, .admin-dialog-actions .btn:focus-visible {
  background: var(--bright);
  border-color: var(--bright);
  color: var(--bg);
}
.admin-dialog-actions .btn:disabled { border-color: var(--line); background: none; color: var(--text-2); cursor: default; }
body.has-dialog { overflow: hidden; }

/* The badge every page shows while an unpublished draft is in this browser. */
.draft-badge {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 9px 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg);
  font-size: 0.84rem;
  color: var(--text-2);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
}
.draft-badge a { font-size: 0.84rem; }

@media (max-width: 767px) {
  /* Six tabs will not sit on one phone line and wrapping them leaves a
     ragged block, so the row becomes one swipeable strip that bleeds to
     both page edges. The mask fades the last tab out on the right, which
     is what tells a thumb there is more to reach. */
  .admin-tabs {
    flex-wrap: nowrap;
    gap: 0 22px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    margin-inline: calc(var(--pad-x) * -1);
    /* The extra right padding is the width of the fade, so the last tab
       clears it once the strip is scrolled to the end. */
    padding-inline: var(--pad-x) calc(var(--pad-x) + 36px);
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 36px), transparent 100%);
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 36px), transparent 100%);
  }
  .admin-tabs::-webkit-scrollbar { display: none; }
  .admin-tab {
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: 0.92rem;
  }
  /* The strip owns its own spacing on phone: nudging one tab out of line
     only lands it on top of its neighbour, so per-tab drags made in the
     editor's Phone view are ignored here. Desktop and tablet still take
     them. These selectors outrank the editor's [data-mb] rules. */
  .admin-tabs[data-mb="admin-tabs"] {
    min-height: 0 !important;
    translate: none !important;
  }
  .admin-tabs .admin-tab[data-mb] { translate: none !important; }
  .admin-row-body { grid-template-columns: minmax(0, 1fr); }
  .shot-frame { max-width: 200px; }
  /* The bar follows a thumb down the page, so it stays shallow: the save
     button takes a full line of its own and the two quiet links share the
     one under it. */
  .admin-bar-inner { padding-block: 10px; gap: 8px 14px; }
  .admin-state { font-size: 0.875rem; }
  .admin-bar-actions { width: 100%; gap: 0 18px; }
  .btn-save { flex: 1 1 100%; justify-content: center; }
  /* A thumb's worth of height on the two quiet links, with the gap above
     them taken back so the bar stays as shallow as it can. */
  .admin-bar-actions .link-btn { display: inline-flex; align-items: center; min-height: 44px; padding: 0 2px; font-size: 0.86rem; }
  .draft-badge { bottom: 74px; }
}
