/* =========================================================================
   Ludic Media Studio — foundation
   Tokens, reset, typography, page chrome (header / footer / skip link).

   This is the SAME design system as the Ludic Group site: the spacing ladder,
   type scale, breakpoint ladder, focus handling and naming convention are
   shared, so a change of habit in one build is a change of habit in both.
   What differs is the brand: the Media Studio wordmark, and yellow instead of
   black as the accent.

   Every colour and dimension below was read off the archived Webflow build
   with a browser (`_build/migration/probe.py`) rather than eyeballed from a
   screenshot; the measured value each token replaces is noted beside it.

   BREAKPOINT LADDER — the only four widths this site uses. Custom properties
   do not work inside @media (a CSS restriction, not an oversight), so the
   numbers are written out and this comment is the single source of truth.

       36rem   576px   small phone  -> large phone
       48rem   768px   phone        -> tablet
       64rem  1024px   tablet       -> laptop
       80rem  1280px   laptop       -> desktop

   NAMING — block__element--modifier. No ID selectors. No !important outside
   @media print and the reduced-motion block.
   ========================================================================= */

/* ---------- fonts -------------------------------------------------------
   Self-hosted so no third-party request fires before the visitor has made a
   consent choice. The original pulled Archivo, Roboto and Roboto Slab from
   Google Fonts — three families, one of them (Roboto Slab) never actually
   applied to anything, and one (Roboto) used only for button labels.

   This build ships ONE family. Archivo carries every heading, every button
   and all body copy, which is what the archived pages do everywhere except
   those button labels. That is 43 KB instead of three font requests to a
   third party, and it is why there is no editorial serif here even though
   the Ludic Group site has one: the Media Studio design never used it.
   ----------------------------------------------------------------------- */
