/* ==========================================================================
   MAIN  —  LACQUER ROOM  ·  ARLINGTON, VA
   --------------------------------------------------------------------------
   Layout and components only. NO COLOURS ARE HARD-CODED HERE — every colour
   comes from a custom property defined in tokens.css. If you find yourself
   typing a hex value in this file, add a token instead.

   Contents
     01  Reset & base
     02  Utilities
     03  Header, nav, status pill
     04  Buttons
     05  Hero
     06  Marquee
     07  Duotone media treatment
     08  Stats
     09  Split / about
     10  Service accordion
     11  Pricing tabs (services page)
     12  Gallery mosaic + lightbox
     13  Reviews carousel
     14  FAQ
     15  CTA band
     16  Visit / contact / hours / map
     17  Footer & mobile call bar
     18  Motion — entrance, reveal, stagger, parallax, hover
     19  Print
   ========================================================================== */


/* ═════════════════════════════════════════════════ 01  RESET & BASE ═══ */

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

html {
  scroll-behavior: smooth;
  /* Anchor targets clear the fixed header instead of hiding under it. */
  scroll-padding-top: calc(var(--header-h) + 1.5rem);
  -webkit-text-size-adjust: 100%;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--c-canvas);
  color: var(--c-text-soft);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* A single fixed noise layer over the whole page. Dark flat backgrounds band
   badly on 6-bit laptop panels; this hides it for ~0 bytes.

   Deliberately NOT mix-blend-mode. Blending a viewport-sized fixed layer
   forces the entire page into one composited group, which costs real frames
   while scrolling on a mid-range phone — and salon traffic is almost all
   phones. At 3.5% opacity the difference between `overlay` and normal
   compositing is not visible; the frame cost is. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  background-image: var(--noise);
  opacity: .035;
  pointer-events: none;
  z-index: 9999;
}

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

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--c-text);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h);
  margin: 0 0 .6em;
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }

p  { margin: 0 0 1.15em; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

a { color: var(--c-lime); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--c-lime-dim); }

ul, ol { margin: 0; padding: 0; }
li { list-style: none; }

button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* Focus. Visible, high-contrast, and never removed — this is the only way a
   keyboard user can tell where they are on a page this dark. */
:focus-visible {
  outline: 2px solid var(--c-lime);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

::selection { background: var(--c-lime); color: var(--c-on-lime); }

/* Themed scrollbar. Progressive enhancement — ignored where unsupported. */
* { scrollbar-color: var(--c-surface-3) var(--c-canvas); scrollbar-width: thin; }


/* ═══════════════════════════════════════════════════ 02  UTILITIES ═══ */

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

.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: calc(var(--section-y) * .6); }

/* Alternating band. Not a different colour — a subtle raise plus hairlines,
   which is how depth is signalled on a near-black canvas. */
.section--raised {
  background: var(--c-surface);
  border-top: 1px solid var(--c-line);
  border-bottom: 1px solid var(--c-line);
}

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

.skip-link {
  position: fixed; top: .5rem; left: .5rem; z-index: 10000;
  transform: translateY(-160%);
  background: var(--c-lime); color: var(--c-on-lime);
  padding: .7rem 1.15rem; border-radius: var(--radius-sm);
  font-weight: 600; text-decoration: none;
  transition: transform var(--dur-fast) var(--ease);
}
.skip-link:focus { transform: translateY(0); color: var(--c-on-lime); }

/* The small monospace kicker above every section heading. */
.label {
  display: inline-flex; align-items: center; gap: .55rem;
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-lime);
  margin: 0 0 1.1rem;
}
.label::before {
  content: ""; width: 26px; height: 1px;
  background: currentColor; opacity: .7;
}
.label--mute { color: var(--c-text-mute); }

.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  color: var(--c-text-soft);
  max-width: 62ch;
}

/* Gradient-filled text. Falls back to solid lime where background-clip:text
   is unsupported, because the @supports guard withholds the transparency. */
.grad-text { color: var(--c-lime); }
@supports (-webkit-background-clip: text) or (background-clip: text) {
  .grad-text {
    background: var(--grad-brand);
    -webkit-background-clip: text; background-clip: text;
    color: transparent;
  }
}
.chrome-text { color: var(--c-text); }
@supports (-webkit-background-clip: text) or (background-clip: text) {
  .chrome-text {
    background: var(--grad-chrome);
    -webkit-background-clip: text; background-clip: text;
    color: transparent;
  }
}

.section-head { max-width: 62ch; margin-bottom: clamp(2.2rem, 4vw, 3.4rem); }
.section-head--split {
  max-width: none; display: grid; gap: 1.5rem 3rem;
  grid-template-columns: minmax(0,1fr);
  align-items: end;
}
@media (min-width: 900px) {
  .section-head--split { grid-template-columns: minmax(0,1.15fr) minmax(0,.85fr); }
  .section-head--split > :last-child { justify-self: end; text-align: right; }
}

/* Hairline rule with a lime gradient core. Used between major blocks. */
.rule {
  height: 1px; border: 0; margin: 0;
  background: linear-gradient(90deg,
    transparent, var(--c-line-strong) 12%, var(--c-lime-edge) 50%,
    var(--c-line-strong) 88%, transparent);
}

/* Scroll progress bar, driven by site.js writing --p (0 to 1). */
.progress {
  position: fixed; inset: 0 0 auto; height: 2px; z-index: 1200;
  transform-origin: 0 50%;
  transform: scaleX(var(--p, 0));
  background: var(--grad-brand);
  pointer-events: none;
}


/* ═══════════════════════════════════ 03  HEADER, NAV, STATUS PILL ═══ */

.header {
  position: fixed; inset: 0 0 auto; z-index: 1100;
  height: var(--header-h);
  display: flex; align-items: center;
  transition: background var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              backdrop-filter var(--dur) var(--ease),
              transform var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}
/* Scrolling down past the fold retracts the bar; scrolling up brings it
   straight back. site.js never sets this while the drawer or the lightbox
   is open, so the bar can't slide out from under an open menu. */
.header.is-hidden { transform: translateY(-100%); }
/* .is-stuck is added by site.js past 40px of scroll. */
.header.is-stuck {
  background: color-mix(in srgb, var(--c-canvas) 82%, transparent);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border-bottom-color: var(--c-line);
}
/* color-mix is recent. Where it is missing the header still needs a
   readable backing, so fall back to the flat canvas colour. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .header.is-stuck { background: var(--c-canvas); }
}

.header > .wrap {
  display: flex; align-items: center; gap: clamp(.75rem, 2vw, 1.75rem);
}

.brand {
  display: inline-flex; align-items: center; gap: .7rem;
  text-decoration: none; color: var(--c-text);
  margin-right: auto; flex-shrink: 0;
}
/* Sized by HEIGHT with width:auto, so a square mark, a wide wordmark and a
   tall stacked logo all sit correctly in the lockup without re-measuring.
   max-width is the guard for the wide case — without it a long wordmark
   pushes the nav off the end of the bar. */
.brand img, .brand svg { height: 38px; width: auto; max-width: 120px; }
.brand__name {
  font-family: var(--font-display);
  font-weight: 700; font-size: 1.06rem;
  letter-spacing: -.02em; line-height: 1.1;
  display: block;
}
.brand__tag {
  display: block;
  font-family: var(--font-mono);
  font-size: 9.5px; letter-spacing: .19em; text-transform: uppercase;
  color: var(--c-text-mute);
}

