@import url("tokens.css");

/* ==========================================================================
   RESET / BASE
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem; /* keeps anchored headings clear of the fixed nav */
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
html[data-motion="reduced"] { scroll-behavior: auto; }

body {
  font-family: var(--font-ui);
  font-size: var(--step-body);
  line-height: 1.4;
  letter-spacing: var(--track-body);
  color: var(--text);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  overflow-wrap: break-word;
  transition: background-color var(--dur-base) var(--ease-in-out),
              color var(--dur-base) var(--ease-in-out);
}

img, picture, svg, video { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; letter-spacing: inherit; color: inherit; }
button { cursor: pointer; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  text-wrap: balance;
}
h1 { font-size: var(--step-hero); line-height: 0.98; letter-spacing: var(--track-hero); }
h2 { font-size: var(--step-h2);   line-height: 1.0;  letter-spacing: var(--track-h2); }
h3 { font-size: var(--step-h3);   line-height: 1.2;  letter-spacing: var(--track-h3); }
h4 { font-size: var(--step-h4);   line-height: 1.3;  letter-spacing: -0.01em; font-family: var(--font-ui); font-weight: 500; }
p  { text-wrap: pretty; }

a { color: inherit; text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); text-underline-offset: 0.22em; }
a:hover { text-decoration-color: currentColor; }

/* ==========================================================================
   ACCESSIBILITY PRIMITIVES
   ========================================================================== */
/* [hidden] must be honoured on SVG too - the UA rule only covers HTML elements,
   and our reset sets svg { display: block }, which would otherwise win. */
[hidden] { display: none !important; }

.visually-hidden {
  position: absolute !important; 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;
}

/* Focus is never removed - only shaped. 3px ring + offset clears WCAG 2.4.13. */
:focus { outline: none; }
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: 2px;
}
.section--inverse :focus-visible, .site-footer :focus-visible { outline-color: var(--focus-inverse); }

.skip-link {
  position: absolute; top: 0; left: 50%;
  transform: translate(-50%, -120%);
  z-index: 999;
  background: var(--accent-fill); color: var(--accent-fill-text);
  padding: 0.875rem 1.5rem; border-radius: 0 0 var(--radius-card) var(--radius-card);
  font-size: var(--step-sm); font-weight: 500; text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus-visible { transform: translate(-50%, 0); }

/* Every interactive target ≥44×44px (WCAG 2.5.8 / 508 motor access) */
.tap-safe { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; }

/* ==========================================================================
   LAYOUT
   ========================================================================== */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }

.section { padding-block: var(--section-y); position: relative; }
.section--alt     { background: var(--surface-alt); }
.section--inverse { background: var(--surface-inverse); color: var(--text-on-inverse); }
.section--inverse .text-muted { color: var(--text-muted-inverse); }
.section--flush-top { padding-block-start: 0; }

/* The reference template's signature move: a label parked in a wide left
   gutter with all content offset into the right column. Collapses to a
   single stacked column below 60rem. */
.split {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: 1fr;
}
.split > * { min-width: 0; }  /* lets tracks shrink below content min-content */
@media (min-width: 60rem) {
  .split { grid-template-columns: minmax(0, 0.32fr) minmax(0, 1fr); gap: var(--sp-5); }
}

.eyebrow {
  display: flex; align-items: center; gap: 0.6rem;
  font-family: var(--font-ui);
  font-size: var(--step-xs); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--text-muted);
}
.section--inverse .eyebrow { color: var(--text-muted-inverse); }
.section--inverse .tick-list, .section--inverse .plain-list { color: var(--text-muted-inverse); }
.eyebrow::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent-on-light); flex: none;
}
.section--inverse .eyebrow::before { background: var(--accent-on-dark); }

.text-muted { color: var(--text-muted); }
.lede { font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.375rem); line-height: 1.5; max-width: var(--measure); }
.stack > * + * { margin-block-start: var(--sp-3); }

/* ==========================================================================
   BUTTONS - pill geometry, 100px radius, from the reference
   ========================================================================== */
.btn {
  display: inline-flex; align-items: center; gap: 0.75rem;
  min-height: 52px; padding: 0.9rem 1.6rem;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  font-size: var(--step-body); font-weight: 500; letter-spacing: -0.01em;
  text-decoration: none; white-space: nowrap;
  transition: background-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.btn__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; opacity: 0.85; }
.btn:hover { transform: translateY(-2px); }
html[data-motion="reduced"] .btn:hover { transform: none; }

@media (max-width: 30rem) {
  .btn { white-space: normal; text-align: center; }
  .booking-embed__placeholder { padding: var(--sp-3); }
}

.btn--primary { background: var(--accent-fill); color: var(--accent-fill-text); }
.btn--primary:hover { background: color-mix(in srgb, var(--accent-fill) 84%, var(--surface)); }

.btn--outline { background: transparent; color: var(--text); border-color: var(--border-strong); }
.btn--outline:hover { border-color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }

.section--inverse .btn--outline, .site-header .btn--outline, .hero .btn--outline { color: #fff; border-color: rgb(255 255 255 / 0.55); }
.section--inverse .btn--outline:hover, .site-header .btn--outline:hover, .hero .btn--outline:hover { background: rgb(255 255 255 / 0.14); border-color: currentColor; }

.btn--light { background: var(--surface); color: var(--text); }
.btn--light:hover { background: color-mix(in srgb, var(--surface) 88%, var(--text)); }

/* Text link with an animated rule - decoration is duplicated by the arrow
   glyph so meaning never rests on colour alone (WCAG 1.4.1). */
.link-arrow {
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-height: 44px; font-weight: 500; text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, currentColor 35%, transparent);
  transition: border-color var(--dur-base), gap var(--dur-base);
}
.link-arrow:hover { border-color: currentColor; gap: 0.85rem; }
.link-arrow::after { content: "\2192"; }

/* ==========================================================================
   SCROLL REVEAL - IntersectionObserver adds .is-visible.
   Rebuilt from the reference's Webflow IX2 (fade + 28px rise, ~0.9s).
   No jQuery, no animation library.
   ========================================================================== */