@font-face {
  font-family: "Archivo";
  src: url("../assets/fonts/archivo-latin.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ---------- tokens ------------------------------------------------------ */
:root {
  /* Brand values. Components read the semantic aliases below, never these,
     which is what keeps a brand change a short edit instead of a rewrite. */
  --black: #000000;
  --white: #ffffff;
  --cream: #f2f2f2;          /* section background, the site's workhorse */
  --black-hover: #1b1b1b;
  --dark-grey: #494949;
  --mid-grey: #767676;       /* 4.54:1 on white — usable for small print */
  --line: #d4d4d4;

  /* MEASURED: the yellow bands, the hero card and the use-case cells are all
     rgb(253, 199, 39). The slider arrows are rgb(255, 215, 40) — a second,
     one-off yellow three units away that nothing else on the site uses. One
     token, the dominant value; the arrows now match everything else.

     #fdc727 carries BLACK text at 13.3:1 and WHITE text at 1.58:1. White on
     yellow is not a near miss, it is illegible, so every yellow band in this
     build sets colour to black and checks.py asserts the pair. */
  --yellow: #fdc727;
  --yellow-deep: #e8b410;    /* hover/active on a yellow fill */

  /* Ludic Group's Deep Blue, for cross-brand links, and the same lightened
     variant the Group site uses on black chrome (#44597b is 2.96:1 on black,
     unusable for a nav link; this is the same hue opened up to 9.87:1). */
  --blue: #44597b;
  --blue-on-dark: #9fb3d4;

  /* Semantic aliases. The accent is yellow: the archived site uses yellow for
     its primary button, its quote bands, its hero card and its use-case
     cells, and reserves black for chrome and for buttons that sit ON yellow. */
  --accent: var(--yellow);
  --accent-on: var(--black);       /* label colour on an accent fill */
  --accent-line: var(--black);     /* accent fills keep a black edge */
  --ink: var(--black);
  --ink-muted: var(--dark-grey);   /* 8.10:1 on white, 7.36:1 on cream */
  --surface: var(--white);
  --surface-alt: var(--cream);
  --surface-accent: var(--yellow);
  --ink-on-invert: var(--white);
  --control-line: var(--black);    /* the original's 2px black input border */

  /* Layout, measured at 1440. Four widths and only four:
       80rem  1280  the chrome — nav logo lands at x=120, as the original does
       66rem  1056  page content: every main section's text column (x=192)
       60rem   960  statements, the client grid, centred section headings
       band         full width less 2.5rem a side — the yellow quote and
                    newsletter bands, measured 1360 wide at x=40 */
  --container: 80rem;
  --container-content: 66rem;
  --container-small: 60rem;
  --container-pad: 2.5rem;
  --band-inset: 2.5rem;
  --header-h: 60px;               /* MEASURED 59.2px */

  /* Spacing ladder. The original had no ladder — 8 / 16 / 24 / 32 / 40 / 48 /
     64 / 112px chosen per element. These are those values put on a scale, and
     it is deliberately the same ladder as the Ludic Group build. */
  --space-2xs: 0.5rem;   /*  8px */
  --space-xs: 1rem;      /* 16px */
  --space-sm: 1.5rem;    /* 24px */
  --space-md: 2rem;      /* 32px */
  --space-lg: 3rem;      /* 48px */
  --space-xl: 5rem;      /* 80px */
  --space-2xl: 7rem;     /* 112px */

  /* Type scale. Left column is the measured value each one replaces.
       12.8px  nav link          -> --text-nav  0.8125rem
       14.4px  service body      -> --text-sm   0.9375rem
       16.0px  body, eyebrow     -> --text-base 1rem
       20.0px  lede, card title  -> --text-md   1.25rem
       24.0px  statement, h3     -> --text-lg   1.5rem
       32.0px  section h2        -> --text-xl   2rem
       48.0px  page h1           -> --text-2xl  3rem
       64.0px  rotated page h1   -> --text-3xl  4rem
     The last two steps are new against the Ludic Group ladder, which stops at
     2rem: this site has a 48px page title and a 64px rotated one and needs
     somewhere for them to live. */
  --text-nav: 0.8125rem;
  --text-xs: 0.875rem;
  --text-sm: 0.9375rem;
  --text-base: 1rem;
  --text-md: 1.25rem;
  --text-lg: 1.5rem;
  --text-xl: 2rem;
  --text-2xl: 3rem;
  --text-3xl: 4rem;

  --leading-tight: 1.1;
  --leading-snug: 1.2;
  --leading-mid: 1.5;
  --leading-body: 1.75;

  /* ---- the card contract ----------------------------------------------
     ONE set of numbers for every card on the site, so a case-study card, a
     "Solutions and formats" card and a related-work card are all the same
     shape. Bespoke per-card sizing is what produced a grid whose cards ended
     at four different heights.

     The rule is: the image area is a FIXED HEIGHT and the artwork is cropped
     to fill it; the text area is a FIXED HEIGHT and the copy is clamped to
     fit. Fix both and every card is identical, every image aligns, and there
     is no slack anywhere for a gap to open up in.

     --card-media-h  11rem = 176px. At the 336px card the desktop grid
                     produces that is a 1.9:1 crop, which is the median shape
                     of the case-study thumbnails — so the largest group of
                     them is barely cropped at all.
     --card-title-lines 2. Measured across the 29 case studies at 336px: 12
                     titles are one line, 10 are two, 3 are three and 4 are
                     four. Two lines truncates 7 of 29 and leaves 12 with a
                     line of slack; three lines truncates 4 and leaves 22 with
                     slack, and makes every card 30px taller for the sake of
                     three of them. Two is the tighter honest choice, and the
                     full title is on the card's own page.
     --card-blurb-lines 2. The blurb is a teaser; it is not the place to read
                     a paragraph.
     -------------------------------------------------------------------- */
  --card-media-h: 11rem;
  --card-title-lines: 2;
  --card-blurb-lines: 2;

  /* --stage-h  the service-hero gallery stage. FIXED, not derived.
     The archive's `.slider-service-hero` is `height: 508px` at desktop, 400px
     under 768 and 300px under 480, and its slide images are `object-fit:
     cover`. Measured off a screenshot of the live page: 507px. One number, the
     same on every gallery, so a page cannot drift from the others. It also
     sizes the rotated title — see .gallery__title. */
  --stage-h: 31.75rem;   /* 508px */

  /* --frame-w  the yellow rule around a service-card picture.
     It used to be painted INTO the artwork: a 5px band inside a 494px-wide
     PNG. Baked in, it scaled with the image, so the same rule rendered at 3.3px
     on a 331px card and at 5px on the source — one design detail with no single
     value. The client supplied the same 26 images without it (`_source/unframed/`,
     see migration/harvest_assets.py::find) and it lives here instead.
     3px, because 5/494 of the 331px card the desktop grid produces is 3.35px:
     the weight the frame already had, now stated once. */
  --frame-w: 3px;

  --duration: 250ms;   /* the original's .25s transition */
  --ease: ease;
}

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

* {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

html {
  -webkit-text-size-adjust: 100%;
  /* Anchors must clear the sticky header or every in-page link — and the
     contact form, which every service page links to as #contact — lands
     underneath it. */
  scroll-padding-top: calc(var(--header-h) + 20px);
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--text-base);
  line-height: var(--leading-body);
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }
/* :where() so this reset has ZERO specificity and any component rule beats it.
   Without it, `ul[role="list"]` is (0,1,1) and a plain class is (0,1,0) — so a
   reset in this file silently outranked `.gallery__thumbs { margin-top: 4rem }`
   in components.css, and the gallery's thumbnail strip sat flush against the
   stage with no gap for as long as that rule has existed. Specificity beats
   source order, and an attribute selector in a reset is easy to not notice. */
:where(ul[role="list"], ol[role="list"]) { list-style: none; margin: 0; padding: 0; }
img, svg, video, iframe { display: block; max-width: 100%; }
img, svg, video { height: auto; }
button { font: inherit; color: inherit; }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--dark-grey); }

