/* Public salon directory page (/s/{slug}). Loaded only on that page via a
   "Styles" section (see _Layout.cshtml) — kept out of site.css because it's
   genuinely page-specific, not a reusable primitive. Mobile-first: this page
   is opened primarily on phones and gets screenshotted, so it needs to read
   as a finished consumer product.

   ---- Theme token architecture ---------------------------------------
   Every color, font family, type size, weight, letter-spacing, radius, and
   border value used below is a CSS custom property (--salon-*), declared
   once here and overridden per named theme in a [data-theme="<id>"] block.
   No rule in this file hardcodes a color/font/size/weight/letter-spacing/
   radius/border value directly. There is no per-tenant CSS file and no
   picker UI — a Tenant selects one of a small, fixed set of named themes
   (currently "editorial", "noir", and "botanical"), full stop.

   :root holds "editorial"'s values directly (not just a copy of them) —
   this is deliberate, not an oversight: the page shell (and any loading/
   retry/not-found state) can render before a Tenant is ever resolved (see
   the cold-start pattern below), so there needs to be a correct look with
   *no* [data-theme] attribute present at all. Making that look ARE
   editorial's actual values means "no theme known yet" and "explicitly
   editorial" are the same state, satisfying "default when unset: editorial"
   for free, including for an unrecognized theme id (no matching
   [data-theme="..."] block exists, so those rules simply don't apply and
   :root's values stand) — see SalonModel.NormalizeTheme for the
   server-side half of that same guarantee.

   data-theme is set on <html> (see wwwroot/js/directory.js's applyTheme),
   not just the inner .directory-shell, once the follow-up data response
   resolves the Tenant — never at initial shell render (SalonModel.OnGet is
   deliberately database-free). That's why body.directory-page-body below
   carries the page's background/text/font-family: it needs to cover the
   full viewport (not just the centered .directory-shell column) for every
   theme, especially noir's (Ink's) dark charcoal background. */

:root {
  /* ---- editorial: warm, serif, hairline dividers, no cards ---- */
  --salon-font-display: 'Cormorant Garamond', Georgia, serif;
  --salon-font-body: 'DM Sans', 'Helvetica Neue', sans-serif;

  --salon-color-bg: #FAF6F0;
  --salon-color-text: #211C17;
  --salon-color-text-secondary: #6B6055;
  --salon-color-text-muted: #857A6E;
  --salon-color-text-faint: #A0958A;
  --salon-color-rule: #E5DDD2;
  --salon-color-accent: #9A5B3D;
  --salon-color-accent-rule: #C9A28C;
  --salon-color-shimmer: rgba(33, 28, 23, 0.05);

  /* Story 93: tokens for the redesigned public page (Stories 94 to 96),
     copied from the approved frames' Clay palette. Nothing uses them yet. */
  --salon-color-card: #FFFFFF;       /* Salon-*.dc.html, Clay P.card */
  --salon-color-soft: #EDE4D8;       /* Salon-*.dc.html, Clay P.tint */
  --salon-color-on-accent: #FCF9F5;  /* Salon-*.dc.html, Clay P.onAccent */
  --salon-color-deep: #4A2A1B;       /* Salon-*.dc.html, Clay P.deep */
  --salon-hero-text: #FCF9F5;        /* Salon-*.dc.html, hero text over photo/gradient */
  --salon-hero-scrim: rgba(20, 14, 10, 0.62); /* Salon-*.dc.html, darkest hero scrim stop */

  --salon-text-size-base: 0.9375rem; /* 15px */

  --salon-name-size: 2.125rem;   /* 34px — not individually specified; sized
                                    relative to the 25px stylist name as the
                                    page's single largest heading */
  --salon-name-weight: 400;
  --salon-name-letter-spacing: 0;

  --salon-label-size: 0.65625rem;  /* 10.5px */
  --salon-label-letter-spacing: 0.2em;

  --salon-policy-size: 0.8125rem; /* 13px — not individually specified */

  --salon-stylist-name-size: 1.5625rem; /* 25px */
  --salon-stylist-name-weight: 400;
  --salon-stylist-name-letter-spacing: normal;

  --salon-bio-size: 0.78125rem;  /* 12.5px */
  --salon-bio-weight: 300;
  --salon-bio-line-height: 1.55;

  /* Space between paragraphs in a multi-paragraph full bio or cancellation
     policy (QA Pass C: save-notice-and-bio-paragraphs fix) — relative
     (em, not rem) so it scales with each element's own font-size rather
     than needing a separate value per theme/element pairing. Not
     individually specified; chosen to read as a clear paragraph break
     without the gap competing with this page's generally tight, hairline-
     rule-based rhythm elsewhere (see .stylist-list, .salon-policy-card__text). */
  --salon-bio-paragraph-gap: 0.75em;

  --salon-avatar-size: 3.875rem; /* 62px */
  --salon-avatar-radius: 50%;
  --salon-avatar-font-size: 1.3125rem; /* 21px */
  --salon-avatar-font-weight: 600; /* not individually specified */
  /* "warm gradient" — not given as explicit stops, built from the two
     warm tones already specified above rather than inventing new hex
     values. */
  --salon-avatar-gradient-from: var(--salon-color-accent);
  --salon-avatar-gradient-to: var(--salon-color-accent-rule);
  --salon-avatar-text-color: #FFFFFF;

  /* Shown in the cover-image area when the Tenant has no CoverPhotoUrl (or
     a set one fails to load) — reuses the same two warm tones as the
     avatar gradient, just reversed, rather than inventing new hex values,
     so it reads as the same "warm gradient" motif at a larger scale. */
  --salon-cover-placeholder-from: var(--salon-avatar-gradient-to);
  --salon-cover-placeholder-to: var(--salon-avatar-gradient-from);

  --salon-action-color: var(--salon-color-accent);
  --salon-action-border-color: var(--salon-color-accent-rule);
  --salon-action-border-width: 1px;
  --salon-action-size: 0.65625rem; /* 10.5px */
  --salon-action-letter-spacing: 0.18em;
  /* Vertical padding chosen so text (10.5px, ~1.2 line-height ≈ 12.6px)
     plus this padding on both sides lands the tap target at ~44px:
     (44 - 12.6) / 2 ≈ 15.7px ≈ 1rem. */
  --salon-action-padding-block: 1rem;
  --salon-action-padding-inline: 0;

  --salon-radius: 0;
  --salon-rule-width: 1px;
}