.reveal {
  opacity: 0;
  transform: translate3d(0, var(--reveal-shift), 0);
  transition: opacity var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
.reveal.is-visible { opacity: 1; transform: translate3d(0, 0, 0); }
.reveal--left  { transform: translate3d(calc(var(--reveal-shift) * -1), 0, 0); }
.reveal--scale { transform: translate3d(0, var(--reveal-shift), 0) scale(0.97); }
.reveal--scale.is-visible { transform: none; }

/* Motion is disabled two ways: the OS preference, and an explicit visitor
   toggle. Reveals must resolve to fully visible, never stay hidden. */
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal--left, .reveal--scale { opacity: 1 !important; transform: none !important; transition: none !important; }
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
html[data-motion="reduced"] .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
html[data-motion="reduced"] *, html[data-motion="reduced"] *::before, html[data-motion="reduced"] *::after {
  animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important;
}

/* ==========================================================================
   HEADER / NAV
   ========================================================================== */
.site-header {
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: var(--z-nav);
  color: #fff;
  transition: background-color var(--dur-base) var(--ease-in-out),
              color var(--dur-base) var(--ease-in-out),
              box-shadow var(--dur-base) var(--ease-in-out);
}
.site-header__inner { display: flex; align-items: center; gap: var(--sp-3); min-height: 84px; }
.site-header.is-stuck {
  background: var(--surface-inverse); color: var(--text-on-inverse);
  box-shadow: 0 1px 0 var(--border-inverse);
}
.brand {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; letter-spacing: -0.03em;
  text-decoration: none; margin-inline-end: auto; padding-block: 0.5rem;
  flex: none;   /* never compress: a nowrap wordmark would spill over the nav */
}
/* One line, and it must stay one line: the name is the brand, and a wrap in
   the middle of "Progressive Legal Solutions LLP" reads as two firms. */
.brand__name { white-space: nowrap; }
/* The whole wordmark, "LLP" included, inherits the header's own colour at full
   strength - bright white on dark, and black in the one combination where the
   header surface is white. No opacity: a dimmed "LLP" was the original
   complaint. Colouring the words in the logo's three inks was tried and
   rejected on looks; see the note in tokens.css before trying it again. */
.brand small {
  font-family: var(--font-ui); font-size: 0.55em; font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  margin-inline-start: 0.15em;
}

/* The mark. It replaces the dot that used to stand in for a logo, so it keeps
   that slot: centred against the wordmark rather than sitting on the baseline,
   because the wordmark's own baseline is above its "SOLUTIONS LLP" line.
   Sized in rem off the wordmark so the pair scale together with text size, and
   the width/height attributes give it a 100:58 aspect ratio so nothing reflows
   between first paint and the SVG arriving. */
.brand__mark { height: 2.15rem; width: auto; flex: none; align-self: center; }

/* Footer: the mark leads the column, above the wordmark, where it has room to
   be read rather than glanced at. */
.brand--stacked { flex-direction: column; align-items: flex-start; gap: 0.7rem; font-size: 1.25rem; }
.brand--stacked .brand__mark { height: 2.5rem; align-self: flex-start; }
/* The one place the name may wrap. The header rule exists because a wrap
   mid-name reads as two firms when it sits inline beside the nav; here the
   wordmark is already a stacked block under the mark, so a second line reads
   as part of the same logotype. Without this it is nowrap at 1.25rem in a
   179px grid column, and it ran 84px into the next footer column at 960px. */
.brand--stacked .brand__name { white-space: normal; }

.nav__list { display: flex; align-items: center; gap: 0.25rem; list-style: none; padding: 0; margin: 0; }
.nav__link {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 0.9rem;
  white-space: nowrap;   /* "Practice areas" wrapped to two lines at 1088px */
  font-size: var(--step-sm); font-weight: 500; text-decoration: none; border-radius: var(--radius-pill);
  transition: background-color var(--dur-base), opacity var(--dur-base);
}
.nav__link:hover { background: rgb(255 255 255 / 0.14); }
.site-header:not(.is-stuck) .nav__link[aria-current="true"],
.nav__link[aria-current="true"] { background: rgb(255 255 255 / 0.18); }
.is-stuck .nav__link[aria-current="true"] { background: rgb(255 255 255 / 0.14); }

.nav__toggle { display: none; }

/* Menu mode. The horizontal nav is dropped for the Menu button once the full
   header stops fitting on one row. Measured at 8px steps from 320 to 2000 with
   the nav forced on: the wordmark, six links and the CTA need 1120px at the
   default text size, 1176px at A+ and 1240px at A++, so 1240px is the one
   threshold that holds for all three and no text size can overflow the header.
   Was 70.49rem, measured at the default text size only, which left A+ and A++
   overflowing. */
@media (max-width: 77rem) {
  .nav__toggle {
    display: inline-flex; align-items: center; gap: 0.6rem;
    min-height: 44px; padding: 0.5rem 1rem;
    background: transparent; color: inherit;
    border: 1px solid var(--border-inverse); border-radius: var(--radius-pill);
    font-size: var(--step-sm); font-weight: 500;
  }
  .nav { position: fixed; inset: 0; z-index: -1; visibility: hidden; }
  .nav[data-open="true"] { z-index: 1; visibility: visible; }
  .nav__list {
    position: fixed; inset-block-start: 0; inset-inline: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--surface-inverse); color: var(--text-on-inverse);
    padding: 5.75rem var(--gutter) 2rem;
    transform: translateY(-102%);
    transition: transform var(--dur-slow) var(--ease-out);
  }
  .nav[data-open="true"] .nav__list { transform: translateY(0); }
  .nav__link { min-height: 56px; padding-inline: 0; font-size: 1.25rem; border-radius: 0; border-block-end: 1px solid var(--border-inverse); }
  .nav__link:hover { background: transparent; text-decoration: underline; }
  .header-cta { display: none; }
}

/* Narrow screens: the wordmark, the mark and the menu button share one row, so
   give the mark back the width it borrowed. Thresholds are measured, not
   guessed - this is the widest the pair fits without the header overflowing,
   at the largest text setting as well as the default. */
@media (max-width: 32.99rem) {
  .site-header .brand { gap: 0.4rem; font-size: 1.25rem; }
  .site-header .brand__mark { height: 1.6rem; }
  /* "LLP" is the first thing to go: dropping it buys ~40px, which is the
     difference between showing the firm's name on a 430px phone and showing
     the mark alone. The link's aria-label still carries the full legal name. */
  .site-header .brand small { display: none; }
}

/* One step smaller again, so the name survives on the narrowest phones that
   can still hold it rather than dropping to the mark at 440px. */
@media (max-width: 27.49rem) {
  .site-header .brand { font-size: 1.1rem; }
  .site-header .brand__mark { height: 1.35rem; }
}

/* Below ~416px one line of "Progressive Legal Solutions" cannot sit beside the
   menu button at any readable size, and the name must not wrap. The mark stands
   in for it alone - the link keeps its aria-label, so its accessible name is
   still the firm, not an image. */
@media (max-width: 25.99rem) {
  .site-header .brand > span { display: none; }
  .site-header .brand__mark { height: 2.3rem; }
}

/* ==========================================================================
   HERO
   ========================================================================== */