/* One global focus ring, and never `outline: none` anywhere else. The outline
   is kept rather than swapped for a box-shadow: box-shadows are discarded in
   Windows High Contrast mode, a silent WCAG 2.4.7 failure you cannot see on
   your own machine. */
:focus-visible {
  outline: 3px solid var(--black);
  outline-offset: 2px;
}
@media (forced-colors: active) { :focus-visible { outline-color: Highlight; } }

/* A black ring is invisible on the black chrome. */
.site-header :focus-visible,
.footer :focus-visible,
.section[data-tone="black"] :focus-visible { outline-color: var(--white); }

::selection { background: var(--yellow); color: var(--black); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

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

.skip-link {
  position: absolute;
  top: 0; left: 50%;
  transform: translate(-50%, -110%);
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--yellow);
  color: var(--black);
  font-weight: 600;
  text-decoration: none;
  transition: transform var(--duration) var(--ease);
}
.skip-link:focus-visible { transform: translate(-50%, 0); color: var(--black); }

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: 1.25rem;
}
@media (min-width: 48rem) { .container { padding-inline: 2rem; } }
@media (min-width: 64rem) { .container { padding-inline: var(--container-pad); } }

/* The content measure. Written as a max-width on the same padded box so it
   never gets wider than the chrome, and lands at 1056px on a 1440 screen —
   the width the original's main column measures. */
.container--content { max-width: calc(var(--container-content) + 2 * var(--container-pad)); }
.container--small { max-width: calc(var(--container-small) + 2 * var(--container-pad)); }
.container--narrow { max-width: 50rem; }

/* ---------- typography -------------------------------------------------- */
/* One family. Weight and size carry the hierarchy — the original leaned on
   Archivo's variable width axis in a couple of places, which the static
   subset cannot reproduce and which nothing depends on. */

.page-title {
  font-size: clamp(2rem, 4.4vw, var(--text-2xl));
  font-weight: 400;
  line-height: var(--leading-snug);
}

.section__title {
  font-size: clamp(1.5rem, 2.8vw, var(--text-xl));
  font-weight: 400;
  line-height: var(--leading-snug);
}

.subsection__title {
  font-size: var(--text-lg);
  font-weight: 400;
  line-height: var(--leading-snug);
}

/* MEASURED 16px, weight 700, uppercase — the original's .text-style-allcaps.
   Kept at body size rather than shrunk: it is a section label above a 48px
   title, not fine print. */
.eyebrow {
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: var(--space-2xs);
}

/* MEASURED 24px / 42px line-height, 960px wide, centred. */
.statement {
  font-size: clamp(1.25rem, 2.2vw, var(--text-lg));
  font-weight: 400;
  line-height: var(--leading-mid);
}
.statement--center { margin-inline: auto; text-align: center; max-width: 56ch; }

.prose__p {
  margin-bottom: var(--space-xs);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  max-width: 62ch;
}
.prose__p:last-child { margin-bottom: 0; }

.section__head { margin-bottom: var(--space-md); }
.section__head--center { text-align: center; max-width: 46rem; margin-inline: auto; }
.section__head--center .prose__p { margin-inline: auto; }
.section__lede { margin-top: var(--space-2xs); font-size: var(--text-md); line-height: var(--leading-mid); }