/* ---- noir (shown to owners as "Ink"): the warm dark theme -- warm
   charcoal, copper accent, and the same serif headings, sans body, type
   sizes and flat layout as editorial. The id stays "noir" so stored
   values keep working; only the look changed (Story 92). Every font/
   size/weight/letter-spacing token below is deliberately identical to
   :root's (editorial) — only the color tokens differ. ---- */
[data-theme="noir"] {
  --salon-font-display: 'Cormorant Garamond', Georgia, serif;
  --salon-font-body: 'DM Sans', 'Helvetica Neue', sans-serif;

  --salon-color-bg: #1C1917;
  --salon-color-text: #F3EDE4;
  --salon-color-text-secondary: #B3A89A;
  --salon-color-text-muted: #9C9386;
  --salon-color-text-faint: #8C8378;
  --salon-color-rule: #3A332D;
  --salon-color-accent: #D9A47E;
  --salon-color-accent-rule: #846550;
  --salon-color-shimmer: rgba(243, 237, 228, 0.06);

  /* Story 93: see :root. Ink's palette in the same frames. */
  --salon-color-card: #262220;       /* Salon-*.dc.html, Ink P.card */
  --salon-color-soft: #2E2925;       /* Salon-*.dc.html, Ink P.tint */
  --salon-color-on-accent: #1C1917;  /* Salon-*.dc.html, Ink P.onAccent */
  --salon-color-deep: #2A211B;       /* Salon-*.dc.html, Ink P.deep */
  --salon-hero-text: #FCF9F5;        /* Salon-*.dc.html, hero text over photo/gradient */
  --salon-hero-scrim: rgba(20, 14, 10, 0.62); /* Salon-*.dc.html, darkest hero scrim stop */

  --salon-text-size-base: 0.9375rem; /* 15px */

  --salon-name-size: 2.125rem;   /* 34px */
  --salon-name-weight: 400;
  --salon-name-letter-spacing: 0;

  --salon-label-size: 0.65625rem;  /* 10.5px */
  --salon-label-letter-spacing: 0.2em;

  --salon-policy-size: 0.8125rem; /* 13px */

  --salon-stylist-name-size: 1.5625rem; /* 25px */
  --salon-stylist-name-weight: 400;
  --salon-stylist-name-letter-spacing: normal;

  --salon-bio-size: 0.78125rem;  /* 12.5px */
  --salon-bio-weight: 300;
  --salon-bio-line-height: 1.55;

  /* See the :root comment — identical to editorial. */
  --salon-bio-paragraph-gap: 0.75em;

  --salon-avatar-size: 3.875rem; /* 62px */
  --salon-avatar-radius: 50%;
  --salon-avatar-font-size: 1.3125rem; /* 21px */
  --salon-avatar-font-weight: 600;
  /* "warm dark gradient": the Themes concept's soft-fill and card tones,
     distinct enough from the #1C1917 page background to read as a
     gradient rather than invisible. Initials take the theme's text color. */
  --salon-avatar-gradient-from: #2E2925;
  --salon-avatar-gradient-to: #262220;
  --salon-avatar-text-color: #F3EDE4;

  /* Same tones as the avatar, reversed, at cover-image scale — see the
     :root comment. */
  --salon-cover-placeholder-from: var(--salon-avatar-gradient-to);
  --salon-cover-placeholder-to: var(--salon-avatar-gradient-from);

  --salon-action-color: var(--salon-color-accent);
  --salon-action-border-color: var(--salon-color-accent-rule);
  --salon-action-border-width: 1px;
  --salon-action-size: 0.65625rem; /* 10.5px */
  --salon-action-letter-spacing: 0.18em;
  --salon-action-padding-block: 1rem;
  --salon-action-padding-inline: 0;

  --salon-radius: 0;
  --salon-rule-width: 1px;
}

/* ---- botanical: same layout/type as editorial, deep green instead of
   warm brown — the pilot salon's own request ("a very earthy plant
   vibe"). Every font/size/weight/letter-spacing token below is
   deliberately identical to :root's (editorial) — only the color tokens
   differ. */