.hero { position: relative; min-height: 100svh; display: flex; align-items: flex-end; padding-block: 8rem var(--sp-6); color: #fff; overflow: hidden; }
/* Base wash. It is what shows through the 12px lanes between mosaic tiles and
   what the hero falls back to if the artwork never arrives. */
.hero__media { position: absolute; inset: 0; z-index: -2; background: linear-gradient(180deg, #1B1D21 0%, #131518 55%, #0D0E10 100%); }
.hero__media::after { content: ""; position: absolute; inset: 0; background: var(--hero-scrim); }
.hero__veil { position: absolute; inset: 0; background: var(--hero-veil); }
.hero__inner { position: relative; width: 100%; }
.hero__kicker { max-width: 34ch; font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem); line-height: 1.5; margin-block-end: var(--sp-4); }
.hero h1 { margin-block-end: var(--sp-4); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
/* Sits under the buttons rather than above the heading: it is a reason to act
   on them, so it should be read after them, not compete with the headline. */
.hero__promise {
  display: grid; grid-template-columns: auto 1fr; gap: 0.6rem;
  margin-block-start: var(--sp-4); max-width: 42ch;
  font-size: var(--step-sm); line-height: 1.5; color: #fff;
}
/* Grid rather than inline-flex so the marker stays on the first line when the
   sentence wraps to two, instead of centring itself against the whole block. */
.hero__promise::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  margin-block-start: 0.55em; background: var(--accent-on-dark, currentColor);
}

/* Word-by-word hero entrance - the reference's masked line reveal */
.hero h1 .word {
  display: inline-block; overflow: hidden; vertical-align: bottom;
  padding-block-end: 0.16em; margin-block-end: -0.16em; /* room for descenders, zero layout cost */
}
.hero h1 .word > span {
  display: inline-block;
  transform: translateY(115%); /* clears the taller clip box */
  transition: transform 1.1s var(--ease-out);
  transition-delay: var(--word-delay, 0ms);
}
.hero.is-ready h1 .word > span { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) { .hero h1 .word > span { transform: none !important; transition: none !important; } }
html[data-motion="reduced"] .hero h1 .word > span { transform: none !important; transition: none !important; }

/* ==========================================================================
   PRACTICE AREAS
   ========================================================================== */
.practice-list { list-style: none; padding: 0; margin: 0; border-block-start: 1px solid var(--border); }
.practice-item { border-block-end: 1px solid var(--border); }
.practice-item__btn {
  width: 100%; display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: var(--sp-2) var(--sp-3); padding: var(--sp-3) 0;
  background: none; border: 0; text-align: left; color: inherit;
}
.practice-item__num { font-family: var(--font-ui); font-size: var(--step-xs); letter-spacing: var(--track-label); color: var(--text-muted); }
.practice-item__title { font-family: var(--font-display); font-size: var(--step-h3); line-height: 1.15; letter-spacing: var(--track-h3); transition: color var(--dur-base); }
.practice-item__btn:hover .practice-item__title { color: var(--accent-on-light); }
.practice-item__icon { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border-strong); display: grid; place-items: center; transition: transform var(--dur-base) var(--ease-out), background-color var(--dur-base); flex: none; }
.practice-item__btn[aria-expanded="true"] .practice-item__icon { transform: rotate(45deg); background: var(--accent-fill); color: var(--accent-fill-text); }
.practice-item__panel { overflow: hidden; display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-slow) var(--ease-in-out); }
.practice-item__panel[data-open="true"] { grid-template-rows: 1fr; }
.practice-item__panel > div { min-height: 0; }
.practice-item__body { padding-block-end: var(--sp-4); max-width: var(--measure); color: var(--text-muted); }
.practice-item__body ul { margin-block-start: var(--sp-2); padding-inline-start: 1.25rem; }
.practice-item__body li + li { margin-block-start: 0.4rem; }

/* ==========================================================================
   STATS
   ========================================================================== */
/* Closes the About section: the firm's own sign-off line, set apart from the
   body copy so it reads as a statement rather than another paragraph. */
.about__closing {
  margin-block-start: var(--sp-3); padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--border);
  font-family: var(--font-display); font-size: var(--step-h4);
  line-height: 1.35; letter-spacing: -0.02em;
}

.stats { display: grid; gap: 1px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); }
.stat { border-block-start: 1px solid var(--border); padding: var(--sp-3) var(--sp-3) var(--sp-3) 0; }
.stat__value { display: block; white-space: nowrap; font-family: var(--font-display); font-size: clamp(2.25rem, 1.5rem + 2.6vw, 3.75rem); line-height: 1; letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
.stat__label { display: block; margin-block-start: 0.5rem; font-size: var(--step-sm); color: var(--text-muted); text-wrap: pretty; }

/* One figure leads. It gets a rule in the accent rather than the border colour
   and a heavier label, so the eye lands on the response time first - the rest
   of the row still reads as one set of figures. */
.stat--lead { border-block-start-width: 3px; border-block-start-color: var(--accent-fill); }
.stat--lead .stat__label { color: var(--text); font-weight: 500; }
@media (min-width: 60rem) {
  .stat--lead .stat__value { font-size: clamp(3rem, 1.9rem + 3.9vw, 5.25rem); }
}

/* ==========================================================================
   TESTIMONIAL SLIDER
   ========================================================================== */
.slider__viewport { overflow: hidden; min-width: 0; }
.slider__track { display: flex; transition: transform var(--dur-slow) var(--ease-in-out); }
html[data-motion="reduced"] .slider__track { transition: none; }
.slider__slide { flex: 0 0 100%; min-width: 0; padding-inline-end: 1px; }
.slider__slide[aria-hidden="true"] { visibility: hidden; }
.quote-mark { font-family: var(--font-display); font-size: 5rem; line-height: 0.6; color: var(--accent-on-dark); display: block; margin-block-end: var(--sp-2); }
.quote {
  font-family: var(--font-display); font-style: italic; font-weight: 400;
  font-size: clamp(1.375rem, 0.95rem + 2.1vw, 2.75rem);
  line-height: 1.28; letter-spacing: -0.025em; max-width: min(32ch, 100%);
}
.quote-cite { display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-4); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border-inverse); font-style: normal; }
/* Quotes carry no name, photograph or occupation. The practice area is what is
   left, and it is the part that tells a visitor whether the comment applies to
   them - so it is set as a label, not as a signature. */
.quote-cite__area { font-size: var(--step-sm); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted-inverse); }
.slider__controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-block-start: var(--sp-4); }
.slider__btn {
  width: 52px; height: 52px; border-radius: 50%;
  background: transparent; color: inherit; border: 1px solid var(--border-strong-inverse);
  display: grid; place-items: center;
  transition: background-color var(--dur-base), border-color var(--dur-base);
}
.slider__btn:hover { background: rgb(255 255 255 / 0.14); border-color: currentColor; }
.slider__btn[aria-disabled="true"] { opacity: 0.4; }
.slider__dots { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-inline-start: auto; list-style: none; padding: 0; margin-block: 0; }
@media (max-width: 26rem) { .slider__dots { margin-inline-start: 0; } }
.slider__dot { width: 44px; height: 44px; display: grid; place-items: center; background: none; border: 0; }
.slider__dot::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: 0.35; transition: opacity var(--dur-base), transform var(--dur-base); }
.slider__dot[aria-current="true"]::before { opacity: 1; transform: scale(1.6); }
.slider__status { font-size: var(--step-sm); color: var(--text-muted-inverse); }

/* ==========================================================================
   TEAM
   ========================================================================== */
.team-grid { display: grid; gap: var(--sp-4) var(--sp-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); }

/* The whole card opens the lawyer's profile page. .member__link is the only
   link in the accessibility tree; its ::after stretches over the card to make
   the entire surface a hit target. The contact links and the booking button sit
   above that overlay so they still work. The focus ring is drawn on the card
   rather than on the name, so it matches the area that actually responds. */
.member { position: relative; transition: transform var(--dur-base) var(--ease-out); }
.member:hover { transform: translateY(-3px); }
html[data-motion="reduced"] .member:hover { transform: none; }
.member:has(.member__link:focus-visible) { outline: 3px solid var(--focus); outline-offset: 5px; }
.member__link { color: inherit; text-decoration: none; display: inline-flex; align-items: baseline; gap: 0.35rem;
  border-bottom: 1px solid color-mix(in srgb, currentColor 25%, transparent); }
.member__link:focus-visible { outline: none; }
.member__link::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius-card); }
.member:hover .member__link { border-color: currentColor; }
.member__link-icon { display: inline-flex; opacity: 0.55; transition: opacity var(--dur-base), transform var(--dur-base) var(--ease-out); }
.member:hover .member__link-icon, .member__link:focus-visible .member__link-icon { opacity: 1; transform: translate(2px, -2px); }
html[data-motion="reduced"] .member__link-icon { transition: none; transform: none !important; }
.member__contact a, .member__book { position: relative; z-index: 1; }

.member__figure { position: relative; overflow: hidden; aspect-ratio: 3 / 3.6; background: var(--surface-alt); }
.member__figure img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-slow) var(--ease-out); }
.member:hover .member__figure img { transform: scale(1.05); }
html[data-motion="reduced"] .member:hover .member__figure img { transform: none; }
.member__name { margin-block-start: var(--sp-2); font-family: var(--font-ui); font-size: 1.125rem; font-weight: 500; }
.member__role { color: var(--text-muted); font-size: var(--step-sm); margin-block-start: 0.15rem; }
/* Says which partner carries the file, in text rather than by position in the
   grid, so it survives reordering and reads the same to a screen reader. */