.nav { display: flex; align-items: center; gap: clamp(.4rem, 1.4vw, 1.15rem); }
.nav__link {
  position: relative;
  color: var(--c-text-soft);
  text-decoration: none;
  font-size: .925rem; font-weight: 500;
  padding: .5rem .25rem;
  transition: color var(--dur-fast) var(--ease);
  white-space: nowrap;
}
.nav__link::after {
  content: ""; position: absolute; left: .25rem; right: .25rem; bottom: .18rem;
  height: 1.5px; background: var(--c-lime);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--dur-fast) var(--ease);
}
.nav__link:hover { color: var(--c-text); }
.nav__link:hover::after,
.nav__link[aria-current="page"]::after { transform: scaleX(1); }
.nav__link[aria-current="page"] { color: var(--c-text); }

/* Live Open / Closed pill. site.js sets .is-open or .is-closed and fills
   the text; before JS runs it is display:none so it can never lie. */
.status {
  display: none; align-items: center; gap: .45rem;
  font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  padding: .34rem .7rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--c-line-strong);
  color: var(--c-text-mute);
  white-space: nowrap;
}
.status.is-open, .status.is-closed { display: inline-flex; }
.status__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.status.is-open  { color: var(--c-open);   border-color: color-mix(in srgb, var(--c-open) 40%, transparent); }
.status.is-closed{ color: var(--c-closed); border-color: color-mix(in srgb, var(--c-closed) 40%, transparent); }
.status.is-open .status__dot { animation: pulse 2.4s var(--ease) infinite; }
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
  50%      { box-shadow: 0 0 0 4px transparent; opacity: .55; }
}
@media (prefers-reduced-motion: reduce) { .status.is-open .status__dot { animation: none; } }

.header__phone {
  display: inline-flex; align-items: center; gap: .45rem;
  color: var(--c-text); text-decoration: none;
  font-family: var(--font-mono); font-size: .8rem; letter-spacing: .02em;
  white-space: nowrap;
}
.header__phone svg { width: 15px; height: 15px; color: var(--c-lime); }
.header__phone:hover { color: var(--c-lime); }

/* Hamburger. Three bars that fold into an X. */
.nav-toggle {
  display: none;
  width: 42px; height: 42px; flex: none;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  position: relative;
}
.nav-toggle span {
  position: absolute; left: 11px; right: 11px; height: 1.5px;
  background: var(--c-text);
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.nav-toggle span:nth-child(1) { top: 15px; }
.nav-toggle span:nth-child(2) { top: 20.5px; }
.nav-toggle span:nth-child(3) { top: 26px; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

/* ── Mobile: nav becomes a full-height drawer ── */
@media (max-width: 980px) {
  .nav-toggle { display: block; order: 3; }
  .header__phone span { display: none; }
  .header__phone { padding: .4rem; }

  .nav {
    position: fixed; inset: var(--header-h) 0 0;
    flex-direction: column; align-items: stretch;
    justify-content: flex-start; gap: 0;
    padding: 1.5rem var(--gutter) 3rem;
    background: var(--c-canvas);
    border-top: 1px solid var(--c-line);
    transform: translateX(100%);
    visibility: hidden;
    overflow-y: auto;
    transition: transform var(--dur) var(--ease-out), visibility var(--dur);
  }
  .nav.is-open { transform: translateX(0); visibility: visible; }
  .nav__link {
    font-family: var(--font-display);
    font-size: 1.6rem; font-weight: 700; letter-spacing: -.02em;
    padding: .85rem 0;
    border-bottom: 1px solid var(--c-line);
  }
  .nav__link::after { display: none; }
  .nav__link[aria-current="page"] { color: var(--c-lime); }
  .nav .btn { margin-top: 1.6rem; justify-content: center; }
}
@media (min-width: 981px) {
  .nav { transform: none !important; visibility: visible !important; }
}

/* The header bar holds five things and a phone is 390px wide. Measured at
   that width the lockup, the status pill, the call icon and the hamburger
   come to ~408px and the last two get pushed off the edge, so they drop out
   in order of how much each earns its space.
   The status pill survives longest because "are they open right now" is the
   most useful thing in the bar — but below 560px it goes too, and the
   sticky call bar plus the hours table carry that job instead. */
@media (max-width: 900px) {
  .brand__tag { display: none; }
  .header__phone { display: none; }   /* .callbar carries click-to-call */
}
@media (max-width: 560px) {
  .status.is-open, .status.is-closed { display: none; }
}

/* Locked while the drawer or the lightbox is open. */
body.is-locked { overflow: hidden; }


/* ═════════════════════════════════════════════════════ 04  BUTTONS ═══ */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--c-text);
  --btn-bd: var(--c-line-strong);
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  padding: .82rem 1.5rem;
  border: 1px solid var(--btn-bd);
  border-radius: var(--radius-pill);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-body);
  font-size: .9rem; font-weight: 600; letter-spacing: .01em;
  text-decoration: none; white-space: nowrap;
  position: relative; overflow: hidden; isolation: isolate;
  /* Hover lift and pointer pull are separate variables rather than two
     competing `transform` declarations: site.js writes --tx / --ty for the
     magnetic pull, CSS owns --lift, and neither can clobber the other. */
  transform: translate3d(var(--tx, 0px), calc(var(--ty, 0px) + var(--lift, 0px)), 0);
  transition: transform var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.btn:hover  { --lift: -2px; }
.btn:active { --lift: 0px; }
/* While the pointer is being tracked the button must follow it frame for
   frame — a 220ms ease on top of that reads as lag, not smoothness. */
.btn.is-magnetic { transition: background var(--dur-fast) var(--ease),
                               border-color var(--dur-fast) var(--ease),
                               box-shadow var(--dur-fast) var(--ease),
                               color var(--dur-fast) var(--ease); }

.btn--primary {
  --btn-bg: var(--c-lime);
  --btn-fg: var(--c-on-lime);
  --btn-bd: var(--c-lime);
}
.btn--primary:hover {
  --btn-bg: var(--c-lime-dim);
  --btn-bd: var(--c-lime-dim);
  color: var(--c-on-lime);
  box-shadow: var(--glow-lime);
}
/* A single specular sweep across the fill on hover. z-index keeps it under
   the label, and the isolation above stops it escaping the pill. */
.btn--primary::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(105deg, transparent 38%, rgba(255,255,255,.5) 50%, transparent 62%);
  transform: translateX(-130%);
  transition: transform .75s var(--ease-out);
}
.btn--primary:hover::after { transform: translateX(130%); }
@media (prefers-reduced-motion: reduce) {
  .btn--primary::after { display: none; }
}
.btn--ghost:hover {
  --btn-bd: var(--c-lime-edge);
  --btn-bg: var(--c-lime-wash);
  color: var(--c-lime);
}
.btn--sm { padding: .58rem 1.1rem; font-size: .82rem; }
.btn--lg { padding: 1.02rem 2rem; font-size: 1rem; }
.btn--block { width: 100%; }
.btn svg { width: 16px; height: 16px; flex: none; }

