/* ==========================================================================
   Executive Grooming Studio — stylesheet

   Hand-written CSS, no framework: the proposal locks the front end to plain
   HTML, CSS and JavaScript.

   CONTENTS
     1. Theme tokens (light and dark)
     2. Reset and base
     3. Typography
     4. Layout
     5. Header and navigation
     6. Components
     7. Feature-specific
     8. Motion
     9. Print

   METHOD
     Mobile first. The base rules describe the narrow layout and media queries
     add complexity for wider screens, so the simplest stylesheet is the one
     phones have to parse.

     Every colour is a semantic token (--surface, --text, --accent), never a
     literal. That is what makes two themes possible without duplicating a
     single rule: only the token VALUES change between themes, and the 400-odd
     lines that use them are written once.
   ========================================================================== */


/* ==========================================================================
   1. THEME TOKENS
   ==========================================================================

   THREE-STATE THEMING, and the order of these blocks matters.

     a) :root                        — the light palette, and the default.
     b) @media (prefers-color-scheme: dark) on :root:not([data-theme="light"])
                                     — follows the operating system, UNLESS the
                                       visitor has explicitly chosen light.
     c) :root[data-theme="dark"]     — an explicit choice of dark wins even on
                                       a machine set to light.

   Together these give: respect the visitor's system setting by default, and
   let an explicit choice override it in BOTH directions. The `data-theme`
   attribute is written to <html> by the toggle in app.js.

   IMPORTANT: this works with JavaScript disabled. With no script, no attribute
   is ever set, so (b) applies and the site still follows the system setting.
   Only the manual toggle needs JavaScript, which matches the rule stated in
   app.js that the site must work without it.
   ---------------------------------------------------------------------- */

:root {
    /* --- Surfaces, from furthest back to nearest front ------------------ */
    --bg:            #f6f3ee;   /* the page itself — warm paper, not clinical white */
    --surface:       #ffffff;   /* cards */
    --surface-2:     #f0ebe3;   /* inset areas, table headers, hover states */

    /* --- Lines --------------------------------------------------------- */
    --border:        #e2dacd;
    --border-strong: #cfc4b2;

    /* Form controls get their OWN border token, deliberately darker than the
       decorative one. WCAG 1.4.11 requires 3:1 for a boundary that IDENTIFIES
       a component, and an input's edge is exactly that — it is what tells the
       visitor there is a box to type in. Decorative rules (card edges, table
       separators) carry no such requirement and stay lighter, because making
       them meet 3:1 would draw the eye to the furniture rather than the
       content. Measured below against each theme's input fill. */
    --field-border:  #80867d;   /* 3.37:1 against the input fill — see below */

    /* --- Text ---------------------------------------------------------- */
    /* A near-black with a trace of green rather than a neutral one, so body
       text sits in the same family as the accent instead of fighting it. */
    --text:          #18211d;
    --text-muted:    #5b6660;   /* 5.4:1 on --bg — passes AA for body text */
    --text-subtle:   #8a938d;   /* decorative only, never load-bearing */

    /* --- Accent -------------------------------------------------------- */
    /* Eucalyptus green. The studio photography is ivory walls, pale oak and
       sage-green capes, and the accent is drawn from that palette so the
       interface and the pictures read as one place. It replaced the original
       gold, which read as heavy once photographs sat beside it (ADR-0010).

       A deep value on light backgrounds and a pale sage on dark ones: the
       pale sage that works on a dark ground fails contrast on white, so the
       two themes use different values of the same hue rather than one
       compromise that is wrong in both. */
    --accent:        #2f5d50;
    --accent-hover:  #234a3f;
    --accent-soft:   rgba(47, 93, 80, .10);
    --on-accent:     #ffffff;

    /* The hero wash, and its FADE-OUT colour.
       --glow-fade must be the same hue at zero alpha, never `transparent`.
       `transparent` is rgba(0,0,0,0), so a gradient running to it interpolates
       through progressively darker greys and paints a dirty ring where the
       colour should simply disappear. That grey halo is what made the wash
       look like a hard-edged shape sitting behind the heading. */
    --glow:          rgba(47, 93, 80, .14);
    --glow-fade:     rgba(47, 93, 80, 0);

    /* --- Status -------------------------------------------------------- */
    /* Success is a yellower leaf green, deliberately a different hue from the
       eucalyptus accent: a "confirmed" badge uses the accent and a
       "completed" badge uses this, and the two must not look alike. */
    --success:       #35722a;
    --success-soft:  rgba(53, 114, 42, .10);
    --danger:        #a83a2c;
    --danger-soft:   rgba(168, 58, 44, .10);
    --warn:          #8a5f0f;
    --warn-soft:     rgba(138, 95, 15, .12);

    /* --- Elevation ----------------------------------------------------- */
    /* Light mode separates surfaces with shadow; dark mode cannot (a shadow
       is invisible on a dark ground) and uses border contrast instead. */
    --shadow-sm:     0 1px 2px rgba(24, 33, 29, .05);
    --shadow:        0 1px 3px rgba(24, 33, 29, .07), 0 4px 12px rgba(24, 33, 29, .05);
    /* A deeper, softer shadow for photographs, which float above the page
       rather than sitting on it the way a card does. */
    --shadow-photo:  0 18px 40px -18px rgba(24, 33, 29, .35);

    /* --- Geometry ------------------------------------------------------ */
    /* Deliberately sharp. Heavily rounded corners read as friendly and
       consumer-app; a near-square edge reads as editorial and tailored, which
       is the register a salon calling itself "Executive" wants. The radius is
       not zero — a hairline of softening keeps the corner from looking like a
       rendering artefact on a high-density screen. */
    --radius-sm:     2px;
    --radius:        3px;
    --wrap:          1100px;
    --wrap-narrow:   34rem;

    /* --- Type ---------------------------------------------------------- */
    /* A system font stack: zero network requests, no third-party dependency,
       and it renders in the typeface the visitor's own platform is tuned for.
       A web font was considered and rejected — it would add an external
       request to every page load on a free-tier host for a refinement the
       careful scale and letter-spacing below already achieve. */
    --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    /* Tells the browser to render form controls, scrollbars and the like in
       the matching scheme. Without it, a dark page gets white scrollbars. */
    color-scheme: light;
}

/* (b) Follow the operating system, unless light was explicitly chosen. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        /* Deep forest rather than black: dark enough to rest the eyes, green
           enough that the page still reads as the same studio at night. */
        --bg:            #17211c;
        --surface:       #1d2923;
        --surface-2:     #24312a;

        --border:        #2f3d36;
        --border-strong: #45564d;
        --field-border:  #6f8077;   /* 3.96:1 against the input fill */

        --text:          #edf2ee;
        --text-muted:    #a4b3ab;   /* 7.6:1 on --bg */
        --text-subtle:   #75867d;

        --accent:        #8fc7ab;   /* pale sage, for a dark ground */
        --accent-hover:  #a8d6bf;
        --accent-soft:   rgba(143, 199, 171, .12);
        --glow:          rgba(143, 199, 171, .10);
        --glow-fade:     rgba(143, 199, 171, 0);
        --on-accent:     #0f1d17;

        --success:       #9ccf7c;
        --success-soft:  rgba(156, 207, 124, .13);
        --danger:        #e8887b;
        --danger-soft:   rgba(232, 136, 123, .13);
        --warn:          #e2ad4f;
        --warn-soft:     rgba(226, 173, 79, .13);

        /* Shadows are near-invisible on a dark ground, so they are reduced to
           almost nothing and separation comes from --border instead. */
        --shadow-sm:     0 1px 2px rgba(0, 0, 0, .3);
        --shadow:        0 1px 3px rgba(0, 0, 0, .35);
        --shadow-photo:  0 18px 40px -18px rgba(0, 0, 0, .6);

        color-scheme: dark;
    }
}

/* (c) An explicit choice of dark wins on any machine. Values duplicated from
   (b) deliberately: CSS custom properties cannot be aliased across selectors
   without a preprocessor, and the proposal rules one out. */