.member__badge {
  display: inline-block; margin-inline-start: 0.5rem; padding: 0.1rem 0.5rem;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  font-size: var(--step-xs); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--text);
}
.member__bio { margin-block-start: 0.75rem; font-size: var(--step-sm); color: var(--text-muted); }
.member__contact { margin-block-start: 0.75rem; display: grid; gap: 0.1rem; font-size: var(--step-sm); }
.member__contact a { text-decoration: none; border-bottom: 1px solid color-mix(in srgb, currentColor 30%, transparent); justify-self: start; min-height: 32px; display: inline-flex; align-items: center; }
.member__actions { margin-block-start: var(--sp-2); }
.member__book { font-size: var(--step-sm); min-height: 44px; padding: 0.6rem 1.1rem; }

/* ==========================================================================
   LAWYER PROFILE  (/team/<slug>/)
   These pages have no hero, so the fixed header never gets its .is-stuck
   class from the scroll observer - it is pinned solid from the start instead,
   and the first section is inset to clear it.
   ========================================================================== */
body[data-header="solid"] .site-header {
  background: var(--surface-inverse); color: var(--text-on-inverse);
  box-shadow: 0 1px 0 var(--border-inverse);
}
.profile-head { padding-block-start: calc(84px + var(--sp-5)); }

.breadcrumb { font-size: var(--step-sm); }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; padding: 0; margin: 0; color: var(--text-muted); }
.breadcrumb li { display: flex; align-items: center; gap: 0.35rem; }
.breadcrumb li + li::before { content: "/"; color: var(--text-muted); }
.breadcrumb a { text-decoration: none; min-height: 32px; display: inline-flex; align-items: center;
  border-bottom: 1px solid color-mix(in srgb, currentColor 30%, transparent); }
.breadcrumb a:hover { border-color: currentColor; }
.breadcrumb [aria-current="page"] { color: var(--text); font-weight: 500; }

.profile-grid { display: grid; gap: var(--sp-4); margin-block-start: var(--sp-4); }
.profile-grid > * { min-width: 0; }
@media (min-width: 55rem) {
  .profile-grid { grid-template-columns: minmax(0, 0.4fr) minmax(0, 1fr); gap: var(--sp-6); align-items: start; }
}
.profile__figure { overflow: hidden; aspect-ratio: 3 / 3.6; background: var(--surface-alt); border-radius: var(--radius-card); }
.profile__figure img { width: 100%; height: 100%; object-fit: cover; }
/* h1 defaults to --step-hero, which is scaled for the full-bleed home hero.
   A name in a two-column layout wants the h2 step instead. */
.profile__name { font-size: var(--step-h2); line-height: 1.0; letter-spacing: var(--track-h2);
  margin-block-start: var(--sp-1); }
@media (min-width: 55rem) { .profile__name { font-size: clamp(2.5rem, 1.4rem + 2.6vw, 4rem); } }
.profile__role { font-family: var(--font-ui); font-size: var(--step-h4); font-weight: 500; color: var(--text-muted); margin-block-start: var(--sp-1); }

.profile__contact { display: grid; gap: var(--sp-3); margin: var(--sp-5) 0 0; padding-block: var(--sp-3);
  border-block: 1px solid var(--border); font-size: var(--step-sm); }
.profile__contact > div { min-width: 0; }
@media (min-width: 40rem) { .profile__contact { grid-template-columns: 1fr 1fr; } }
.profile__contact dt { font-size: var(--step-xs); font-weight: 500; letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--text-muted); }
.profile__contact dd { margin: 0.3rem 0 0; }
.profile__contact a { text-decoration: none; min-height: 32px; display: inline-flex; align-items: center;
  border-bottom: 1px solid color-mix(in srgb, currentColor 30%, transparent); }
.profile__contact a:hover { border-color: currentColor; }

.profile__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-block-start: var(--sp-4); }
.profile__subhead { font-family: var(--font-ui); font-size: var(--step-h4); font-weight: 500; }
.profile-cols { display: grid; gap: var(--sp-5); }
.profile-cols > * { min-width: 0; }
@media (min-width: 48rem) { .profile-cols { grid-template-columns: 1fr 1fr; gap: var(--sp-6); } }

.plain-list + .profile__subhead { margin-block-start: var(--sp-4); }
.tick-list, .plain-list { list-style: none; padding: 0; margin-block-start: var(--sp-2);
  display: grid; gap: 0.55rem; font-size: var(--step-body); color: var(--text-muted); }
.tick-list li { position: relative; padding-inline-start: 1.35rem; }
/* Marker is a shape, not a colour cue - it carries no meaning on its own. */
.tick-list li::before { content: ""; position: absolute; inset-inline-start: 0; inset-block-start: 0.5em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent-on-light); }
.section--inverse .tick-list li::before { background: var(--accent-on-dark); }

.article-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--sp-4); }
.article-list__item { padding-block-end: var(--sp-4); border-block-end: 1px solid var(--border); }
.article-list__item:last-child { padding-block-end: 0; border-block-end: 0; }
.article-list__title { font-family: var(--font-display); font-size: var(--step-h4); font-weight: 400; letter-spacing: -0.02em; }
.article-list__title a { text-decoration: none; border-bottom: 1px solid color-mix(in srgb, currentColor 25%, transparent); }
.article-list__title a:hover { border-color: currentColor; }

/* ==========================================================================
   BOOKING
   ========================================================================== */
/* Reflow safety: enlarged text (WCAG 1.4.4) plus a 320px viewport
   (WCAG 1.4.10) is the case that breaks `auto` grid minimums. */
.booking-grid > *, .map-grid > *, .footer-grid > *, .stats > *,
.choice > *, .practice-item__btn > *, .quote-cite > *, .parking-item > * { min-width: 0; }

.booking-grid { display: grid; gap: var(--sp-4); }
.booking-single { max-width: 62ch; }
@media (min-width: 60rem) { .booking-grid { grid-template-columns: 1fr 1fr; align-items: start; gap: var(--sp-6); } }
.lawyer-choice { display: grid; gap: 0.75rem; }
.choice {
  display: grid; grid-template-columns: auto 1fr; gap: 0.9rem; align-items: center;
  padding: var(--sp-2); border: 1px solid var(--border-strong-inverse); border-radius: var(--radius-card);
  cursor: pointer; transition: background-color var(--dur-base), border-color var(--dur-base);
}
.choice:hover { background: rgb(255 255 255 / 0.06); }
.choice:has(input:checked) { border-color: currentColor; background: rgb(255 255 255 / 0.1); }
/* Selection is signalled by border + fill + the native radio, not colour alone */
.choice input { width: 22px; height: 22px; accent-color: var(--accent-on-dark); margin: 0; }
.choice input:focus-visible { outline: 3px solid var(--focus-inverse); outline-offset: 3px; }
.choice__name { font-weight: 500; }
.choice__role { font-size: var(--step-sm); color: var(--text-muted-inverse); }

.booking-embed { border: 1px solid var(--border-inverse); border-radius: var(--radius-card); background: rgb(255 255 255 / 0.05); display: grid; }
/* The 420px floor exists to hold a calendar iframe without the page jumping
   when it loads. With only the fallback copy inside, that floor is 300px of
   empty box, so it applies to the embedded state rather than the panel. */
