/* ==========================================================================
   Barbershop — stylesheet

   Re-skinning for the next client: change the palette block below and
   web/config.js. Nothing else should need to move.

   Fonts are a system stack on purpose. Both competitors pull webfonts from a
   CDN and pay a render delay for it; this loads instantly and costs nothing.
   ========================================================================== */

/* ── Palette ──────────────────────────────────────────────
   Two themes, one set of names.

   The palette is taken from the shop's own logo rather than chosen: sampling
   it gives #1976D2 blue thirteen times, #D32F2F red six, and a steel ramp
   (#475569 / #64748B / #94A3B8) fourteen. Blue leads because blue leads in the
   artwork — not because blue is the safe choice for a website.

   LIGHT — white shop, steel and blue. The page is genuinely #fff, not an
   off-white pretending. Depth comes from cool alternating bands, a hairline on
   every card, and a shadow soft enough to read as light rather than as a smudge.

   DARK — midnight navy. Navy for the same reason suits are navy: it is the
   colour of things made well. It carries real chroma, so it never looks like a
   dimmed screen, and it holds the logo blue better than it ever held brass.

   Both are complete and equal. Nothing below is a tinted version of the other:
   the neutrals carry a steel cast in light and a deeper blue cast in dark,
   because a grey borrowed across a theme flip is the thing that makes a toggle
   look cheap.

   BLUE COMES IN TWO WEIGHTS, and the split is why one stylesheet can serve
   both. On navy a single bright blue does both jobs. On white it cannot — the
   logo's own #1976D2 is 4.6:1 against white, which is legal for text and
   genuinely uncomfortable to read at 14px.

     --accent / --accent-hi     text, icons, hairlines. Readable on the ground.
     --accent-fill / -hi        backgrounds only.
     --on-accent                what goes on top of --accent-fill. Brass took a
                                near-black label and blue cannot, so the label
                                is a token now rather than a hex repeated in
                                nine rules — white in light, near-black in dark.

   In dark the first four collapse back to one bright blue. Every rule in this
   file is already written against the right one, so the flip is free.

   RED IS NOT AN ACCENT HERE. Red already meant "closed" on this site, and a
   brand red alongside a status red gives the same colour two meanings. So the
   brand red *is* the status red — one red, doing the job it already did — and
   otherwise red appears only in the logo and in the splash's barber-pole
   stripe, which is the one surface where a pole stripe is literally correct.

   HOW THE THEME IS CHOSEN: an inline script in every <head> stamps
   data-theme on <html> before first paint — the visitor's saved choice if they
   have one, otherwise light. `prefers-color-scheme` is deliberately not
   consulted: the shop is a white barbershop, and following the OS would put
   every dark-mode phone on the navy version. Dark is opt-in. See Theme in
   shared.js for how to reverse that if the client ever wants it.

   Contrast is measured, not eyeballed. Smallest margin in each theme:
     light   --text 17.8:1   --accent 7.1:1   label on --accent-fill 5.7:1
     dark    --text 14.4:1   --accent 7.2:1   label on --accent-fill 8.2:1
   (Re-measured against --bg and --surface at the repalette. The dark --text
    figure that stood here before, 15.8:1, did not reproduce.)
   ───────────────────────────────────────────────────────── */

/* Light is the default, for everyone, on every first visit. */
:root {
  color-scheme: light;

  --bg:          #ffffff;   /* the page */
  --bg-alt:      #f1f4f8;   /* alternating sections, cool */
  --surface:     #ffffff;   /* cards */
  --surface-2:   #f7f9fc;   /* raised / hover */
  --line:        #d4dbe4;
  --line-soft:   #e5eaf1;

  --text:        #16181d;   /* near-black, never pure #000 */
  --text-dim:    #4c515a;
  --text-faint:  #5f6570;   /* 5.9:1 on white, still 4.8:1 at the foot of the
                               footer gradient, which is the darkest ground any
                               small text on this site ever lands on */

  /* The logo blue is #1976D2. At 4.6:1 on white that is legal for text and
     uncomfortable to actually read, so --accent is the same hue walked down
     until it is comfortable, and --accent-fill sits one step above it where a
     white label clears 5.7:1. */
  --accent:      #1257a8;   /* logo blue you can read: 7.1:1 */
  --accent-hi:   #0e4485;
  --accent-fill:    #1565c0;   /* logo blue you can only sit on */
  --accent-fill-hi: #1257a8;
  /* What goes ON --accent-fill. Brass took a near-black label; blue cannot,
     so this is a token now rather than a hex repeated in nine rules. */
  --on-accent:   #ffffff;

  --accent-soft: rgba(25, 118, 210, 0.11);
  --accent-line: rgba(18, 87, 168, 0.30);

  --ok:          #0f7a45;
  --ok-soft:     rgba(15, 122, 69, 0.10);
  --ok-line:     rgba(15, 122, 69, 0.30);
  /* The logo red, walked down to 5.6:1. Red already meant "closed" here, so
     rather than add a second red for the brand, the brand red does that job
     and the site keeps exactly one. */
  --closed:      #c62828;
  --closed-soft: rgba(198, 40, 40, 0.09);
  --closed-line: rgba(198, 40, 40, 0.28);

  /* Surfaces that float over the page and need their own ground. */
  --nav-bg:      rgba(255, 255, 255, 0.92);
  --nav-shadow:  0 8px 24px rgba(18, 30, 48, .10);
  --bar-bg:      rgba(255, 255, 255, 0.97);
  --bar-shadow:  0 -8px 26px rgba(18, 30, 48, .11);
  --band-mid:    #eef2f7;   /* midpoint of the alternating band gradient */
  --footer-end:  #e8eef5;
  --ribbon-bg:   #e9f1fb;

  /* The hero photograph sits under a scrim so the text on top can read. The
     scrim is the one thing that genuinely inverts between themes: on white it
     bleaches the photo, on navy it darkens it. Same job, opposite direction. */
  --hero-photo:  .42;
  --hero-scrim:
    linear-gradient(180deg, rgba(255,255,255,.80) 0%, rgba(255,255,255,.90) 55%, var(--bg) 100%),
    radial-gradient(900px 500px at 50% 30%, rgba(255,255,255,.42), transparent 70%);
  /* Two light sources rather than one flat wash: the logo blue at the top,
     a steel pool at the foot. Both stay under .12 — a saturated blue wash on
     white reads as a screen tint rather than as light. */
  --hero-glow:
    radial-gradient(900px 460px at 78% -8%, rgba(25,118,210,.11), transparent 64%),
    radial-gradient(780px 430px at 4% 108%, rgba(100,116,139,.10), transparent 62%),
    linear-gradient(178deg, rgba(60,80,110,.035), transparent 55%);
  --page-glow:
    radial-gradient(880px 420px at 72% -12%, rgba(25,118,210,.10), transparent 64%),
    radial-gradient(760px 400px at 6% 112%, rgba(100,116,139,.09), transparent 62%);

  --btn-ghost-line: #bcc6d2;
  --avatar-ring: rgba(0, 0, 0, .07);
  --badge-line:  var(--line);
  --star:        #9a6d04;   /* stars stay gold; that is a convention, not a brand */
  --heart:       #c0392b;
  --quote-op:    .16;
  /* The splash wash. Both are deliberately near-invisible: the job is to stop
     the white reading as flat, not to be seen. */
  --splash-glow:  rgba(25,118,210,.10);
  --splash-floor: rgba(20,30,48,.055);

  --radius:      14px;
  --radius-sm:   9px;
  --maxw:        1140px;

  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  /* Tinted rather than neutral grey, so a shadow looks like light falling
     across the page instead of a grey smudge under a box. Cool now, to match
     the ground it falls on. */
  --shadow:      0 16px 40px rgba(18, 30, 48, 0.11);
  /* Cards sit on white, so the border does the defining and the shadow only
     lifts. Two layers: a tight one for the edge, a wide one for the light. */
  --shadow-card: 0 1px 2px rgba(18, 30, 48, .06), 0 10px 26px rgba(18, 30, 48, .06);
}

/* Dark. Only the values that actually differ are restated — anything absent
   here (radii, the max width, the font stack) is deliberately shared. */
:root[data-theme='dark'] {
  color-scheme: dark;

  --bg:          #1a2332;   /* midnight navy, lifted */
  --bg-alt:      #202b3c;
  --surface:     #253143;
  --surface-2:   #2d3b4f;
  --line:        #3a4a60;
  --line-soft:   #2f3d50;

  --text:        #f2f5f9;   /* crisp off-white, never pure #fff */
  --text-dim:    #b3c0d0;
  --text-faint:  #93a1b5;   /* 6.0:1 on --bg, and still 5.0:1 on --surface,
                               which is the lighter ground and so the real
                               constraint — the old #8493a8 was 4.2:1 there */

  /* Both weights collapse to one bright blue, the way brass did: on the navy
     ground it is already 7.2:1, so there is nothing to walk down. It is the
     logo blue lifted, not a different hue. */
  --accent:      #7fb3f0;
  --accent-hi:   #a3c8f6;
  --accent-fill:    #7fb3f0;
  --accent-fill-hi: #a3c8f6;
  /* Near-black on that blue is 8.2:1 — dark keeps the dark label. */
  --on-accent:   #101720;

  --accent-soft: rgba(127, 179, 240, 0.13);
  --accent-line: rgba(127, 179, 240, 0.34);

  --ok:          #4fbf87;
  --ok-soft:     rgba(79, 191, 135, 0.14);
  --ok-line:     rgba(79, 191, 135, 0.34);
  --closed:      #e06c5f;
  --closed-soft: rgba(224, 108, 95, 0.14);
  --closed-line: rgba(224, 108, 95, 0.34);

  --nav-bg:      rgba(26, 35, 50, 0.94);
  --nav-shadow:  0 10px 30px rgba(5, 10, 20, .4);
  --bar-bg:      rgba(26, 35, 50, 0.96);
  --bar-shadow:  0 -10px 30px rgba(5, 10, 20, .45);
  --band-mid:    #232f41;
  --footer-end:  #16202e;
  --ribbon-bg:   #16283d;

  --hero-photo:  .5;
  --hero-scrim:
    linear-gradient(180deg, rgba(19,26,38,.72) 0%, rgba(19,26,38,.86) 55%, var(--bg) 100%),
    radial-gradient(900px 500px at 50% 30%, rgba(19,26,38,.35), transparent 70%);
  --hero-glow:
    radial-gradient(900px 460px at 78% -8%, rgba(127,179,240,.15), transparent 64%),
    radial-gradient(780px 430px at 4% 108%, rgba(58,96,150,.22), transparent 62%),
    linear-gradient(178deg, rgba(210,228,255,.04), transparent 55%);
  --page-glow:
    radial-gradient(880px 420px at 72% -12%, rgba(127,179,240,.14), transparent 64%),
    radial-gradient(760px 400px at 6% 112%, rgba(58,96,150,.20), transparent 62%);

  --btn-ghost-line: var(--line);
  --avatar-ring: rgba(255, 255, 255, .09);
  /* The Google badge is a white pill in both themes. On navy it pops on its
     own; on white it needs an edge, so only light gives it a border. */
  --badge-line:  transparent;
  --star:        #f0a500;
  --heart:       #e8767c;
  --quote-op:    .09;
  --splash-glow:  rgba(127,179,240,.15);
  --splash-floor: rgba(0,0,0,.22);

  --shadow:      0 18px 44px rgba(5, 10, 20, 0.55);
  --shadow-card: 0 1px 2px rgba(0, 0, 0, .22), 0 10px 26px rgba(5, 10, 20, .34);
}