/* ---------- section rhythm ---------------------------------------------- */
/* Three paddings, and only three, stepped down on small screens rather than
   holding the original's 112px at every width. */
.section { padding-block: var(--space-xl); }
.section--tight { padding-block: var(--space-lg); }
.section--loose { padding-block: var(--space-xl); }

@media (min-width: 64rem) {
  .section--loose { padding-block: var(--space-2xl); }
}
@media (max-width: 47.999rem) {
  .section, .section--loose { padding-block: var(--space-lg); }
  .section--tight { padding-block: var(--space-md); }
}

.section[data-tone="cream"] { background: var(--surface-alt); }
.section[data-tone="black"] { background: var(--black); color: var(--white); }
.section[data-tone="black"] .eyebrow { color: var(--yellow); }
/* Yellow bands always carry black copy: white measures 1.58:1 here. */
.section[data-tone="yellow"] { background: var(--surface-accent); color: var(--black); }

/* Two bands of the same tone in a row read as one; drop the seam. */
.section[data-tone="cream"] + .section[data-tone="cream"],
.section[data-tone="yellow"] + .section[data-tone="yellow"],
.section[data-tone="black"] + .section[data-tone="black"] { padding-top: 0; }

/* ---------- bands -------------------------------------------------------
   The archived site insets its yellow bands by 40px from each side of the
   VIEWPORT, so a white gutter shows either side — measured 1360 wide at x=40
   on a 1440 screen. At the client's direction there are now two treatments,
   and neither is that one.

   `.band` is FULL WIDTH: the colour runs edge to edge with no gutter, and the
   copy inside still sits on the content measure. The mailing-list band uses
   it on every page.

   `.band--panel` lines the colour up with the page's CONTENT COLUMN, so a
   quote band is exactly as wide as the copy in the section above it. This is
   the one that needed real care. "40px in from the viewport" and "as wide as
   the content" are different widths — at 1440 they are 40px apart on each
   side — so the panel cannot just be a narrower margin. The colour is painted
   on an INNER element that fills the container's content box, which is the
   only way to land on 1056px at x=192 at every width without restating the
   gutter ladder a third time.

   That is also why `.section--panel` clears the section's own background:
   `data-tone="yellow"` stays on the section, because that is what the button
   and eyebrow rules key off, but the section no longer paints it.
   ----------------------------------------------------------------------- */
.band { margin-inline: 0; }

.section--panel[data-tone] { background: none; }

.band__panel {
  background: var(--surface-accent);
  color: var(--black);
  padding: var(--space-lg) var(--space-sm);
}
@media (min-width: 48rem) { .band__panel { padding: var(--space-xl) var(--space-md); } }

.section--panel[data-tone="black"] .band__panel { background: var(--black); color: var(--white); }
.section--panel[data-tone="cream"] .band__panel { background: var(--surface-alt); color: var(--ink); }

/* ---------- buttons ----------------------------------------------------- */
/* MEASURED: 48px tall, 8px/24px padding, 2px solid black, 16px label.
   Two variants, both from the original:
     primary    yellow fill, black label  (.button-4.primary)
     secondary  white fill, black label   (.button-4.secondary)
   On a yellow band a yellow button would vanish, so there the primary flips
   to a black fill with a white label — which is exactly what the original
   does on its quote bands. */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.625rem 1.5rem;
  border: 2px solid var(--accent-line);
  background: var(--accent);
  color: var(--accent-on);
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 400;
  line-height: var(--leading-mid);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--duration) var(--ease),
              border-color var(--duration) var(--ease),
              color var(--duration) var(--ease);
}
/* EVERY BUTTON INVERTS ON HOVER. The fill and the label swap; the 2px border
   stays exactly where it was, so no state moves the box by a single pixel —
   which is why the border is on the resting state in the first place.

   Four contexts, and the rule is the same in all four:

     on white/cream, primary    yellow fill, black label  ->  black fill, YELLOW label
     on white/cream, secondary  white fill,  black label  ->  black fill, white label
     on a yellow band, primary  black fill,  white label  ->  no fill, black label
     on a black band, primary   yellow fill, black label  ->  no fill, yellow label

   On a yellow band the inverted primary drops its fill to show the band
   through, which leaves a black-bordered button with a black label. That is
   the true inversion and it reads as a deliberate outline rather than a
   disappearance — and it does mean a hovered primary there looks like a
   resting secondary. Those bands carry one button each, so nothing is
   ambiguous in practice; if a band ever carries two, give the secondary a
   different resting treatment rather than softening this.

   Every pair below is in the PAIRS table in checks.py, hover states included:
   yellow-on-black is 13.3:1, black-on-yellow 13.3:1, white-on-black 21:1. */