.booking-embed:has(iframe), .booking-embed:has(#booking-consent) { min-height: 420px; }
.booking-embed > * { min-width: 0; }
.booking-embed iframe { width: 100%; min-height: 640px; border: 0; border-radius: var(--radius-card); background: #fff; }
.booking-embed__placeholder { padding: var(--sp-4); display: grid; align-content: center; justify-items: start; gap: var(--sp-2); text-align: left; }

/* ==========================================================================
   FORMS
   ========================================================================== */
.field { display: grid; gap: 0.4rem; margin-block-end: var(--sp-2); }
.field label { font-size: var(--step-sm); font-weight: 500; }
.field .req { color: var(--accent-on-dark); }
.field__hint { font-size: var(--step-xs); color: var(--text-muted-inverse); }
.field input, .field textarea, .field select {
  width: 100%; padding: 0.85rem 1rem; min-height: 52px;
  background: rgb(255 255 255 / 0.06); color: inherit;
  border: 1px solid var(--border-strong-inverse); border-radius: 8px;
  transition: border-color var(--dur-base), background-color var(--dur-base);
}
.field input:hover, .field textarea:hover { background: rgb(255 255 255 / 0.1); }
.field textarea { min-height: 130px; resize: vertical; }
.field [aria-invalid="true"] { border-color: #FFB4B4; border-width: 2px; }
/* Errors carry an icon and text, never colour alone */
.field__error { font-size: var(--step-sm); color: #FFC9C9; display: none; align-items: center; gap: 0.4rem; }
.field__error::before { content: "\26A0"; }
.field__error[data-show="true"] { display: flex; }
.form-status { margin-block-start: var(--sp-2); font-size: var(--step-sm); }

/* ==========================================================================
   MAP + PARKING
   ========================================================================== */
.map-grid { display: grid; gap: var(--sp-4); }
@media (min-width: 64rem) { .map-grid { grid-template-columns: 1.15fr 1fr; gap: var(--sp-5); align-items: start; } }
.map-frame { position: relative; border-radius: var(--radius-card); overflow: hidden; border: 1px solid var(--border); background: var(--surface-alt); aspect-ratio: 4 / 3; }
.map-frame iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Click-to-load: no third-party request (or cookie) until the visitor asks */
.map-consent { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: var(--sp-2); padding: var(--sp-4); text-align: center; background: var(--surface-alt); }
.parking-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0; }
.parking-item { display: grid; gap: 0.35rem; padding-block: var(--sp-3); border-block-start: 1px solid var(--border); }
.parking-item__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 0.75rem; }
.parking-item__name { font-weight: 500; font-size: 1.0625rem; }
.parking-item__dist { font-size: var(--step-xs); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted); }
.parking-item__note { font-size: var(--step-sm); color: var(--text-muted); }
.badge {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: var(--step-xs); font-weight: 500; letter-spacing: 0.02em;
  padding: 0.25rem 0.6rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); color: var(--text-muted);
}
.badge--accessible { border-color: var(--accent-on-light); color: var(--accent-on-light); }

/* ==========================================================================
   BLOG
   ========================================================================== */
.post-grid { display: grid; gap: var(--sp-4) var(--sp-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); }
.post { display: flex; flex-direction: column; }
.post__figure { overflow: hidden; aspect-ratio: 16 / 10; background: var(--surface-alt); border-radius: var(--radius-card); }
.post__figure img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-slow) var(--ease-out); }
.post:hover .post__figure img { transform: scale(1.04); }
html[data-motion="reduced"] .post:hover .post__figure img { transform: none; }
.post__meta { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; margin-block-start: var(--sp-2); font-size: var(--step-xs); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted); }
.post__title { margin-block-start: 0.6rem; font-family: var(--font-display); font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.02em; }
.post__title a { text-decoration: none; }
.post__title a:hover { text-decoration: underline; }
.post__excerpt { margin-block-start: 0.6rem; font-size: var(--step-sm); color: var(--text-muted); }
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-block-end: var(--sp-4); }
.filter-bar button {
  min-height: 44px; padding: 0.55rem 1.1rem; border-radius: var(--radius-pill);
  background: transparent; border: 1px solid var(--border-strong); font-size: var(--step-sm); font-weight: 500;
  transition: background-color var(--dur-base), color var(--dur-base), border-color var(--dur-base);
}
.filter-bar button[aria-pressed="true"] { background: var(--accent-fill); color: var(--accent-fill-text); border-color: var(--accent-fill); }

/* ==========================================================================
   FOOTER
   ========================================================================== */
.site-footer { background: var(--surface-inverse); color: var(--text-on-inverse); padding-block: var(--section-y) var(--sp-4); }
.site-footer .text-muted { color: var(--text-muted-inverse); }
.footer-grid { display: grid; gap: var(--sp-5); }
@media (min-width: 60rem) { .footer-grid { grid-template-columns: 1.4fr repeat(4, 1fr); } }
.footer-col h3 { font-family: var(--font-ui); font-size: var(--step-sm); font-weight: 500; letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted-inverse); margin-block-end: var(--sp-2); }
.footer-col ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.15rem; }
.footer-col a { text-decoration: none; min-height: 40px; display: inline-flex; align-items: center; border-bottom: 1px solid transparent; }
.footer-col a:hover { border-color: currentColor; }
.footer-bottom { margin-block-start: var(--sp-5); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border-inverse); display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); justify-content: space-between; font-size: var(--step-sm); color: var(--text-muted-inverse); }
.legal-note { font-size: var(--step-xs); color: var(--text-muted-inverse); max-width: 62ch; margin-block-start: var(--sp-3); line-height: 1.5; }

/* ==========================================================================
   MODAL (booking + post reader)
   ========================================================================== */
dialog.modal {
  /* The `* { margin: 0 }` reset beats the UA's `dialog { margin: auto }`, which
     is what centres a modal dialog inside its inset:0 box - without this the
     panel opens pinned to the top-left corner. */
  margin: auto;
  width: min(920px, calc(100vw - 2rem)); max-height: min(90svh, 900px);
  padding: 0; border: 0; border-radius: var(--radius-card);
  background: var(--surface); color: var(--text);
  box-shadow: 0 40px 80px -20px rgb(0 0 0 / 0.45);
  overflow: hidden;
}
dialog.modal::backdrop { background: rgb(15 16 19 / 0.7); backdrop-filter: blur(3px); }
.modal__head { display: flex; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-3) var(--sp-3) var(--sp-2); border-block-end: 1px solid var(--border); }
.modal__title { font-family: var(--font-display); font-size: var(--step-h3); }
.modal__close { margin-inline-start: auto; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--border-strong); background: transparent; display: grid; place-items: center; flex: none; }
.modal__close:hover { background: var(--surface-alt); }
.modal__body { padding: var(--sp-3); overflow-y: auto; max-height: 70svh; }
.modal__body iframe { width: 100%; min-height: 620px; border: 0; }
.prose { max-width: var(--measure); }
.prose h3 { margin-block: var(--sp-3) var(--sp-1); }
.prose p + p { margin-block-start: var(--sp-2); }
.prose ul { margin-block-start: var(--sp-2); padding-inline-start: 1.25rem; }

/* ==========================================================================
   ACCESSIBILITY CONTROL PANEL (visitor-facing, permanent)
   ========================================================================== */