/* The theme flip is deliberately instant.
 *
 * A `transition: background-color` on <body> was the obvious first idea and it
 * is a trap. In any environment that is not compositing frames — embedded
 * webviews, and the same class of browser that already broke this project's
 * scroll and IntersectionObserver code — a started transition never advances,
 * and the page is left stuck on the *old* theme's background with the new
 * theme's cards on top of it. Verified: with the transition declared the body
 * reads rgb(26,35,50) after switching to light; remove it and the same element
 * resolves to #ffffff immediately.
 *
 * Losing a 250ms cross-fade costs nothing. A white page with navy furniture is
 * a broken site.
 *
 * The same reasoning applies to transitions that already existed for hover —
 * `.nav` animates its background, `.btn` its border. A theme flip retriggers
 * every one of them, so the header cross-fades over 250ms while the buttons
 * take 160ms and the cards change instantly. Even where it does not freeze,
 * that staggered ripple is what makes a theme toggle feel cheap.
 *
 * So the switch turns every transition off for one beat. Theme.apply() adds
 * this class, commits the new attribute, and drops it again on a timer. If that
 * timer somehow never runs, transitions simply stay off — the colours are still
 * correct, which is the failure this whole comment exists to guarantee. */
.theme-switching, .theme-switching *,
.theme-switching *::before, .theme-switching *::after {
  transition: none !important;
}

/* ── Base ─────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* The nav is sticky, so an anchor jump would land with the heading hidden
     underneath it. This offsets every scroll target by the nav height —
     cleaner and jank-free compared with doing it in JavaScript. */
  scroll-padding-top: 84px;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

h1, h2, h3, h4 { margin: 0; line-height: 1.12; font-weight: 800; letter-spacing: -0.02em; }
p { margin: 0; }

:focus-visible {
  outline: 2px solid var(--accent-hi);
  outline-offset: 3px;
  border-radius: 4px;
}

.container { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 22px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: 12px; top: -60px; z-index: 200;
  background: var(--accent-fill); color: var(--on-accent); padding: 10px 16px;
  border-radius: var(--radius-sm); font-weight: 700; transition: top .15s;
}
.skip-link:focus { top: 12px; }

/* ── Buttons ──────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 13px 24px; border-radius: 100px; border: 1px solid transparent;
  font: inherit; font-weight: 700; font-size: 15px; text-decoration: none;
  cursor: pointer; transition: transform .12s ease, background .16s, border-color .16s, color .16s;
  white-space: nowrap;
}
.btn:active { transform: translateY(1px); }

/* The doubled class is deliberate, and it has been earned twice.
 *
 * A button is almost always inside some region that also says "links in here
 * are brass" — `.nav-links a`, `.footer a`, `.footer-direct a`. Those rules sit
 * later in the file, so with a single class they win, and the button's label
 * silently repaints to a link colour on top of a brass fill. That shipped once
 * in the nav (grey on brass, unreadable) and again in the footer.
 *
 * `.btn.btn-primary` is (0,2,0) and outranks any `.region a` rule, so a new
 * section style can never take a button's label with it.
 *
 * Near-black on brass is 7.5:1. Note it is the *fill* brass — `--accent` is
 * deliberately too dark to sit a black label on. */
.btn.btn-primary { background: var(--accent-fill); color: var(--on-accent); }
/* Hover darkens the brass rather than brightening it — on a white page a
   lighter hover reads as the button switching off. */
.btn.btn-primary:hover { background: var(--accent-fill-hi); color: var(--on-accent); }
.btn.btn-ghost { background: transparent; color: var(--text); border-color: var(--btn-ghost-line); }
.btn.btn-ghost:hover { border-color: var(--accent-line); color: var(--accent-hi); }
.btn-sm { padding: 9px 18px; font-size: 14px; }
.btn-lg { padding: 16px 30px; font-size: 16px; }
.btn-block { width: 100%; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn:disabled:active { transform: none; }

/* ── Top bar ──────────────────────────────────────────────────────────────
   Thin strip above the header for social links and the phone number — the
   pattern both competitors use, and it gets two pieces of contact information
   above the fold without spending any of the header's attention on them.
   ────────────────────────────────────────────────────────────────────────── */
/* Kept, but not currently used on any public page — the strip above the nav was
   removed at the client's request and everything on it (social, phone) moved
   into the nav itself. Re-adding it is a markup change only. Same for
   `.demo-ribbon` further down. */
.topbar-strip {
  background: var(--bg-alt);
  border-bottom: 1px solid var(--line-soft);
  font-size: 13px;
}
.topbar-strip .container {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; min-height: 40px;
}
.topbar-note { color: var(--text-faint); }
.social-row { display: flex; align-items: center; gap: 6px; }
.social-row a,
.social-row .is-pending {
  display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: 50%;
  color: var(--text-dim); text-decoration: none;
  transition: color .18s, background .18s;
}
.social-row a:hover { color: var(--accent); background: var(--accent-soft); }

/* An account the shop has not sent over yet. Present, so the row keeps its
   shape, but visibly not a control — no hover, no pointer, and faint enough
   that it reads as "not yet" rather than as a broken link. */
.social-row .is-pending { color: var(--text-faint); opacity: .45; cursor: default; }

/* ── Theme toggle ─────────────────────────────────────────────────────────
   Sun and moon are stacked in the same box and cross-faded, so the button
   never changes size mid-swap and there is no reflow of the row it sits in.
   The icon shown is the theme you will GET, not the one you are in — that is
   the convention people already read correctly without a tooltip.
   ────────────────────────────────────────────────────────────────────────── */
.theme-toggle {
  position: relative; flex: none;
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: none; border: 1px solid transparent; color: var(--text-dim);
  cursor: pointer; padding: 0;
  transition: color .18s, background .18s, border-color .18s;
}
.theme-toggle:hover { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-line); }
.theme-toggle svg {
  grid-area: 1 / 1; width: 17px; height: 17px; fill: currentColor;
  transition: opacity .22s ease, transform .32s cubic-bezier(.2,.8,.2,1);
}
.theme-toggle .i-sun  { opacity: 0; transform: rotate(-90deg) scale(.6); }
.theme-toggle .i-moon { opacity: 1; transform: none; }
/* In dark, the button offers to take you back to light. */
:root[data-theme='dark'] .theme-toggle .i-sun  { opacity: 1; transform: none; }
:root[data-theme='dark'] .theme-toggle .i-moon { opacity: 0; transform: rotate(90deg) scale(.6); }

/* One mount, two presentations. On a wide screen it is the icon alone; in the
   phone panel the label appears beside it, because this is the one control on
   the site whose effect a glyph does not explain. Same button either way, so
   there is no second copy to keep in sync. */
.nav-theme-label { display: none; }
.social-row svg { width: 16px; height: 16px; fill: currentColor; display: block; }

/* ── Header ───────────────────────────────────────────────────────────────
   Logo hard left, everything else hard right — the arrangement Omar's uses and
   the one a shop sign actually has. The centred logo it replaced looked good
   empty but squeezed the right-hand group until the phone number wrapped onto
   two lines.
   ────────────────────────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 100;
  background: var(--nav-bg);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line-soft);
  transition: box-shadow .25s, background .25s;
}
.nav.is-stuck { box-shadow: var(--nav-shadow); }

/* The header runs wider than the page's 1140px body.
 *
 * A centred logo needs room for the WIDER of the two side groups on both
 * sides, not the average — the right group (social, theme, phone, button) is
 * 430px against the pages' 280px, so at 1140 the logo overlapped the social
 * icons by 32px even though it was mathematically dead centre. 1300 gives the
 * narrow side its surplus and still leaves ~48px of clear air on the tight
 * side. A header wider than the body it sits above is a deliberate look, not
 * an accident. */
.nav .container {
  position: relative;
  max-width: 1300px;
  display: flex; align-items: center;
  padding-top: 18px; padding-bottom: 18px;
}

/* Pages left, logo centred, everything else right.
 *
 * The logo is taken out of flow and centred on the container, rather than
 * being a middle column. A `1fr auto 1fr` grid centres the logo in the *grid*,
 * which is only the same thing when the two side groups are equal width — and
 * here the right group (social, theme, phone, button) is nearly twice the left.
 * That is what pulled the logo visibly off-centre last time.
 *
 * The side groups are one flex row with `margin-left: auto` on the first
 * utility item, so the page links sit hard left, the utilities hard right, and
 * the space they leave in the middle is where the logo lands. See the
 * breakpoint note below for why that space is always big enough. */
.brand {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  /* Generous on the sides, gentle top and bottom — the gap either side of a
     shop sign is what makes it read as a sign rather than as another item in
     the row. */
  padding: 6px clamp(30px, 4.5vw, 66px);
  display: grid; grid-template-columns: auto auto; align-items: center;
  column-gap: 13px; text-decoration: none;
  /* No `transition: transform` here. It was left over from a hover lift that no
     longer exists, and now that transform carries the centring it is actively
     harmful: crossing the breakpoint animates translate(-50%,-50%) → none, and
     where the clock is stopped it parks on the start value and drags the logo
     112px off the left edge of the screen. Measured. */
}
.brand-mark { grid-row: 1 / 3; grid-column: 1; }
.brand-name { grid-column: 2; align-self: end; }
.brand-tagline { grid-column: 2; align-self: start; }

.brand-mark {
  width: 56px; height: 56px; border-radius: 14px; flex: none;
  background: linear-gradient(150deg, var(--accent-fill) 0%, #0e4485 100%);
  display: grid; place-items: center; color: var(--on-accent); font-size: 26px; font-weight: 900;
  box-shadow: 0 8px 22px rgba(21,101,192,.24);
  transition: width .28s, height .28s, font-size .28s;
}
.brand-name {
  font-weight: 800; font-size: 19px; letter-spacing: .01em;
  transition: font-size .28s, opacity .2s;
}
.brand-tagline {
  font-size: 10.5px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--accent); font-weight: 700;
}

/* Shrink the header once the page scrolls, so the logo is generous on arrival
   and out of the way afterwards. */
.nav.is-stuck .container { padding-top: 8px; padding-bottom: 8px; }
.nav.is-stuck .brand-mark { width: 38px; height: 38px; font-size: 18px; }
.nav.is-stuck .brand-name { font-size: 16px; }
.nav.is-stuck .brand-tagline { display: none; }

/* One list, two groups. The four page links run from the left edge; the
   utilities are pushed to the right edge by `margin-left: auto` on the first
   of them, and the space that opens between the two is the logo's. Social
   leads the utilities, immediately after the last page link. */