:root[data-theme="dark"] {
    --bg:            #17211c;
    --surface:       #1d2923;
    --surface-2:     #24312a;

    --border:        #2f3d36;
    --border-strong: #45564d;
    --field-border:  #6f8077;   /* 3.96:1 against the input fill */

    --text:          #edf2ee;
    --text-muted:    #a4b3ab;
    --text-subtle:   #75867d;

    --accent:        #8fc7ab;
    --accent-hover:  #a8d6bf;
    --accent-soft:   rgba(143, 199, 171, .12);
    --glow:          rgba(143, 199, 171, .10);
    --glow-fade:     rgba(143, 199, 171, 0);
    --on-accent:     #0f1d17;

    --success:       #9ccf7c;
    --success-soft:  rgba(156, 207, 124, .13);
    --danger:        #e8887b;
    --danger-soft:   rgba(232, 136, 123, .13);
    --warn:          #e2ad4f;
    --warn-soft:     rgba(226, 173, 79, .13);

    --shadow-sm:     0 1px 2px rgba(0, 0, 0, .3);
    --shadow:        0 1px 3px rgba(0, 0, 0, .35);
    --shadow-photo:  0 18px 40px -18px rgba(0, 0, 0, .6);

    color-scheme: dark;
}


/* ==========================================================================
   2. RESET AND BASE
   ========================================================================== */

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

html {
    -webkit-text-size-adjust: 100%;
    /* Paired with overflow-x on body, below. With it set on html too, html's
       value is the one handed up to the viewport, and body keeps its own clip
       instead of passing it on — so the overflow is cut off at the page edge
       rather than merely having its scrollbar hidden, which iOS Safari ignores. */
    overflow-x: clip;
}

body {
    margin: 0;
    min-height: 100vh;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;

    /* Colour is the only thing that changes between themes, so a short
       transition makes the toggle feel deliberate rather than jarring.
       Suppressed entirely under prefers-reduced-motion (section 8). */
    transition: background-color .25s ease, color .25s ease;

    /* The hero's wash deliberately spills 25% past each side of the hero, so
       it fades out instead of stopping at a hard edge (section 7). On a phone
       that spill is wider than the screen, and it made the whole page slide
       sideways under a thumb — measured at 69px of horizontal scroll on a
       390px-wide phone. Clipping here, at the edge of the viewport, removes
       the scroll without clipping the wash anywhere it can be seen.

       `clip`, not `hidden`: `hidden` would make the body a scroll container,
       which breaks the sticky header. `clip` cuts the overflow off and
       creates no scroller at all. */
    overflow-x: clip;
}

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

/* A visible, high-contrast keyboard focus ring. Removing outlines makes a
   site unusable without a mouse, so the default is REPLACED, never deleted.
   :focus-visible shows it for keyboard users without ringing every mouse
   click. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 4px;
}


/* ==========================================================================
   3. TYPOGRAPHY
   ==========================================================================
   A deliberate scale rather than ad-hoc sizes. clamp() lets headings grow
   with the viewport between a readable floor and a controlled ceiling, so no
   heading breakpoints are needed. Negative letter-spacing on large text is
   what stops big headings looking loose — most of the "premium" feel of a
   page is this detail rather than the colours.
   ========================================================================== */

h1, h2, h3, h4 {
    margin: 0 0 .5rem;
    line-height: 1.2;
    font-weight: 650;
    letter-spacing: -.02em;
    color: var(--text);
    text-wrap: balance;   /* avoids a single orphaned word on the last line */
}

h1 { font-size: clamp(1.75rem, 1.2rem + 2.4vw, 2.6rem); }
h2 { font-size: clamp(1.3rem, 1.05rem + 1.1vw, 1.7rem); }
h3 { font-size: 1.075rem; letter-spacing: -.01em; }
h4 { font-size: .95rem; }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

a {
    color: var(--accent);
    text-decoration: none;
    text-underline-offset: 3px;
    transition: color .15s ease;
}
a:hover { color: var(--accent-hover); text-decoration: underline; }

strong { font-weight: 650; }

.lead        { font-size: 1.075rem; color: var(--text-muted); max-width: 46rem; }
.muted       { color: var(--text-muted); }
.subtle      { color: var(--text-subtle); }
.small       { font-size: .875rem; }
.tiny        { font-size: .78rem; }
.center      { text-align: center; }
.mono        { font-family: var(--font-mono); font-size: .85em; letter-spacing: .02em; }

/* An uppercase micro-label. Used for table headers and section eyebrows;
   the wide tracking is what stops small uppercase text looking cramped. */
.eyebrow {
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .13em;
    color: var(--text-muted);
}


/* ==========================================================================
   4. LAYOUT
   ========================================================================== */

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

.page {
    padding-block: 2rem 4rem;
    min-height: 60vh;
}

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

/* Vertical rhythm without a margin on every element. */
.stack > * + *    { margin-top: 1rem; }
.stack-sm > * + * { margin-top: .5rem; }
.stack-lg > * + * { margin-top: 1.75rem; }

/* auto-fit collapses the columns to one on a narrow screen with no media
   query at all — the grid does the responsive work itself. */
.grid {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 265px), 1fr));
}
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }

.row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
.row-end { justify-content: flex-end; }
.spacer { flex: 1 1 auto; }

.section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 2.25rem 0 1rem;
}
.section-head :is(h1, h2, h3) { margin: 0; }


/* ==========================================================================
   5. HEADER AND NAVIGATION
   ========================================================================== */

.site-header {
    position: sticky;
    top: 0;
    z-index: 30;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    border-bottom: 1px solid var(--border);

    /* The frosted effect is progressive enhancement: where backdrop-filter is
       unsupported the header is simply a solid bar, which is fine. */
    backdrop-filter: saturate(1.6) blur(12px);
    -webkit-backdrop-filter: saturate(1.6) blur(12px);
}

/* color-mix is recent. This fallback runs first and is overridden above by
   browsers that understand it, so older ones get a solid, legible header. */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
    .site-header { background: var(--surface); }
}

/* NOTE ON min-height, because this caused a real bug.

   The header is a WRAPPING flex container: the brand and the two icon buttons
   sit on the first line, and on mobile the open navigation wraps onto a second.

   min-height used to be declared here, on the container. That works only while
   there is one line — a single flex line stretches to fill the container's
   height, so the bar measured 66px. The moment the menu opened there were two
   lines, their combined content exceeded 66px, and each line fell back to its
   natural height. The brand row visibly collapsed from 66px to about 38px:
   opening the menu made the top bar SHORTER.

   The height belongs to the first row's content, not to the container, so it
   is set on .brand — the tallest item on that row, which therefore establishes
   the line's height whether or not anything has wrapped below it. */
.header-inner {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}

.brand {
    display: flex;
    align-items: center;
    gap: .65rem;
    min-height: 66px;
    margin-right: auto;
    color: var(--text);
    text-decoration: none;
}
.brand:hover { color: var(--text); text-decoration: none; }

/* --- Logo ---------------------------------------------------------------
   A sharp-cornered rule around the scissors mark, matching the geometry the
   rest of the interface now uses, with the wordmark set in letterspaced caps.
   Wide tracking is what makes small caps read as a mark rather than as a
   heading that happens to be uppercase. */
.brand-mark {
    display: grid;
    place-items: center;
    width: 40px; height: 40px;
    flex: none;
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    color: var(--accent);
    transition: background-color .25s ease, color .25s ease,
                transform .35s cubic-bezier(.5,1.6,.4,1);
}
.brand-mark svg { width: 22px; height: 22px; }
.brand:hover .brand-mark { background: var(--accent); color: var(--on-accent); }

.brand-text {
    display: flex;
    flex-direction: column;
    gap: 1px;
    line-height: 1.1;
}
.brand-name {
    font-size: .82rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text);
}
/* A hairline rule to the right of "STUDIO", which is the detail that turns a
   stacked wordmark into a lockup. */
.brand-sub {
    display: flex;
    align-items: center;
    gap: .45rem;
    font-size: .62rem;
    font-weight: 500;
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--text-subtle);
}
.brand-sub::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--border-strong);
}