/* Arrow that slides on hover. */
.btn--arrow svg { transition: transform var(--dur-fast) var(--ease); }
.btn--arrow:hover svg { transform: translateX(4px); }

.btn-row { display: flex; flex-wrap: wrap; gap: .75rem; }


/* ════════════════════════════════════════════════════════ 05  HERO ═══ */

.hero {
  position: relative;
  min-height: min(94svh, 900px);
  display: flex; align-items: flex-end;
  padding-top: calc(var(--header-h) + 3rem);
  padding-bottom: clamp(2rem, 5vw, 4rem);
  overflow: hidden;
  isolation: isolate;
}
.hero > .wrap { position: relative; z-index: 3; width: 100%; }

/* Photo sits behind, pushed right, so the type column stays clean. */
.hero__media {
  position: absolute; inset: 0; z-index: 0;
}
.hero__media img {
  width: 100%; height: 100%; object-fit: cover;
  object-position: 72% 40%;
}
/* Layered scrims: a vertical one so the fixed header stays readable, plus a
   horizontal one so the headline never sits on a bright part of the photo. */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(180deg, var(--c-canvas) 0%, transparent 32%, transparent 62%, var(--c-canvas) 100%),
    linear-gradient(95deg, var(--c-canvas) 8%, color-mix(in srgb, var(--c-canvas) 72%, transparent) 46%, transparent 82%);
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .hero::before {
    background:
      linear-gradient(180deg, var(--c-canvas) 0%, transparent 32%, transparent 62%, var(--c-canvas) 100%),
      linear-gradient(95deg, var(--c-canvas) 8%, rgba(11,11,15,.78) 46%, transparent 82%);
  }
}

/* Pointer-tracked glow. site.js writes --mx / --my as percentages; with no
   JS (or a touch device) it stays parked top-left at low opacity, which
   still reads as intentional lighting. */
.hero__glow {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: radial-gradient(
    46rem 46rem at var(--mx, 30%) var(--my, 35%),
    var(--c-violet-wash) 0%,
    transparent 62%);
  opacity: .9;
  transition: opacity var(--dur) var(--ease);
}
@media (prefers-reduced-motion: reduce), (hover: none) {
  .hero__glow { --mx: 30% !important; --my: 35% !important; }
}

.hero__inner { max-width: min(100%, 640px); }
@media (max-width: 900px) { .hero__inner { max-width: none; } }

.hero__title {
  font-size: var(--fs-mega);
  line-height: var(--lh-mega);
  letter-spacing: var(--ls-mega);
  margin: 0 0 1.4rem;
  text-transform: uppercase;
}
/* Second line takes the chrome sweep; first stays flat white. The contrast
   between the two is what gives the wordmark its metallic read.

   Each line is a two-element pair: .hero__line is the mask the word slides
   up through, and the inner span is the word itself.

   width:max-content on the inner span matters more than it looks.
   .chrome-text paints its gradient as a background and clips it to the
   glyphs, so any text falling outside the element's background box renders
   transparent — i.e. invisible. A block-level span is as wide as its
   container, which is fine until the word is wider than the container.
   Sizing the box to the text guarantees the gradient covers every letter.

   The padding/margin pair on the mask is for descenders: overflow:hidden
   would otherwise shave the tail off a "y" or a "q" at rest. */
.hero__line {
  display: block;
  overflow: hidden;
  padding-bottom: .1em;
  margin-bottom: -.1em;
}
.hero__line > span {
  display: block;
  width: max-content;
  max-width: 100%;
}

.hero__sub {
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  color: var(--c-text-soft);
  max-width: 46ch;
  margin-bottom: 2rem;
}

/* Meta strip under the buttons: three mono facts, hairline-separated. */
.hero__meta {
  display: flex; flex-wrap: wrap; gap: .75rem 1.75rem;
  margin-top: 2.4rem; padding-top: 1.5rem;
  border-top: 1px solid var(--c-line);
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase;
  color: var(--c-text-mute);
}
.hero__meta b { color: var(--c-lime); font-weight: 400; }

/* Scroll cue. A hairline with a lime dot that runs down it on a loop —
   enough to say "there is more below" without a bouncing chevron. */
.hero__cue {
  position: absolute; z-index: 3;
  right: var(--gutter); bottom: clamp(2rem, 5vw, 4rem);
  display: none; align-items: center; gap: .8rem;
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .22em; text-transform: uppercase;
  color: var(--c-text-mute); text-decoration: none;
}
@media (min-width: 1100px) { .hero__cue { display: flex; } }
.hero__cue:hover { color: var(--c-lime); }
.hero__cue i {
  display: block; width: 1px; height: 76px;
  background: var(--c-line-strong);
  position: relative; overflow: hidden;
}
.hero__cue i::after {
  content: ""; position: absolute; inset: 0 auto auto 0;
  width: 1px; height: 26px; background: var(--c-lime);
  animation: cueRun 2.4s var(--ease) infinite;
}
@keyframes cueRun {
  0%   { transform: translateY(-26px); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: translateY(76px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__cue i::after { animation: none; transform: none; opacity: .8; }
}

/* Interior page headers — same language, a third of the height. */
.pagehead {
  position: relative; isolation: isolate; overflow: hidden;
  padding-top: calc(var(--header-h) + clamp(3.5rem, 8vw, 6.5rem));
  padding-bottom: clamp(2.5rem, 6vw, 5rem);
  border-bottom: 1px solid var(--c-line);
}
.pagehead__media { position: absolute; inset: 0; z-index: 0; }
.pagehead__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 45%; }
.pagehead::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, var(--c-canvas) 6%, rgba(11,11,15,.72) 55%, var(--c-canvas) 100%);
}
.pagehead > .wrap { position: relative; z-index: 2; }
.pagehead h1 { font-size: var(--fs-h1); text-transform: uppercase; letter-spacing: var(--ls-mega); margin-bottom: .5rem; }
.pagehead p { color: var(--c-text-soft); max-width: 54ch; margin: 0; }

/* Breadcrumb-ish mono trail. */
.trail {
  font-family: var(--font-mono); font-size: var(--fs-label);
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--c-text-faint); margin-bottom: 1.1rem;
}
.trail a { color: var(--c-text-mute); text-decoration: none; }
.trail a:hover { color: var(--c-lime); }


/* ═════════════════════════════════════════════════════ 06  MARQUEE ═══ */

/* Infinite ticker. The track holds the item list twice; translating it by
   exactly -50% therefore lands on an identical frame and loops seamlessly.
   aria-hidden on the duplicate keeps screen readers from reading it twice. */