.nav-links {
  flex: 1; display: flex; align-items: center;
  gap: 28px; list-style: none; margin: 0; padding: 0;
}
.nav-links > li { display: flex; align-items: center; }

/* The theme switch sits with the nav links, not with the utilities — it is a
   page control, and beside Location it is found without hunting between the
   phone number and the icon row. The social row still opens the right-hand
   group, so `margin-left: auto` moves with it. */
.nav-links .nav-theme + .nav-social { margin-left: auto; }
.nav-links .nav-social .social-row { gap: 4px; }
.nav-links .nav-social .social-row a { width: 36px; height: 36px; }
.nav-links .nav-social .social-row svg { width: 19px; height: 19px; }
.nav-links .nav-theme { margin-left: -12px; }

/* `:not(.btn)` matters. This rule sits after .btn-primary in the file, so
   without it the Book Now button in the nav inherited --text-dim and rendered
   muted grey on brass — unreadable, and the first thing you notice. */
.nav-links a:not(.btn) {
  text-decoration: none; color: var(--text-dim); font-size: 14.5px; font-weight: 600;
  transition: color .15s;
}
.nav-links a:not(.btn):hover { color: var(--text); }

.nav-book { flex: none; margin-left: 24px; }

/* The phone now lives inside `.nav-links`, so `.nav-links a:not(.btn)` matches
   it and outranks a bare `.nav-phone` — which is exactly how it ended up dim
   grey and half a shade from invisible. The extra class wins it back. The
   number is the one thing in the header someone might be squinting at while
   holding a phone in the other hand, so it gets full --text, not --text-dim. */
.nav-links a.nav-phone, .nav-phone {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  color: var(--text); text-decoration: none; font-size: 14.5px; font-weight: 700;
}
.nav-links a.nav-phone:hover, .nav-phone:hover { color: var(--accent-hi); }

/* Scroll-spy: the section you are looking at is marked in the nav. Cheap to
   do and it is most of what makes a one-pager feel navigable rather than
   like one long scroll. */
.nav-links a[aria-current='true'] { color: var(--accent); }

.nav-toggle {
  display: none; background: none; border: 1px solid var(--line); color: var(--text);
  border-radius: 10px; width: 44px; height: 44px; cursor: pointer;
  place-items: center; padding: 0; flex: none;
}
.nav-toggle-bars { display: block; width: 18px; }
.nav-toggle-bars i {
  display: block; height: 2px; background: currentColor; border-radius: 2px;
  transition: transform .28s cubic-bezier(.2,.8,.2,1), opacity .2s;
}
.nav-toggle-bars i + i { margin-top: 4px; }
/* Bars morph into a cross rather than swapping to a "✕" glyph — smoother, and
   it does not shift the button width mid-animation. */
.nav-toggle[aria-expanded='true'] .nav-toggle-bars i:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded='true'] .nav-toggle-bars i:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded='true'] .nav-toggle-bars i:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* The full row needs real room; below this the pages collapse behind the
   toggle, but Book Now stays out in the header where it can be tapped without
   opening anything first. */
/* The centred logo needs a real gap between the two side groups to sit in.
   Below this there is not one, so the layout goes back to logo-left with the
   pages behind the toggle — measured minimum for the three-part row is ~1283px,
   so the switch happens with room to spare rather than at the exact moment the
   logo would start touching a link. */
@media (max-width: 1280px) {
  .nav .container { gap: 12px; padding-top: 11px; padding-bottom: 11px; }
  /* Back into the flow, hard left. */
  .brand {
    position: static; transform: none; padding: 0;
    margin-right: auto;
  }
  .nav-toggle { display: grid; }
  .brand-mark { width: 44px; height: 44px; font-size: 20px; }
  .brand-name { font-size: 16.5px; }
  .brand-tagline { font-size: 9.5px; letter-spacing: .18em; }

  /* A dropdown panel, not a full-screen sheet.
   *
   * The sheet was `position: fixed; inset: 0`, and it was broken on every
   * phone: `.nav` carries `backdrop-filter`, and a filter or backdrop-filter
   * makes an element the containing block for its fixed-position descendants.
   * So `inset: 0` resolved against the 126px-tall header instead of the
   * viewport, and the "full-screen" menu rendered as a stub with the page
   * showing through underneath it.
   *
   * Anchoring the panel to the header with `position: absolute` sidesteps that
   * completely — and with only four links it is the better shape anyway. */
  /* The panel opens instantly, on `display` alone. No transition, no animation.
   *
   * Both were tried and both fail the same way. The sheet this replaced hid
   * itself with `opacity: 0; visibility: hidden` and transitioned them; where
   * the browser is not compositing, that transition never advances and tapping
   * the toggle appears to do nothing — measured, the panel still read
   * `visibility: hidden` after a click. Swapping to a keyframe animation was no
   * better: with the clock stopped it simply parks on the `from` frame, which
   * measured opacity 0 and a 10px offset that pushed the panel up under the
   * header.
   *
   * Anything whose *start* state is invisible can strand the menu shut. A menu
   * that cannot be opened is a broken site; a menu that appears without sliding
   * is a menu. Same trade already made for the theme flip. */
  .nav-links {
    display: none;
    position: absolute; top: 100%; left: 0; right: 0; z-index: 90;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: 6px 22px 18px;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow);
    max-height: calc(100dvh - 100%); overflow-y: auto;
  }
  .nav-links.open { display: flex; }

  .nav-links > li { border-bottom: 1px solid var(--line-soft); }
  .nav-links > li:last-child { border-bottom: 0; }
  /* The desktop rules that push the utilities to the right edge have to be
     unwound at matching specificity, or `margin-left: auto` shunts the social
     row to the far side of a full-width panel. */
  .nav-links .nav-theme + .nav-social { margin-left: 0; }
  .nav-links .nav-theme { margin-left: 0; }
  .nav-links a:not(.btn) {
    display: flex; align-items: center; width: 100%; min-height: 54px;
    font-size: 17.5px; font-weight: 700; color: var(--text);
  }
  .nav-links a[aria-current='true'] { color: var(--accent); }

  /* Social becomes a real row of tap targets rather than 30px decorations. */
  .nav-links .nav-social { padding: 12px 0; }
  .nav-links .nav-social .social-row { gap: 8px; }
  .nav-links .nav-social .social-row a { width: 46px; height: 46px; }
  .nav-links .nav-social .social-row svg { width: 20px; height: 20px; }

  /* The theme switch is the one control whose effect a glyph does not explain,
     and a panel has room for words. */
  .nav-links .nav-theme {
    justify-content: space-between; gap: 16px; min-height: 56px; margin-left: 0;
  }
  .nav-theme-label { display: inline; font-size: 16px; font-weight: 700; color: var(--text); }
  .nav-links .nav-theme .theme-toggle { width: 46px; height: 46px; border-color: var(--line); }
  .nav-links .nav-theme .theme-toggle svg { width: 20px; height: 20px; }

  .nav-links .nav-phone-item { padding: 6px 0; }
  .nav-links a.nav-phone { min-height: 48px; font-size: 16px; }

  html.menu-open, html.menu-open body { overflow: hidden; }
}

/* Very narrow: the tagline under the mark is the first thing to go, then the
   shop name, so the logo never crowds Book Now. */
@media (max-width: 430px) {
  .brand-tagline { display: none; }
  .brand-name { font-size: 15.5px; }
  .nav-book { padding-left: 14px; padding-right: 14px; }
}
@media (max-width: 350px) {
  .brand-name { display: none; }
}

/* ── Status pill (open now / next available) ──────────────────────────── */
.status {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 8px 16px 8px 13px; border-radius: 100px;
  font-size: 13.5px; font-weight: 700; letter-spacing: .01em;
  border: 1px solid var(--line);
  background: var(--surface);
}
.status.is-open   { background: var(--ok-soft);     border-color: var(--ok-line);     color: var(--ok); }
.status.is-closed { background: var(--closed-soft); border-color: var(--closed-line); color: var(--closed); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }
.status.is-open .status-dot { animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .status.is-open .status-dot { animation: none; }
  * { transition-duration: .01ms !important; }
}

/* ── Hero ─────────────────────────────────────────────────────────────── */
/* A real photo behind the hero, dimmed hard enough that white text sits at
   AA over any part of it. Both competitors do this and it is most of what
   separates "a website" from "a shop with a website". */
.hero-bg {
  position: absolute; inset: 0; z-index: 0;
  background-size: cover; background-position: center;
  /* Scaled slightly so the blur never shows a soft edge at the frame. */
  filter: blur(2px) saturate(1.05); transform: scale(1.06);
  opacity: var(--hero-photo);
}

/* ── Hero video ───────────────────────────────────────────────────────────
   Sits inside `.hero-bg`, so it inherits the same blur, scale and scrim the
   photo gets and the two are interchangeable.

   `--hero-photo` is overridden up for video. A still can afford to be a faint
   texture; a clip at the same opacity is just a grey shimmer and nobody can
   tell what it is — which defeats the point of filming it.

   The scrim is shaped rather than flat. A wash heavy enough to guarantee the
   headline everywhere also drains the footage everywhere, which is the trade
   the first cut made and it cost too much: at p = .60 under a .38 wash the
   clip read as pale blue-grey haze. Contrast is only needed where the words
   are, so almost all of it is now a soft ellipse behind the text block and the
   full-bleed layer is light enough to leave the frame recognisable.

   Measured, not estimated: sampled against the darkest pixel under each line
   across twelve frames of the shipped clip, in both themes. Worst of the
   twelve —

     h1     4.5:1   (73.6px, so large text; its bar is 3:1)
     sub    6.6:1   (17.5px, bar 4.5:1)
     meta   4.8:1   (14px,   bar 4.5:1)

   Away from the text the ellipse falls off to ~.14 total alpha, which is where
   the footage actually shows and where nothing has to be legible. Re-measure
   if the clip is ever swapped: a much darker cut eats the h1 margin first.
   The check is in git history — sample the video into a canvas, composite
   video·p over --bg then the scrim over that, and compare against the computed
   colour of each line.
   ────────────────────────────────────────────────────────────────────────── */
.hero-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  /* Faded in on `playing`, so a decoding first frame never shows as black. */
  opacity: 0; transition: opacity .6s ease;
}
.hero-video.is-ready { opacity: 1; }

/* Scoped to a hero that actually got a video, so the photo hero is untouched. */
.hero-bg:has(.hero-video) {
  --hero-photo: .92;
  filter: saturate(1.04);
}
.hero-bg:has(.hero-video)::after {
  background:
    radial-gradient(940px 700px at 50% 52%, rgba(255,255,255,.54), transparent 76%),
    linear-gradient(180deg, rgba(255,255,255,.09) 0%, rgba(255,255,255,.14) 55%, var(--bg) 100%);
}
:root[data-theme='dark'] .hero-bg:has(.hero-video)::after {
  background:
    radial-gradient(940px 700px at 50% 52%, rgba(19,26,38,.58), transparent 76%),
    linear-gradient(180deg, rgba(19,26,38,.22) 0%, rgba(19,26,38,.34) 55%, var(--bg) 100%);
}