/* The wordmark is the first thing to go when space is tight; the mark alone
   still identifies the site. */
@media (max-width: 380px) {
    .brand-text { display: none; }
}

/* --- Staff portal (ADR-0016) ----------------------------------------------
   The same shell, marked so that nobody mistakes the portal for the public
   site: the wordmark's second line reads "Staff portal" in the accent colour,
   and a bar of the accent runs along the top of the header. The line carries
   meaning here, so it takes the accent (7.4:1 light, 7.9:1 dark, measured on
   the header's own ground) rather than the
   decorative subtle grey it has on the public site. */
.is-staff-portal .site-header { box-shadow: inset 0 3px 0 var(--accent); }
.is-staff-portal .brand-sub   { color: var(--accent); font-weight: 650; }

/* Icon-only square buttons in the header (theme toggle, menu). */
.icon-btn {
    display: grid;
    place-items: center;
    width: 38px; height: 38px;
    flex: none;
    padding: 0;
    background: transparent;
    /* --field-border, not --border: these buttons have no fill, so their
       outline is the only thing marking the hit area, which makes it a
       component boundary under WCAG 1.4.11 rather than decoration. */
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color .15s ease, border-color .15s ease;
}
.icon-btn:hover { background: var(--surface-2); border-color: var(--text-muted); }

/* --- Theme toggle animation ------------------------------------------------
   The icon is one SVG that morphs rather than two that swap.

   LIGHT (the default): a sun — full disc, rays extended, mask circle parked
   off-canvas at cx=24 so it cuts nothing.

   DARK: the mask circle slides in to cx=17, biting a crescent out of the disc;
   the rays retract towards the centre and fade out; the whole icon rotates a
   little so the change reads as one movement rather than three.

   Every property animated here (transform, opacity, cx) is cheap. The easing
   overshoots slightly, which is what makes it feel mechanical rather than
   linear. All of it is suppressed under prefers-reduced-motion by the global
   rule in section 8. */

/* The transform is COMPOSED from custom properties rather than written out at
   each selector. That way the theme sets only the rotation and the :active
   state sets only the scale, and neither has to know the other's value — which
   is what a plain `transform:` at each selector would have forced, since a
   second transform declaration replaces the first rather than adding to it. */
.theme-icon {
    --icon-rot: 0deg;
    --icon-scale: 1;
    overflow: visible;
    transform: rotate(var(--icon-rot)) scale(var(--icon-scale));
    transition: transform .5s cubic-bezier(.5, 1.6, .4, 1);
}

/* translateX, not the `cx` attribute. SVG geometry properties are animatable
   from CSS only in newer browsers; `transform` has worked on SVG elements
   everywhere for years, and here it does the identical job. */
.moon-cutout {
    transition: transform .5s cubic-bezier(.5, 1.6, .4, 1);
}

.theme-rays {
    transform-origin: center;
    transition: transform .45s cubic-bezier(.5, 1.6, .4, 1), opacity .3s ease;
}

/* The dark form of the icon: the mask slides in to bite a crescent out of the
   disc, the rays retract to the centre and fade, and the whole icon rotates so
   the change reads as one movement rather than three. Duplicated across the
   two dark selectors for the same reason the palette is — no preprocessor. */
:root[data-theme="dark"] .theme-icon   { --icon-rot: -25deg; }
:root[data-theme="dark"] .moon-cutout  { transform: translateX(-7px); }
:root[data-theme="dark"] .theme-rays   { transform: scale(.4); opacity: 0; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-icon  { --icon-rot: -25deg; }
    :root:not([data-theme="light"]) .moon-cutout { transform: translateX(-7px); }
    :root:not([data-theme="light"]) .theme-rays  { transform: scale(.4); opacity: 0; }
}

/* A soft glow behind the button on hover, and a springy press. */
.theme-toggle { position: relative; overflow: hidden; }
.theme-toggle::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at center, var(--accent-soft) 0%, transparent 70%);
    opacity: 0;
    transition: opacity .3s ease;
}
.theme-toggle:hover::before { opacity: 1; }
.theme-toggle:active .theme-icon { --icon-scale: .85; }

/* Mobile first: the navigation is closed until the toggle opens it. */
.site-nav {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: .1rem;
    padding-bottom: .85rem;
}
.site-nav.is-open { display: flex; }

/* :not(.btn) matters, and this was a real bug.
   `.site-nav a` has specificity (0,1,1) — one class plus one element — while
   `.btn` has only (0,1,0). Without the exclusion the nav rule therefore beats
   the button rule on `color`, and the "Create account" button kept its accent
   background while taking the nav's MUTED text colour: 1.18:1 contrast, which
   is effectively invisible. Excluding buttons here is the correct fix rather
   than raising the button's specificity, because the nav rule was never meant
   to apply to them in the first place. */
.site-nav a:not(.btn):not(.avatar),
.site-nav .link-button {
    display: block;
    padding: .6rem .7rem;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: .9rem;
    font-weight: 500;
    text-decoration: none;
    transition: background-color .15s ease, color .15s ease;
}

/* An animated underline that grows from the left, rather than a hard
   background swap. transform is used instead of width because it is composited
   on the GPU and does not force a layout on every frame. */
.site-nav a:not(.btn):not(.avatar)::after {
    content: "";
    display: block;
    height: 1.5px;
    margin-top: 2px;
    background: var(--accent);
    border-radius: 2px;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .25s cubic-bezier(.4, 0, .2, 1);
}
.site-nav a:not(.btn):not(.avatar):hover::after,
.site-nav a[aria-current="page"]::after { transform: scaleX(1); }

.site-nav a:not(.btn):not(.avatar):hover,
.site-nav .link-button:hover {
    background: var(--surface-2);
    color: var(--text);
    text-decoration: none;
}

/* The nav's own button keeps its full styling and needs a little breathing
   room from the links above it on the mobile stack. */
.site-nav .btn { margin-top: .4rem; align-self: flex-start; }
@media (min-width: 860px) {
    .site-nav .btn { margin-top: 0; margin-left: .4rem; }
}

/* The current page, marked server-side with aria-current. */
.site-nav a[aria-current="page"] {
    color: var(--text);
    background: var(--accent-soft);
}

.inline-form { margin: 0; display: inline-flex; }
.link-button {
    background: none;
    border: 0;
    font: inherit;
    cursor: pointer;
    text-align: left;
    width: 100%;
}

.nav-user {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .6rem .7rem;
    border-top: 1px solid var(--border);
    margin-top: .35rem;
}
.avatar {
    display: grid;
    place-items: center;
    width: 30px; height: 30px;
    flex: none;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: .72rem;
    font-weight: 700;
}

/* The initials are also the way to the account page (change password). A
   circle that fills on hover, like the logo mark, rather than the underline
   the text links use. Excluded from those rules above with :not(.avatar). */
a.avatar { text-decoration: none; transition: background-color .2s ease, color .2s ease; }
a.avatar:hover,
.site-nav a.avatar[aria-current="page"] {
    background: var(--accent);
    color: var(--on-accent);
    text-decoration: none;
}

@media (min-width: 860px) {
    .nav-toggle { display: none; }

    .site-nav {
        display: flex !important;   /* !important overrides the JS-set inline state */
        flex-direction: row;
        align-items: center;
        width: auto;
        gap: .15rem;
        padding-bottom: 0;
    }
    .nav-user {
        border-top: 0;
        margin-top: 0;
        margin-left: .35rem;
        padding-left: .85rem;
        border-left: 1px solid var(--border);
    }
}

.site-footer {
    border-top: 1px solid var(--border);
    background: var(--surface);
    padding-block: 2rem;
    font-size: .82rem;
    color: var(--text-muted);
}
.site-footer p { margin: 0 0 .3rem; }


/* ==========================================================================
   6. COMPONENTS
   ========================================================================== */

/* --- Card --------------------------------------------------------------- */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.35rem;
    box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: 1rem; }