[data-theme="botanical"] {
  --salon-font-display: 'Cormorant Garamond', Georgia, serif;
  --salon-font-body: 'DM Sans', 'Helvetica Neue', sans-serif;

  --salon-color-bg: #F4F6F1;
  --salon-color-text: #1F2A22;
  --salon-color-text-secondary: #56635A;
  --salon-color-text-muted: #6E7971;
  --salon-color-text-faint: #8C958D;
  --salon-color-rule: #D9E0D3;
  --salon-color-accent: #3F6249;
  --salon-color-accent-rule: #A8BFAE;
  --salon-color-shimmer: rgba(31, 42, 34, 0.05);

  /* Story 93: see :root. Sage's palette in the same frames. */
  --salon-color-card: #FFFFFF;       /* Salon-*.dc.html, Sage P.card */
  --salon-color-soft: #E1E8DC;       /* Salon-*.dc.html, Sage P.tint */
  --salon-color-on-accent: #F7FAF5;  /* Salon-*.dc.html, Sage P.onAccent */
  --salon-color-deep: #1F3526;       /* Salon-*.dc.html, Sage P.deep */
  --salon-hero-text: #FCF9F5;        /* Salon-*.dc.html, hero text over photo/gradient */
  --salon-hero-scrim: rgba(20, 14, 10, 0.62); /* Salon-*.dc.html, darkest hero scrim stop */

  --salon-text-size-base: 0.9375rem; /* 15px */

  --salon-name-size: 2.125rem;   /* 34px */
  --salon-name-weight: 400;
  --salon-name-letter-spacing: 0;

  --salon-label-size: 0.65625rem;  /* 10.5px */
  --salon-label-letter-spacing: 0.2em;

  --salon-policy-size: 0.8125rem; /* 13px */

  --salon-stylist-name-size: 1.5625rem; /* 25px */
  --salon-stylist-name-weight: 400;
  --salon-stylist-name-letter-spacing: normal;

  --salon-bio-size: 0.78125rem;  /* 12.5px */
  --salon-bio-weight: 300;
  --salon-bio-line-height: 1.55;

  /* See the :root comment — identical to editorial, same as every other
     type/size/weight token in this block. */
  --salon-bio-paragraph-gap: 0.75em;

  --salon-avatar-size: 3.875rem; /* 62px */
  --salon-avatar-radius: 50%;
  --salon-avatar-font-size: 1.3125rem; /* 21px */
  --salon-avatar-font-weight: 600;
  /* Given explicitly (deep-green gradient), not derived from
     accent/accent-rule this time — matches the pilot's exact request. */
  --salon-avatar-gradient-from: #6E8F76;
  --salon-avatar-gradient-to: #3E5A46;
  --salon-avatar-text-color: #FFFFFF;

  /* Also given explicitly, for the same reason. */
  --salon-cover-placeholder-from: #8FA894;
  --salon-cover-placeholder-to: #405C48;

  --salon-action-color: var(--salon-color-accent);
  --salon-action-border-color: var(--salon-color-accent-rule);
  --salon-action-border-width: 1px;
  --salon-action-size: 0.65625rem; /* 10.5px */
  --salon-action-letter-spacing: 0.18em;
  --salon-action-padding-block: 1rem;
  --salon-action-padding-inline: 0;

  --salon-radius: 0;
  --salon-rule-width: 1px;
}

/* ---- Page-wide: background/text/font cover the full viewport, not just
   .directory-shell's centered column — important for noir's (Ink's) dark
   charcoal background to reach the screen edges on wider viewports. ---- */

body.directory-page-body {
  background: var(--salon-color-bg);
  color: var(--salon-color-text);
  font-family: var(--salon-font-body);
  font-size: var(--salon-text-size-base);
}

/* Story 49: overflow-wrap is inherited, so declaring it once here reaches
   every stored value the page prints (salon name, Instagram, address,
   phone, hours, license, policy). `anywhere` rather than `break-word`
   because only `anywhere` also lowers min-content width, which is what
   stops a long unbroken run (a pasted URL, an email address) from widening
   the page past a phone screen. Text that already fits is unaffected. */
.directory-shell {
  max-width: 32rem;
  margin: 0 auto;
  padding-inline: 1rem;
  overflow-wrap: anywhere;
}

/* ---- Loading skeleton -------------------------------------------------- */
/* Renders immediately (no database round trip) so first paint never looks
   blank while the follow-up data request is in flight. Always shown before
   a Tenant (and therefore a theme) is known, so this necessarily always
   renders in the default (editorial, via :root) look — see the file-level
   comment above. */

.directory-skeleton-header,
.directory-skeleton-card {
  background: var(--salon-color-bg);
  border: var(--salon-rule-width) solid var(--salon-color-rule);
  border-radius: var(--salon-radius);
  margin-bottom: 0.75rem;
  position: relative;
  overflow: hidden;
}

/* Story 94: the hero's rounded shape and phone height, so first paint looks
   like a hero waiting. */
.directory-skeleton-header {
  height: clamp(420px, 75vh, 600px);
  height: clamp(420px, 75svh, 600px);
  margin-top: 0.75rem;
  border-radius: 22px;
}

.directory-skeleton-card {
  height: 4.5rem;
}

.directory-skeleton-header::after,
.directory-skeleton-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--salon-color-shimmer), transparent);
  animation: directory-shimmer 1.4s infinite;
}

@keyframes directory-shimmer {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(100%);
  }
}

/* Appears above the skeleton cards once the follow-up data request has
   taken longer than ~2s (see directory.js's showWakingUp / cold-start.js's
   onWakingUp) — without this, the ~60s cold-start wait is pure silent
   shimmer, which reads as broken rather than "working, just slow." */
.directory-loading__status {
  text-align: center;
  color: var(--salon-color-text-muted);
  font-size: var(--salon-text-size-base);
  margin: 0 0 1rem;
}

/* ---- "Not found" / "waking up, retry" states --------------------------- */
/* Also always pre-Tenant (see above), so also always the default look. */

.directory-message {
  text-align: center;
  padding-block: 4rem 1rem;
}

.directory-message h1 {
  font-family: var(--salon-font-display);
  font-size: var(--salon-name-size);
  font-weight: var(--salon-name-weight);
  margin-bottom: 0.5rem;
}

/* Retry button on #directory-retry (ColdStart.fetchWithRetry's exhausted-
   retries state). The --salon-action-* token family — transparent,
   bottom-rule, uppercase, action-sized — so it reads as a member of this
   page's own button family rather than a recoloured Bootstrap
   .btn.btn-primary. Deliberately inline-block, not full-width:
   .directory-message above is already text-align: center, so this centers
   as a text link. Its theme tokens
   never vary in practice, because this state always renders before a
   Tenant (and therefore a theme) is resolved — see the theme-token-architecture note at the top
   of this file — so only :root's (editorial) token values ever apply
   here. */