.a11y-fab {
  position: fixed; inset-block-end: 1rem; inset-inline-start: 1rem; z-index: var(--z-panel);
  width: 52px; height: 52px; border-radius: 50%;
  background: var(--accent-fill); color: var(--accent-fill-text);
  border: 2px solid var(--surface); display: grid; place-items: center;
  box-shadow: 0 8px 24px -8px rgb(0 0 0 / 0.5);
}
.a11y-panel {
  position: fixed; inset-block-end: 4.75rem; inset-inline-start: 1rem; z-index: var(--z-panel);
  width: min(310px, calc(100vw - 2rem));
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  box-shadow: 0 24px 48px -16px rgb(0 0 0 / 0.35);
  padding: var(--sp-3);
}
.a11y-panel[hidden] { display: none; }
.a11y-panel h2 { font-family: var(--font-ui); font-size: var(--step-sm); font-weight: 600; letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted); }
.a11y-group { margin-block-start: var(--sp-2); }
.a11y-group > legend { font-size: var(--step-sm); font-weight: 500; padding: 0; margin-block-end: 0.4rem; }
.a11y-group { border: 0; padding: 0; }
.seg { display: flex; gap: 0.25rem; background: var(--surface-alt); padding: 3px; border-radius: var(--radius-pill); }
.seg button {
  flex: 1; min-height: 40px; padding: 0.4rem 0.5rem; border: 0; border-radius: var(--radius-pill);
  background: transparent; font-size: var(--step-xs); font-weight: 500;
  transition: background-color var(--dur-fast), color var(--dur-fast);
}
.seg button[aria-pressed="true"] { background: var(--accent-fill); color: var(--accent-fill-text); }
.seg button[aria-pressed="true"]::after { content: " \2713"; }
.a11y-panel__reset { margin-block-start: var(--sp-2); font-size: var(--step-xs); background: none; border: 0; text-decoration: underline; padding: 0.5rem 0; min-height: 44px; }

/* ==========================================================================
   DESIGN LAB - client palette chooser, gated behind ?design=1
   ========================================================================== */
.design-lab {
  position: fixed; inset-block-start: 50%; inset-inline-start: 1rem; transform: translateY(-50%);
  z-index: var(--z-panel); width: min(268px, calc(100vw - 2rem));
  background: var(--surface-raised); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  box-shadow: 0 24px 48px -16px rgb(0 0 0 / 0.35); padding: var(--sp-3);
  font-size: var(--step-sm);
}
.design-lab h2 { font-family: var(--font-ui); font-size: var(--step-xs); font-weight: 600; letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text-muted); }
.lab-swatches { display: grid; gap: 0.5rem; margin-block-start: var(--sp-2); }
.lab-swatch { display: grid; grid-template-columns: auto 1fr; gap: 0.65rem; align-items: center; width: 100%; padding: 0.55rem; background: transparent; border: 1px solid var(--border-strong); border-radius: 8px; text-align: left; }
.lab-swatch[aria-pressed="true"] { border-color: var(--text); border-width: 2px; padding: calc(0.55rem - 1px); }
.lab-swatch[aria-pressed="true"] .lab-swatch__label::after { content: " \2713"; }
.lab-chips { display: flex; }
.lab-chip { width: 15px; height: 26px; border: 1px solid rgb(0 0 0 / 0.2); }
.lab-chip:first-child { border-radius: 4px 0 0 4px; }
.lab-chip:last-child { border-radius: 0 4px 4px 0; }
.lab-swatch__label { font-weight: 500; }
.lab-swatch__ratio { font-size: var(--step-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.lab-readout { margin-block-start: var(--sp-2); padding-block-start: var(--sp-2); border-block-start: 1px solid var(--border); font-size: var(--step-xs); }
.lab-readout table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.lab-readout th, .lab-readout td { text-align: left; padding: 0.2rem 0; font-weight: 400; }
.lab-readout td:last-child { text-align: right; }
.lab-pass { color: #1B7F3B; font-weight: 600; }
.lab-fail { color: #B3261E; font-weight: 600; }
[data-theme="dark"] .lab-pass { color: #6EE7A0; }
[data-theme="dark"] .lab-fail { color: #FF9C94; }


/* ==========================================================================
   MOTION LAYER
   The reference build's sense of movement, rebuilt for a law firm: slow,
   low-amplitude and always subordinate to the reading. Three rules held
   throughout -
     1. Nothing moves that a visitor has to read while it moves.
     2. Every entrance is a CSS animation with `both` fill, never a
        JS-applied class, so a script failure cannot leave content invisible.
     3. Ambient (infinitely looping) motion is decorative only and is removed
        outright under reduced motion, forced colours and print.
   ========================================================================== */

/* ---------- shared keyframes ---------- */
@keyframes pls-enter {
  from { opacity: 0; transform: translate3d(0, 18px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes pls-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---------- scroll progress rail ----------
   A 2px accent line along the bottom edge of the pinned header, scaled to how
   far down the document the visitor is. It only appears once the header has
   gone solid, so it never draws a bright line across the hero art. */
.scroll-rail {
  position: absolute; inset-block-end: 0; inset-inline: 0; height: 2px;
  overflow: hidden; pointer-events: none;
  opacity: 0; transition: opacity var(--dur-base) var(--ease-out);
}
.site-header.is-stuck .scroll-rail,
body[data-header="solid"] .scroll-rail { opacity: 1; }
.scroll-rail__fill {
  display: block; height: 100%; width: 100%;
  /* --accent-fill is a charcoal, which is the header's own background: the rail
     has to use the accent cut for dark surfaces or it paints itself invisible. */
  background: var(--accent-on-dark);
  transform: scaleX(var(--progress, 0)); transform-origin: 0 50%;
}

/* ---------- hero: ambient light ----------
   Two very large, very soft washes drifting behind the pillars on a half-minute
   cycle. They sit inside .hero__media, before its scrim pseudo-element, so the
   scrim still paints over them and the contrast of the headline is unchanged. */
.hero__glow {
  position: absolute; border-radius: 50%;
  pointer-events: none; filter: blur(90px); will-change: transform;
}
.hero__glow--a {
  width: 46vw; height: 46vw; inset-block-start: -12vw; inset-inline-start: -8vw;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent-on-dark) 40%, transparent) 0%, transparent 68%);
  animation: pls-drift-a 34s var(--ease-in-out) infinite;
}
.hero__glow--b {
  width: 40vw; height: 40vw; inset-block-end: -14vw; inset-inline-end: -6vw;
  background: radial-gradient(circle, rgb(255 255 255 / 0.16) 0%, transparent 70%);
  animation: pls-drift-b 46s var(--ease-in-out) infinite;
}
@keyframes pls-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(5%, 7%, 0) scale(1.14); }
}
@keyframes pls-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.06); }
  50%      { transform: translate3d(-6%, -5%, 0) scale(0.94); }
}

/* Scroll parallax. The variable is written by main.js on a rAF; with no script
   it resolves to 0px and the art simply sits still. */
.hero__media { transform: translate3d(0, var(--hero-parallax, 0px), 0); will-change: transform; }

/* ---------- hero: the practice-area mosaic ----------
   Columns of practice-area artwork drifting in alternating directions, a
   minute and a half to the cycle. That is roughly a fifth of the speed the
   reference build runs at: this is a law firm's front door, and the point is
   depth behind the headline, not a showreel.

   Inset past the edges on all four sides so no column end is ever on screen,
   and faded in rather than snapped so a slow connection resolves as artwork
   arriving rather than the page changing under the reader. */
.hero__mosaic {
  position: absolute; inset: -8% -6%;
  display: grid; grid-template-columns: repeat(6, 1fr); column-gap: 12px;
  opacity: 0;
  animation: pls-mosaic-in 2s 250ms var(--ease-out) forwards;
}
@keyframes pls-mosaic-in { to { opacity: var(--mosaic-opacity, 0.5); } }

.hero__col { display: flex; flex-direction: column; will-change: transform; }
.hero__col:nth-child(odd)  { animation: pls-rail-up   140s linear infinite; }
.hero__col:nth-child(even) { animation: pls-rail-down 172s linear infinite; }
/* Offsetting the start times stops the six columns reading as one moving
   sheet - negative delays, so every column is already mid-cycle at load. */
.hero__col:nth-child(2) { animation-delay: -31s; }
.hero__col:nth-child(3) { animation-delay: -64s; }
.hero__col:nth-child(4) { animation-delay: -12s; }
.hero__col:nth-child(5) { animation-delay: -96s; }
.hero__col:nth-child(6) { animation-delay: -50s; }

/* Off-screen the rails are invisible but still compositing six layers every
   frame for the whole length of the page. Parked by main.js the moment the
   hero leaves the viewport. */
.hero__mosaic[data-paused="true"] .hero__col { animation-play-state: paused; }

@keyframes pls-rail-up   { from { transform: translate3d(0, 0, 0); }    to { transform: translate3d(0, -50%, 0); } }
@keyframes pls-rail-down { from { transform: translate3d(0, -50%, 0); } to { transform: translate3d(0, 0, 0); } }

/* Margin, not gap - see the note in tools/build.mjs. With `gap` the -50%
   translate lands half a gap out and the loop visibly jumps every cycle. */
.hero__tile {
  flex: 0 0 auto; margin-block-end: 12px;
  aspect-ratio: 3 / 4; border-radius: 10px; overflow: hidden;
  background: #17181B;
}
.hero__tile img {
  width: 100%; height: 100%; object-fit: cover;
  /* Barely touched. These frames are already lamplit mahogany shot dark; the
     restraint is done by --mosaic-opacity and --hero-veil above, and pulling
     brightness down here as well only turned them to mud. Contrast behind the
     headline is measured on the composited pixels, not guessed - see README. */
  filter: saturate(1.02) contrast(1.04) brightness(1);
}

/* Fewer, wider columns as the viewport narrows: at six columns on a phone each
   tile is 60px across and the artwork turns to noise. Two columns keeps the
   tiles large enough to be read as pictures. */
@media (max-width: 60rem) {
  .hero__mosaic { grid-template-columns: repeat(4, 1fr); }
  .hero__col:nth-child(n + 5) { display: none; }
}
@media (max-width: 34rem) {
  .hero__mosaic { grid-template-columns: repeat(2, 1fr); column-gap: 10px; }
  .hero__col:nth-child(n + 3) { display: none; }
  .hero__tile { margin-block-end: 10px; border-radius: 8px; }
}

/* ---------- hero: staged entrance ----------
   The heading's word-by-word mask runs from 120ms at 90ms a word; these pick up
   around it so the block assembles in reading order rather than all at once. */
.hero__kicker  { animation: pls-enter 900ms var(--ease-out)   60ms both; }
.hero__actions { animation: pls-enter 900ms var(--ease-out)  620ms both; }
.hero__promise { animation: pls-enter 900ms var(--ease-out)  760ms both; }

/* ---------- hero: scroll cue ----------
   Decorative and aria-hidden: the section below is reachable by the nav, the
   skip link and by scrolling, so this adds no information, only a hint. */
.hero__cue {
  /* Kept clear of the accessibility button parked in the same bottom corner. */
  position: absolute; inset-block-end: 0.5rem; inset-inline-end: 3.5rem;
  display: grid; justify-items: center; gap: 0.7rem;
  font-family: var(--font-ui); font-size: var(--step-xs); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: #fff;
  animation: pls-enter 900ms var(--ease-out) 1100ms both;
}
.hero__cue-line { position: relative; display: block; width: 1px; height: 52px; overflow: hidden; background: rgb(255 255 255 / 0.25); }
.hero__cue-line::after {
  content: ""; position: absolute; inset-inline: 0; height: 42%; background: #fff;
  animation: pls-cue 2.4s var(--ease-in-out) infinite;
}
@keyframes pls-cue {
  0%   { transform: translateY(-110%); }
  100% { transform: translateY(250%); }
}
/* Below this width the cue would sit on top of the buttons. */
@media (max-width: 60rem) { .hero__cue { display: none; } }

/* ---------- nav: underline ----------
   Hover and current state both draw the same rule, so the pointer preview
   matches the resting indicator. The pill fill stays as the non-motion cue. */
.nav__link { position: relative; }
.nav__link::after {
  content: ""; position: absolute; inset-inline: 0.9rem; inset-block-end: 0.5rem; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--dur-base) var(--ease-out);
}
.nav__link:hover::after, .nav__link[aria-current="true"]::after { transform: scaleX(1); }