.marquee {
  overflow: hidden;
  border-block: 1px solid var(--c-line);
  background: var(--c-surface);
  padding-block: .95rem;
  user-select: none;
  /* Fade both ends so items enter and leave instead of being guillotined
     by the viewport edge. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.marquee__track {
  display: flex; width: max-content;
  animation: marquee 38s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee ul { display: flex; align-items: center; }
.marquee li {
  display: flex; align-items: center; gap: 2.2rem;
  padding-right: 2.2rem;
  font-family: var(--font-mono);
  font-size: .78rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--c-text-mute); white-space: nowrap;
}
.marquee li::after {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--c-lime); flex: none;
}
@keyframes marquee { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee { overflow-x: auto; }
}


/* ═════════════════════════════════ 07  DUOTONE MEDIA TREATMENT ═══════ */

/* The house photo style. Grayscale the image, then lay a violet→lime wash
   over it with mix-blend-mode:color. On hover the wash lifts and the real
   photo shows through — which is what makes a gallery worth clicking.
   Wrap any <img> in <figure class="duo"> to opt in. */
/* display:block is load-bearing, not tidy-up. Inside the gallery tiles this
   is a <span>, not a <figure>, because a <button>'s content model is
   phrasing content only and <figure> is flow content — valid markup matters
   more than a tidier element name. */
.duo {
  display: block;
  position: relative; overflow: hidden; margin: 0;
  background: var(--c-surface-2);
  isolation: isolate;
}
.duo img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(1) contrast(1.12) brightness(.92);
  transition: filter var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.duo::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: var(--duotone);
  mix-blend-mode: color;
  opacity: .8;
  transition: opacity var(--dur) var(--ease);
}
.duo--hoverable:hover img,
.duo--hoverable:focus-within img { filter: none; transform: scale(1.04); }
.duo--hoverable:hover::after,
.duo--hoverable:focus-within::after { opacity: 0; }

/* Lighter treatment for large feature photos, where full duotone is too much. */
.duo--soft img { filter: grayscale(.55) contrast(1.06); }
.duo--soft::after { opacity: .32; }

/* Work photography gets a much lighter wash than atmosphere photography.
   Customers pick a salon by looking at actual nail colours, so a full
   duotone over the lookbook hides the one thing they came to see — and on a
   touch device there is no hover to reveal it. Atmosphere shots (hero,
   studio, CTA bands) keep the full effect, which is where it earns its
   keep anyway. */
.strip .duo img,
.mosaic .duo img { filter: grayscale(.45) contrast(1.05); }
.strip .duo::after,
.mosaic .duo::after { opacity: .26; }

/* mix-blend-mode:color on a wrapper is well supported, but if it is not the
   ::after would paint an opaque slab over the photo. Hide it in that case. */
@supports not (mix-blend-mode: color) {
  .duo::after { display: none; }
  .duo img { filter: grayscale(.4) contrast(1.05); }
}


/* ═══════════════════════════════════════════════════════ 08  STATS ═══ */

.stats {
  display: grid; gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  background: var(--c-line);
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  overflow: hidden;
}
.stats li { background: var(--c-canvas); padding: clamp(1.4rem, 3vw, 2.2rem); }
.stats .n {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(2rem, 4vw, 2.9rem); font-weight: 700;
  letter-spacing: -.04em; line-height: 1;
  color: var(--c-text); margin-bottom: .5rem;
}
.stats .k {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .17em; text-transform: uppercase;
  color: var(--c-text-mute);
}


/* ═══════════════════════════════════════════════════════ 09  SPLIT ═══ */

.split {
  display: grid; gap: clamp(2rem, 5vw, 4.5rem);
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}
@media (min-width: 900px) {
  .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .split--reverse > :first-child { order: 2; }
}
.split__media { border-radius: var(--radius-lg); aspect-ratio: 4 / 5; }
@media (max-width: 899px) { .split__media { aspect-ratio: 16 / 11; } }

/* Stacked, offset photo pair. Only on wide screens — it collapses to one. */
.stack { position: relative; }
.stack .duo:first-child { border-radius: var(--radius-lg); aspect-ratio: 4/5; }
.stack .duo:last-child {
  position: absolute; right: -8%; bottom: -12%;
  width: 46%; aspect-ratio: 1;
  border-radius: var(--radius);
  border: 6px solid var(--c-canvas);
  box-shadow: var(--shadow-lg);
}
@media (max-width: 899px) { .stack .duo:last-child { display: none; } }

/* Bulleted feature list with lime ticks. */
.ticks { display: grid; gap: .85rem; margin: 1.75rem 0 0; }
.ticks li {
  display: grid; grid-template-columns: 20px 1fr; gap: .8rem;
  align-items: start; color: var(--c-text-soft);
}
/* 2px, not 1px. A 1px rule lands on a half-pixel at some zoom levels and
   line-heights, and the browser antialiases it to a different grey per row —
   the list ends up looking like the dashes are randomly two colours. */
.ticks li::before {
  content: ""; margin-top: .55rem;
  width: 20px; height: 2px; background: var(--c-lime);
}


/* ══════════════════════════════════════════ 10  SERVICE ACCORDION ═══ */

/* Services as expanding rows rather than a card grid: a long, quiet list
   that opens to reveal a photo and detail. Built on <details> so it works
   with zero JavaScript; site.js only adds the one-open-at-a-time behaviour
   and the smooth height. */
.acc { border-top: 1px solid var(--c-line); }
.acc__item { border-bottom: 1px solid var(--c-line); }

.acc__sum {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center; gap: clamp(.8rem, 2vw, 1.8rem);
  padding: clamp(1.25rem, 2.6vw, 1.9rem) 0;
  cursor: pointer;
  list-style: none;
  transition: color var(--dur-fast) var(--ease), padding-inline var(--dur) var(--ease);
}
.acc__sum::-webkit-details-marker { display: none; }
.acc__sum:hover { color: var(--c-lime); }
.acc__sum:hover .acc__name { color: var(--c-lime); }

.acc__num {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .12em;
  color: var(--c-text-faint);
  min-width: 2.5ch;
}
.acc__name {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 3.4vw, 2.25rem);
  font-weight: 700; letter-spacing: var(--ls-h); line-height: 1.1;
  color: var(--c-text);
  transition: color var(--dur-fast) var(--ease);
}
.acc__from {
  font-family: var(--font-mono);
  font-size: .8rem; color: var(--c-lime);
  white-space: nowrap;
}
@media (max-width: 620px) { .acc__from { display: none; } }