/* ...but not inside a grid, where the gap already spaces the cards. This was a
   real bug: `.card + .card` matched every card except the first, so the first
   card in each grid sat 1rem higher than the rest of its row. */
.grid > .card + .card { margin-top: 0; }
.card-flush { padding: 0; overflow: hidden; }

/* ---------------------------------------------------------------------------
   CLICKABLE CARD

   A card that LOOKS interactive must BE interactive. The previous version
   lifted on hover while only the small button inside actually responded to a
   click — the styling made a promise the markup did not keep, and clicking the
   card body did nothing.

   Fixed with the "stretched link" pattern rather than by wrapping the whole
   card in an <a>. Wrapping would put headings, prices and other links inside a
   link, which is invalid where those include interactive elements and produces
   an unreadable announcement for a screen reader — it reads the entire card as
   one long link name.

   Instead the card is a positioning context and the card's PRIMARY link grows
   an invisible ::after that covers it. There is still exactly one link, with
   one sensible label, and the whole card is the hit area.
   ------------------------------------------------------------------------ */
.card-link {
    position: relative;
    display: flex;
    flex-direction: column;
    transition: transform .2s cubic-bezier(.2,.7,.3,1),
                box-shadow .2s ease,
                border-color .2s ease;
}
.card-link:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow);
    border-color: var(--border-strong);
}

/* An accent rule that draws itself across the top edge on hover. Sits ON the
   border, which is why it is offset by -1px on three sides. */
.card-link::before {
    content: "";
    position: absolute;
    top: -1px; left: -1px; right: -1px;
    height: 2px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .35s cubic-bezier(.2,.7,.3,1);
    pointer-events: none;
}
.card-link:hover::before { transform: scaleX(1); }

/* A fine bracket in the bottom-right corner — the deco detail that makes the
   sharpened corners look intentional rather than merely unrounded. */
.card-link::after {
    content: "";
    position: absolute;
    right: 9px; bottom: 9px;
    width: 11px; height: 11px;
    border-right: 1px solid var(--accent);
    border-bottom: 1px solid var(--accent);
    opacity: 0;
    transform: translate(-4px, -4px);
    transition: opacity .3s ease, transform .3s cubic-bezier(.2,.7,.3,1);
    pointer-events: none;
}
.card-link:hover::after { opacity: .85; transform: none; }

/* The stretched hit area.
   ::before, NOT ::after, and the reason matters: .btn already uses ::after for
   its hover sheen, so a stretched button would have two rules fighting over
   one pseudo-element — and .btn:hover::after would win, translating the hit
   area off the card. ::before is unused on buttons and cannot collide. */
.stretch::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}

/* When the stretched link is itself a button, two of the button's own
   properties would otherwise trap the overlay inside it:

     position: relative — makes the BUTTON the containing block, so `inset: 0`
                          would cover the button rather than the card;
     overflow: hidden   — clips anything drawn outside the button's box.

   Setting the button back to static hands the containing block to .card-link,
   which is the positioned ancestor we actually want. The sheen is dropped for
   these buttons: with overflow no longer clipping it, it would sweep across
   the whole card. No loss — on a card that is entirely clickable, the card's
   own lift and accent rule are the affordance, and the sheen would compete. */
.btn.stretch { position: static; overflow: visible; }
.btn.stretch::after { display: none; }

/* For a second link or a form inside a card whose body is already a link. */
.card-above { position: relative; z-index: 2; }

/* Keyboard users never see :hover, so the focus state has to do the same job.
   :focus-within on the card means tabbing to the link lights the whole card. */
.card-link:focus-within {
    border-color: var(--accent);
    box-shadow: var(--shadow);
}
.card-link:focus-within::before { transform: scaleX(1); }

/* The visible outline belongs on the CARD, not on the invisible stretched
   overlay — an outline drawn around a zero-size ::after would be invisible. */
.card-link .stretch:focus-visible { outline: none; }
.card-link:focus-within { outline: 2px solid var(--accent); outline-offset: 3px; }

.card-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .5rem;
}
.card-head :is(h2, h3) { margin: 0; }

.card-foot {
    margin-top: auto;
    padding-top: .9rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    flex-wrap: wrap;
}

/* ---------------------------------------------------------------------------
   CARD PHOTOGRAPH

   A picture across the top of a card, bleeding to the card's edges. The
   negative margin cancels the card's own padding, so the photograph needs no
   wrapper of its own and every existing card can take one with a single line.

   The frame, not the picture, owns the shape: aspect-ratio reserves the space
   before the image arrives, and object-fit: cover crops whatever file is
   supplied to fill it. A 4:3 original and a 16:9 replacement therefore look
   the same on the page, and nothing below the card jumps as it loads.
   ------------------------------------------------------------------------ */
.card-media {
    position: relative;
    aspect-ratio: 3 / 2;
    margin: -1.35rem -1.35rem 1.1rem;
    overflow: hidden;
    /* One pixel inside the card's radius, so the corner sits flush with the
       card's border instead of poking through it. */
    border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
    background: var(--surface-2);
}
.card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Portraits are taller, and cropped from the upper third so a face is never
   cut off at the forehead the way a centred crop would. */
.card-media-portrait { aspect-ratio: 4 / 5; }
.card-media-portrait img { object-position: 50% 22%; }

/* Square on a phone. A full-width 4:5 portrait is taller than most of the
   screen, so three stylists took three screens of scrolling. */
@media (max-width: 619px) {
    .card-media-portrait { aspect-ratio: 1 / 1; }
}

/* THE PLACEHOLDER. Photo::img() returns nothing for a service that has no
   picture yet — one an administrator added this morning, say — which leaves
   this frame EMPTY. :empty then draws the studio's scissors in it, so a new
   service looks unfinished rather than broken.

   :empty only matches when there is not even whitespace inside, which is why
   every template writes the frame and its PHP tag on a single line. */
.card-media:empty::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 44px;
    height: 44px;
    background: var(--text-subtle);
    -webkit-mask: var(--scissors) center / contain no-repeat;
            mask: var(--scissors) center / contain no-repeat;
    opacity: .7;
}

/* The same scissors as the header mark, as a mask so it takes any colour. */
:root {
    --scissors: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='10' cy='23.5' r='3.2'/%3E%3Ccircle cx='22' cy='23.5' r='3.2'/%3E%3Cpath d='M12.3 21.3 L23.2 5.5'/%3E%3Cpath d='M19.7 21.3 L8.8 5.5'/%3E%3C/svg%3E");
}

/* The clickable card's accent rule and corner bracket must paint ABOVE the
   photograph. The picture zooms on hover, and a transformed element is painted
   in the positioned layer — after both pseudo-elements, which come first in
   the document — so without this the zoom would slide over them. */
.card-link::before,
.card-link::after { z-index: 2; }

/* --- Buttons ------------------------------------------------------------ */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .45rem;
    padding: .65rem 1.15rem;
    background: var(--accent);
    color: var(--on-accent);
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: .92rem;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color .15s ease, border-color .15s ease,
                transform .1s ease, color .15s ease;
}
.btn:hover {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: var(--on-accent);
    text-decoration: none;
}
.btn:active { transform: translateY(1px); }

/* Transparent fill, so the border is what distinguishes this from plain text —
   a component boundary, and therefore --field-border rather than --border-strong. */
.btn-ghost {
    background: transparent;
    color: var(--text);
    border-color: var(--field-border);
}
.btn-ghost:hover { background: var(--surface-2); color: var(--text); border-color: var(--field-border); }

.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

/* For a button sitting on a photograph. The ground under it is a darkened
   picture in BOTH themes, so this variant is fixed white rather than themed —
   a token that flips with the theme would put a dark button on a dark photo. */