.button:hover,
.button:focus-visible {
  background: var(--black);
  border-color: var(--black);
  color: var(--yellow);
}

.button--secondary { background: var(--white); color: var(--black); }
.button--secondary:hover,
.button--secondary:focus-visible {
  background: var(--black);
  border-color: var(--black);
  color: var(--white);
}

.button--sm { padding: 0.375rem 1rem; font-size: var(--text-sm); }

/* On yellow: black fill, white label. On black: yellow fill, black label. */
.section[data-tone="yellow"] .button,
.hero__card .button,
.quote-pair .button,
.case__testimonial .button {
  background: var(--black);
  border-color: var(--black);
  color: var(--white);
}
.section[data-tone="yellow"] .button:hover,
.section[data-tone="yellow"] .button:focus-visible,
.hero__card .button:hover,
.hero__card .button:focus-visible,
.quote-pair .button:hover,
.quote-pair .button:focus-visible {
  background: transparent;
  border-color: var(--black);
  color: var(--black);
}

.section[data-tone="yellow"] .button--secondary,
.hero__card .button--secondary { background: transparent; color: var(--black); }
.section[data-tone="yellow"] .button--secondary:hover,
.section[data-tone="yellow"] .button--secondary:focus-visible,
.hero__card .button--secondary:hover,
.hero__card .button--secondary:focus-visible { background: var(--black); color: var(--white); }

.section[data-tone="black"] .button {
  background: var(--yellow);
  border-color: var(--yellow);
  color: var(--black);
}
.section[data-tone="black"] .button:hover,
.section[data-tone="black"] .button:focus-visible {
  background: transparent;
  border-color: var(--yellow);
  color: var(--yellow);
}

.button-group { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-top: var(--space-sm); }
.button-group--center { justify-content: center; }

.link-button {
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit;
  text-decoration: underline; text-underline-offset: 0.18em;
  cursor: pointer;
}

/* A text link with the original's yellow circular arrow after it. */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--text-sm);
  text-decoration: none;
  font-weight: 500;
}
.arrow-link__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 1.5rem; height: 1.5rem;
  border-radius: 50%;
  background: var(--yellow);
  color: var(--black);
  transition: transform var(--duration) var(--ease);
}
.arrow-link:hover { color: var(--black); text-decoration: underline; }
.arrow-link:hover .arrow-link__icon { transform: translateX(3px); }

/* ---------- header ------------------------------------------------------ */
/* MEASURED: black bar 59.2px tall, wordmark 172.9x15 at x=120, nav links
   12.8px white with 8px/16px padding, right-aligned. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--black);
  color: var(--white);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  max-width: var(--container);
  min-height: var(--header-h);
  margin-inline: auto;
  padding-inline: 1.25rem;
}
@media (min-width: 48rem) { .site-header__inner { padding-inline: 2rem; } }
@media (min-width: 64rem) { .site-header__inner { padding-inline: var(--container-pad); } }

.site-header__logo { display: flex; align-items: center; margin-right: auto; }
/* The wordmark is the supplied SVG, never typeset — brand rule. 15px tall,
   matching .nav4_logo-2 on the original. */
.site-header__logo img {
  width: auto;
  height: 0.9375rem;
  transition: filter var(--duration) var(--ease);
}

/* YELLOW ON HOVER, from the white file, with a filter rather than a second
   asset. The alternatives are both worse: shipping a yellow wordmark doubles
   a 14 KB file and gives the brand two masters to keep in step, and `mask`
   with a background-color turns into a solid coloured rectangle if the mask
   is ever dropped. A filter degrades to "no colour change", which is the
   right way for this to fail.

   The numbers are not decoration and they are not guesses. `brightness(0)`
   flattens the white artwork to black; `invert(78%)` lifts it to a mid grey so
   the sepia has something to tint; sepia warms it; saturate and hue-rotate
   push it onto the brand hue. The values were found by a numerical search
   against the token rather than eyeballed — `_build/migration/solve_filter.py`
   is the search — and MEASURED IN A BROWSER they render rgb(252, 198, 38)
   against a target of #fdc727 = rgb(253, 199, 39). One unit per channel,
   which is not perceptible, and 13.3:1 on the black bar either way.

   An earlier hand-tuned chain rendered rgb(252, 191, 18) — 21 units out on
   blue — while its comment claimed one. browser_checks.py now samples the
   rendered pixel and fails if it drifts more than 6 units per channel, so the
   comment and the pixel cannot disagree again.

   Re-run the search if --yellow changes: there is no arithmetic that derives
   these from the token, and that is the honest cost of doing this in one
   file with one asset. */