/* Plus sign that becomes a minus. Two spans, one rotated. */
.acc__ico {
  position: relative; width: 34px; height: 34px; flex: none;
  border: 1px solid var(--c-line-strong); border-radius: 50%;
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.acc__ico::before, .acc__ico::after {
  content: ""; position: absolute; inset: 50% 9px auto; height: 1.5px;
  background: currentColor; transform: translateY(-50%);
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.acc__ico::after { transform: translateY(-50%) rotate(90deg); }
.acc__sum:hover .acc__ico { border-color: var(--c-lime-edge); background: var(--c-lime-wash); }
.acc__item[open] .acc__ico::after { transform: translateY(-50%) rotate(0deg); opacity: 0; }
.acc__item[open] .acc__name { color: var(--c-lime); }

.acc__body {
  display: grid; gap: clamp(1.25rem, 3vw, 2.5rem);
  grid-template-columns: minmax(0,1fr);
  padding: 0 0 clamp(1.75rem, 3vw, 2.5rem);
}
@media (min-width: 780px) {
  .acc__body { grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr); align-items: start; }
}
.acc__body .duo { border-radius: var(--radius); aspect-ratio: 16/10; }
.acc__body p { color: var(--c-text-soft); }
.acc__tags { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; }
.acc__tags li {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .12em; text-transform: uppercase;
  color: var(--c-text-mute);
  border: 1px solid var(--c-line); border-radius: var(--radius-pill);
  padding: .3rem .7rem;
}

/* Smooth open/close. site.js animates max-height; without JS the details
   element still opens instantly, which is a perfectly good fallback. */
.acc__reveal { overflow: hidden; }
.js-ready .acc__reveal { transition: max-height var(--dur) var(--ease-out); }

/* The row's contents drift in a beat behind the height, so the panel reads
   as unfolding rather than as a box that suddenly contains things. */
.js-ready .acc__item[open] .acc__body > * { animation: accIn .6s var(--ease-out) both; }
.js-ready .acc__item[open] .acc__body > :nth-child(2) { animation-delay: .09s; }
@keyframes accIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
/* The whole row nudges inward while it is open — the same gesture as the
   summary's padding transition, carried through to the open state. */
.acc__item[open] .acc__sum { padding-inline: clamp(0rem, 1vw, .75rem); }
@media (prefers-reduced-motion: reduce) {
  .js-ready .acc__item[open] .acc__body > * { animation: none; }
}


/* ══════════════════════════════════════════ 11  PRICING (SERVICES) ═══ */

/* Tabbed price lists. Each panel is a two-column grid of priced rows.
   With JS off, site.js never hides anything, so every panel renders
   stacked and the page still works — see .tabs__panel default below. */
.tabs__list {
  display: flex; flex-wrap: wrap; gap: .5rem;
  margin-bottom: clamp(2rem, 4vw, 3rem);
  border-bottom: 1px solid var(--c-line);
  padding-bottom: 1.25rem;
}
.tabs__tab {
  font-family: var(--font-mono);
  font-size: .74rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--c-text-mute);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-pill);
  padding: .55rem 1.05rem;
  transition: all var(--dur-fast) var(--ease);
}
.tabs__tab:hover { color: var(--c-text); border-color: var(--c-line-strong); background: var(--c-surface-2); }
.tabs__tab[aria-selected="true"] {
  background: var(--c-lime); color: var(--c-on-lime);
  border-color: var(--c-lime);
}
.tabs__panel { margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.js-ready .tabs__panel[hidden] { display: none; }
/* Switching category replays this — site.js restarts the animation on the
   panel it reveals, so changing tab has the same movement as landing on
   the page with that tab already open. */
.js-ready .tabs__panel { animation: panelIn .55s var(--ease-out) both; }
@keyframes panelIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .js-ready .tabs__panel { animation: none; }
}

.pricelist { display: grid; gap: 0 clamp(2rem, 5vw, 4.5rem); }
@media (min-width: 860px) { .pricelist { grid-template-columns: 1fr 1fr; } }

.price-row {
  display: grid; grid-template-columns: 1fr auto;
  gap: .5rem 1.25rem; align-items: baseline;
  padding: 1.05rem 0;
  border-bottom: 1px solid var(--c-line);
}
.price-row__name {
  font-family: var(--font-display);
  font-size: 1.02rem; font-weight: 600; color: var(--c-text);
  letter-spacing: -.01em;
}
/* Qualifiers inside a service name — "(under 12)", "(gel)". */
.price-row__name em {
  font-style: normal;
  font-family: var(--font-mono);
  font-size: .76rem; letter-spacing: .04em;
  color: var(--c-text-faint);
}
.price-row__desc {
  grid-column: 1 / -1;
  font-size: .855rem; color: var(--c-text-mute); line-height: 1.6;
  margin: .2rem 0 0; max-width: 50ch;
}
.price-row__price {
  font-family: var(--font-mono);
  font-size: .95rem; color: var(--c-lime);
  font-variant-numeric: tabular-nums;
}
.price-row.is-popular .price-row__name::after {
  content: "Popular";
  display: inline-block; vertical-align: middle;
  margin-left: .6rem;
  font-family: var(--font-mono);
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--c-on-lime); background: var(--c-lime);
  border-radius: var(--radius-pill);
  padding: .16rem .5rem;
}

.panel-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: .75rem;
  margin-bottom: 1.25rem;
}
.panel-head h2 { margin: 0; font-size: clamp(1.5rem, 3vw, 2.1rem); }
.panel-head span {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase;
  color: var(--c-text-faint);
}

/* Small print under the menu. */
.fineprint {
  font-size: .84rem; color: var(--c-text-faint);
  border-left: 1px solid var(--c-line-strong);
  padding-left: 1.1rem;
}


/* ─────────────────────────────── Lookbook strip (home page) ───────── */

/* A full-bleed scrolling row that deliberately runs off the right edge, so
   it reads as "there is more" rather than as a finished grid. It sits
   OUTSIDE .wrap; the padding below re-aligns its left edge with the wrap
   content so it still lines up with the heading above it. */
.strip {
  --strip-inset: var(--gutter);
  display: flex;
  gap: clamp(.6rem, 1.4vw, 1.1rem);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-inline: var(--strip-inset);
  /* scroll-padding MUST match padding, or scroll-snap silently defeats it.
     The snapport defaults to the scrollport edge, so "snap the first item to
     start" resolves to scrollLeft == padding-left — the row loads already
     scrolled by exactly the inset and the first tile sits half off-screen. */
  scroll-padding-inline: var(--strip-inset);
  padding-block-end: 1rem;
  scrollbar-width: none;
}
/* Line the row's left edge up with the .wrap content above it, while still
   letting it bleed off the right edge. */
@supports (padding: max(0px)) {
  .strip { --strip-inset: max(var(--gutter), calc((100% - var(--wrap)) / 2 + var(--gutter))); }
}
.strip::-webkit-scrollbar { display: none; }

.strip__item {
  flex: 0 0 clamp(190px, 25vw, 300px);
  scroll-snap-align: start;
  display: block;
  text-decoration: none;
  color: var(--c-text-mute);
}
.strip__item .duo {
  aspect-ratio: 3 / 4;
  border-radius: var(--radius);
  border: 1px solid var(--c-line);
  margin-bottom: .8rem;
}
.strip__cap {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease);
}
.strip__item:hover .strip__cap,
.strip__item:focus-visible .strip__cap { color: var(--c-lime); }


/* ══════════════════════════════════ 12  GALLERY MOSAIC + LIGHTBOX ═══ */

/* Deliberately irregular: a 6-column grid where a few tiles span two
   columns or two rows. Reads as a curated wall, not a contact sheet. */
.mosaic {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: clamp(90px, 13vw, 165px);
  gap: clamp(.5rem, 1.2vw, 1rem);
  /* Dense packing back-fills the holes the wide and tall tiles leave behind.
     Reading order can differ from visual order as a result — acceptable here
     because the tiles are an unordered set of photos, not a sequence. */
  grid-auto-flow: dense;
}
/* Every tile is 2 columns wide except .t-wide, which is 4. Keep the spans in
   each visual row adding up to exactly 6 — the tile order in gallery.html is
   2,2,2 / 4,2 / 2,4 / 2,2,2 / 4,2 — or the grid leaves holes. Row spans are
   deliberately uniform: a 3-row tile cannot be back-filled by a 2-row one,
   so mixing heights reintroduces the gaps this ordering exists to avoid. */
.mosaic > * { grid-column: span 2; grid-row: span 2; }
.mosaic > .t-wide { grid-column: span 4; }
@media (max-width: 900px) {
  .mosaic { grid-template-columns: repeat(4, 1fr); }
  .mosaic > .t-wide { grid-column: span 4; }
}
@media (max-width: 560px) {
  .mosaic { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 42vw; }
  .mosaic > *, .mosaic > .t-wide { grid-column: span 1; grid-row: span 1; }
}