/* Dimmed greys are a device for a calm flat ground; over moving footage they
   stop being "secondary" and start being "hard to read" — measured, the sub
   dropped to 2.9:1 against the darkest frame. Over video the whole text column
   goes to full --text and the hierarchy is carried by size and weight instead,
   which is what actually separates these three lines anyway. */
.hero:has(.hero-video) .hero-sub,
.hero:has(.hero-video) .hero-meta { color: var(--text); }
/* The meta line sits at the bottom of the column where the ellipse has already
   fallen off, so it gets no opacity haircut on top of that — measured, .82 put
   it at 3.7:1. The ellipse is pulled down far enough to reach it. */
.hero:has(.hero-video) .hero-meta .dot { opacity: .45; }
/* The barber-pole stripes are removed so the hero background remains crisp and clean. */
.hero::after,
.hero:has(.hero-video)::after,
.hero:has(.hero-video)::before {
  display: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .hero-video { display: none; }
}
.hero-bg::after {
  content: ''; position: absolute; inset: 0;
  background: var(--hero-scrim);
}

.hero { position: relative; padding: 74px 0 82px; overflow: hidden; }
/* Two warm light sources rather than one flat wash — a brass glow top-right
   and a deeper amber pooling bottom-left. */
.hero::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: var(--hero-glow);
}
.hero .container { position: relative; z-index: 1; }

/* Centred single column. The opening-hours card that used to sit alongside is
   gone — hours live in the footer, and the hero's only job is to say what this
   is and give one obvious thing to tap. Fewer decisions above the fold. */
.hero-inner { max-width: 46rem; margin: 0 auto; text-align: center; position: relative; z-index: 1; }

/* Google rating badge. AJ's leads with this and it is the single most
   persuasive element on their page — social proof before any claim we make
   about ourselves. */
.google-badge {
  display: inline-flex; align-items: center; gap: 10px;
  background: #fff; color: #1f2328;
  border: 1px solid var(--badge-line); border-radius: 100px; padding: 6px 16px 6px 7px;
  font-size: 13.5px; font-weight: 600; text-decoration: none;
  box-shadow: var(--shadow-card);
  transition: transform .18s;
}
.google-badge:hover { transform: translateY(-2px); }
.google-badge .g-score {
  background: #1f2328; color: #fff; border-radius: 100px;
  padding: 3px 10px; font-weight: 800; font-variant-numeric: tabular-nums;
}
/* Not --star: the badge is a white pill in both themes, so its stars take
   the light theme's dark gold either way. Bright gold here is 2.1:1. */
.google-badge .g-stars { color: #9a6d04; letter-spacing: 1px; }
.google-badge .g-count { color: #5f6672; }

.hero h1 {
  font-size: clamp(2.5rem, 7vw, 4.6rem);
  white-space: pre-line;
  margin: 22px 0 20px;
}
.hero h1 em { font-style: normal; color: var(--accent); }

.hero-sub {
  color: var(--text-dim); font-size: 17.5px;
  max-width: 34rem; margin: 0 auto 32px;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-bottom: 28px; }

.hero-meta {
  display: flex; flex-wrap: wrap; gap: 6px 12px; justify-content: center;
  color: var(--text-faint); font-size: 14px;
}
.hero-meta .dot { opacity: .5; }

.hero-scroll {
  display: grid; place-items: center;
  width: 42px; height: 42px; margin: 54px auto 0;
  border: 1px solid var(--line); border-radius: 50%;
  color: var(--text-faint); text-decoration: none; font-size: 15px;
  animation: hero-bob 2.6s ease-in-out infinite;
  transition: border-color .2s, color .2s;
}
.hero-scroll:hover { border-color: var(--accent-line); color: var(--accent); }
@keyframes hero-bob { 0%,100% { transform: translateY(0) } 50% { transform: translateY(6px) } }

/* Hours card */
.hours-card {
  background: linear-gradient(170deg, var(--surface) 0%, var(--bg-alt) 100%);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 24px 26px; box-shadow: var(--shadow);
}
.hours-card h3 {
  font-size: 12px; text-transform: uppercase; letter-spacing: .14em;
  color: var(--accent); margin-bottom: 16px; font-weight: 700;
}
.hours-row {
  display: flex; justify-content: space-between; gap: 16px;
  padding: 9px 0; border-bottom: 1px solid var(--line-soft); font-size: 14.5px;
}
.hours-row:last-of-type { border-bottom: 0; }
.hours-row.today { color: var(--accent-hi); font-weight: 700; }
.hours-row .day { color: var(--text-dim); }
.hours-row.today .day { color: var(--accent-hi); }
.hours-row .time { font-variant-numeric: tabular-nums; }
.hours-note {
  margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--line-soft);
  font-size: 13.5px; color: var(--text-faint);
}

@media (max-width: 900px) {
  .hero { padding: 40px 0 46px; }
  .hero-scroll { display: none; }
}

/* ── Sections ─────────────────────────────────────────────────────────── */
.section { padding: 78px 0; border-top: 1px solid var(--line-soft); }
/* Alternating bands give the long page rhythm; the gradient keeps the warmth
   from going flat over a full screen height. */
.section.alt {
  background: linear-gradient(180deg, var(--bg-alt), var(--band-mid) 60%, var(--bg-alt));
}

.section-head { margin-bottom: 38px; max-width: 46rem; }
.eyebrow {
  display: block; font-size: 12px; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 12px;
}
.section-head h2 { font-size: clamp(1.85rem, 3.6vw, 2.5rem); margin-bottom: 12px; }
.section-head p { color: var(--text-dim); font-size: 16.5px; }

/* ── Services ─────────────────────────────────────────────────────────── */
.svc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: 14px; }

.svc {
  display: flex; align-items: flex-start; gap: 16px;
  background: linear-gradient(160deg, var(--surface), var(--surface-2));
  box-shadow: var(--shadow-card);
  border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px 22px;
  text-decoration: none; transition: border-color .16s, background .16s, transform .16s;
}
.svc:hover { border-color: var(--accent-line); background: var(--surface-2); transform: translateY(-2px); }

.svc { align-items: center; }
.svc-body { flex: 1; min-width: 0; }
.svc-name { font-weight: 700; font-size: 16.5px; display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.svc-desc { color: var(--text-faint); font-size: 14px; margin-top: 5px; }

/* Prices and durations live in Square and appear at booking. The arrow is the
   only affordance the card needs — it says "this leads somewhere" without
   pretending to be a price list. */
.svc-go {
  flex: none; color: var(--text-faint); font-size: 18px;
  transition: transform .2s, color .2s;
}
.svc:hover .svc-go { color: var(--accent); transform: translateX(4px); }

.tag-popular {
  font-size: 10.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid var(--accent-line); padding: 2px 8px; border-radius: 100px;
}

.svc-foot {
  margin-top: 24px; padding: 16px 20px; border-radius: var(--radius);
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  color: var(--text-dim); font-size: 14.5px;
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
}

/* ── Barbers ──────────────────────────────────────────────────────────── */
.filter-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 26px;
}
.filter-label { font-size: 13.5px; color: var(--text-faint); margin-right: 4px; }
.chip {
  background: var(--surface); border: 1px solid var(--line); color: var(--text-dim);
  padding: 7px 15px; border-radius: 100px; font: inherit; font-size: 13.5px; font-weight: 600;
  cursor: pointer; transition: all .15s;
}
.chip:hover { border-color: var(--accent-line); color: var(--text); }
.chip[aria-pressed='true'] {
  background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent);
}

.barber {
  background: linear-gradient(165deg, var(--surface), var(--surface-2));
  box-shadow: var(--shadow-card);
  border: 1px solid var(--line);
  border-radius: var(--radius); padding: 24px; display: flex; flex-direction: column;
  transition: border-color .16s, transform .16s;
}
.barber:hover { border-color: var(--accent-line); transform: translateY(-2px); }
.barber[hidden] { display: none; }

.barber-top { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; }
.avatar {
  width: 60px; height: 60px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  font-size: 21px; font-weight: 800; color: #101720;
  border: 2px solid var(--avatar-ring);
}
.barber-name { font-size: 17.5px; font-weight: 800; }
.barber-title { font-size: 13.5px; color: var(--accent); font-weight: 600; margin-top: 2px; }
.barber-bio { color: var(--text-faint); font-size: 14.5px; flex: 1; }

.badges { display: flex; flex-wrap: wrap; gap: 7px; margin: 15px 0; }
.badge {
  font-size: 12px; font-weight: 600; padding: 4px 11px; border-radius: 100px;
  background: var(--bg); border: 1px solid var(--line); color: var(--text-dim);
}
.badge.lang { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-hi); }

.barber-next {
  font-size: 13.5px; color: var(--text-dim); margin-bottom: 14px;
  padding-top: 14px; border-top: 1px solid var(--line-soft);
}
.barber-next b { color: var(--ok); font-weight: 700; }

.empty-state {
  grid-column: 1 / -1; text-align: center; padding: 42px 20px;
  color: var(--text-faint); border: 1px dashed var(--line); border-radius: var(--radius);
}

/* Escape hatch for the customer who does not have a regular yet. Without it,
   picking a barber reads as compulsory. */
.team-any {
  text-align: center; margin-top: 26px;
  color: var(--text-faint); font-size: 15px;
}
.team-any a { color: var(--accent-hi); text-decoration: none; font-weight: 600; }
.team-any a:hover { text-decoration: underline; }

/* ── Inner-page hero ──────────────────────────────────────────────────────
   Shorter than the homepage hero and centred. It has one job — say what the
   page is — so it gets about half the height and none of the status furniture.
   ────────────────────────────────────────────────────────────────────────── */
.page-hero { position: relative; overflow: hidden; padding: 84px 0 72px; text-align: center; }
.page-hero::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: var(--page-glow);
}
.page-hero .container { position: relative; z-index: 1; }
.page-hero h1 {
  font-size: clamp(2.1rem, 5.2vw, 3.2rem); line-height: 1.07; letter-spacing: -.02em;
}
.page-hero > .container > p {
  color: var(--text-dim); font-size: 17px; margin: 16px auto 0; max-width: 40rem;
}
.page-hero-actions {
  display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 30px;
}

/* ── Team cards (services page) ───────────────────────────────────────────
   Omar's barber cards are a photo, a name and a button. These carry the title,
   a line of story and the real next opening, so choosing a barber is an
   informed decision rather than a coin flip on a face.
   ────────────────────────────────────────────────────────────────────────── */
.team-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(258px, 1fr)); gap: 20px; }

.team-card {
  background: linear-gradient(165deg, var(--surface), var(--surface-2));
  box-shadow: var(--shadow-card);
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; display: flex; flex-direction: column;
  transition: border-color .16s, transform .16s;
}
.team-card:hover { border-color: var(--accent-line); transform: translateY(-3px); }

/* A fixed portrait ratio so a row of cards lines up no matter what the shop
   uploads — phone snapshots are never all the same shape. */