.site-header__logo:hover img,
.site-header__logo:focus-visible img {
  filter: brightness(0) saturate(100%) invert(78%) sepia(68%)
          saturate(977%) hue-rotate(338deg) brightness(107%) contrast(98%);
}

.site-header__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  margin-right: -0.625rem;
  background: none;
  border: 0;
  color: var(--white);
  cursor: pointer;
}
.site-header__toggle-bars,
.site-header__toggle-bars::before,
.site-header__toggle-bars::after {
  display: block;
  width: 22px; height: 2px;
  background: currentColor;
  transition: transform var(--duration) var(--ease), opacity var(--duration) var(--ease);
}
.site-header__toggle-bars { position: relative; }
.site-header__toggle-bars::before,
.site-header__toggle-bars::after { content: ""; position: absolute; left: 0; }
.site-header__toggle-bars::before { top: -7px; }
.site-header__toggle-bars::after { top: 7px; }
[aria-expanded="true"] .site-header__toggle-bars { background: transparent; }
[aria-expanded="true"] .site-header__toggle-bars::before { transform: translateY(7px) rotate(45deg); }
[aria-expanded="true"] .site-header__toggle-bars::after { transform: translateY(-7px) rotate(-45deg); }

.site-nav {
  position: fixed;
  inset: var(--header-h) 0 auto 0;
  max-height: calc(100dvh - var(--header-h));
  overflow-y: auto;
  background: var(--black);
  transform: translateY(-8px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration) var(--ease), transform var(--duration) var(--ease),
              visibility var(--duration) var(--ease);
}
.site-nav[data-open] { transform: none; opacity: 1; visibility: visible; }

.site-nav__list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0 1.25rem var(--space-sm);
  list-style: none;
}

.site-nav__link {
  display: block;
  width: 100%;
  padding: 0.75rem 0;
  background: none;
  border: 0;
  border-bottom: 1px solid rgb(255 255 255 / 0.2);
  color: var(--white);
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 400;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--duration) var(--ease);
}
/* Hover is the brand yellow. On black it measures 13.3:1, so unlike the
   Ludic Group site's Deep Blue it needs no lightening to be legible. */
.site-nav__link:hover,
.site-nav__sublink:hover { color: var(--yellow); }
.site-nav__link[aria-current="page"],
.site-nav__toggle[data-nav-current] { color: var(--yellow); font-weight: 600; }

.site-nav__toggle { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.site-nav__caret { flex: none; transition: transform var(--duration) var(--ease); }
[aria-expanded="true"] > .site-nav__caret { transform: rotate(180deg); }

.site-nav__submenu { display: grid; margin: 0; padding: 0 0 0 var(--space-xs); list-style: none; }

.site-nav__sublink {
  display: block;
  padding: 0.625rem 0;
  color: var(--white);
  font-family: inherit;
  font-size: var(--text-sm);
  text-decoration: none;
  border-bottom: 1px solid rgb(255 255 255 / 0.12);
  transition: color var(--duration) var(--ease);
}
.site-nav__sublink[aria-current="page"] { color: var(--yellow); font-weight: 600; }

@media (min-width: 64rem) {
  .site-header__toggle { display: none; }
  .site-nav {
    position: static;
    max-height: none;
    overflow: visible;
    background: none;
    transform: none;
    opacity: 1;
    visibility: visible;
  }
  .site-nav__list { flex-direction: row; align-items: center; gap: 0; padding: 0; }
  .site-nav__item { position: relative; }
  .site-nav__link {
    width: auto;
    padding: 0.5rem 1rem;
    border: 0;
    font-size: var(--text-nav);
  }

  /* The panel opens on hover, as the original does — but the hover is applied
     in JS and visibility keys off [data-open] alone, never off `:hover` in
     this selector. With `:hover` in the CSS the panel stays visible while the
     pointer rests on it, so Escape sets aria-expanded=false and nothing
     moves: one state, four ways in (hover, click, Enter, Escape) is the only
     version that stays honest.

     Collapsed only when the document is scripted (`html[data-js]`, set by an
     inline script in the head). With scripting off nothing can reopen it, so
     it stays on show and the Services links remain reachable. */
  .site-nav__submenu {
    position: absolute;
    top: 100%;
    left: 0.5rem;
    z-index: 60;
    min-width: 15rem;
    padding: 0.375rem 0;
    background: var(--black);
    border: 0;
    transition: opacity var(--duration) var(--ease), transform var(--duration) var(--ease),
                visibility var(--duration) var(--ease);
  }
  [data-js] .site-nav__submenu {
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.25rem);
  }
  [data-js] .site-nav__item--group[data-open] > .site-nav__submenu {
    opacity: 1;
    visibility: visible;
    transform: none;
  }
  .site-nav__sublink {
    padding: 0.5rem 1rem;
    border: 0;
    font-size: var(--text-nav);
    white-space: nowrap;
  }
}