.btn-light { background: #fff; color: #18211d; border-color: #fff; }
.btn-light:hover { background: #e3ede7; color: #18211d; border-color: #e3ede7; }

.btn-small { padding: .4rem .8rem; font-size: .82rem; }
.btn-block { width: 100%; }

.btn:disabled, .btn[aria-disabled="true"] {
    opacity: .5;
    cursor: not-allowed;
    transform: none;
}

/* --- Forms -------------------------------------------------------------- */
label {
    display: block;
    margin-bottom: .35rem;
    font-size: .85rem;
    font-weight: 550;
    color: var(--text);
}
label.choice {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    font-weight: 450;
    padding: .5rem .65rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background-color .15s ease, border-color .15s ease;
}
label.choice:hover { background: var(--surface-2); border-color: var(--border-strong); }
label.choice input { width: auto; margin-top: .25rem; flex: none; }

input[type=text], input[type=email], input[type=password], input[type=tel],
input[type=number], input[type=date], input[type=time], input[type=file],
input[type=search], select, textarea {
    width: 100%;
    padding: .65rem .8rem;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: .95rem;
    transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
input::placeholder, textarea::placeholder { color: var(--text-subtle); }

textarea { min-height: 120px; resize: vertical; line-height: 1.6; }

.field { margin-bottom: 1.1rem; }
.field:last-child { margin-bottom: 0; }
.field-hint { margin: .4rem 0 0; font-size: .8rem; color: var(--text-muted); }
.field-error {
    margin: .4rem 0 0;
    font-size: .8rem;
    font-weight: 550;
    color: var(--danger);
}
input.has-error, textarea.has-error, select.has-error { border-color: var(--danger); }
input.has-error:focus, textarea.has-error:focus {
    box-shadow: 0 0 0 3px var(--danger-soft);
    border-color: var(--danger);
}

/* --- Alerts ------------------------------------------------------------- */
.alert {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    margin-bottom: 1rem;
    padding: .8rem 1rem;
    border: 1px solid;
    border-radius: var(--radius-sm);
    font-size: .92rem;
}
.alert::before { font-weight: 700; line-height: 1.5; }
.alert-success { border-color: var(--success); background: var(--success-soft); color: var(--text); }
.alert-success::before { content: "✓"; color: var(--success); }
.alert-error   { border-color: var(--danger);  background: var(--danger-soft);  color: var(--text); }
.alert-error::before { content: "!"; color: var(--danger); }

/* --- Badges ------------------------------------------------------------- */
.badge {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .22rem .6rem;
    border: 1px solid;
    border-radius: var(--radius-sm);
    font-size: .7rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: .07em;
    white-space: nowrap;
}
/* A dot as well as the colour, so status is not conveyed by colour alone —
   which matters for colour-blind users and is a WCAG requirement. */
.badge::before {
    content: "";
    width: 6px; height: 6px;
    border-radius: 50%;
    background: currentColor;
}
.badge-confirmed { color: var(--accent);  border-color: var(--accent);  background: var(--accent-soft); }
.badge-completed { color: var(--success); border-color: var(--success); background: var(--success-soft); }
.badge-cancelled { color: var(--text-muted); border-color: var(--border-strong); background: var(--surface-2); }
/* A no-show is the client's failure, not the salon's, so it reads as a warning
   rather than as a quiet cancellation. */
.badge-no_show   { color: var(--danger);  border-color: var(--danger);  background: var(--danger-soft); }

/* --- Tables ------------------------------------------------------------- */
/* Wide tables scroll inside their own container rather than making the whole
   page scroll sideways on a phone. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-radius: var(--radius);
}
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.table-scroll table { min-width: 580px; }

th {
    padding: .7rem .75rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
    text-align: left;
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--text-muted);
    white-space: nowrap;
}
td {
    padding: .8rem .75rem;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background-color .12s ease; }
tbody tr:hover { background: var(--surface-2); }

/* A key/value table inside a card, rather than a data grid. */
.table-detail { font-size: .95rem; }
.table-detail th {
    width: 34%;
    background: transparent;
    text-transform: none;
    letter-spacing: 0;
    font-size: .85rem;
    font-weight: 550;
    color: var(--text-muted);
    white-space: normal;
}
.table-detail tr:hover { background: transparent; }

/* --- Misc --------------------------------------------------------------- */
.meta {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem 1.1rem;
    font-size: .875rem;
    color: var(--text-muted);
}
.meta-item { display: inline-flex; align-items: center; gap: .35rem; }

.price { color: var(--accent); font-weight: 650; font-variant-numeric: tabular-nums; }
.price-lg { font-size: 1.15rem; }

.pill {
    display: inline-flex;
    align-items: center;
    padding: .2rem .6rem;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    border: 1px solid var(--border);
    font-size: .78rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.empty {
    padding: 2rem 1rem;
    text-align: center;
    color: var(--text-muted);
    font-style: italic;
}

.divider { height: 1px; background: var(--border); border: 0; margin: 1.5rem 0; }

.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: 1.25rem;
    flex-wrap: wrap;
}


/* ==========================================================================
   7. FEATURE-SPECIFIC
   ========================================================================== */

/* --- Hero --------------------------------------------------------------- */
/* THE WASH BEHIND THE HEADING — rebuilt, because the first version had a
   visible hard edge against the text. Three separate faults caused it:

     1. The gradient ran to `transparent`, which is rgba(0,0,0,0). A browser
        interpolates that channel by channel, so the colour faded through a band
        of dirty grey before disappearing — reading as a distinct ring rather
        than as light. It now fades to --glow-fade: the SAME hue at zero alpha,
        so only the alpha changes and nothing but the accent is ever painted.

     2. The colour stop ended at 62% and the element was clipped by
        `overflow: hidden` on .hero, so wherever the circle met the hero's
        boundary it was cut off in a straight line. The wash now fades fully to
        zero well inside its own box and nothing is clipped.

     3. One hard-edged circle looks like a shape. Two overlapping ellipses at
        different sizes and opacities look like light, because the eye cannot
        find the boundary of either. */
.hero {
    position: relative;
    padding: clamp(2.75rem, 6vw, 5rem) 0 clamp(2rem, 4vw, 3rem);
}

.hero::before {
    content: "";
    position: absolute;
    inset: -60% -25% -30% -25%;
    background:
        radial-gradient(48% 46% at 26% 42%, var(--glow) 0%, var(--glow-fade) 68%),
        radial-gradient(40% 38% at 76% 20%, var(--glow) 0%, var(--glow-fade) 70%);
    pointer-events: none;
}

/* The content sits in its own stacking layer ABOVE the wash.
   The previous version pushed the wash to z-index:-1 instead, which relies on
   the parent not creating a stacking context and on the body background being
   painted to the canvas — true here, but fragile, and it would vanish the
   moment any ancestor gained a transform or an opacity. Raising the content is
   the version that cannot break. */
.hero-inner { position: relative; z-index: 1; max-width: 44rem; }

.hero h1 { margin-bottom: .75rem; }

/* The lead paragraph is the ONLY body text sitting over the wash, and the wash
   tints the ground beneath it. At the densest point — where both gradients
   overlap — --text-muted measures 3.62:1 in light, under the 4.5:1 required.
   Two ways to fix it: thin the wash until muted text passes, which needed an
   alpha of 0.07 and left the wash too faint to be worth having; or give this
   one paragraph the full text colour, which measures 10.0:1 over the same
   ground. The second keeps the design and is the safer of the two, since it
   does not depend on where the gradients happen to fall at a given viewport
   width. Scoped to .hero, so .lead elsewhere keeps its muted treatment. */
.hero .lead { color: var(--text); }
.hero-actions { display: flex; flex-wrap: wrap; gap: .65rem; margin-top: 1.75rem; }

/* A hairline rule beside the eyebrow, in the deco register the sharpened
   corners set up.

   The eyebrow takes the full text colour over the wash, not the muted grey it
   uses elsewhere. At 11px it is small text, so the 3:1 allowance for large
   text does not apply and it needs the full 4.5:1 — which muted grey missed at
   3.62:1 and the accent scrapes past at 4.55:1 — too narrow a margin to rely on
   as the gradients move. Full text colour measures 10.0:1. The
   letterspacing and small size already separate it from the heading, so it
   does not need to be lighter as well. */
.hero .eyebrow {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin-bottom: .85rem;
    color: var(--text);
}
.hero .eyebrow::after {
    content: "";
    flex: 1 1 auto;
    max-width: 5rem;
    height: 1px;
    background: linear-gradient(90deg, var(--border-strong), transparent);
}

/* --- Hero with photographs ---------------------------------------------- */
/* Text on the left, a photograph on the right, and a smaller detail shot
   overlapping its corner. The text never sits ON a photograph, which keeps its
   contrast a property of the palette rather than of whatever the picture
   happens to contain under a given word at a given width. */
.hero-split {
    display: grid;
    gap: clamp(2.25rem, 5vw, 3.5rem);
    align-items: center;
}
@media (min-width: 860px) {
    .hero-split { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); }
}

.hero-media {
    position: relative;
    z-index: 1;
    /* Room below for the detail shot, which hangs off the bottom edge. */
    margin-bottom: 1.5rem;
}

/* A fine accent frame offset behind the photograph — the same deco register
   as the corner brackets on the cards. Drawn as a pseudo-element so it costs no
   markup, and inset rather than sized so it follows the photo at any width. */
.hero-media::before {
    content: "";
    position: absolute;
    inset: 1.1rem -1.1rem -1.1rem 1.1rem;
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    opacity: .5;
    pointer-events: none;
}

.hero-photo,
.hero-detail {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--surface-2);
    box-shadow: var(--shadow-photo);
}
.hero-photo img,
.hero-detail img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Landscape on a phone, where a portrait photograph would push the services
   off the first screen entirely; portrait beside the text on a wide screen. */
.hero-photo { aspect-ratio: 4 / 3; }
.hero-photo img { object-position: 50% 30%; }
@media (min-width: 860px) {
    .hero-photo { aspect-ratio: 4 / 5; }
}

.hero-detail {
    position: absolute;
    left: -.75rem;
    bottom: -1.5rem;
    width: 34%;
    aspect-ratio: 1;
    /* A mat in the page colour separates the two photographs, the way a
       mount separates prints in a frame. */
    border: 5px solid var(--bg);
}
.hero-detail:empty { display: none; }
@media (min-width: 860px) {
    .hero-detail { left: -2.25rem; }
}

/* Three facts under the buttons. Full text colour, not muted, for the same
   reason as the lead above: this row can sit over the wash. The numbers are
   large bold text, which only needs 3:1, and the accent clears that easily. */
.hero-facts {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem 2rem;
    margin: 2.25rem 0 0;
    padding: 1.1rem 0 0;
    list-style: none;
    border-top: 1px solid var(--border);
    font-size: .85rem;
    color: var(--text);
}
.hero-facts strong {
    display: block;
    font-size: 1.4rem;
    line-height: 1.2;
    letter-spacing: -.02em;
    color: var(--accent);
}

/* --- Home page sections --------------------------------------------------- */
.section { margin-top: clamp(3rem, 7vw, 5rem); }
.section > .section-head:first-child { margin-top: 0; }

/* Bottom-aligned, not baseline-aligned, once a heading carries an eyebrow:
   baseline alignment would line the "View all" link up with the small
   eyebrow text instead of with the heading it belongs to. */
.section .section-head { align-items: flex-end; }
.section-head .eyebrow { margin-bottom: .35rem; }

/* --- Gallery ("Our work") -------------------------------------------------- */
.gallery {
    display: grid;
    gap: .75rem;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    margin: 0;
    padding: 0;
    list-style: none;
}
@media (min-width: 620px) {
    .gallery { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
}

.gallery-item {
    position: relative;
    display: block;
    aspect-ratio: 3 / 4;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: #fff;
}
.gallery-item:hover { color: #fff; text-decoration: none; }
.gallery-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The caption sits on the photograph, so its ground is a photograph and its
   contrast cannot come from the palette. A scrim supplies it instead. It is a
   flat 80% behind the text and fades out only ABOVE it, because a fade that
   ran through the text measured just 4.4:1 at the top of the first line over a
   pure-white patch of picture. Now the worst case, a caption wrapped to three
   lines on a phone, still measures 7.2:1. The shadow is a second line of
   defence. */
.gallery-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 2.25rem .9rem .8rem;
    background: linear-gradient(to top,
                rgba(10, 16, 13, .8) 0%,
                rgba(10, 16, 13, .8) 58%,
                rgba(10, 16, 13, 0) 100%);
    font-size: .82rem;
    font-weight: 550;
    line-height: 1.35;
    text-shadow: 0 1px 2px rgba(0, 0, 0, .4);
}

/* --- Photo viewer (gallery lightbox) ----------------------------------------
   A native <dialog>, built by app.js. showModal() supplies the hard parts for
   free: the page behind becomes inert, focus is trapped inside, Escape closes
   it, and focus returns to the thumbnail that opened it. */
.lightbox {
    width: min(94vw, 1000px);
    max-width: none;
    max-height: none;
    padding: 0;
    border: 0;
    background: transparent;
    color: #fff;
    overflow: visible;
}
.lightbox::backdrop { background: rgba(8, 14, 11, .9); }

.lightbox-figure {
    display: grid;
    justify-items: center;
    gap: .8rem;
    margin: 0;
}
.lightbox-img {
    width: auto;
    max-width: 100%;
    max-height: 80vh;
    border-radius: var(--radius);
    box-shadow: 0 30px 60px -20px rgba(0, 0, 0, .6);
}
.lightbox-caption {
    font-size: .9rem;
    text-align: center;
    color: #fff;
}
.lightbox-count {
    margin-left: .6rem;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, .75);
}

/* Fixed to the viewport, not the picture, so the controls stay under the
   thumb in the same place whatever the shape of the photo being shown. */
.lightbox-btn {
    position: fixed;
    top: 50%;
    display: grid;
    place-items: center;
    width: 44px;              /* the minimum comfortable touch target */
    height: 44px;
    margin-top: -22px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, .45);
    border-radius: 50%;
    background: rgba(255, 255, 255, .1);
    color: #fff;
    font: inherit;
    font-size: 1.35rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color .15s ease;
}
.lightbox-btn:hover { background: rgba(255, 255, 255, .22); }
.lightbox-btn:focus-visible { outline-color: #fff; }
.lightbox-prev  { left: 1rem; }
.lightbox-next  { right: 1rem; }
.lightbox-close { top: 1.5rem; right: 1rem; margin-top: 0; font-size: 1.1rem; }

/* --- Call-to-action band ---------------------------------------------------- */
/* The studio photograph behind a closing invitation to book. isolation makes
   the band its own stacking context, so the negative z-indexes below layer the
   photo and scrim beneath the text without slipping behind the page itself. */
.cta-band {
    position: relative;
    isolation: isolate;
    display: grid;
    align-items: center;
    min-height: clamp(18rem, 34vw, 24rem);
    margin-top: clamp(3rem, 7vw, 5rem);
    overflow: hidden;
    border-radius: var(--radius);
    background: #1b2a23;      /* shown only if the photograph is missing */
    color: #fff;
}
.cta-photo {
    position: absolute;
    inset: 0;
    z-index: -2;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The scrim. At 72% it guarantees white text 7.4:1 even over the brightest
   window in the photograph. On a wide screen it lightens towards the right,
   where there is no text, so the room shows through; across the text column it
   never drops below 74%. */
.cta-band::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(12, 22, 18, .72);
}
@media (min-width: 860px) {
    .cta-band::before {
        background: linear-gradient(100deg,
                    rgba(12, 22, 18, .86) 0%,
                    rgba(12, 22, 18, .74) 50%,
                    rgba(12, 22, 18, .4) 100%);
    }
}

.cta-content {
    max-width: 34rem;
    padding: clamp(1.75rem, 5vw, 3.25rem);
}
.cta-content h2 {
    color: #fff;
    font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.2rem);
}
.cta-content p { margin-bottom: 1.5rem; color: #fff; }

/* --- Sign-in and registration ----------------------------------------------- */
/* One column on a phone, where the form is the whole job. Beside a photograph
   from 860px, where the space would otherwise be empty margin. */
.auth-split {
    display: grid;
    gap: 1.5rem;
    max-width: var(--wrap-narrow);
    margin-inline: auto;
}
.auth-photo { display: none; }

@media (min-width: 860px) {
    .auth-split {
        max-width: 58rem;
        grid-template-columns: minmax(0, 1fr) minmax(0, .85fr);
    }
    .auth-photo {
        display: block;
        position: relative;
        overflow: hidden;
        border-radius: var(--radius);
        background: var(--surface-2);
        box-shadow: var(--shadow-photo);
    }
    /* Absolutely positioned so the photograph takes the FORM's height rather
       than imposing its own — the form decides how tall the row is. */
    .auth-photo img {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        object-fit: cover;
        /* The panel is squarer than the portrait photograph, so something is
           cropped; anchoring to the top keeps the faces and loses the chair. */
        object-position: 50% 0;
    }
}

/* --- Booking journey photographs ------------------------------------------- */
/* Step 2 repeats the chosen service with a thumbnail beside it. */
.selected-service {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0 1.1rem;
    align-items: center;
}
.selected-service > :not(.selected-thumb) { grid-column: 2; }
.selected-thumb {
    grid-row: span 2;
    width: 96px;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}
.selected-thumb img { width: 100%; height: 100%; object-fit: cover; }
/* No picture for this service: fall back to the plain card layout rather than
   leaving an empty column. */
.selected-service:has(> .selected-thumb:empty) { display: block; }
.selected-thumb:empty { display: none; }

/* A stylist's face beside their name in the confirmation summary. */
.person-inline { display: inline-flex; align-items: center; gap: .6rem; }
.avatar-photo {
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: 50%;
    object-fit: cover;
    object-position: 50% 20%;
}

/* --- Step indicator for the booking journey ----------------------------- */
.steps {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: 1.5rem;
    flex-wrap: wrap;
    font-size: .82rem;
}
.step { display: inline-flex; align-items: center; gap: .45rem; color: var(--text-subtle); }
.step-num {
    display: grid;
    place-items: center;
    width: 22px; height: 22px;
    flex: none;
    border-radius: 50%;
    border: 1px solid var(--border-strong);
    font-size: .72rem;
    font-weight: 700;
}
.step-done { color: var(--text-muted); }
.step-done .step-num { background: var(--success-soft); border-color: var(--success); color: var(--success); }
.step-now  { color: var(--text); font-weight: 600; }
.step-now .step-num { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.step-sep { color: var(--text-subtle); }

/* --- Availability slot picker ------------------------------------------- */
.day-block + .day-block { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }

.day-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .75rem;
    flex-wrap: wrap;
}
.day-head h3 { margin: 0; }

.slots { display: flex; flex-wrap: wrap; gap: .45rem; }

.slot {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    padding: .5rem .8rem;
    min-width: 5.6rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    line-height: 1.25;
    transition: background-color .15s ease, border-color .15s ease,
                color .15s ease, transform .1s ease;
}
.slot:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
    text-decoration: none;
    transform: translateY(-1px);
}
.slot-time { font-weight: 650; font-size: .95rem; font-variant-numeric: tabular-nums; }
.slot-end  { font-size: .72rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.slot:hover .slot-end { color: var(--on-accent); opacity: .8; }

/* --- Allergy callout ----------------------------------------------------- */
/* Deliberately the loudest element on the stylist's schedule. The proposal
   treats visibility of these notes as a SAFETY requirement, not a nicety, so
   it gets a coloured ground, a heavy left rule and an icon — three signals,
   not one, so it survives being skimmed on a phone. */
.allergy {
    display: flex;
    gap: .7rem;
    margin: .85rem 0;
    padding: .75rem .9rem;
    background: var(--warn-soft);
    border: 1px solid var(--warn);
    border-left-width: 4px;
    border-radius: var(--radius-sm);
    font-size: .9rem;
}
.allergy::before { content: "⚠"; color: var(--warn); font-size: 1rem; line-height: 1.4; }
.allergy strong {
    display: block;
    margin-bottom: .15rem;
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .1em;
    color: var(--warn);
}

/* --- Appointment row on the stylist schedule ---------------------------- */
.appointment { border-left: 3px solid var(--accent); }
.appointment-cancelled { border-left-color: var(--border-strong); opacity: .68; }
.appointment-completed { border-left-color: var(--success); }
.appointment-no_show   { border-left-color: var(--danger); opacity: .8; }

/* --- The stylist's week view -------------------------------------------- */
.week-day-today { border-color: var(--accent); }
.week-list { list-style: none; margin: 0; padding: 0; }
.week-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .35rem .9rem;
    padding: .6rem 0 .6rem .75rem;
    border-left: 3px solid var(--accent);
    border-top: 1px solid var(--border);
}
.week-item:first-child { border-top: 0; }
.week-item > span:nth-child(2) { flex: 1 1 14rem; }
.week-item .time-range { min-width: 6.5rem; }
/* The note takes its own full-width line under the appointment it belongs to. */
.week-allergy { flex-basis: 100%; margin: .25rem 0 0; }