/* ---------- buttons and links ---------- */
.btn { box-shadow: 0 0 0 0 rgb(0 0 0 / 0); transition: box-shadow var(--dur-base) var(--ease-out); }
.btn:hover { box-shadow: 0 12px 26px -16px rgb(0 0 0 / 0.6); }
.btn__dot { transition: transform var(--dur-base) var(--ease-out); }
.btn:hover .btn__dot { transform: scale(1.55); }
.link-arrow::after { transition: transform var(--dur-base) var(--ease-out); }
.link-arrow:hover::after { transform: translateX(3px); }

/* ---------- eyebrow dot draws in with its block ---------- */
.reveal .eyebrow::before {
  transform: scale(0);
  transition: transform var(--dur-slow) var(--ease-out) 160ms;
}
.reveal.is-visible .eyebrow::before { transform: scale(1); }

/* ---------- practice list ----------
   The row shifts a few pixels towards the reader on hover and an accent rule
   runs along its base while it is open, which is the one place the list needs
   to say "this is the row you are in" without relying on colour alone (the
   plus/cross icon already rotates). */
.practice-item { position: relative; }
.practice-item::after {
  content: ""; position: absolute; inset-block-end: -1px; inset-inline-start: 0;
  width: 100%; height: 1px; background: var(--accent-fill);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--dur-slow) var(--ease-out);
}
.practice-item:hover::after,
.practice-item:has(.practice-item__btn[aria-expanded="true"])::after { transform: scaleX(1); }
.practice-item__btn { transition: transform var(--dur-base) var(--ease-out), color var(--dur-base); }
.practice-item__btn:hover { transform: translateX(6px); }

/* Panel contents settle in behind the row opening rather than arriving with it. */
.practice-item__body > * {
  opacity: 0; transform: translate3d(0, 8px, 0);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
}
.practice-item__panel[data-open="true"] .practice-item__body > * { opacity: 1; transform: none; }
.practice-item__panel[data-open="true"] .practice-item__body > *:nth-child(1) { transition-delay: 90ms; }
.practice-item__panel[data-open="true"] .practice-item__body > *:nth-child(2) { transition-delay: 150ms; }
.practice-item__panel[data-open="true"] .practice-item__body > *:nth-child(3) { transition-delay: 210ms; }

/* ---------- team portraits ----------
   The portrait settles back from a slight overscale as the card reveals. Scoped
   with :not(.is-visible) so the rule stops existing afterwards and the hover
   zoom, which is a lower-specificity selector, is free to take over. */
.member.reveal:not(.is-visible) .member__figure img { transform: scale(1.07); }

/* ---------- blog filter crossfade ----------
   The grid dips before the swap and comes back after it, so the change reads as
   one movement instead of a jump. The count is still announced by the live
   region - this is decoration over an already-accessible change. */
.post { transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); }
.post-grid.is-filtering .post { opacity: 0.15; transform: translate3d(0, 8px, 0); }