.team-photo {
  width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block;
  background-size: cover; background-position: center;
  border-bottom: 1px solid var(--line-soft);
}
.team-photo-placeholder {
  display: grid; place-items: center;
  font-size: 46px; font-weight: 800; color: #101720; letter-spacing: .02em;
}

.team-body { padding: 20px 20px 22px; display: flex; flex-direction: column; flex: 1; }
.team-name { font-size: 18px; font-weight: 800; }
.team-title { font-size: 13px; font-weight: 600; color: var(--accent); margin-top: 3px; }
.team-bio { color: var(--text-faint); font-size: 14.5px; margin-top: 12px; flex: 1; }
.team-next {
  font-size: 13px; color: var(--text-dim); margin: 14px 0;
  padding-top: 14px; border-top: 1px solid var(--line-soft);
}
.team-next b { color: var(--ok); font-weight: 700; }

/* ── Service menu (services page) ─────────────────────────────────────────
   Grouped the way a barber talks about the work — cuts, beard, the two
   together, threading — and every row shows the duration next to the price.
   Neither competitor publishes durations, and "45 min" is what tells someone
   whether a skin fade fits in a lunch break.
   ────────────────────────────────────────────────────────────────────────── */
.menu-group { margin-bottom: 42px; }
.menu-group:last-of-type { margin-bottom: 0; }

.menu-group-head {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  padding-bottom: 12px; border-bottom: 1px solid var(--line);
}
.menu-group-head h3 { font-size: 20px; font-weight: 800; letter-spacing: -.01em; }
.menu-group-head p { color: var(--text-faint); font-size: 14px; }

.menu-list { list-style: none; margin: 0; padding: 0; }
.menu-item {
  display: flex; align-items: center; gap: 20px;
  padding: 16px 14px; border-radius: 12px; text-decoration: none;
  border-bottom: 1px solid var(--line-soft);
  transition: background .16s;
}
.menu-list li:last-child .menu-item { border-bottom: 0; }
.menu-item:hover { background: var(--surface); }

.menu-item-main { flex: 1; min-width: 0; }
.menu-item-name {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  font-size: 16.5px; font-weight: 700; color: var(--text);
}
.menu-item-desc { display: block; color: var(--text-faint); font-size: 14px; margin-top: 4px; }

.menu-item-meta { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.menu-item-dur { font-size: 12.5px; color: var(--text-faint); white-space: nowrap; }
.menu-item-price {
  font-size: 19px; font-weight: 800; color: var(--accent-hi);
  font-variant-numeric: tabular-nums;
}

.menu-foot { margin-top: 32px; text-align: center; color: var(--text-faint); font-size: 14.5px; }
.work-more { text-align: center; margin-top: 28px; }

/* On a phone the cards turn sideways — a portrait photo per barber at full
   width would be half a screen each, and six of them is a lot of scrolling to
   reach the prices. Landscape keeps the story visible, which is the whole
   reason the section exists. */
@media (max-width: 560px) {
  .page-hero { padding: 52px 0 46px; }
  .page-hero-actions .btn { width: 100%; }

  .team-grid { grid-template-columns: 1fr; gap: 12px; }
  .team-card { flex-direction: row; align-items: stretch; }
  .team-photo {
    width: 116px; flex: none; aspect-ratio: auto; align-self: stretch;
    border-bottom: 0; border-right: 1px solid var(--line-soft);
  }
  .team-photo-placeholder { font-size: 30px; }
  .team-body { padding: 15px 16px 16px; }
  .team-bio { margin-top: 8px; font-size: 14px; }
  .team-next { margin: 11px 0; padding-top: 11px; }

  .menu-item { gap: 14px; padding: 15px 10px; }
  .menu-item-price { font-size: 17.5px; }
}

/* ── Legal pages ──────────────────────────────────────────────────────────
   Long-form prose, so the column is narrow enough to read comfortably and the
   headings carry real space above them. Nobody reads these pages for pleasure;
   the least we can do is not make them hard work.
   ────────────────────────────────────────────────────────────────────────── */
.legal { border-top: 0; }
.legal .container { max-width: 44rem; }
.legal h1 { font-size: clamp(1.9rem, 4.4vw, 2.6rem); letter-spacing: -.02em; }
.legal h2 {
  font-size: 19px; font-weight: 800; margin: 38px 0 10px;
  padding-top: 18px; border-top: 1px solid var(--line-soft);
}
.legal p { color: var(--text-dim); font-size: 16px; line-height: 1.72; margin-bottom: 12px; }
.legal ul { color: var(--text-dim); font-size: 16px; line-height: 1.72; padding-left: 20px; }
.legal li { margin-bottom: 8px; }
.legal li b, .legal p b { color: var(--text); font-weight: 700; }
.legal a { color: var(--accent-hi); }
.legal-updated { font-size: 13.5px; color: var(--text-faint); margin-top: 10px; }

/* ── Location ─────────────────────────────────────────────────────────── */
.loc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: stretch; }
.loc-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 28px;
}
.loc-card h3 { font-size: 19px; margin-bottom: 14px; }
.loc-line { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line-soft); }
.loc-line:last-child { border-bottom: 0; }
.loc-line .k { color: var(--text-faint); font-size: 13.5px; min-width: 74px; flex: none; }
.loc-line .v { font-size: 15px; }

/* Hours as three stacked rows, not one run-on line. */
.loc-hours-row {
  display: flex; gap: 12px; justify-content: space-between;
  font-variant-numeric: tabular-nums;
}
.loc-hours-row + .loc-hours-row { margin-top: 5px; }
.loc-hours-row .d { color: var(--text-dim); min-width: 62px; }
.loc-hours-row .t { white-space: nowrap; }
.loc-hours-row.today .d, .loc-hours-row.today .t { color: var(--accent-hi); font-weight: 600; }
.loc-line .v:has(.loc-hours-row) { flex: 1; }
.loc-line a { color: var(--accent-hi); text-decoration: none; }
.loc-line a:hover { text-decoration: underline; }

.map-embed {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; min-height: 320px; background: var(--surface);
  display: grid; place-items: center; color: var(--text-faint); text-align: center; padding: 24px;
}
.map-embed iframe { width: 100%; height: 100%; min-height: 320px; border: 0; display: block; }

@media (max-width: 820px) { .loc-grid { grid-template-columns: 1fr; } }

/* ── About stats ──────────────────────────────────────────────────────── */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-top: 28px; }
.stat {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px; text-align: center;
}
.stat-v { font-size: 25px; font-weight: 800; color: var(--accent); }
.stat-k { font-size: 12.5px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .09em; margin-top: 5px; }

/* ── CTA band ─────────────────────────────────────────────────────────────
   The homepage band was removed at the client's request; `.cta-actions` is
   still used by the 404 page, so the block stays and stays theme-aware. */
.cta {
  background: var(--page-glow);
  border-top: 1px solid var(--accent-line); border-bottom: 1px solid var(--accent-line);
  padding: 58px 0; text-align: center;
}
.cta h2 { font-size: clamp(1.7rem, 3.4vw, 2.3rem); margin-bottom: 12px; }
.cta p { color: var(--text-dim); margin-bottom: 26px; }
.cta-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* ── Footer ───────────────────────────────────────────────────────────── */
/* Hours + address appear on every page. AJ's site omits both entirely, and
   "what time do they close" is one of the most common local searches. */
.footer {
  background: linear-gradient(180deg, var(--bg-alt), var(--footer-end));
  border-top: 1px solid var(--line);
  padding: 58px 0 26px;
}
.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1.1fr 1.1fr; gap: 34px; }

/* Brand block */
.footer-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; text-decoration: none; }
.footer-brand .brand-mark { width: 48px; height: 48px; font-size: 22px; border-radius: 12px; }
.footer-brand-name { font-weight: 800; font-size: 18px; color: var(--text); }
.footer-brand-tag {
  font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--accent); font-weight: 700;
}
.footer .social-row { margin-top: 14px; }
.footer .social-row a { border: 1px solid var(--line); }

/* Hours, grouped into ranges rather than seven near-identical rows. */
.footer-hours-list { list-style: none; margin: 0; padding: 0; }
.footer-hours-list li {
  display: flex; justify-content: space-between; gap: 14px;
  padding: 6px 0; font-variant-numeric: tabular-nums;
}
.footer-hours-list .d { color: var(--text-dim); }
.footer-hours-list .t { color: var(--text); font-weight: 600; white-space: nowrap; }
.footer-hours-list li.today .d,
.footer-hours-list li.today .t { color: var(--accent-hi); }

.footer-note {
  margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-soft);
  font-size: 13px; color: var(--text-faint);
}

/* Direct line block */
.footer-direct a { color: var(--accent-hi); text-decoration: none; font-weight: 600; }
.footer-direct a:hover { text-decoration: underline; }

.footer-legal {
  display: flex; gap: 18px; flex-wrap: wrap;
}
.made-with {
  display: inline-flex; align-items: center; gap: 6px;
}
.made-with .heart {
  color: var(--heart); display: inline-block;
  animation: heartbeat 2.4s ease-in-out infinite;
}
@keyframes heartbeat {
  0%, 100% { transform: scale(1); }
  8%  { transform: scale(1.25); }
  16% { transform: scale(1); }
  24% { transform: scale(1.18); }
  32% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .made-with .heart { animation: none; } }
.footer h4 {
  font-size: 12px; text-transform: uppercase; letter-spacing: .14em;
  color: var(--accent); margin-bottom: 14px;
}
.footer p, .footer li { color: var(--text-dim); font-size: 14.5px; }
.footer ul { list-style: none; margin: 0; padding: 0; }
.footer li { padding: 4px 0; }
/* `:not(.btn)` for the same reason the nav needs it: this rule sits after
   .btn-primary in the file, so without it the footer's Book button inherited
   --text-dim and rendered grey on brass — 3.3:1, and the one button down there
   that matters. */
.footer a:not(.btn) { color: var(--text-dim); text-decoration: none; }
.footer a:not(.btn):hover { color: var(--accent-hi); }
.footer .brand { margin-bottom: 14px; }
.footer-hours { display: flex; justify-content: space-between; gap: 14px; font-variant-numeric: tabular-nums; }
.footer-bottom {
  margin-top: 36px; padding-top: 20px; border-top: 1px solid var(--line-soft);
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  color: var(--text-faint); font-size: 13px;
}
@media (max-width: 980px) { .footer-grid { grid-template-columns: 1fr 1fr; gap: 30px; } }
@media (max-width: 620px) {
  .footer-grid { grid-template-columns: 1fr; gap: 30px; }
  .footer .social-row a { width: 44px; height: 44px; }

  /* On a phone the footer is a single column, so a left-aligned logo floating
     above centred-feeling content looks accidental. Centre the brand block and
     the credit; the three information columns stay left-aligned because lists
     of hours and addresses are read, not admired. */
  .footer-brand { align-items: center; text-align: center; width: 100%; }
  .footer .social-row { justify-content: center; }

  .footer-bottom {
    flex-direction: column; align-items: center; gap: 12px; text-align: center;
  }
  .footer-legal { justify-content: center; }
  .made-with { justify-content: center; }
}