.directory-message__action {
  display: inline-block;
  margin-top: 1rem;
  width: fit-content;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  background: none;
  color: var(--salon-action-color);
  border: none;
  border-bottom: var(--salon-action-border-width) solid var(--salon-action-border-color);
  font-family: var(--salon-font-body);
  font-size: var(--salon-action-size);
  letter-spacing: var(--salon-action-letter-spacing);
  padding-block: var(--salon-action-padding-block);
  padding-inline: var(--salon-action-padding-inline);
}

/* ---- Width (Story 94) ------------------------------------------------------
   From 768px the shell widens for the hero (the frame's 1232px hero plus
   padding). #directory-content becomes a grid: the hero spans the full
   width, and every other child (the info section, the team list, the
   review link, the policy) sits in a centred 32rem column, so sections not
   yet redesigned keep today's reading width (Stories 95 and 96 widen them).
   A grid rather than a per-section max-width because the review link
   carries an inline margin a section rule could not centre.
   :not([hidden]) so the grid's display never defeats the hidden attribute. */
@media (min-width: 768px) {
  .directory-shell {
    max-width: 80rem;
  }

  #directory-content:not([hidden]) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 32rem) minmax(0, 1fr);
  }

  #directory-content > * {
    grid-column: 2;
  }

  #directory-content > .salon-hero {
    grid-column: 1 / -1;
  }

  /* Story 95: the info strip and the policy card span the shell's full
     width, as the desktop frame does. Inside the 32rem column the three
     strip cells were about 170px each ("By appointment" wrapped, so
     ::first-line held only "By") and the policy text beside the 320px
     review column was 88px wide (QA round 1). */
  #directory-content > .salon-info,
  #directory-content > .salon-policy-card {
    grid-column: 1 / -1;
  }

  /* Story 96: the team section spans the full width too, so the four-column
     grid at 1280px has the shell's 80rem, not the 32rem column. */
  #directory-content > .salon-team {
    grid-column: 1 / -1;
  }
}

/* ---- Hero (Story 94) -------------------------------------------------------
   header#salon-hero[data-cover] is one of:
     banner  - a wide logo (wider than 2.2:1), shown whole in a rounded box
               with the image's own ratio; the name is visually hidden (the
               logo carries it); the one button (Book or Text the salon,
               Story 101), if any, sits below on the page background.
     photo   - a room photo filling a tall rounded box, the name and buttons
               over it at the bottom left behind a scrim.
     none    - no cover (or a broken / 0-size one): the themed gradient in
               the same box as the photo, same scrim and text.
     pending - still loading; looks exactly like "none" (directory.js keeps
               the placeholder showing until the image decides).
   directory.js sets --hero-cover-ratio (width / height) on the hero when it
   decides the case. Colours only from tokens. */

.salon-hero {
  --hero-radius: 22px;
  --hero-cover-max-height: 200px;
  --hero-cover-ratio: 3;
  --hero-height: clamp(420px, 75vh, 600px);
  --hero-height: clamp(420px, 75svh, 600px);
  position: relative;
  margin-block: 0.75rem 1.5rem;
}

@media (min-width: 768px) {
  .salon-hero {
    --hero-radius: 28px;
    --hero-cover-max-height: 520px;
    --hero-height: clamp(520px, 80vh, 700px);
    --hero-height: clamp(520px, 80svh, 700px);
    margin-top: 1.5rem;
  }
}

/* Photo, no photo and pending: one tall rounded box, the cover (or the
   gradient) filling it, the text at the bottom. min-height, not height, so a
   very long name grows the box instead of being clipped. */
.salon-hero:not([data-cover="banner"]) {
  display: flex;
  flex-direction: column;
  min-height: var(--hero-height);
  border-radius: var(--hero-radius);
  overflow: hidden;
  color: var(--salon-hero-text);
}

.salon-hero__cover,
.salon-hero__placeholder {
  display: block;
}

/* The author `display` above would beat the UA's [hidden] rule (origin is
   the first cascade tie-breaker), so the hidden attribute is restated. */
.salon-hero__cover[hidden],
.salon-hero__placeholder[hidden] {
  display: none;
}