.tile {
  position: relative; overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--c-line);
  padding: 0; width: 100%; height: 100%;
  cursor: zoom-in;
  background: var(--c-surface-2);
  transition: transform var(--dur) var(--ease-out),
              border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur) var(--ease-out);
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-5px);
  border-color: var(--c-lime-edge);
  box-shadow: var(--shadow);
}
.tile .duo { width: 100%; height: 100%; }
.tile__cap {
  position: absolute; inset: auto 0 0; z-index: 2;
  padding: 1.5rem .9rem .8rem;
  background: linear-gradient(transparent, rgba(11,11,15,.86));
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .13em; text-transform: uppercase;
  color: var(--c-text); text-align: left;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.tile:hover .tile__cap, .tile:focus-visible .tile__cap { opacity: 1; transform: translateY(0); }

/* ── Lightbox ── */
.lightbox {
  position: fixed; inset: 0; z-index: 2000;
  display: none;
  align-items: center; justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(5,5,8,.94);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.lightbox.is-open { display: flex; }
.lightbox__img {
  max-width: min(1100px, 100%); max-height: 82svh;
  width: auto; height: auto; object-fit: contain;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.lightbox__cap {
  position: absolute; inset: auto 0 clamp(1rem, 3vw, 2rem);
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase;
  color: var(--c-text-mute);
}
.lightbox__btn {
  position: absolute;
  width: 48px; height: 48px;
  display: grid; place-items: center;
  border: 1px solid var(--c-line-strong);
  border-radius: 50%;
  color: var(--c-text);
  background: rgba(20,20,25,.7);
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.lightbox__btn:hover { background: var(--c-lime); color: var(--c-on-lime); border-color: var(--c-lime); }
.lightbox__btn svg { width: 20px; height: 20px; }
.lightbox__close { top: clamp(1rem,3vw,2rem); right: clamp(1rem,3vw,2rem); }
.lightbox__prev  { left: clamp(.5rem,2vw,2rem);  top: 50%; transform: translateY(-50%); }
.lightbox__next  { right: clamp(.5rem,2vw,2rem); top: 50%; transform: translateY(-50%); }
.lightbox__prev:hover, .lightbox__next:hover { transform: translateY(-50%); }


/* ═════════════════════════════════════════ 13  REVIEWS CAROUSEL ═════ */

/* CSS scroll-snap does the carousel; site.js only wires the arrows, the
   dots and the auto-advance. Without JS it is still a swipeable strip. */
.reviews {
  display: flex; gap: clamp(1rem, 2vw, 1.5rem);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-bottom: 1rem;
  scrollbar-width: none;
}
.reviews::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { .reviews { scroll-behavior: auto; } }

.review {
  flex: 0 0 min(100%, 420px);
  scroll-snap-align: start;
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 3vw, 2.2rem);
  display: flex; flex-direction: column;
}
.review__stars { display: flex; gap: .18rem; margin-bottom: 1rem; color: var(--c-lime); }
.review__stars svg { width: 15px; height: 15px; }
.review blockquote {
  margin: 0 0 1.5rem;
  font-family: var(--font-display);
  font-size: 1.12rem; line-height: 1.55; color: var(--c-text);
  letter-spacing: -.01em;
  flex: 1;
}
.review figcaption {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase;
  color: var(--c-text-mute);
  padding-top: 1.1rem; border-top: 1px solid var(--c-line);
}

.carousel-ctl { display: flex; align-items: center; gap: .6rem; margin-top: 1.5rem; }
.carousel-ctl button {
  width: 42px; height: 42px; display: grid; place-items: center;
  border: 1px solid var(--c-line-strong); border-radius: 50%;
  color: var(--c-text);
  transition: all var(--dur-fast) var(--ease);
}
.carousel-ctl button:hover { background: var(--c-lime); color: var(--c-on-lime); border-color: var(--c-lime); }
.carousel-ctl button svg { width: 17px; height: 17px; }
.carousel-dots { display: flex; gap: .4rem; margin-left: auto; }
.carousel-dots button {
  width: 26px; height: 3px; border-radius: 2px;
  background: var(--c-line-strong); border: 0; padding: 0;
  transition: background var(--dur-fast) var(--ease);
}
.carousel-dots button[aria-current="true"] { background: var(--c-lime); }


/* ═════════════════════════════════════════════════════════ 14  FAQ ═══ */

.faq { display: grid; gap: .7rem; }
.faq__item {
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur-fast) var(--ease);
}
.faq__item[open] { border-color: var(--c-lime-edge); }
.faq__sum {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.15rem clamp(1rem, 2vw, 1.5rem);
  cursor: pointer; list-style: none;
  font-family: var(--font-display); font-weight: 600;
  color: var(--c-text);
}
.faq__sum::-webkit-details-marker { display: none; }
.faq__sum:hover { color: var(--c-lime); }
.faq__sum i {
  flex: none; width: 12px; height: 12px; position: relative;
  transition: transform var(--dur-fast) var(--ease);
}
.faq__sum i::before, .faq__sum i::after {
  content: ""; position: absolute; background: currentColor;
}
.faq__sum i::before { inset: 5.25px 0 auto; height: 1.5px; }
.faq__sum i::after  { inset: 0 5.25px auto; width: 1.5px; height: 12px; }
.faq__item[open] .faq__sum i { transform: rotate(45deg); color: var(--c-lime); }
.faq__body {
  padding: 0 clamp(1rem, 2vw, 1.5rem) 1.35rem;
  color: var(--c-text-soft); font-size: .955rem;
  max-width: 68ch;
}


/* ════════════════════════════════════════════════════ 15  CTA BAND ═══ */

.cta {
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--c-line);
  padding: clamp(2.5rem, 6vw, 5rem) clamp(1.5rem, 4vw, 4rem);
  text-align: center;
}
.cta__media { position: absolute; inset: 0; z-index: -2; }
/* The photo is texture, not subject. Dark enough that white display type sits
   at full contrast on it, light enough that you can still tell it is a photo —
   at brightness .42 under the old scrim it read as a flat black panel. */
.cta__media img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1) brightness(.55); }
.cta::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(60% 90% at 50% 120%, var(--c-violet-wash), transparent 70%),
    linear-gradient(rgba(11,11,15,.62), rgba(11,11,15,.80));
}
.cta h2 { font-size: clamp(2rem, 5vw, 3.6rem); text-transform: uppercase; letter-spacing: var(--ls-mega); }
.cta p { color: var(--c-text-soft); max-width: 48ch; margin-inline: auto; }
.cta .btn-row { justify-content: center; margin-top: 2rem; }


/* ═══════════════════════════════════════ 16  VISIT / HOURS / MAP ════ */

.info-grid {
  display: grid; gap: clamp(1.5rem, 3vw, 2.5rem);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 900px) { .info-grid { grid-template-columns: minmax(0,.9fr) minmax(0,1.1fr); align-items: start; } }

.card {
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  position: relative; isolation: isolate;
  transition: border-color var(--dur) var(--ease);
}
/* Pointer-tracked wash. site.js writes --mx / --my as percentages on hover.
   z-index:-1 inside the card's own stacking context puts it above the card
   background and below the text, so nothing needs a z-index of its own. */