/* "Any available stylist" sits first among the stylist cards and has no
   portrait, so it is marked by a dashed accent edge instead. */
.any-stylist { border-style: dashed; border-color: var(--accent); justify-content: center; }

/* The reschedule confirmation: old time struck through beside the new one. */
.time-was { color: var(--text-muted); text-decoration: line-through; }
.slot-current { border-style: dashed; color: var(--text-muted); cursor: default; }

.time-range {
    font-variant-numeric: tabular-nums;
    font-weight: 650;
    letter-spacing: -.01em;
}

/* --- Stat tiles on the admin dashboard ---------------------------------- */
.stat { text-align: left; }
.stat-value {
    display: block;
    font-size: clamp(1.7rem, 1.3rem + 1.6vw, 2.3rem);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
}
.stat-label { margin: .2rem 0 0; font-size: .82rem; color: var(--text-muted); }

/* --- Reports (ADR-0012) ------------------------------------------------- */

/* The filter form: fields side by side on a wide screen and stacked on a
   phone, with no media query — the same auto-fit technique as .grid. */
.report-filters {
    display: grid;
    gap: .75rem 1rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
    align-items: end;
    margin-bottom: 1.25rem;
}
.report-filters .field { margin: 0; }

/* The summary tiles above a report: narrower than the dashboard's, so four
   sit on one row on a laptop and two by two on a phone. */