/* ---------- dialogs ---------- */
dialog.modal[open] { animation: pls-modal-in 320ms var(--ease-out) both; }
dialog.modal[open]::backdrop { animation: pls-fade 320ms var(--ease-out) both; }
@keyframes pls-modal-in {
  from { opacity: 0; transform: translate3d(0, 14px, 0) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- menu-mode nav: links arrive after the sheet ---------- */
@media (max-width: 77rem) {
  .nav__link::after { display: none; }   /* the sheet's own rules do this job */
  .nav__list .nav__link {
    opacity: 0; transform: translate3d(0, -10px, 0);
    transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
  }
  .nav[data-open="true"] .nav__link { opacity: 1; transform: none; }
  .nav[data-open="true"] li:nth-child(1) .nav__link { transition-delay:  90ms; }
  .nav[data-open="true"] li:nth-child(2) .nav__link { transition-delay: 135ms; }
  .nav[data-open="true"] li:nth-child(3) .nav__link { transition-delay: 180ms; }
  .nav[data-open="true"] li:nth-child(4) .nav__link { transition-delay: 225ms; }
  .nav[data-open="true"] li:nth-child(5) .nav__link { transition-delay: 270ms; }
  .nav[data-open="true"] li:nth-child(6) .nav__link { transition-delay: 315ms; }
}

/* ---------- reduced motion ----------
   The blanket rules further up already collapse every duration, which stops the
   loops after one pass but leaves the ambient art parked mid-cycle. These
   remove it instead, and force the states that a killed transition would
   otherwise strand. Both switches are covered: the OS preference and the
   visitor's own Motion control in the display panel. */
@media (prefers-reduced-motion: reduce) {
  .hero__glow, .hero__cue { display: none; }
  .hero__media { transform: none !important; }
  /* The mosaic stays - it is the hero's artwork, not its motion - but it stops
     dead and skips the fade-in, which is an entrance rather than a loop. */
  .hero__col { animation: none !important; }
  .hero__mosaic { animation: none !important; opacity: var(--mosaic-opacity, 0.5); }
  .reveal .eyebrow::before { transform: scale(1); }
  .practice-item__body > * { opacity: 1; transform: none; }
  .post-grid.is-filtering .post { opacity: 1; transform: none; }
  .nav__list .nav__link { opacity: 1; transform: none; }
}
html[data-motion="reduced"] .hero__glow,
html[data-motion="reduced"] .hero__cue { display: none; }
html[data-motion="reduced"] .hero__media { transform: none !important; }
html[data-motion="reduced"] .hero__col { animation: none !important; }
html[data-motion="reduced"] .hero__mosaic { animation: none !important; opacity: var(--mosaic-opacity, 0.5); }
html[data-motion="reduced"] .reveal .eyebrow::before { transform: scale(1); }
html[data-motion="reduced"] .practice-item__body > * { opacity: 1; transform: none; }
html[data-motion="reduced"] .post-grid.is-filtering .post { opacity: 1; transform: none; }
html[data-motion="reduced"] .nav__list .nav__link { opacity: 1; transform: none; }
html[data-motion="reduced"] .btn:hover .btn__dot,
html[data-motion="reduced"] .practice-item__btn:hover,
html[data-motion="reduced"] .link-arrow:hover::after { transform: none; }

/* ==========================================================================
   WINDOWS CONTRAST THEMES  →  forced-colors: active
   The OS replaces our palette with its own. Three things break by default:
   background-color is ignored (so buttons lose their shape and every dot
   indicator vanishes), decorative background imagery survives (so the browser
   paints opaque text "backplates" over the hero, which looks broken), and
   anything whose meaning IS colour stops communicating.
   Only system colour keywords are honoured here.
   ========================================================================== */
@media (forced-colors: active) {
  /* Suppress the decorative hero art so the UA has no reason to draw backplates. */
  .hero__media { display: none; }
  .hero { background: Canvas; color: CanvasText; }

  /* The alternating dark/light rhythm is gone - every surface is Canvas now.
     Rule lines keep the sections legibly separate. */
  .section--inverse, .site-footer, .section--alt { border-block-start: 1px solid CanvasText; }

  /* background-color is ignored, so a filled button is indistinguishable from
     text without a border. */
  .btn, .slider__btn, .modal__close, .practice-item__icon,
  .filter-bar button, .a11y-fab, .lab-swatch, .choice {
    border: 1px solid ButtonBorder;
  }
  .btn--primary, .btn--light, .a11y-fab { background: ButtonFace; color: ButtonText; }
  .a11y-fab { border-width: 2px; }

  /* Dots and rules are painted with background-color, which is dropped. */
  .eyebrow::before, .btn__dot, .slider__dot::before,
  .tick-list li::before { background: CanvasText; }

  /* The mark is three flat brand inks and carries no text, so forcing it to
     the system palette would merge its shapes into one block. */
  .brand__mark { forced-color-adjust: none; }

  /* Fills are dropped, so the pinned profile header would read as white text
     on Canvas. A rule line separates it from the page instead. */
  body[data-header="solid"] .site-header { background: Canvas; color: CanvasText; border-block-end: 1px solid CanvasText; }
  .profile__contact, .article-list__item { border-color: CanvasText; }

  /* Selected and current states must not rely on a fill. */
  .filter-bar button[aria-pressed="true"], .seg button[aria-pressed="true"],
  .lab-swatch[aria-pressed="true"], .nav__link[aria-current="true"],
  .choice:has(input:checked) {
    border: 2px solid Highlight;
    forced-color-adjust: none;
    background: Canvas;
    color: CanvasText;
  }

  /* The palette swatches are the one place real colour IS the content -
     opt them out so the client can still compare palettes in this mode. */
  .lab-chip { forced-color-adjust: none; }

  .practice-item, .stat, .parking-item, .quote-cite, .footer-bottom,
  .modal__head, .field input, .field textarea, .field select {
    border-color: CanvasText;
  }
  .modal { border: 1px solid CanvasText; }
  .quote-mark, .badge--accessible { color: CanvasText; }

  /* Motion layer. Ambient art and the scroll cue are pure decoration and the
     OS has already thrown our palette away, so they go. The two indicator
     rules are painted with background-color, which is dropped here, so they
     are repainted in a system colour instead. */
  .hero__glow, .hero__cue, .scroll-rail { display: none; }
  .nav__link::after, .practice-item::after { background: CanvasText; }
}

/* ==========================================================================
   PRINT
   ========================================================================== */
@media print {
  .a11y-fab, .a11y-panel, .design-lab, .hero__media, .slider__controls, .filter-bar,
  .hero__cue, .scroll-rail { display: none !important; }

  /* Paper keeps the mark: the header stays, stripped back to the brand and
     unpinned so it prints once at the top instead of floating over the flow. */
  .site-header { position: static !important; background: none !important; color: #000 !important; box-shadow: none !important; }
  .site-header__inner { min-height: 0; padding-block: 0 0.5rem; }
  .nav, .nav__toggle, .header-cta { display: none !important; }
  body { color: #000; background: #fff; }
  .section--inverse, .site-footer { background: #fff !important; color: #000 !important; }
  a::after { content: " (" attr(href) ")"; font-size: 0.75em; }
  .brand::after { content: none; }   /* "(#home)" beside the mark is just noise */
  .reveal { opacity: 1 !important; transform: none !important; }
  .hero__kicker, .hero__actions, .hero__promise { animation: none !important; opacity: 1 !important; }
  .practice-item__body > * { opacity: 1 !important; transform: none !important; }
}