/* ==========================================================================
   Intro splash
   Sits on top of a fully rendered page, so content is never blocked. See
   splash.js for the rules it has to obey.
   ========================================================================== */
/* Held back until the splash exists.
 *
 * splash.js builds the overlay from script, which cannot happen until the
 * scripts at the end of <body> run — by which point the browser has already
 * painted the page. The result was a frame or two of the real site, then the
 * logo dropping on top of it, which reads as a fault rather than an intro.
 *
 * An inline script in <head> adds this class before first paint, and splash.js
 * takes it off the moment the overlay is in the DOM. It has its own 3s failsafe
 * in that inline script, so a scripting error can never leave the page blank.
 * Only the homepage carries it, and only when the splash is actually going to
 * run — a repeat visit in the same session never sets it at all. */
html.splash-pending { background: var(--bg); }
html.splash-pending body { visibility: hidden; }

.splash {
  position: fixed; inset: 0; z-index: 9999;
  background: var(--bg);
  display: grid; place-items: center;
  overflow: hidden; cursor: pointer;
  animation: splash-in .45s ease both;
}
/* A quiet wash, and nothing else.
 *
 * This used to carry drifting barber-pole stripes — 3px of red at 11% every
 * 26px, on the diagonal. The idea was a nod to the pole in the logo; the effect
 * on a white screen was a faint pink hatching that read as a printing fault
 * rather than as a flourish, and it competed with the artwork it was supposed
 * to be framing.
 *
 * What replaces it is meant not to be noticed: a soft brand-blue halo behind
 * the lockup so the white is not flat, and a barely-there cool floor to give
 * the screen a bottom. Two stops, no movement, no pattern. The logo is the
 * only thing on this screen worth looking at. */
.splash::before {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(1100px 720px at 50% 38%, var(--splash-glow), transparent 72%),
    linear-gradient(180deg, transparent 55%, var(--splash-floor) 100%);
}

.splash-inner { position: relative; text-align: center; padding: 20px; }

.splash-mark {
  width: 104px; height: 104px; margin: 0 auto 22px;
  border-radius: 26px;
  background: linear-gradient(150deg, var(--accent-fill) 0%, #0e4485 100%);
  display: grid; place-items: center;
  font-size: 42px; font-weight: 900; letter-spacing: -0.03em; color: var(--on-accent);
  box-shadow: 0 22px 60px rgba(21,101,192,.30);
  animation: splash-mark 1.1s cubic-bezier(.2,.8,.2,1) both;
}

.splash-name {
  font-size: clamp(1.5rem, 4.6vw, 2.2rem); font-weight: 800;
  letter-spacing: -0.02em; color: var(--text);
  animation: splash-rise .8s cubic-bezier(.2,.8,.2,1) .22s both;
}
.splash-tag {
  margin-top: 8px; font-size: 13px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--accent);
  animation: splash-rise .8s cubic-bezier(.2,.8,.2,1) .34s both;
}
.splash-rule { margin: 24px auto 0; width: 132px; height: 2px; background: var(--line); overflow: hidden; }
.splash-rule i { display: block; height: 100%; width: 100%; background: var(--accent-fill);
  transform-origin: left; animation: splash-wipe 1.5s cubic-bezier(.4,0,.2,1) .2s both; }

.splash-skip {
  position: absolute; right: 20px; bottom: 20px;
  background: none; border: 1px solid var(--line); color: var(--text-faint);
  border-radius: 100px; padding: 7px 16px; font: inherit; font-size: 12.5px; cursor: pointer;
}
.splash-skip:hover { color: var(--text); border-color: var(--accent-line); }

.splash-out { animation: splash-fade 1.4s ease-in-out forwards; pointer-events: none; }

/* Stop the page scrolling behind the overlay while it is up. */
html.splash-open, html.splash-open body { overflow: hidden; }

@keyframes splash-in   { from { opacity: 0 } to { opacity: 1 } }
@keyframes splash-fade { to { opacity: 0; transform: scale(1.015) } }
@keyframes splash-mark {
  from { opacity: 0; transform: translateY(16px) scale(.82) rotate(-8deg) }
  to   { opacity: 1; transform: none }
}
@keyframes splash-rise { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }
@keyframes splash-wipe { from { transform: scaleX(0) } to { transform: scaleX(1) } }

/* splash.js already bails out on prefers-reduced-motion; this is the belt to
   its braces in case the markup ever ships server-rendered. */
@media (prefers-reduced-motion: reduce) {
  .splash { display: none; }
  html.splash-open, html.splash-open body { overflow: auto; }
}

/* ── Section reveal ───────────────────────────────────────────────────────
   Sections fade up as they come into view. Applied by IntersectionObserver in
   app.js, which adds `.is-in`. Content is visible by default and only hidden
   once JS confirms it can un-hide it — so with JS broken or blocked the page
   still reads normally rather than being blank.
   ────────────────────────────────────────────────────────────────────────── */
/* The hiding class is applied to each element by JS, not inherited from an
   ancestor. An ancestor-based rule (`.js-reveal .reveal`) is one stylesheet
   quirk away from leaving the whole page invisible; this way the only element
   that can ever be hidden is one JS has already committed to revealing. */
.reveal.will-reveal {
  opacity: 0; transform: translateY(22px);
  transition: opacity .7s cubic-bezier(.2,.8,.2,1), transform .7s cubic-bezier(.2,.8,.2,1);
}
.reveal.will-reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal.will-reveal { opacity: 1; transform: none; transition: none; }
  .hero-scroll { animation: none; }
}

/* ── Sticky book bar (phones) ─────────────────────────────────────────────
   Nearly all of a barbershop's traffic is mobile. This keeps the one action
   that matters in thumb reach however far down the page someone is, and it
   only appears once they have scrolled past the hero's own buttons.
   ────────────────────────────────────────────────────────────────────────── */
.book-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  display: none; gap: 10px; align-items: center;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
  background: var(--bar-bg); backdrop-filter: blur(14px);
  box-shadow: var(--bar-shadow);
  border-top: 1px solid var(--line);
  transform: translateY(110%); transition: transform .32s cubic-bezier(.2,.8,.2,1);
}
.book-bar.is-up { transform: none; }
.book-bar .btn-primary { flex: 1; min-height: 50px; }
.book-bar .btn-ghost { flex: 0 0 auto; min-height: 50px; }

@media (max-width: 780px) {
  .book-bar { display: flex; }
  /* Room so the footer is never trapped behind the bar. */
  .footer { padding-bottom: 96px; }
}

/* ── Demo ribbon ──────────────────────────────────────────────────────── */
.demo-ribbon {
  background: var(--ribbon-bg); border-bottom: 1px solid var(--accent-line);
  color: var(--accent-hi); font-size: 13px; text-align: center; padding: 8px 16px;
}
.demo-ribbon b { color: var(--text); }

/* ==========================================================================
   Gallery
   ========================================================================== */

/* Masonry via CSS columns. Barbershop photos arrive in whatever aspect ratio
   the barber's phone produced, and columns absorb that without cropping
   anyone's work — which a fixed grid would. */
.gallery-grid { columns: 3 260px; column-gap: 14px; }
.gallery-item {
  break-inside: avoid; margin: 0 0 14px; position: relative; display: block;
  border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line);
  background: var(--surface); cursor: zoom-in; width: 100%; padding: 0;
}
.gallery-item img {
  width: 100%; height: auto; display: block;
  transition: transform .4s cubic-bezier(.2,.8,.2,1);
  background: var(--surface-2);
}
.gallery-item:hover img { transform: scale(1.035); }
.gallery-item:hover { border-color: var(--accent-line); }

.gallery-cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 26px 14px 12px; text-align: left;
  background: linear-gradient(to top, rgba(0,0,0,.82), transparent);
  color: #fff; font-size: 13.5px; font-weight: 600;
  opacity: 0; transition: opacity .22s;
}
.gallery-item:hover .gallery-cap,
.gallery-item:focus-visible .gallery-cap { opacity: 1; }

.gallery-empty {
  border: 1px dashed var(--line); border-radius: var(--radius);
  padding: 54px 24px; text-align: center; color: var(--text-faint);
}

/* ── Homepage carousel ────────────────────────────────────────────────────
   A slow continuous drift rather than a click-through slideshow. The images
   are duplicated once and the track translates exactly -50%, so the loop is
   seamless with no jump at the wrap point.

   It pauses on hover and on keyboard focus — a strip that keeps sliding while
   you are trying to look at one photo is an irritation, not a feature.
   ────────────────────────────────────────────────────────────────────────── */
.carousel {
  position: relative; overflow: hidden;
  /* Feathered edges, so photos enter and leave rather than being chopped. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

/* Spacing is `margin-right` on each item, deliberately NOT `gap`.
   With gap, the track measures 2·S + g·(2n−1), so −50% lands g/2 short of the
   duplicate's start and the loop stutters ~8px every cycle. With a trailing
   margin on every item the track is exactly 2·(S + g·n), and −50% is exact. */
.carousel-track {
  display: flex; width: max-content;
  animation: carousel-drift 64s linear infinite;
  will-change: transform;
}
.carousel:hover .carousel-track,
.carousel:focus-within .carousel-track { animation-play-state: paused; }

@keyframes carousel-drift {
  from { transform: translate3d(0, 0, 0); }
  /* -50% because the track holds the set twice. */
  to   { transform: translate3d(-50%, 0, 0); }
}

.carousel-item {
  flex: 0 0 auto;
  margin-right: 16px;
  /* Sized off the viewport, not a fixed max, so the strip fills a wide screen
     instead of sitting as a small band in the middle of it. */
  width: clamp(200px, 21vw, 420px);
  aspect-ratio: 4 / 5;
  border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--line); background: var(--surface);
  padding: 0; cursor: zoom-in; position: relative;
  transition: transform .45s cubic-bezier(.2,.8,.2,1), border-color .3s;
}
.carousel-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.carousel-item:hover, .carousel-item:focus-visible {
  transform: translateY(-6px) scale(1.02); border-color: var(--accent-line);
}
.carousel-item::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(10,16,26,.55), transparent 45%);
  opacity: .75; transition: opacity .3s;
}
.carousel-item:hover::after { opacity: .3; }

.carousel-cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 22px 14px 12px; text-align: left;
  color: #fff; font-size: 13px; font-weight: 600;
  transform: translateY(6px); opacity: 0; transition: all .3s;
}
.carousel-item:hover .carousel-cap { transform: none; opacity: 1; }

/* Anyone who has asked for less movement gets a still grid instead. This kind
   of continuous drift is a genuine nausea trigger for some people. */
@media (prefers-reduced-motion: reduce) {
  .carousel-track {
    animation: none;
    flex-wrap: wrap; width: 100%; justify-content: center; gap: 14px;
  }
  .carousel { -webkit-mask-image: none; mask-image: none; }
  .carousel-item { width: clamp(150px, 30%, 240px); margin-right: 0; }
  .carousel-item:hover { transform: none; }
  /* The duplicates exist only to make the drift loop; with no drift they are
     just the same photos twice. */
  .carousel-item[aria-hidden] { display: none; }
}