/* ---------- footer ------------------------------------------------------ */
.footer {
  background: var(--black);
  color: var(--white);
  padding-top: var(--space-xl);
}

.footer__inner {
  display: grid;
  gap: var(--space-md);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: 1.25rem;
}
@media (min-width: 48rem) { .footer__inner { padding-inline: 2rem; } }
/* MEASURED: a wide left column holding the wordmark, address and social row,
   with the link lists pushed hard right. Not four equal columns. */
@media (min-width: 64rem) {
  .footer__inner {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    padding-inline: var(--container-pad);
  }
}

.footer__logo { width: auto; height: 1.0625rem; }

.footer__details { margin-top: var(--space-md); display: grid; gap: var(--space-sm); }
.footer__label { font-weight: 700; }

.footer__menu { display: grid; gap: var(--space-md); }
@media (min-width: 30rem) { .footer__menu { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .footer__menu { grid-template-columns: repeat(2, 12rem); gap: var(--space-lg); } }

.footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-md);
  padding: 0;
  list-style: none;
}
.footer__social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem; height: 2rem;
  color: var(--white);
  transition: color var(--duration) var(--ease);
}
.footer__social-link:hover { color: var(--yellow); }

.footer__heading {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--white);
  margin-bottom: var(--space-2xs);
}

.footer__text { color: var(--white); font-size: var(--text-xs); line-height: 1.6; }

.footer__list { display: grid; gap: 0.5rem; margin: 0; padding: 0; list-style: none; align-content: start; }

.footer__link {
  color: var(--white);
  font-size: var(--text-xs);
  line-height: var(--leading-mid);
  text-decoration: none;
  transition: color var(--duration) var(--ease);
}
.footer__link:hover { color: var(--yellow); text-decoration: none; }

.footer__link--button { background: none; border: 0; padding: 0; cursor: pointer; text-align: left; font-family: inherit; }

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  width: 100%;
  max-width: var(--container);
  margin: var(--space-lg) auto 0;
  padding: var(--space-sm) 1.25rem var(--space-md);
  border-top: 1px solid rgb(255 255 255 / 0.25);
}
@media (min-width: 48rem) { .footer__legal { padding-inline: 2rem; } }
@media (min-width: 64rem) { .footer__legal { padding-inline: var(--container-pad); } }

.footer__credit { color: var(--white); font-size: var(--text-xs); }
.footer__credits { display: grid; gap: 0.25rem; }

.footer__legal-list { display: flex; flex-wrap: wrap; gap: 0.5rem var(--space-sm); margin: 0; padding: 0; list-style: none; }
/* The legal row keeps its underline at rest — it is a run of small links in a
   crowded strip and needs the affordance — but drops it on hover along with
   everything else in the footer. */
.footer__legal-list .footer__link { text-decoration: underline; }
.footer__legal-list .footer__link:hover { text-decoration: none; color: var(--yellow); }

/* ---------- print ------------------------------------------------------- */
@media print {
  .site-header, .footer, .consent, .skip-link, .filters,
  .gallery__controls, .gallery__thumbs { display: none !important; }
  body { color: #000; font-size: 11pt; }
  a::after { content: " (" attr(href) ")"; font-size: 9pt; }
}