.card::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; pointer-events: none;
  background: radial-gradient(20rem 20rem at var(--mx, 50%) var(--my, 0%),
              var(--c-lime-wash), transparent 68%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.card:hover { border-color: var(--c-line-strong); }
.card:hover::after, .card:focus-within::after { opacity: 1; }
@media (hover: none) { .card::after { display: none; } }

.contact-list { display: grid; gap: 1.4rem; }
.contact-list li { display: grid; grid-template-columns: 40px 1fr; gap: 1rem; align-items: start; }
.contact-list .ico {
  width: 40px; height: 40px; display: grid; place-items: center;
  border: 1px solid var(--c-lime-edge); border-radius: var(--radius-sm);
  background: var(--c-lime-wash); color: var(--c-lime);
}
.contact-list .ico svg { width: 17px; height: 17px; }
.contact-list .k {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase;
  color: var(--c-text-mute); margin-bottom: .2rem;
}
.contact-list .v { color: var(--c-text); font-size: 1.02rem; line-height: 1.5; }
.contact-list a.v { text-decoration: none; }
.contact-list a.v:hover { color: var(--c-lime); }

/* Hours table. data-day / data-open / data-close on each row drive both the
   status pill and the "Today" tag — see site.js. */
.hours { width: 100%; border-collapse: collapse; }
.hours th, .hours td { text-align: left; padding: .8rem 0; border-bottom: 1px solid var(--c-line); }
.hours th {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase;
  color: var(--c-text-mute); font-weight: 400;
}
.hours td { color: var(--c-text); font-family: var(--font-mono); font-size: .9rem; }
.hours td:last-child { text-align: right; }
.hours tr:last-child th, .hours tr:last-child td { border-bottom: 0; }
.hours tr.is-today th, .hours tr.is-today td { color: var(--c-lime); }
.hours tr.is-today th::after {
  content: "Today";
  margin-left: .6rem;
  font-size: 9px; letter-spacing: .12em;
  color: var(--c-on-lime); background: var(--c-lime);
  border-radius: var(--radius-pill); padding: .14rem .45rem;
}
.hours td.closed { color: var(--c-text-faint); }

.map {
  border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--c-line);
  background: var(--c-surface-2);
  aspect-ratio: 16 / 11;
}
.map iframe { width: 100%; height: 100%; border: 0; filter: grayscale(1) invert(.92) contrast(.85) hue-rotate(180deg); }
@media (max-width: 620px) { .map { aspect-ratio: 4 / 3; } }


/* ════════════════════════════════ 17  FOOTER & MOBILE CALL BAR ══════ */

.footer {
  border-top: 1px solid var(--c-line);
  background: var(--c-surface);
  padding-top: clamp(3rem, 6vw, 5rem);
  padding-bottom: 2rem;
}
.footer__grid {
  display: grid; gap: clamp(2rem, 4vw, 3rem);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 760px)  { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr; } }
@media (min-width: 1040px) { .footer__grid { grid-template-columns: 1.6fr 1fr 1fr 1.2fr; } }

.footer h3 {
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .18em; text-transform: uppercase;
  color: var(--c-text-mute); font-weight: 400;
  margin-bottom: 1.1rem;
}
.footer__links { display: grid; gap: .6rem; }
.footer__links a { color: var(--c-text-soft); text-decoration: none; font-size: .93rem; }
.footer__links a:hover { color: var(--c-lime); }
.footer__blurb { color: var(--c-text-mute); font-size: .93rem; max-width: 34ch; margin-top: 1.1rem; }

.social { display: flex; gap: .5rem; margin-top: 1.25rem; }
.social a {
  width: 38px; height: 38px; display: grid; place-items: center;
  border: 1px solid var(--c-line-strong); border-radius: 50%;
  color: var(--c-text-mute);
  transition: all var(--dur-fast) var(--ease);
}
.social a:hover { color: var(--c-on-lime); background: var(--c-lime); border-color: var(--c-lime); }
.social svg { width: 16px; height: 16px; }

.footer__base {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--c-line);
  display: flex; flex-wrap: wrap; gap: .75rem 1.5rem;
  justify-content: space-between; align-items: center;
  font-family: var(--font-mono);
  font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase;
  color: var(--c-text-faint);
}
.footer__base a { color: var(--c-text-mute); text-decoration: none; }
.footer__base a:hover { color: var(--c-lime); }

/* Sticky call bar. Phone taps are most of the conversions on a salon site,
   so on mobile the two primary actions never scroll away. */
.callbar {
  position: fixed; inset: auto 0 0; z-index: 1150;
  display: none;
  gap: 1px;
  background: var(--c-line);
  border-top: 1px solid var(--c-line);
}
.callbar a {
  flex: 1;
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .95rem .5rem;
  background: var(--c-surface);
  color: var(--c-text); text-decoration: none;
  font-size: .88rem; font-weight: 600;
}
.callbar a.is-primary { background: var(--c-lime); color: var(--c-on-lime); }
.callbar svg { width: 16px; height: 16px; }
@media (max-width: 760px) {
  .callbar { display: flex; }
  /* Reserve room so the footer is never trapped behind the bar. */
  body { padding-bottom: 54px; }
}


/* ═══════════════════════════════════ 18  MOTION ═════════════════════ */

/* Everything in this section obeys three rules:

     1. Nothing is ever hidden unless JavaScript has proved it is alive.
        The <head> of every page adds .js-ready, and a 3s failsafe timer
        adds .reveal-fallback if site.js never loads. Content is never
        trapped behind an animation that will not run.
     2. Only opacity, transform, clip-path and filter are animated. Those
        are the properties a compositor can handle without re-laying out
        the page, which is what keeps this smooth on the mid-range phones
        that are most of a salon's traffic.
     3. Every effect has a reduced-motion off switch at the bottom of the
        section. Not a shorter duration — off.                          */


/* ───────────────────────────────────────── 18.1  Scroll reveal ────── */

/* The hidden state is scoped with :not(.is-in) rather than overridden by a
   separate .is-in rule. That is not a style preference — it is the only
   way to keep this correct. Putting a descendant selector inside :not()
   gives the whole rule that argument's specificity, so
   `.js-ready .reveal:not(.reveal-fallback .reveal)` scores (0,4,0) and
   silently outranks `.js-ready .reveal.is-in` at (0,3,0). Every revealed
   element then stays at opacity 0 no matter what the script does. Written
   this way the two states cannot both match, so there is nothing to win. */
.js-ready .reveal {
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out),
              clip-path var(--dur-slow) var(--ease-out),
              filter var(--dur-slow) var(--ease-out);
  transition-delay: var(--d, 0s);
}
.js-ready:not(.reveal-fallback) .reveal:not(.is-in) {
  opacity: 0;
  transform: translate3d(0, var(--rise), 0);
}

/* Directional variants. Same specificity as the base rule, so they win on
   source order alone — keep them below it. */
.js-ready:not(.reveal-fallback) .reveal--left:not(.is-in)  { transform: translate3d(calc(var(--slide) * -1), 0, 0); }
.js-ready:not(.reveal-fallback) .reveal--right:not(.is-in) { transform: translate3d(var(--slide), 0, 0); }
.js-ready:not(.reveal-fallback) .reveal--zoom:not(.is-in)  { transform: scale(.94); }
/* Wipes upward from the bottom edge. For photography, where a fade reads
   as "not loaded yet" but a wipe reads as deliberate. */