.salon-hero:not([data-cover="banner"]) .salon-hero__cover,
.salon-hero:not([data-cover="banner"]) .salon-hero__placeholder {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.salon-hero__placeholder {
  background: linear-gradient(135deg, var(--salon-color-accent), var(--salon-color-deep));
}

/* Banner: the whole image, in a box with the image's own ratio. width 100%
   with a max-width of (max height x ratio) and height auto means the box is
   never taller than the max height and never letterboxes the picture.
   Story 102: from 768px the max height is 520px, so on the 1248px row every
   banner of 2.4:1 or wider fills the row (the same edges as the info strip
   and the team); a narrower banner reaches the cap and margin-inline: auto
   centres it, inset equally on both sides. */
.salon-hero[data-cover="banner"] .salon-hero__cover {
  width: 100%;
  height: auto;
  max-width: calc(var(--hero-cover-max-height) * var(--hero-cover-ratio));
  max-height: var(--hero-cover-max-height);
  object-fit: contain;
  object-position: center;
  margin-inline: auto;
  border-radius: var(--hero-radius);
}

.salon-hero__body {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.salon-hero[data-cover="banner"] .salon-hero__body {
  padding: 22px 6px 0;
}

/* Over the photo or gradient (phone): the scrim is the body's own
   background, holding --salon-hero-scrim behind the whole text block and
   fading to transparent only in the padding above it. */
.salon-hero:not([data-cover="banner"]) .salon-hero__body {
  margin-top: auto;
  padding: 5rem 22px 26px;
  background: linear-gradient(to top, var(--salon-hero-scrim) 0, var(--salon-hero-scrim) calc(100% - 4.5rem), transparent 100%);
}

@media (min-width: 768px) {
  .salon-hero__body {
    gap: 16px;
  }

  /* Story 102: no inline padding, so the one button starts on the row's
     left edge, the same line as the banner, the info strip and the team. */
  .salon-hero[data-cover="banner"] .salon-hero__body {
    padding: 36px 0 0;
  }

  /* Desktop: the text sits bottom left within 800px; the scrim runs from
     the left, holding its darkest value across the whole text column and
     the full hero height, then fading. */
  .salon-hero:not([data-cover="banner"]) .salon-hero__body {
    box-sizing: border-box;
    width: 100%;
    max-width: calc(800px + 128px);
    padding: 0 64px 64px;
    background: none;
  }

  .salon-hero:not([data-cover="banner"])::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(90deg, var(--salon-hero-scrim) 0, var(--salon-hero-scrim) min(928px, calc(100% - 64px)), transparent min(1160px, 100%));
  }
}

.salon-hero__eyebrow {
  margin: 0;
  font-family: var(--salon-font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.salon-hero__eyebrow[hidden] {
  display: none;
}

.salon-hero__name {
  margin: 0;
  font-family: var(--salon-font-display);
  font-size: clamp(54px, calc(25.11px + 7.704vw), 104px);
  font-weight: 500;
  line-height: 0.98;
  overflow-wrap: anywhere;
}

/* Banner case: the name stays in the document (the heading a screen reader
   reads) but takes no visible space. Our own rule, not Bootstrap's. */
.salon-hero__name.salon-hero__name--hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (min-width: 768px) {
  .salon-hero__eyebrow {
    font-size: 13px;
  }
}

/* Actions: Book and/or Text the salon (Story 97 reversed Story 101's "one
   button at most"), each taking the full row under 768px (stacked) and
   sized to its label, side by side, from 768px. */
.salon-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 4px;
}

/* Every hero control gets a visible focus ring: the text colour on the page
   background, the hero text colour over a hero. */
.salon-hero a:focus-visible {
  outline: 2px solid var(--salon-color-text);
  outline-offset: 3px;
}

.salon-hero:not([data-cover="banner"]) a:focus-visible {
  outline-color: var(--salon-hero-text);
}

@media (min-width: 768px) {
  .salon-hero__actions {
    flex-wrap: nowrap;
    gap: 12px;
    margin-top: 8px;
  }
}

/* ---- Info strip (Story 95): Visit, Hours and Contact in one rounded card
   after the hero. The card's own background is the rule colour and its
   cells sit 1px apart on the card fill, so the dividing rules come from the
   gap: a hidden cell (display: none) takes no gap with it, so there is never
   a stray rule. Stacked under 768px, side by side and equal width from
   768px. Every cell, Visit included, shows at every width (Story 101: the
   hero carries no address or directions). Every value wraps (Story 49). */
.salon-info {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin: 0 0 1.5rem;
  overflow: hidden;
  border: 1px solid var(--salon-color-rule);
  border-radius: 18px;
  background: var(--salon-color-rule);
  overflow-wrap: anywhere;
}

.salon-info[hidden] {
  display: none;
}

.salon-info__cell {
  min-width: 0;
  padding: 18px 20px;
  background: var(--salon-color-card);
  color: var(--salon-color-text);
}

.salon-info__cell[hidden] {
  display: none;
}

.salon-info__label {
  margin: 0 0 6px;
  color: var(--salon-color-text-secondary);
  font-family: var(--salon-font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* The Visit street line. Address is a single-line <input> (Salon details),
   so default whitespace collapsing is correct here. */
.salon-info__value {
  margin: 0;
  color: var(--salon-color-text);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.4;
}

.salon-info__rest {
  margin: 2px 0 0;
  color: var(--salon-color-text-secondary);
  font-size: 15px;
  line-height: 1.4;
}

.salon-info__rest[hidden] {
  display: none;
}

/* Hours are collected via a <textarea> (Salon details): an owner typing
   "Mon-Fri 9-5" / "Sat 10-3" / "Sun closed" on separate lines is exactly
   what that control invites, so newlines are kept. The first line reads as
   the value (semi-bold, text colour), the rest as detail (secondary). */
.salon-info__hours {
  margin: 0;
  color: var(--salon-color-text-secondary);
  font-size: 15px;
  line-height: 1.5;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.salon-info__hours::first-line {
  color: var(--salon-color-text);
  font-size: 17px;
  font-weight: 600;
}

/* Phone, Instagram and Get directions: links in the accent, underlined, at
   least 44px tall. Phone and Address are single-line <input> fields, so no
   white-space rule. */
.salon-info__link {
  display: flex;
  align-items: center;
  width: fit-content;
  max-width: 100%;
  min-height: 44px;
  color: var(--salon-color-accent);
  font-family: var(--salon-font-body);
  font-size: 17px;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  overflow-wrap: anywhere;
}

.salon-info__link:hover,
.salon-info__link:focus-visible {
  color: var(--salon-color-accent);
}

.salon-info__link[hidden] {
  display: none;
}

/* License number (Story 10): a compliance field, not decoration --
   Florida requires a licensed massage establishment's number to appear in
   advertising, and this page is advertising. Legible size, the readable
   secondary colour, shown at every width. */
.salon-info__license {
  margin: 4px 0 0;
  color: var(--salon-color-text-secondary);
  font-size: var(--salon-text-size-base);
  line-height: 1.5;
}

.salon-info__license[hidden] {
  display: none;
}

/* A visible focus ring on every strip, policy card and footer link. */
.salon-info a:focus-visible,
.salon-policy-card a:focus-visible,
.directory-powered-by a:focus-visible {
  outline: 2px solid var(--salon-color-text);
  outline-offset: 3px;
}

@media (min-width: 768px) {
  .salon-info {
    flex-direction: row;
    border-radius: 20px;
    margin-bottom: 2rem;
  }

  .salon-info__cell {
    flex: 1 1 0;
    padding: 22px 26px;
  }
}

/* ---- Policy card (Story 95): after the team. The cancellation policy and
   the Google review block on the soft fill; stacked under 768px, the review
   block a 320px column on the right from 768px. */
.salon-policy-card {
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin: 0 0 1rem;
  padding: 22px 20px;
  border-radius: 18px;
  background: var(--salon-color-soft);
  color: var(--salon-color-text);
  overflow-wrap: anywhere;
}

.salon-policy-card[hidden],
.salon-policy-card__body[hidden] {
  display: none;
}

.salon-policy-card__body {
  min-width: 0;
}

.salon-policy-card__heading {
  margin: 0 0 10px;
  color: var(--salon-color-text);
  font-family: var(--salon-font-display);
  font-size: 26px;
  font-weight: 500;
  line-height: 1.15;
}

/* QA Pass C: this element holds one real <p> per paragraph
   (directory.js's appendParagraphs) instead of a single pre-line text
   node. No white-space: pre-line: a split-on-newline paragraph never
   itself contains a newline, so default wrapping is already correct. */
.salon-policy-card__text {
  margin: 0;
  color: var(--salon-color-text-secondary);
  font-size: 15px;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.salon-policy-card__text[hidden] {
  display: none;
}

.salon-policy-card__text p {
  margin: 0;
}

.salon-policy-card__text p + p {
  margin-top: var(--salon-bio-paragraph-gap);
}

.salon-review {
  min-width: 0;
}

.salon-review[hidden] {
  display: none;
}

.salon-review__prompt {
  margin: 0 0 10px;
  color: var(--salon-color-text-secondary);
  font-size: 15px;
}

/* Story 26's "Leave us a Google review", built by directory.js. The text is
   the text colour, not the accent (the accent on the soft fill is about
   4.24:1 in Clay, below 4.5); the accent is the border. */
.salon-review-link {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 100%;
  min-height: 48px;
  padding: 0.5rem 1rem;
  border: 1px solid var(--salon-color-accent);
  border-radius: 12px;
  background: transparent;
  color: var(--salon-color-text);
  font-family: var(--salon-font-body);
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.salon-review-link:hover,
.salon-review-link:focus-visible {
  color: var(--salon-color-text);
  text-decoration: underline;
}

@media (min-width: 768px) {
  .salon-policy-card {
    flex-direction: row;
    align-items: flex-start;
    gap: 40px;
    padding: 30px 32px;
    border-radius: 20px;
  }

  .salon-policy-card__body {
    flex: 1 1 0;
  }

  .salon-policy-card__heading {
    font-size: 30px;
  }

  .salon-review {
    flex: 0 0 320px;
    width: 320px;
  }
}

/* Story 64: "Book an appointment", the salon-level entry to online booking;
   Story 94 moved it into the hero as its primary pill, shared with "Text the
   salon" (shown instead when booking is off). At least 52px tall, the
   theme's accent as the fill and --salon-color-on-accent as the text (Story
   93's contrast-checked pair); full width under 768px. */
.salon-hero__primary,
.salon-book {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  flex: 1 1 100%;
  min-width: 0;
  min-height: 52px;
  padding: 0.75rem 1.5rem;
  border-radius: 26px;
  background: var(--salon-color-accent);
  color: var(--salon-color-on-accent);
  font-family: var(--salon-font-body);
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  text-align: center;
}

.salon-hero__primary:hover,
.salon-hero__primary:focus-visible,
.salon-book:hover,
.salon-book:focus-visible {
  color: var(--salon-color-on-accent);
  text-decoration: underline;
}

@media (min-width: 768px) {
  .salon-hero__primary,
  .salon-book {
    flex: 0 0 auto;
    min-height: 56px;
    padding: 0 30px;
    border-radius: 28px;
    font-size: 17px;
  }
}

/* Story 97 (Austin, 25 Oct 2026): "Book an appointment" and "Text the salon"
   together. Book stays the filled primary; Text, when Book is shown before
   it, is the outlined secondary in the hero's own text colour (the page
   text on a banner, --salon-hero-text over a photo), so it reads on every
   cover case. Stacked full width under 768px and side by side from 768px
   come from the rules above unchanged; box-sizing keeps the 52/56px height
   with the border. With booking off, Text alone stays the filled pill. */
.salon-book:not([hidden]) ~ .salon-hero__primary {
  border: 2px solid currentColor;
  background: transparent;
  color: inherit;
}

.salon-book:not([hidden]) ~ .salon-hero__primary:hover,
.salon-book:not([hidden]) ~ .salon-hero__primary:focus-visible {
  color: inherit;
}

/* Same footgun as .salon-hero__cover[hidden] above: the author `display`
   on these would beat the UA's [hidden] rule, so this restates it. */
.salon-hero__primary[hidden] {
  display: none;
}

.salon-book[hidden] {
  display: none;
}

/* ---- Meet the team (Story 96) ----------------------------------------------
   section#salon-team: the heading, the cards in a grid (one column under
   600px, two from 600px, four from 1024px) and the empty line. Every card
   is shown (Story 111). Under 600px each card is the phone card (photo tile
   on the left); from 600px, where a card sits beside another, the photo is
   on top. Colours only from tokens. Every stored value wraps (Story 49). */
.salon-team {
  margin: 0 0 1.5rem;
  overflow-wrap: anywhere;
}

.salon-team__heading {
  margin: 0.5rem 0 1rem;
  color: var(--salon-color-text);
  font-family: var(--salon-font-display);
  font-size: 38px;
  font-weight: 500;
  line-height: 1.05;
}

.stylist-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

/* The phone card: the photo tile on the left, the details beside it. Cards
   in one grid row stretch to the same height; the actions sit at the
   bottom of the details column. */
.stylist-card {
  display: flex;
  align-items: stretch;
  gap: 14px;
  min-width: 0;
  box-sizing: border-box;
  padding: 14px;
  border: 1px solid var(--salon-color-rule);
  border-radius: 18px;
  background: var(--salon-color-card);
  color: var(--salon-color-text);
  overflow-wrap: anywhere;
}

/* Empty state: a salon with no team members yet. Text-only, under the
   heading, so it reads as "this page is finished, just early". */
.stylist-list__empty {
  margin: 0;
  padding-block: 1rem 1.5rem;
  color: var(--salon-color-text-secondary);
  font-family: var(--salon-font-body);
  font-size: var(--salon-text-size-base);
  text-align: center;
}

/* Fixed-size box regardless of whether a real photo or the initials tile
   ends up inside it, so neither a broken-image icon nor a layout shift. */
.stylist-card__photo-box {
  flex: none;
  width: 92px;
  height: 116px;
  border-radius: 12px;
  overflow: hidden;
}

.stylist-card__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The initials tile, for no photo or a photo that failed: the soft fill
   with the initials in the secondary colour (Story 93's pairs, 4.87 to
   6.15:1), never a gradient whose dark end drops dark initials below 3:1. */
.stylist-card__avatar-fallback {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--salon-color-soft);
  color: var(--salon-color-text-secondary);
  font-family: var(--salon-font-display);
  font-size: 32px;
  font-weight: 500;
  line-height: 1;
}

.stylist-card__details {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.stylist-card__name {
  margin: 0;
  color: var(--salon-color-text);
  font-family: var(--salon-font-body);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* The role line: the short Bio. Collected via a <textarea> (Stylists.cshtml),
   so newlines are kept (pre-line). */
.stylist-card__bio {
  margin: 4px 0 0;
  color: var(--salon-color-accent);
  font-family: var(--salon-font-body);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
  overflow-wrap: anywhere;
  white-space: pre-line;
}

/* Story 13: the full bio, shown only when a team member has one, expanded
   in place by its own "Read more". */
.stylist-card__fullbio {
  margin: 4px 0 0;
  font-family: var(--salon-font-body);
  font-size: var(--salon-bio-size);
  font-weight: var(--salon-bio-weight);
  line-height: var(--salon-bio-line-height);
  color: var(--salon-color-text-secondary);
  overflow-wrap: anywhere;
}

/* QA Pass C: same fix, same reasoning as .salon-policy-card__text p above —
   this element now holds one real <p> per paragraph (directory.js's
   appendParagraphs) instead of a single pre-line text node. white-space:
   pre-line is gone for the same reason: a split-on-newline paragraph never
   itself contains a newline. Typography (font/size/weight/line-height/
   color) is inherited from .stylist-card__fullbio above; only paragraph
   spacing is declared here. */
.stylist-card__fullbio p {
  margin: 0;
}

.stylist-card__fullbio p + p {
  margin-top: var(--salon-bio-paragraph-gap);
}

/* The "Read more" / "Show less" control (Story 13): a quieter text button
   than the card's actions, its own line (block), at least 44px tall. A real
   <button>, so it is in the Tab order with no extra work. */
.stylist-card__bio-toggle {
  display: block;
  width: fit-content;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--salon-color-text-secondary);
  font-family: var(--salon-font-body);
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.stylist-card__bio-toggle:hover,
.stylist-card__bio-toggle:focus-visible {
  color: var(--salon-color-accent);
}

/* The card's actions, at the bottom of the card: the main action (built by
   directory.js's buildActionElement) and, beside it, the Instagram icon
   button when she has a handle and the main action is not already "DM on
   Instagram". Story 103: one row at every width -- "Book with {first name}"
   takes the remaining width (wrapping inside itself) and the 44px icon sits
   beside it. */
.stylist-card__actions {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  margin-top: auto;
  padding-top: 10px;
}

/* "Book with {first name}", "DM on Instagram" or "Text {first name}": an
   outlined accent button; the booking link is the filled accent with the
   on-accent text (Story 93's contrast-checked pair). At least 44px, corners
   12px; a long first name wraps inside the button. */
.stylist-card__action {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  padding: 0.5rem 1rem;
  border: 1px solid var(--salon-color-accent);
  border-radius: 12px;
  background: transparent;
  color: var(--salon-color-accent);
  font-family: var(--salon-font-body);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  overflow-wrap: anywhere;
}

/* Story 97: a bookable team member's "Book with" is a <button> (it opens
   Loomora's booking with her chosen), drawn exactly like the link form. */
button.stylist-card__action {
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}

.stylist-card__action:hover,
.stylist-card__action:focus-visible {
  color: var(--salon-color-accent);
  text-decoration: underline;
}

.stylist-card__action.stylist-card__action--book,
.stylist-card__action.stylist-card__action--book:hover,
.stylist-card__action.stylist-card__action--book:focus-visible {
  background: var(--salon-color-accent);
  color: var(--salon-color-on-accent);
}

/* "DM on Instagram" with its glyph (Story 103): when the words wrap (320px)
   their anonymous flex item fills the row, so centred lines left a 20px gap
   after the glyph (QA S103-2). Start-aligned lines keep the 8px gap; on one
   line the item shrinks to its text and the pair stays centred. */
.stylist-card__action--instagram {
  text-align: start;
}

/* The second, Instagram button (Story 103): a 44 x 44px icon-only box, the
   rule colour as its border, the text colour as its glyph. It keeps its
   44px height and sits at the top of the row when "Book with" wraps to more
   lines (align-self), so the two share a top edge. */
.stylist-card__instagram {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: flex-start;
  box-sizing: border-box;
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  border: 1px solid var(--salon-color-rule);
  border-radius: 12px;
  background: transparent;
  color: var(--salon-color-text);
  font-family: var(--salon-font-body);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  overflow-wrap: anywhere;
}

/* No word to underline: hover and focus darken the border to the text
   colour instead (the focus ring below still applies). */
.stylist-card__instagram:hover,
.stylist-card__instagram:focus-visible {
  border-color: var(--salon-color-text);
  color: var(--salon-color-text);
}

/* Story 103: the Instagram glyph, drawn once for all three Instagram links
   (the info card's @handle, "DM on Instagram", the icon button). A
   single-colour outline mark -- a rounded square, a circle and a dot --
   inline as a data: URL used as a mask, filled with the link's own colour
   (currentColor). No network request, no image file; generated content
   adds nothing to the link's accessible name. Do not add a gradient or
   change the proportions (Story 103, open question 1). 18px with an 8px
   gap before a text label; 22px alone in the icon button (below). */
#salon-instagram::before,
.stylist-card__instagram::before,
.stylist-card__action--instagram::before {
  content: "";
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-inline-end: 8px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'%3E%3Crect x='2' y='2' width='20' height='20' rx='5.5'/%3E%3Ccircle cx='12' cy='12' r='4.5'/%3E%3Ccircle cx='17.5' cy='6.5' r='1.25' fill='black' stroke='none'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'%3E%3Crect x='2' y='2' width='20' height='20' rx='5.5'/%3E%3Ccircle cx='12' cy='12' r='4.5'/%3E%3Ccircle cx='17.5' cy='6.5' r='1.25' fill='black' stroke='none'/%3E%3C/svg%3E") center / contain no-repeat;
}

.stylist-card__instagram::before {
  width: 22px;
  height: 22px;
  margin-inline-end: 0;
}

/* Forced colours (Windows High Contrast): paint the glyph in the forced link
   colour. forced-color-adjust: none stops the mask's fill being overridden
   to Canvas, but it also means currentColor would resolve to the theme's
   UNFORCED colour (invisible on a forced Canvas: QA finding S103-1), so the
   fill is the LinkText system colour, which the link text itself uses. */
@media (forced-colors: active) {
  #salon-instagram::before,
  .stylist-card__instagram::before,
  .stylist-card__action--instagram::before {
    forced-color-adjust: none;
    background-color: LinkText;
  }
}

/* A visible focus ring on every control in the team section. */
.salon-team a:focus-visible,
.salon-team button:focus-visible {
  outline: 2px solid var(--salon-color-text);
  outline-offset: 3px;
}

/* From 600px: two columns, and the card turns over, the photo on top
   (aspect 4:5) and the details below it with the actions at the bottom. */
@media (min-width: 600px) {
  .stylist-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .stylist-card {
    flex-direction: column;
    gap: 0;
    padding: 0;
    overflow: hidden;
  }

  .stylist-card__photo-box {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 5;
    border-radius: 0;
  }

  .stylist-card__avatar-fallback {
    font-size: 80px;
  }

  .stylist-card__details {
    flex: 1 1 auto;
    padding: 18px;
  }
}

@media (min-width: 768px) {
  .salon-team {
    margin-bottom: 2rem;
  }

  .salon-team__heading {
    margin-top: 1rem;
    font-size: 56px;
  }
}

/* From 1024px: four columns, the desktop card's type. */
@media (min-width: 1024px) {
  .stylist-list {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 24px;
  }

  .stylist-card {
    border-radius: 20px;
  }

  .stylist-card__details {
    padding: 22px;
  }

  .stylist-card__name {
    font-size: 22px;
  }

  .stylist-card__bio {
    margin-top: 6px;
    font-size: 15px;
  }

  .stylist-card__actions {
    padding-top: 14px;
  }

  /* The Instagram icon button stays 44 x 44px (Story 103). */
  .stylist-card__action {
    min-height: 48px;
    font-size: 15px;
  }
}

/* ---- Minimal layout footer ("powered by", not product chrome) --------- */
/* See Pages/Directory/_DirectoryLayout.cshtml: this page deliberately has
   no Loomora nav/header, only this small footer credit. Themed for
   legibility against either background rather than a fixed color. */

/* Story 95: centred, 13px, the secondary colour (readable on the page
   background, Story 93's pair), 24px above on a phone and 32px from 768px.
   Shared with the booking screens and NotFound, which get the same look. */
.directory-powered-by {
  text-align: center;
  padding-block: 24px 2rem;
  font-size: 13px;
  color: var(--salon-color-text-secondary);
}

.directory-powered-by a {
  color: var(--salon-color-text-secondary);
}

.directory-powered-by .directory-powered-by__link {
  /* Story 123: the Privacy and Terms links after "Powered by Loomora", in
     the same secondary colour (contrast checked per theme in
     JsTests/legal-footer-contrast.test.mjs). 44px tall tap targets, never
     broken mid-word. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: 10px;
  white-space: nowrap;
  color: var(--salon-color-text-secondary);
}

@media (min-width: 768px) {
  .directory-powered-by {
    padding-top: 32px;
  }
}