/* Speed is set per breakpoint, and the numbers go UP as the screen gets
   smaller, which looks backwards until you work out what the eye is measuring.
   The track moves at a constant px/second, but what reads as "fast" is how
   long one photo takes to cross the screen — and the screen is a quarter of
   the width on a phone. At the old 40s a photo crossed a phone in about four
   seconds, which is a slideshow going past rather than a strip you can look
   at. These durations keep that crossing time roughly even everywhere. */
@media (max-width: 1040px) { .carousel-track { animation-duration: 90s; } }

@media (max-width: 620px) {
  .carousel-track { animation-duration: 120s; }
  /* Nearly full width on a phone — a photo of a haircut at 46vw is too small
     to judge, which defeats the point of showing it. */
  .carousel-item { width: 78vw; margin-right: 12px; aspect-ratio: 4 / 5; }
}

/* ==========================================================================
   Slideshow

   One component, used by the reviews and by the barbers. Both are lists that
   outgrew a grid: twelve review cards is a wall of text to scroll past on the
   way to the footer, and a shop with five barbers pushed everything below it
   off the screen. A slideshow keeps the section a fixed height however long
   the list gets.

   The scrolling is native — `overflow-x` plus scroll snap — so a phone swipes
   it with no JavaScript involved, and the arrows, dots and pause button in
   shared.js only ever call `scrollTo`. That is deliberate: a hand-rolled
   transform carousel has to reimplement momentum, snapping and touch, and gets
   all three slightly wrong.
   ========================================================================== */
.slider {
  --per: 3;             /* how many fit at once; read back by makeSlider() */
  --slide-gap: 16px;
  position: relative;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* The cards lift on hover and their shadow spreads. Without room for it the
     overflow clips the shadow into a hard line along the top edge — the
     negative margin gives the padding back so nothing shifts on the page. */
  padding: 8px 4px 14px; margin: -8px -4px -14px;
  /* The dots are the scroll indicator. A native bar underneath says the same
     thing twice and sits across the bottom card shadow. */
  scrollbar-width: none;
}
.slider::-webkit-scrollbar { display: none; }
.slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.slider-track { display: flex; gap: var(--slide-gap); align-items: stretch; }
.slider-track > * {
  flex: 0 0 calc((100% - (var(--per) - 1) * var(--slide-gap)) / var(--per));
  scroll-snap-align: start;
}

/* Fewer slides than fit on the screen: there is nothing to scroll, so it drops
   back to a plain row that fills the width. Two barbers held in three fixed
   columns left an empty third, which reads as a barber who failed to load. */
.slider.is-static { overflow-x: visible; scroll-snap-type: none; }
.slider.is-static .slider-track { flex-wrap: wrap; justify-content: center; }
.slider.is-static .slider-track > * { flex: 1 1 300px; max-width: 560px; }

@media (max-width: 1040px) { .slider { --per: 2; } }
@media (max-width: 700px)  { .slider { --per: 1; --slide-gap: 12px; } }

/* Anyone who has asked for less movement still gets the slideshow — it is the
   autoplay that is the problem, and makeSlider() switches that off for them.
   Smooth scrolling goes too, so the arrows jump rather than glide. */
@media (prefers-reduced-motion: reduce) {
  .slider { scroll-behavior: auto; }
}

.slider-controls {
  display: flex; align-items: center; justify-content: center;
  gap: 10px; margin-top: 24px; flex-wrap: wrap;
}
.slider-controls[hidden] { display: none; }

.slider-btn {
  width: 36px; height: 36px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--surface); color: var(--text-dim);
  cursor: pointer; transition: border-color .16s, color .16s, background .16s;
}
.slider-btn:hover { border-color: var(--accent-line); color: var(--accent); background: var(--surface-2); }
.slider-btn svg { width: 14px; height: 14px; fill: currentColor; display: block; }

.slider-dots { display: flex; align-items: center; gap: 7px; margin: 0 2px; }
.slider-dot {
  width: 8px; height: 8px; flex: none; padding: 0;
  border: 0; border-radius: 50%; background: var(--line); cursor: pointer;
  transition: width .22s, background .22s, border-radius .22s;
}
.slider-dot:hover { background: var(--text-faint); }
/* The current page is a pill rather than a bigger dot: at 8px a "slightly
   larger circle" is a difference nobody notices in passing. */
.slider-dot[aria-current='true'] { width: 22px; border-radius: 4px; background: var(--accent); }

/* ==========================================================================
   Reviews
   ========================================================================== */
.review {
  background: linear-gradient(165deg, var(--surface), var(--surface-2));
  box-shadow: var(--shadow-card);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 22px 24px; display: flex; flex-direction: column; gap: 12px;
  position: relative; overflow: hidden;
}
.review::before {
  content: '"'; position: absolute; top: -22px; right: 12px;
  font-size: 110px; line-height: 1; color: var(--accent-fill); opacity: var(--quote-op);
  font-family: Georgia, serif;
}
.review-stars { color: var(--star); letter-spacing: 2px; font-size: 14px; }
.review-text { color: var(--text-dim); font-size: 15px; line-height: 1.65; flex: 1; }
.review-foot { display: flex; align-items: center; gap: 11px; }
.review-av {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-weight: 800; font-size: 13px; color: #101720;
}
.review-name { font-weight: 700; font-size: 14.5px; }
.review-when { font-size: 12.5px; color: var(--text-faint); }

.reviews-cta { text-align: center; margin-top: 30px; }
.reviews-note {
  text-align: center; margin-top: 14px;
  font-size: 12.5px; color: var(--text-faint);
}

/* Lightbox */
.lightbox {
  position: fixed; inset: 0; z-index: 500; display: grid; place-items: center;
  /* Deliberately dark even on a white site: a photo viewer's job is to get
     everything except the photo out of the way. */
  background: rgba(18,16,12,.93); padding: 30px 16px;
  animation: splash-in .18s ease both;
}
.lightbox img {
  max-width: min(1100px, 94vw); max-height: 84vh; width: auto; height: auto;
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.lightbox-cap { color: var(--text-dim); font-size: 14.5px; margin-top: 14px; text-align: center; max-width: 40rem; }
.lightbox-close, .lightbox-nav {
  position: absolute; background: var(--surface); border: 1px solid var(--line);
  color: var(--text); border-radius: 50%; width: 46px; height: 46px;
  font-size: 20px; cursor: pointer; display: grid; place-items: center;
}
.lightbox-close { top: 18px; right: 18px; }
.lightbox-nav.prev { left: 18px; top: 50%; transform: translateY(-50%); }
.lightbox-nav.next { right: 18px; top: 50%; transform: translateY(-50%); }
.lightbox-close:hover, .lightbox-nav:hover { border-color: var(--accent); color: var(--accent); }
.lightbox-count { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
  color: var(--text-faint); font-size: 13px; font-variant-numeric: tabular-nums; }

@media (max-width: 620px) {
  .gallery-grid { columns: 2 140px; column-gap: 10px; }
  .gallery-item { margin-bottom: 10px; }
  .lightbox-nav { width: 42px; height: 42px; }
  .lightbox-nav.prev { left: 8px; }
  .lightbox-nav.next { right: 8px; }
}

/* ==========================================================================
   Booking — embedded Square widget
   The shop's calendar lives in Square, but the customer stays on this domain.
   ========================================================================== */

.booking-frame-wrap {
  position: relative;
  max-width: 900px; margin: 0 auto;
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: var(--surface);
  box-shadow: var(--shadow);
  min-height: 620px;
}

.booking-frame {
  width: 100%; min-height: 760px; border: 0; display: block;
  /* Square's widget renders on a white ground, same as the page now does —
     the fade is only so it does not pop in half-laid-out. */
  opacity: 0; transition: opacity .45s ease;
  background: #fff;
}
.booking-frame.is-ready { opacity: 1; }

.booking-skeleton {
  position: absolute; inset: 0; display: grid; place-items: center;
  gap: 14px; color: var(--text-faint); font-size: 14.5px;
  background: var(--surface); z-index: 1;
}
.booking-spinner {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--accent);
  animation: booking-spin .9s linear infinite;
}
@keyframes booking-spin { to { transform: rotate(360deg); } }

.booking-fallback {
  padding: 60px 24px; text-align: center;
}

.booking-help {
  text-align: center; margin-top: 22px;
  color: var(--text-faint); font-size: 14.5px;
}

@media (prefers-reduced-motion: reduce) {
  .booking-spinner { animation-duration: 2.4s; }
  .booking-frame { transition: none; opacity: 1; }
}

@media (max-width: 620px) {
  .booking-frame-wrap { min-height: 560px; border-radius: var(--radius-sm); }
  .booking-frame { min-height: 700px; }
}

/* ==========================================================================
   Booking flow — built-in engine
   Used when SHOP.booking.provider === 'builtin'. Live code, not dead: the
   next shop may not own Square.
   ========================================================================== */
.book-wrap { max-width: 760px; margin: 0 auto; padding: 44px 22px 80px; }

/* State classes are namespaced `is-*` on purpose. An earlier version used a
   bare `.done` here, which silently collided with the confirmation screen's
   `.done` rule below and gave the completed step 26px of padding, knocking it
   out of line with the others. */