.js-ready:not(.reveal-fallback) .reveal--wipe:not(.is-in)  { opacity: 1; clip-path: inset(0 0 100% 0); }
.js-ready .reveal--wipe { clip-path: inset(0 0 0 0); }


/* ───────────────────────────────────────────── 18.2  Stagger ──────── */

/* A container marked data-stagger releases its children one after another.
   site.js writes --sd on each child from the step in the attribute, so the
   cascade below never has to know how many children there are. */
.js-ready [data-stagger] > * {
  transition: opacity .7s var(--ease-out) var(--sd, 0s),
              transform .7s var(--ease-out) var(--sd, 0s);
}
.js-ready:not(.reveal-fallback) [data-stagger]:not(.is-in) > * {
  opacity: 0;
  transform: translate3d(0, var(--rise), 0);
}


/* ────────────────────────────────── 18.3  Word-by-word headings ───── */

/* site.js wraps each word of the section headings in a mask + inner span.
   The mask is inline-block so the line still wraps and text-wrap:balance
   still works; the inner span is what actually travels. */
.js-ready .word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: .08em;
  margin-bottom: -.08em;
}
.js-ready .word > span {
  display: inline-block;
  transition: transform .8s var(--ease-out) var(--wd, 0s);
}
.js-ready:not(.reveal-fallback) .reveal-words:not(.is-in) .word > span { transform: translateY(108%); }


/* ─────────────────────────────────────────── 18.4  Entrances ──────── */

/* The hero plays on load rather than on scroll — it is already in view, so
   there is nothing to wait for. Every child except the wordmark rises;
   the wordmark has its own line-by-line reveal below. */
@keyframes riseIn {
  from { opacity: 0; transform: translate3d(0, 30px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes lineUp {
  from { transform: translate3d(0, 108%, 0); }
  to   { transform: none; }
}

.js-ready .hero__inner > *:not(.hero__title) {
  animation: riseIn .95s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * .09s + .12s);
}
.js-ready .hero__inner > :nth-child(1) { --i: 0; }
.js-ready .hero__inner > :nth-child(2) { --i: 1; }
.js-ready .hero__inner > :nth-child(3) { --i: 2; }
.js-ready .hero__inner > :nth-child(4) { --i: 3; }
.js-ready .hero__inner > :nth-child(5) { --i: 4; }

.js-ready .hero__line > span {
  animation: lineUp 1.15s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * .13s + .22s);
}
.js-ready .hero__line:nth-of-type(2) > span { --i: 1; }

.js-ready .hero__cue { animation: riseIn 1s var(--ease-out) both; animation-delay: .95s; }

/* Interior pages get the same idea at a third of the amplitude. */
.js-ready .pagehead > .wrap > * {
  animation: riseIn .85s var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * .08s + .1s);
}
.js-ready .pagehead > .wrap > :nth-child(1) { --i: 0; }
.js-ready .pagehead > .wrap > :nth-child(2) { --i: 1; }
.js-ready .pagehead > .wrap > :nth-child(3) { --i: 2; }


/* ─────────────────────────────────────────── 18.5  Parallax ───────── */

/* Background photography drifts against the scroll. site.js writes --py in
   pixels; the extra 18% of height is the slack that keeps the frame full
   at either end of the travel, and the -7.6% re-centres the oversized
   image inside its frame.

   That offset is in the transform rather than a negative margin-top on
   purpose: percentage margins resolve against the containing block's
   WIDTH, so on a 1920×900 hero `margin-top:-9%` would lift the photo by
   173px instead of 81 and shear the top off it. A percentage inside
   translate resolves against the element's own height, which is the axis
   we actually mean.

   The class is added by site.js, never written in the HTML — so a browser
   with no JS, or a visitor who has asked for reduced motion, never gets an
   oversized image that nothing is left to position. */
.js-ready .has-parallax { overflow: hidden; }
.js-ready .has-parallax img {
  height: 118%;
  transform: translate3d(0, calc(-7.6% + var(--py, 0px)), 0);
  will-change: transform;
}


/* ──────────────────────────────────── 18.6  Nav drawer entrance ───── */

@media (max-width: 980px) {
  .js-ready .nav.is-open > * {
    animation: riseIn .5s var(--ease-back) both;
    animation-delay: calc(var(--i, 0) * .055s + .08s);
  }
  .js-ready .nav > :nth-child(1) { --i: 0; }
  .js-ready .nav > :nth-child(2) { --i: 1; }
  .js-ready .nav > :nth-child(3) { --i: 2; }
  .js-ready .nav > :nth-child(4) { --i: 3; }
  .js-ready .nav > :nth-child(5) { --i: 4; }
}


/* ──────────────────────────────────────── 18.7  Lightbox motion ───── */

.lightbox.is-open { animation: lbFade .3s var(--ease) both; }
.lightbox.is-open .lightbox__img { animation: lbPop .5s var(--ease-out) both; }
@keyframes lbFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes lbPop {
  from { opacity: 0; transform: scale(.94) translateY(12px); }
  to   { opacity: 1; transform: none; }
}


/* ─────────────────────────────────────── 18.8  Reduced motion ─────── */

@media (prefers-reduced-motion: reduce) {
  .js-ready .reveal,
  .js-ready [data-stagger] > * {
    opacity: 1 !important; transform: none !important;
    filter: none !important; clip-path: none !important;
    transition: none !important;
  }
  .js-ready .word > span { transform: none !important; transition: none !important; }
  .js-ready .hero__inner > *,
  .js-ready .hero__line > span,
  .js-ready .hero__cue,
  .js-ready .pagehead > .wrap > *,
  .js-ready .nav.is-open > *,
  .lightbox.is-open,
  .lightbox.is-open .lightbox__img { animation: none !important; }
  .js-ready .has-parallax img { height: 100%; transform: none; will-change: auto; }
}


/* ═══════════════════════════════════════════════════════ 19  PRINT ═══ */

/* Salons genuinely print their price list. Force it light, drop the
   furniture, and open every tab panel. */
@media print {
  :root { --c-canvas: #fff; --c-text: #000; --c-text-soft: #222; --c-text-mute: #555;
          --c-lime: #000; --c-line: #ccc; --c-surface: #fff; }
  body { background: #fff; color: #000; }
  body::after,
  .header, .callbar, .nav-toggle, .marquee, .cta, .footer__base,
  .tabs__list, .progress, .lightbox, .social, .btn { display: none !important; }
  .tabs__panel[hidden] { display: block !important; }
  .pagehead { padding-top: 0; }
  .pagehead__media, .pagehead::before { display: none; }
  .price-row { break-inside: avoid; border-bottom: 1px solid #ddd; }
  .price-row__price { color: #000; }
  /* Nothing may reach the printer mid-animation. */
  .reveal, [data-stagger] > *, .word > span, .hero__inner > *, .pagehead > .wrap > * {
    opacity: 1 !important; transform: none !important;
    filter: none !important; clip-path: none !important; animation: none !important;
  }
  .has-parallax img { height: auto !important; transform: none !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .75em; color: #666; }
}