.report-stats { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); }

/* The customer picker: one wide select and its button, side by side. */
.report-filters-pick { grid-template-columns: minmax(0, 1fr) auto; }

.eyebrow a { color: inherit; text-decoration: none; }
.eyebrow a:hover { text-decoration: underline; }

/* Figures line up on the decimal point only when they are right-aligned and
   every digit is the same width. */
.num    { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.nowrap { white-space: nowrap; }

.report-table-title { margin: 0; padding: .9rem .75rem .6rem; font-size: 1rem; }

.report-table tfoot td {
    border-top: 2px solid var(--border-strong);
    border-bottom: 0;
    background: var(--surface-2);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

.badge-late { margin-left: .35rem; color: var(--danger); border-color: var(--danger); background: var(--danger-soft); }


/* ==========================================================================
   8. MOTION
   ==========================================================================

   The rules that follow are the ONLY animation in the system, and each earns
   its place by doing one of two jobs: telling the eye where new content came
   from, or confirming that a press registered. Nothing here loops, nothing
   moves without being asked to, and nothing delays the visitor's ability to
   act — every animation is on a property the compositor can handle (transform
   and opacity), so none of them force a layout.

   All of it is switched off under prefers-reduced-motion at the end of this
   section.
   ---------------------------------------------------------------------- */

@keyframes rise {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}
@keyframes fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes slide-in {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: none; }
}

/* --- Page entrance ------------------------------------------------------- */
/* The main content rises in as one block. `both` holds the opening frame
   before the animation starts, which is what prevents a flash of the finished
   state on a slow first paint. */
.page > * { animation: rise .4s cubic-bezier(.2, .7, .3, 1) both; }

/* Cards in a grid arrive in sequence rather than all at once, which reads as
   deliberate rather than as a stutter. Six steps is enough — beyond that the
   last card would wait long enough to feel broken, so the delay is capped by
   simply not declaring more. */
.grid > *:nth-child(1) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .02s both; }
.grid > *:nth-child(2) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .06s both; }
.grid > *:nth-child(3) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .10s both; }
.grid > *:nth-child(4) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .14s both; }
.grid > *:nth-child(5) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .18s both; }
.grid > *:nth-child(n+6) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .22s both; }