.steps { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 34px; }
.step-pip { flex: 1; min-width: 0; }
.step-pip .bar { height: 4px; border-radius: 4px; background: var(--line); transition: background .25s; }
.step-pip .lbl {
  font-size: 12px; color: var(--text-faint); margin-top: 9px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.step-pip.is-done .bar, .step-pip.is-active .bar { background: var(--accent); }
.step-pip.is-active .lbl { color: var(--accent); }
.step-pip.is-done .lbl { color: var(--text-dim); }

.step { display: none; }
.step.active { display: block; animation: fade .22s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.step h2 { font-size: 1.7rem; margin-bottom: 7px; }
.step-hint { color: var(--text-dim); margin-bottom: 24px; font-size: 15.5px; }

.pick-list { display: grid; gap: 10px; }
.pick {
  display: flex; align-items: center; gap: 15px; width: 100%; text-align: left;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 17px 20px; color: var(--text); font: inherit; cursor: pointer;
  transition: border-color .15s, background .15s;
}
.pick:hover { border-color: var(--accent-line); background: var(--surface-2); }
.pick[aria-pressed='true'] { border-color: var(--accent); background: var(--accent-soft); }
.pick-main { flex: 1; min-width: 0; }
.pick-title { font-weight: 700; font-size: 16px; }
.pick-sub { color: var(--text-faint); font-size: 13.5px; margin-top: 3px; }
.pick-right { text-align: right; flex: none; }
.pick-price { font-weight: 800; color: var(--accent); font-size: 18px; font-variant-numeric: tabular-nums; }
.pick-dur { font-size: 12.5px; color: var(--text-faint); margin-top: 2px; }

/* Date strip */
.date-strip {
  display: flex; gap: 9px; overflow-x: auto; padding: 4px 4px 14px;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
}
.date-btn {
  flex: none; width: 76px; scroll-snap-align: start;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 12px 6px; text-align: center; cursor: pointer; color: var(--text); font: inherit;
  transition: all .15s;
}
.date-btn:hover:not(:disabled) { border-color: var(--accent-line); }
.date-btn[aria-pressed='true'] { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }
.date-btn:disabled { opacity: .3; cursor: not-allowed; }
.date-dow { font-size: 11.5px; text-transform: uppercase; letter-spacing: .07em; opacity: .75; }
.date-day { font-size: 21px; font-weight: 800; line-height: 1.25; font-variant-numeric: tabular-nums; }
.date-mon { font-size: 11.5px; opacity: .75; }

/* Time slots */
.slot-note {
  font-size: 13.5px; color: var(--text-faint); margin: 18px 0 12px;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 9px; }
.slot {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 12px 8px; font: inherit; font-size: 14.5px; font-weight: 600; color: var(--text);
  cursor: pointer; transition: all .14s; font-variant-numeric: tabular-nums;
}
.slot:hover { border-color: var(--accent-line); color: var(--accent-hi); }
.slot[aria-pressed='true'] { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }

.slot-group-label {
  grid-column: 1 / -1; font-size: 11.5px; text-transform: uppercase; letter-spacing: .12em;
  color: var(--text-faint); margin-top: 14px; font-weight: 700;
}
.slot-group-label:first-child { margin-top: 0; }

/* Form */
.field { margin-bottom: 17px; }
.field label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 7px; }
.field .req { color: var(--accent); }
.field input, .field textarea {
  width: 100%; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 13px 15px; color: var(--text);
  font: inherit; font-size: 16px; /* 16px stops iOS zooming on focus */
}
.field input:focus, .field textarea:focus { outline: none; border-color: var(--accent); }
.field .help { font-size: 12.5px; color: var(--text-faint); margin-top: 6px; }
.field.err input { border-color: var(--closed); }
.field .err-msg { font-size: 13px; color: var(--closed); margin-top: 6px; }

.check { display: flex; gap: 11px; align-items: flex-start; margin: 20px 0; }
.check input { width: 18px; height: 18px; margin-top: 2px; flex: none; accent-color: var(--accent); }
.check label { font-size: 13.5px; color: var(--text-dim); line-height: 1.5; }

/* Summary */
.summary {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px 22px; margin-bottom: 24px;
}
.summary-row { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; font-size: 15px; }
.summary-row .k { color: var(--text-faint); }
.summary-row .v { font-weight: 600; text-align: right; }
.summary-total {
  border-top: 1px solid var(--line); margin-top: 8px; padding-top: 14px;
  font-size: 18px; font-weight: 800;
}
.summary-total .v { color: var(--accent); }

.step-actions { display: flex; gap: 11px; margin-top: 26px; }
.step-actions .btn-ghost { flex: none; }
.step-actions .btn-primary { flex: 1; }

/* Confirmation screen */
.confirm-screen { text-align: center; padding: 26px 0; }
.confirm-mark {
  width: 68px; height: 68px; border-radius: 50%; margin: 0 auto 22px;
  background: var(--ok-soft); border: 2px solid var(--ok); color: var(--ok);
  display: grid; place-items: center; font-size: 32px;
}
.confirm-screen h2 { font-size: 1.9rem; margin-bottom: 10px; }
.confirm-screen p { color: var(--text-dim); margin-bottom: 24px; }
.confirm-screen .summary { text-align: left; }

@media (max-width: 620px) {
  .book-wrap { padding: 26px 18px 60px; }
  .step-pip .lbl { display: none; }
  .step-actions { flex-direction: column-reverse; }
  .step-actions .btn { width: 100%; }
}

/* ── Touch targets ────────────────────────────────────────────────────────
   Apple's guidance is a 44x44px minimum, and a barbershop's traffic is almost
   entirely phones. Several controls sit at 37-42px at their desktop sizes, so
   they get floored here rather than shrunk to fit.
   ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .btn { min-height: 48px; }
  .btn-sm { min-height: 44px; }
  .chip {
    min-height: 44px;
    display: inline-flex; align-items: center;
    padding-top: 0; padding-bottom: 0;
  }
  .nav-toggle { width: 44px; height: 44px; }
  .slot { min-height: 46px; }
  .date-btn { min-height: 76px; }
  .nav-links.open a { min-height: 44px; display: flex; align-items: center; }
}

/* Two owners, two numbers — stacked, each labelled with whose it is, so a
   customer is never choosing blind between a pair of bare digits. */
#locPhones { display: flex; flex-direction: column; gap: 4px; }
.loc-phone { display: flex; align-items: baseline; gap: 8px; }
.loc-phone-who {
  font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-dim);
}

/* ── Logo mark ────────────────────────────────────────────────────────
   With SHOP.markImage set, the badge stops being a gold tile with a letter
   in it and becomes the logo itself. The gold gradient and its glow are
   dropped rather than left underneath: the mark is red, steel and blue, and
   the two palettes fight when stacked. Sizes stay exactly as they were, so
   the nav's shrink-on-scroll transition still works untouched. */
.brand-mark.has-logo {
  background: none;
  box-shadow: none;
  border-radius: 0;
}
.brand-mark.has-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.splash-mark.has-logo {
  background: none;
  box-shadow: none;
  border-radius: 0;
  animation: splash-mark 1.1s cubic-bezier(.2,.8,.2,1) both;
}
.splash-mark.has-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* ── Logo mark sizing ─────────────────────────────────────────────────
   The image mark needs a bigger box than the text mark did. The gold tile
   used to fill its box edge to edge, so 56px of tile read as 56px of brand;
   an unfilled logo on transparent reads noticeably smaller at the same
   measurement. These bump each state by roughly a sixth. Scoped to
   .has-logo so the text fallback keeps the proportions it was drawn for. */
.brand-mark.has-logo { width: 66px; height: 66px; }
.nav.is-stuck .brand-mark.has-logo { width: 46px; height: 46px; }
.footer-brand .brand-mark.has-logo { width: 58px; height: 58px; }

/* 1280px is where the nav collapses and .brand-mark drops to 44px. An
   override on any other breakpoint would land in the wrong place in the
   cascade and simply not apply. */
@media (max-width: 1280px) {
  .brand-mark.has-logo { width: 52px; height: 52px; }
}
@media (max-width: 430px) {
  .brand-mark.has-logo { width: 46px; height: 46px; }
}

/* ── The full lockup in the header and footer ─────────────────────────
   The whole sign rather than the razor-and-pole mark cropped out of it.
   It carries the shop's name, so wherever it appears the name and tagline
   lines beside it are dropped — see renderBrandMark() for why.

   Sizing is set by legibility, not by taste. The lockup is square and
   "MAIN STREET" is 18% of its height, so the lettering inside is roughly a
   fifth of whatever number goes here: at 84px the name reads at ~15px and
   the ribbon at ~9px, which is the floor. Going smaller is what made a
   cropped mark necessary in the first place. */
.brand-mark.has-logo.is-lockup {
  width: 84px; height: 84px;
}
.brand-mark.is-lockup ~ .brand-name,
.brand-mark.is-lockup ~ .brand-tagline,
.brand-mark.is-lockup ~ .footer-brand-name,
.brand-mark.is-lockup ~ .footer-brand-tag { display: none; }

/* With no text beside it the lockup is the whole brand, so it stops being a
   two-column grid and the side padding does the work of setting it apart. */
.brand:has(.brand-mark.is-lockup) {
  grid-template-columns: auto;
  padding-block: 0;
}

/* The header has to be told to make room — but only where the logo is not in
 * the flow to make its own.
 *
 * Above 1280px `.brand` is absolutely positioned and centred, so the row's
 * height comes from the links; drop an 84px lockup into a 60px bar and it
 * simply hangs over the hero. Below that breakpoint the brand goes back into
 * the flow and sets the height itself, and the same min-height would only pad
 * a phone header out to 109px for a 56px logo. Hence the media query: this is
 * a fix for a specific layout, not a global one. */
@media (min-width: 1281px) {
  .nav .container:has(.brand-mark.is-lockup) {
    min-height: 108px;
    padding-top: 10px; padding-bottom: 10px;
  }
  .nav.is-stuck .container:has(.brand-mark.is-lockup) { min-height: 72px; }
}

/* Scrolled: small enough to be out of the way, still above the floor where
   the lettering turns to mush. */
.nav.is-stuck .brand-mark.has-logo.is-lockup { width: 56px; height: 56px; }

/* The footer has room the header does not, and this is the one place on the
   page the sign can be shown at a size that does it justice. */
.footer-brand .brand-mark.has-logo.is-lockup { width: 140px; height: 140px; }

/* Below 1280px the brand is back in the flow and sets the row height itself,
   so there is no min-height to manage — only the logo to keep in proportion
   with a header that now has a menu button in it. */
@media (max-width: 1280px) {
  .brand-mark.has-logo.is-lockup { width: 66px; height: 66px; }
}
@media (max-width: 430px) {
  .brand-mark.has-logo.is-lockup { width: 56px; height: 56px; }
  .footer-brand .brand-mark.has-logo.is-lockup { width: 116px; height: 116px; }
}

/* ── Splash artwork ───────────────────────────────────────────────────
   The splash is the only screen with room for the full stacked lockup, so
   it runs as large as the viewport allows rather than at a fixed size. The
   min() caps it on a desktop monitor; the vw term is what carries it on a
   phone. Height is capped too, so a short landscape window does not push the
   tagline off the bottom. */
.splash-mark.has-logo {
  background: none;
  box-shadow: none;
  border-radius: 0;
  animation: splash-mark 1.1s cubic-bezier(.2,.8,.2,1) both;
}
.splash-mark.has-logo img { width: 100%; height: 100%; object-fit: contain; display: block; }

.splash-mark.has-logo.is-lockup {
  width:  min(84vw, 520px);
  height: min(84vw, 520px, 60vh);
  margin: 0 auto 12px;
}
/* A short landscape window is the case that breaks: at 60vh the lockup plus
   the tagline and rule below it stop fitting, so the cap tightens there. */
@media (max-height: 560px) {
  .splash-mark.has-logo.is-lockup { height: min(84vw, 520px, 52vh); }
}

/* Without the lockup the compact mark is used, and the name still prints
   below it, so it stays badge-sized. */
.splash-mark.has-logo:not(.is-lockup) { width: 132px; height: 132px; }

/* ── Light and dark logo variants ─────────────────────────────────────
   The artwork ships in two versions: dark ink for a white ground, white and
   brighter blue for the navy one. Both are in the DOM and CSS picks; the
   theme is stamped on <html> before first paint, so the right one is up from
   the first frame. Swapping an img src on theme change would flash the wrong
   artwork for a beat, which is exactly the cheap-looking thing the palette
   comment above is at pains to avoid. */
.brand-mark.has-logo .for-dark,
.splash-mark.has-logo .for-dark { display: none; }

:root[data-theme='dark'] .brand-mark.has-logo .for-light,
:root[data-theme='dark'] .splash-mark.has-logo .for-light { display: none; }

:root[data-theme='dark'] .brand-mark.has-logo .for-dark,
:root[data-theme='dark'] .splash-mark.has-logo .for-dark { display: block; }