/* A flash message is the one thing on the page the visitor did not ask to see,
   so it announces itself by dropping in from above. */
.alert { animation: slide-in .35s cubic-bezier(.2, .7, .3, 1) both; }

/* Slots stagger horizontally. There can be forty of them, so the delay is
   derived from a small repeating cycle rather than declared per child — the
   eye reads it as a sweep either way. */
.slot { animation: fade .3s ease both; }
.slot:nth-child(4n+1) { animation-delay: .00s; }
.slot:nth-child(4n+2) { animation-delay: .04s; }
.slot:nth-child(4n+3) { animation-delay: .08s; }
.slot:nth-child(4n+4) { animation-delay: .12s; }

/* --- The hero's light ---------------------------------------------------- */
/* A very slow drift on the radial wash behind the hero. 18 seconds is long
   enough that it is never caught moving — it simply looks alive rather than
   printed. Transform only, so it costs nothing. */
@keyframes drift {
    0%, 100% { transform: scale(1)    translateY(0); }
    50%      { transform: scale(1.07) translateY(1.5%); }
}
.hero::before { animation: drift 18s ease-in-out infinite; }

/* --- Button sheen -------------------------------------------------------- */
/* A band of light crosses the primary button on hover. Confined to .btn and
   excluded from the ghost and danger variants, which are secondary actions and
   should not compete for attention. */
.btn { position: relative; overflow: hidden; }
.btn::after {
    content: "";
    position: absolute;
    top: 0; bottom: 0;
    left: -60%;
    width: 45%;
    background: linear-gradient(100deg, transparent, rgba(255,255,255,.28), transparent);
    /* translateX, NOT `left`. Animating `left` makes the browser recompute
       layout on every frame of the sweep; a transform is handled by the
       compositor and touches no layout at all. The travel is expressed as a
       percentage of the band's OWN width, which is why 388% rather than the
       175% of button width it covers. */
    transform: translateX(0) skewX(-18deg);
    transition: transform .55s cubic-bezier(.3, .8, .3, 1);
    pointer-events: none;
}
.btn:hover::after { transform: translateX(388%) skewX(-18deg); }
.btn-ghost::after, .btn-danger::after { display: none; }

/* --- Brand and avatar ---------------------------------------------------- */
.brand-mark { transition: background-color .2s ease, color .2s ease, transform .35s cubic-bezier(.5,1.6,.4,1); }
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.06); }

/* --- Cross-document page transitions ------------------------------------- */
/* Progressive enhancement, and the reason it is worth having on a plain
   multi-page PHP site: it gives navigation between pages a soft cross-fade
   without a single line of JavaScript and without turning the site into a
   single-page application, which the proposal rules out. Browsers that do not
   support it ignore the block entirely and navigate as they always have. */
@view-transition { navigation: auto; }

/* --- Theme switch -------------------------------------------------------- */
/* Colour transitions are applied ONLY while the theme is actually changing.
   app.js adds this class for the length of the switch and then removes it.
   Leaving a transition on every element permanently would make every hover and
   every focus lag behind the pointer. */
.theme-switching,
.theme-switching *,
.theme-switching *::before,
.theme-switching *::after {
    transition: background-color .3s ease,
                border-color .3s ease,
                color .3s ease,
                fill .3s ease !important;
}

/* --- Photographs ---------------------------------------------------------- */
/* The hero photograph settles into place as the page opens — a slight
   zoom-out while it fades up, which reads as a camera finding focus rather
   than an image popping in. It runs once and stops. */
@keyframes settle {
    from { opacity: 0; transform: scale(1.06); }
    to   { opacity: 1; transform: none; }
}
.hero-photo img { animation: settle 1.1s cubic-bezier(.2, .7, .3, 1) both; }
.hero-detail    { animation: rise .6s cubic-bezier(.2, .7, .3, 1) .35s both; }

/* A slow push-in on a card's photograph while the card is hovered or has
   keyboard focus. The frame clips it, so the card itself never changes size. */
.card-media img,
.gallery-item img { transition: transform .6s cubic-bezier(.2, .7, .3, 1); }
.card-link:hover .card-media img,
.card-link:focus-within .card-media img,
.gallery-item:hover img,
.gallery-item:focus-visible img { transform: scale(1.045); }

/* The gallery staggers in like the card grids above. */
.gallery > :nth-child(1) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .02s both; }
.gallery > :nth-child(2) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .07s both; }
.gallery > :nth-child(3) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .12s both; }
.gallery > :nth-child(4) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .17s both; }
.gallery > :nth-child(5) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .22s both; }
.gallery > :nth-child(n+6) { animation: rise .45s cubic-bezier(.2,.7,.3,1) .27s both; }

/* The photo viewer fades its backdrop in and grows the picture slightly.
   The scale is on the FIGURE, not on the dialog: a transform on the dialog
   would become the containing block for its position: fixed buttons, and
   they would jump across the screen when the animation finished. */
@keyframes grow-in {
    from { opacity: 0; transform: scale(.97); }
    to   { opacity: 1; transform: none; }
}
.lightbox[open] { animation: fade .25s ease both; }
.lightbox[open] .lightbox-figure { animation: grow-in .3s cubic-bezier(.2, .7, .3, 1) both; }

/* --- Reveal on scroll ------------------------------------------------------ */
/* Sections further down the home page play their entrance when they scroll
   into view instead of all at once on load, when nobody can see them.

   The mechanism reuses the entrance animations above rather than adding a
   second system. app.js puts .reveal-pending on a section that starts below
   the fold; this rule cancels its animation and hides it; when it scrolls into
   view the class is removed, the ordinary rules apply again, and removing
   `animation: none` restarts the animation from its first frame.

   Two things keep this safe. The class is added only by JavaScript, so with
   scripts off nothing is ever hidden. And it is never added under
   prefers-reduced-motion, so a visitor who has asked for less movement sees
   every section immediately. These rules must stay BELOW the entrance rules:
   the selectors are equally specific, so the later one wins. */
.reveal-pending,
.reveal-pending .grid > *,
.reveal-pending .gallery > * {
    animation: none;
    opacity: 0;
}

/* --- Reduced motion ------------------------------------------------------ */
/* Respect a visitor who has asked their system for less movement. Vestibular
   disorders make animation genuinely unpleasant, and this is a one-rule fix
   that disables everything above without unpicking any of it. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
    .hero::before { animation: none !important; }
}


/* ==========================================================================
   9. PRINT
   ========================================================================== */

/* A stylist may well print the day's schedule and pin it up. */
@media print {
    .site-header, .site-footer, .pager, .btn, .icon-btn, form { display: none !important; }
    /* Photographs are decoration on paper and cost a great deal of ink. */
    .hero-media, .card-media, .gallery, .cta-band, .auth-photo, .selected-thumb { display: none !important; }
    body { background: #fff; color: #000; }
    .card { border: 1px solid #999; box-shadow: none; break-inside: avoid; }
    .allergy { border: 2px solid #000; background: #fff; }
    a { color: #000; text-decoration: underline; }

    /* Reports run across several sheets. The column headings repeat on each
       one; the totals row prints once, at the end, rather than repeating on
       every page where it would read as a page subtotal; and no row is split
       between two sheets. */
    .table-scroll, .card-flush { overflow: visible; }
    .table-scroll table { min-width: 0; }
    thead { display: table-header-group; }
    tfoot { display: table-row-group; }
    tr { break-inside: avoid; }
    th, .report-table tfoot td { background: #eee; color: #000; }
    .report-meta { color: #000; }
    .report-table a { text-decoration: none; }
}
